Compare commits

..

8 Commits

11 changed files with 206 additions and 68 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>
+163 -56
View File
@@ -4,33 +4,38 @@ using System.Linq;
using System.Reflection; using System.Reflection;
using HarmonyLib; using HarmonyLib;
using MelonLoader; using MelonLoader;
using Il2CppInterop.Runtime;
using Il2CppScheduleOne.Growing; using Il2CppScheduleOne.Growing;
using Il2CppScheduleOne.ItemFramework; using Il2CppScheduleOne.ItemFramework;
using Il2CppScheduleOne.ObjectScripts; using Il2CppScheduleOne.ObjectScripts;
using Il2CppScheduleOne.PlayerTasks.Tasks;
using Il2CppScheduleOne.ObjectScripts.Soil;
using UnityEngine;
[assembly: MelonInfo(typeof(AbsorbentSoil.AbsorbentSoilMod), "Absorbent Soil", "0.1.1", "AttilaG")] [assembly: MelonInfo(typeof(AbsorbentSoil.AbsorbentSoilMod), "Absorbent Soil", "0.1.0", "AttilaG")]
[assembly: MelonGame(null, "Schedule I")] [assembly: MelonGame(null, "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 = true;
public override void OnInitializeMelon() public override void OnInitializeMelon()
{ {
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}"); MelonLogger.Msg($"Cleared additive memory on scene load: {sceneName}");
} }
} }
@@ -73,27 +78,28 @@ namespace AbsorbentSoil
: Array.Empty<string>(); : 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) public static void Forget(Pot pot)
{ {
if (pot == null) if (pot == null)
return; return;
Forget(PotKeyHelper.GetPotKey(pot)); string key = PotKeyHelper.GetPotKey(pot);
if (!string.IsNullOrWhiteSpace(key))
AdditivesByPotKey.Remove(key);
} }
public static void Forget(string potKey) public static void Clear() => AdditivesByPotKey.Clear();
{
if (string.IsNullOrWhiteSpace(potKey))
return;
if (AdditivesByPotKey.Remove(potKey) && AbsorbentSoilMod.VerboseLogging)
MelonLogger.Msg($"Cleared retained additives for pot '{potKey}'.");
}
public static void ClearAll()
{
AdditivesByPotKey.Clear();
}
} }
internal static class PotKeyHelper internal static class PotKeyHelper
@@ -103,10 +109,13 @@ namespace AbsorbentSoil
if (pot == null) if (pot == null)
return string.Empty; return string.Empty;
// Most Schedule I buildable/grid items have a GUID inherited from a base class.
// Use reflection so this survives minor EA API shifts.
object guid = ReadMember(pot, "GUID") ?? ReadMember(pot, "Guid") ?? ReadMember(pot, "guid"); object guid = ReadMember(pot, "GUID") ?? ReadMember(pot, "Guid") ?? ReadMember(pot, "guid");
if (guid != null && !string.IsNullOrWhiteSpace(guid.ToString())) if (guid != null && !string.IsNullOrWhiteSpace(guid.ToString()))
return guid.ToString(); return guid.ToString();
// Fallback for testing only. Not stable across reloads, but better than hard failing.
return $"scene:{pot.GetInstanceID()}"; return $"scene:{pot.GetInstanceID()}";
} }
@@ -116,8 +125,7 @@ namespace AbsorbentSoil
return null; return null;
Type type = instance.GetType(); Type type = instance.GetType();
while (type != null)
{
PropertyInfo prop = AccessTools.Property(type, name); PropertyInfo prop = AccessTools.Property(type, name);
if (prop != null) if (prop != null)
{ {
@@ -130,40 +138,72 @@ namespace AbsorbentSoil
try { return field.GetValue(instance); } catch { } try { return field.GetValue(instance); } catch { }
} }
type = type.BaseType;
}
return null; return null;
} }
} }
internal static class SoilHelper internal static class SoilHelper
{ {
private static readonly FieldInfo RemainingSoilUsesField = private static readonly Dictionary<string, int> RemainingUsesByPotKey = new();
AccessTools.Field(typeof(GrowContainer), "_remainingSoilUses");
public static bool IsRetainingSoil(Pot pot) public static int DecrementRemainingSoilUses(Pot pot)
{ {
if (pot == null || string.IsNullOrWhiteSpace(pot.SoilID)) if (pot == null)
return false; return 0;
string soilId = pot.SoilID.ToLowerInvariant(); string key = PotKeyHelper.GetPotKey(pot);
if (string.IsNullOrWhiteSpace(key))
return 0;
return soilId.Contains("longlifesoil") || int current = RemainingUsesByPotKey.TryGetValue(key, out int uses) ? uses : 0;
soilId.Contains("extralonglifesoil"); 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) public static int GetRemainingSoilUses(Pot pot)
{ {
if (pot == null || RemainingSoilUsesField == null) if (pot == null)
return 0; return 0;
return (int)RemainingSoilUsesField.GetValue(pot); string key = PotKeyHelper.GetPotKey(pot);
if (string.IsNullOrWhiteSpace(key))
return 0;
return RemainingUsesByPotKey.TryGetValue(key, out int uses) ? uses : 0;
} }
public static bool CanRetainAdditives(Pot pot) public static bool CanCaptureNewAdditives(Pot pot)
{ {
return IsRetainingSoil(pot) && GetRemainingSoilUses(pot) > 0; 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);
} }
} }
@@ -171,7 +211,6 @@ namespace AbsorbentSoil
internal static class Pot_ApplyAdditive_Patch internal static class Pot_ApplyAdditive_Patch
{ {
private static bool _suppressCapture; private static bool _suppressCapture;
private static readonly MethodInfo ApplyAdditiveMethod = AccessTools.Method(typeof(Pot), "ApplyAdditive");
public static void Postfix(Pot __instance, AdditiveDefinition __result, string additiveID, bool isInitialApplication) public static void Postfix(Pot __instance, AdditiveDefinition __result, string additiveID, bool isInitialApplication)
{ {
@@ -183,7 +222,7 @@ namespace AbsorbentSoil
if (__instance == null || __result == null || string.IsNullOrWhiteSpace(additiveID)) if (__instance == null || __result == null || string.IsNullOrWhiteSpace(additiveID))
return; return;
if (!SoilHelper.IsRetainingSoil(__instance)) if (!SoilHelper.CanCaptureNewAdditives(__instance))
return; return;
AdditiveMemory.Remember(__instance, additiveID); AdditiveMemory.Remember(__instance, additiveID);
@@ -199,7 +238,8 @@ namespace AbsorbentSoil
if (pot == null || string.IsNullOrWhiteSpace(additiveID)) if (pot == null || string.IsNullOrWhiteSpace(additiveID))
return; return;
if (ApplyAdditiveMethod == null) MethodInfo applyAdditive = AccessTools.Method(typeof(Pot), "ApplyAdditive");
if (applyAdditive == null)
{ {
MelonLogger.Warning("Could not find Pot.ApplyAdditive via reflection."); MelonLogger.Warning("Could not find Pot.ApplyAdditive via reflection.");
return; return;
@@ -208,7 +248,7 @@ namespace AbsorbentSoil
try try
{ {
_suppressCapture = true; _suppressCapture = true;
ApplyAdditiveMethod.Invoke(pot, new object[] { additiveID, true }); applyAdditive.Invoke(pot, new object[] { additiveID, true });
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -228,25 +268,18 @@ namespace AbsorbentSoil
{ {
try try
{ {
MelonLogger.Msg($"Plant.Initialize fired. Plant={__instance?.name}, Pot={__instance?.Pot?.name}");
if (__instance == null || __instance.Pot == null) if (__instance == null || __instance.Pot == null)
return; return;
Pot pot = __instance.Pot; Pot actualPot = __instance.Pot;
var additiveIds = AdditiveMemory.Get(actualPot);
if (!SoilHelper.CanRetainAdditives(pot)) MelonLogger.Msg($"Plant.Initialize pot key={PotKeyHelper.GetPotKey(actualPot)}, remembered additives={additiveIds.Count}");
{
AdditiveMemory.Forget(pot);
return;
}
IReadOnlyList<string> additiveIds = AdditiveMemory.Get(pot);
if (additiveIds.Count == 0)
return;
foreach (string additiveId in additiveIds) foreach (string additiveId in additiveIds)
Pot_ApplyAdditive_Patch.ReapplyWithoutRecapture(pot, additiveId); Pot_ApplyAdditive_Patch.ReapplyWithoutRecapture(actualPot, additiveId);
MelonLogger.Msg($"Reapplied {additiveIds.Count} retained additive(s) to new plant in pot '{PotKeyHelper.GetPotKey(pot)}'.");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -260,13 +293,86 @@ namespace AbsorbentSoil
{ {
private static void Postfix(Pot __instance) private static void Postfix(Pot __instance)
{ {
if (__instance == null) try
return; {
int remaining = SoilHelper.DecrementRemainingSoilUses(__instance);
if (!SoilHelper.CanRetainAdditives(__instance)) MelonLogger.Msg($"[Absorbent Soil] Harvest consumed soil use. Remaining uses={remaining}");
if (remaining <= 0)
{ {
AdditiveMemory.Forget(__instance); AdditiveMemory.Forget(__instance);
MelonLogger.Msg("[Absorbent Soil] Cleared retained additives because soil has no retaining uses left."); 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}");
} }
} }
} }
@@ -276,6 +382,7 @@ namespace AbsorbentSoil
{ {
public static void Prefix(Pot __instance) public static void Prefix(Pot __instance)
{ {
// If the pot itself is destroyed/sold, stop tracking its additives.
try { AdditiveMemory.Forget(__instance); } try { AdditiveMemory.Forget(__instance); }
catch { } catch { }
} }
@@ -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+df8a62295eea38ad16e750280eea878da48af3ae")]
[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 205b227cc6bf36a9df78289d110ec64054a771277aee03d496358bdc65fdfb7b
@@ -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.