Compare commits

..

8 Commits

11 changed files with 206 additions and 68 deletions
+1
View File
@@ -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>
+173 -66
View File
@@ -4,33 +4,38 @@ 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.1", "AttilaG")]
[assembly: MelonInfo(typeof(AbsorbentSoil.AbsorbentSoilMod), "Absorbent Soil", "0.1.0", "AttilaG")]
[assembly: MelonGame(null, "Schedule I")]
namespace AbsorbentSoil
{
public sealed class AbsorbentSoilMod : MelonMod
{
internal static bool VerboseLogging = false;
internal static bool VerboseLogging = true;
public override void OnInitializeMelon()
{
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();
AdditiveMemory.Clear();
MelonLogger.Msg($"Cleared additive memory on scene load: {sceneName}");
}
}
@@ -73,27 +78,28 @@ namespace AbsorbentSoil
: 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;
Forget(PotKeyHelper.GetPotKey(pot));
string key = PotKeyHelper.GetPotKey(pot);
if (!string.IsNullOrWhiteSpace(key))
AdditivesByPotKey.Remove(key);
}
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();
}
public static void Clear() => AdditivesByPotKey.Clear();
}
internal static class PotKeyHelper
@@ -103,10 +109,13 @@ namespace AbsorbentSoil
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()}";
}
@@ -116,21 +125,17 @@ namespace AbsorbentSoil
return null;
Type type = instance.GetType();
while (type != null)
PropertyInfo prop = AccessTools.Property(type, name);
if (prop != null)
{
PropertyInfo prop = AccessTools.Property(type, name);
if (prop != null)
{
try { return prop.GetValue(instance); } catch { }
}
try { return prop.GetValue(instance); } catch { }
}
FieldInfo field = AccessTools.Field(type, name);
if (field != null)
{
try { return field.GetValue(instance); } catch { }
}
type = type.BaseType;
FieldInfo field = AccessTools.Field(type, name);
if (field != null)
{
try { return field.GetValue(instance); } catch { }
}
return null;
@@ -139,31 +144,66 @@ namespace AbsorbentSoil
internal static class SoilHelper
{
private static readonly FieldInfo RemainingSoilUsesField =
AccessTools.Field(typeof(GrowContainer), "_remainingSoilUses");
private static readonly Dictionary<string, int> RemainingUsesByPotKey = new();
public static bool IsRetainingSoil(Pot pot)
public static int DecrementRemainingSoilUses(Pot pot)
{
if (pot == null || string.IsNullOrWhiteSpace(pot.SoilID))
return false;
if (pot == null)
return 0;
string soilId = pot.SoilID.ToLowerInvariant();
string key = PotKeyHelper.GetPotKey(pot);
if (string.IsNullOrWhiteSpace(key))
return 0;
return soilId.Contains("longlifesoil") ||
soilId.Contains("extralonglifesoil");
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 || RemainingSoilUsesField == null)
if (pot == null)
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
{
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)
{
@@ -183,7 +222,7 @@ namespace AbsorbentSoil
if (__instance == null || __result == null || string.IsNullOrWhiteSpace(additiveID))
return;
if (!SoilHelper.IsRetainingSoil(__instance))
if (!SoilHelper.CanCaptureNewAdditives(__instance))
return;
AdditiveMemory.Remember(__instance, additiveID);
@@ -199,7 +238,8 @@ namespace AbsorbentSoil
if (pot == null || string.IsNullOrWhiteSpace(additiveID))
return;
if (ApplyAdditiveMethod == null)
MethodInfo applyAdditive = AccessTools.Method(typeof(Pot), "ApplyAdditive");
if (applyAdditive == null)
{
MelonLogger.Warning("Could not find Pot.ApplyAdditive via reflection.");
return;
@@ -208,7 +248,7 @@ namespace AbsorbentSoil
try
{
_suppressCapture = true;
ApplyAdditiveMethod.Invoke(pot, new object[] { additiveID, true });
applyAdditive.Invoke(pot, new object[] { additiveID, true });
}
catch (Exception ex)
{
@@ -228,25 +268,18 @@ namespace AbsorbentSoil
{
try
{
MelonLogger.Msg($"Plant.Initialize fired. Plant={__instance?.name}, Pot={__instance?.Pot?.name}");
if (__instance == null || __instance.Pot == null)
return;
Pot pot = __instance.Pot;
Pot actualPot = __instance.Pot;
var additiveIds = AdditiveMemory.Get(actualPot);
if (!SoilHelper.CanRetainAdditives(pot))
{
AdditiveMemory.Forget(pot);
return;
}
IReadOnlyList<string> additiveIds = AdditiveMemory.Get(pot);
if (additiveIds.Count == 0)
return;
MelonLogger.Msg($"Plant.Initialize pot key={PotKeyHelper.GetPotKey(actualPot)}, remembered additives={additiveIds.Count}");
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)}'.");
Pot_ApplyAdditive_Patch.ReapplyWithoutRecapture(actualPot, additiveId);
}
catch (Exception ex)
{
@@ -260,13 +293,86 @@ namespace AbsorbentSoil
{
private static void Postfix(Pot __instance)
{
if (__instance == null)
return;
if (!SoilHelper.CanRetainAdditives(__instance))
try
{
AdditiveMemory.Forget(__instance);
MelonLogger.Msg("[Absorbent Soil] Cleared retained additives because soil has no retaining uses left.");
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}");
}
}
}
@@ -276,6 +382,7 @@ namespace AbsorbentSoil
{
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,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+df8a62295eea38ad16e750280eea878da48af3ae")]
[assembly: System.Reflection.AssemblyProductAttribute("AbsorbentSoil")]
[assembly: System.Reflection.AssemblyTitleAttribute("AbsorbentSoil")]
[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.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.