Decompiled source of SeedRecoveryFix v1.3.3

BepInEx/plugins/SeedRecoveryFix-1.3.3/SeedRecoveryFix.dll

Decompiled 18 hours ago
using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading;
using BepInEx;
using BepInEx.Configuration;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyTitle("SeedRecoveryFix")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("SeedRecoveryFix")]
[assembly: AssemblyCopyright("Copyright ©  2026")]
[assembly: AssemblyTrademark("")]
[assembly: ComVisible(false)]
[assembly: Guid("2f6ad4af-9f56-43ec-a3e1-b72c72e310c5")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace SeedRecoveryFix;

[BepInPlugin("BloodDraco.seedrecoveryfix", "SeedRecoveryFix", "1.3.3")]
public class SeedRecoveryFixPlugin : BaseUnityPlugin
{
	public const string ModGuid = "BloodDraco.seedrecoveryfix";

	public const string ModName = "SeedRecoveryFix";

	public const string ModVersion = "1.3.3";

	private ConfigEntry<bool> OnlyHost;

	private ConfigEntry<int> WaitForPlayersTimeoutSeconds;

	private ConfigEntry<int> RetryCountBeforeOrbit;

	private ConfigEntry<int> RestartDelayMs;

	private ConfigEntry<bool> LogLastExceptionOnTimeout;

	private ConfigEntry<int> ExceptionRecencySeconds;

	private bool waitSeen;

	private float waitStartTime;

	private bool finishedSeen;

	private int recoveryAttempts;

	private bool chatTimerStartedSent;

	private string lastExceptionCondition;

	private string lastExceptionStack;

	private float lastExceptionTime;

	private void Awake()
	{
		//IL_00d3: Unknown result type (might be due to invalid IL or missing references)
		//IL_00dd: Expected O, but got Unknown
		OnlyHost = ((BaseUnityPlugin)this).Config.Bind<bool>("Watchdog", "OnlyHost", true, "Only host triggers recovery actions.");
		WaitForPlayersTimeoutSeconds = ((BaseUnityPlugin)this).Config.Bind<int>("Detection", "WaitForPlayersTimeoutSeconds", 120, "Seconds after 'Waiting for all players to load!' before recovery triggers.");
		RetryCountBeforeOrbit = ((BaseUnityPlugin)this).Config.Bind<int>("Recovery", "RetryCountBeforeOrbit", 1, "How many retries (abort+restart) before orbit fallback.");
		RestartDelayMs = ((BaseUnityPlugin)this).Config.Bind<int>("Recovery", "RestartDelayMs", 1500, "Delay between abort and restart in milliseconds.");
		LogLastExceptionOnTimeout = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "LogLastExceptionOnTimeout", true, "When a timeout triggers, print the most recent exception seen in logs (to LogOutput.log).");
		ExceptionRecencySeconds = ((BaseUnityPlugin)this).Config.Bind<int>("Debug", "ExceptionRecencySeconds", 45, "Only report the last exception if it occurred within this many seconds of the timeout.");
		Application.logMessageReceived += new LogCallback(OnUnityLog);
		((BaseUnityPlugin)this).Logger.LogInfo((object)"SeedRecoveryFix 1.3.3 loaded.");
	}

	private void OnDestroy()
	{
		//IL_0007: Unknown result type (might be due to invalid IL or missing references)
		//IL_0011: Expected O, but got Unknown
		Application.logMessageReceived -= new LogCallback(OnUnityLog);
	}

	private void Update()
	{
		if ((!OnlyHost.Value || IsHostBestEffort()) && waitSeen && !finishedSeen && Time.realtimeSinceStartup - waitStartTime >= (float)WaitForPlayersTimeoutSeconds.Value)
		{
			waitSeen = false;
			DoRecovery();
		}
	}

	private void OnUnityLog(string condition, string stackTrace, LogType type)
	{
		//IL_0009: Unknown result type (might be due to invalid IL or missing references)
		//IL_000b: Invalid comparison between Unknown and I4
		if (string.IsNullOrEmpty(condition))
		{
			return;
		}
		if ((int)type == 4 || condition.IndexOf("NullReferenceException", StringComparison.OrdinalIgnoreCase) >= 0 || condition.IndexOf("Exception:", StringComparison.OrdinalIgnoreCase) >= 0)
		{
			lastExceptionCondition = condition;
			lastExceptionStack = stackTrace;
			lastExceptionTime = Time.realtimeSinceStartup;
		}
		if (condition.IndexOf("Waiting for all players to load", StringComparison.OrdinalIgnoreCase) >= 0)
		{
			waitSeen = true;
			finishedSeen = false;
			waitStartTime = Time.realtimeSinceStartup;
			recoveryAttempts = 0;
			chatTimerStartedSent = false;
			if (!chatTimerStartedSent)
			{
				chatTimerStartedSent = true;
				ChatStatus("Timer started (watching load)");
			}
		}
		else if (condition.IndexOf("Players finished generating the new floor", StringComparison.OrdinalIgnoreCase) >= 0 || condition.IndexOf("Dungeon has finished generating", StringComparison.OrdinalIgnoreCase) >= 0)
		{
			finishedSeen = true;
			waitSeen = false;
			recoveryAttempts = 0;
			chatTimerStartedSent = false;
		}
	}

	private void DoRecovery()
	{
		recoveryAttempts++;
		if (LogLastExceptionOnTimeout.Value)
		{
			LogLastExceptionIfRecent();
		}
		if (recoveryAttempts <= Mathf.Max(0, RetryCountBeforeOrbit.Value))
		{
			ChatStatus("Retrying seed generation...");
			((BaseUnityPlugin)this).Logger.LogWarning((object)$"SeedRecoveryFix: STUCK LOAD → RETRY {recoveryAttempts}/{RetryCountBeforeOrbit.Value} (abort + restart).");
			if (!TryAbortAndRestartRound())
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)"SeedRecoveryFix: Could not abort+restart round (method not found).");
			}
		}
		else
		{
			ChatStatus("Going to orbit (recovery fallback)");
			((BaseUnityPlugin)this).Logger.LogWarning((object)"SeedRecoveryFix: STUCK LOAD → ORBIT FALLBACK (abort only, no restart).");
			if (!TryAbortRoundOnly())
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)"SeedRecoveryFix: Could not abort round (method not found).");
			}
		}
	}

	private void LogLastExceptionIfRecent()
	{
		if (string.IsNullOrEmpty(lastExceptionCondition))
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)"SeedRecoveryFix: No recent exceptions captured before timeout.");
			return;
		}
		float num = Time.realtimeSinceStartup - lastExceptionTime;
		if (num > (float)Mathf.Max(1, ExceptionRecencySeconds.Value))
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)$"SeedRecoveryFix: Last exception was {num:0.0}s ago (outside recency window).");
			return;
		}
		((BaseUnityPlugin)this).Logger.LogError((object)"SeedRecoveryFix: Most recent exception before timeout:");
		((BaseUnityPlugin)this).Logger.LogError((object)lastExceptionCondition);
		if (!string.IsNullOrEmpty(lastExceptionStack))
		{
			((BaseUnityPlugin)this).Logger.LogError((object)lastExceptionStack);
		}
	}

	private void ChatStatus(string msg)
	{
		((BaseUnityPlugin)this).Logger.LogInfo((object)("[ChatStatus] " + msg));
		try
		{
			Type type = FindType("HUDManager");
			if (type == null)
			{
				return;
			}
			object singletonFromType = GetSingletonFromType(type);
			if (singletonFromType == null)
			{
				return;
			}
			string[] obj = new string[3] { "AddTextToChatOnServer", "AddTextToChat", "AddChatMessage" };
			string text = "[SeedRecoveryFix] " + msg;
			string[] array = obj;
			foreach (string b in array)
			{
				MethodInfo[] methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				foreach (MethodInfo methodInfo in methods)
				{
					if (string.Equals(methodInfo.Name, b, StringComparison.Ordinal))
					{
						ParameterInfo[] parameters = methodInfo.GetParameters();
						if (parameters.Length == 1 && parameters[0].ParameterType == typeof(string))
						{
							methodInfo.Invoke(singletonFromType, new object[1] { text });
							return;
						}
					}
				}
			}
		}
		catch
		{
		}
	}

	private static object GetSingletonFromType(Type t)
	{
		PropertyInfo propertyInfo = t.GetProperty("Instance", BindingFlags.Static | BindingFlags.Public) ?? t.GetProperty("instance", BindingFlags.Static | BindingFlags.Public);
		if (propertyInfo != null)
		{
			return propertyInfo.GetValue(null, null);
		}
		FieldInfo fieldInfo = t.GetField("Instance", BindingFlags.Static | BindingFlags.Public) ?? t.GetField("instance", BindingFlags.Static | BindingFlags.Public);
		if (fieldInfo != null)
		{
			return fieldInfo.GetValue(null);
		}
		return null;
	}

	private bool TryAbortRoundOnly()
	{
		object singleton = GetSingleton("StartOfRound");
		if (singleton == null)
		{
			return false;
		}
		string[] names = new string[6] { "EndGameServerRpc", "EndGame", "ShipLeaveServerRpc", "ShipLeave", "ForceEndRoundServerRpc", "ForceEndRound" };
		return InvokeFirstParameterless(singleton, names);
	}

	private bool TryAbortAndRestartRound()
	{
		bool flag = TryAbortRoundOnly();
		if (RestartDelayMs.Value > 0)
		{
			Thread.Sleep(RestartDelayMs.Value);
		}
		object singleton = GetSingleton("StartOfRound");
		if (singleton == null)
		{
			return flag;
		}
		string[] names = new string[4] { "StartGameServerRpc", "StartGame", "BeginGameServerRpc", "BeginGame" };
		return InvokeFirstParameterless(singleton, names) || flag;
	}

	private static object GetSingleton(string typeName)
	{
		Type type = FindType(typeName);
		if (type == null)
		{
			return null;
		}
		PropertyInfo propertyInfo = type.GetProperty("Instance", BindingFlags.Static | BindingFlags.Public) ?? type.GetProperty("instance", BindingFlags.Static | BindingFlags.Public);
		if (propertyInfo != null)
		{
			return propertyInfo.GetValue(null, null);
		}
		FieldInfo fieldInfo = type.GetField("Instance", BindingFlags.Static | BindingFlags.Public) ?? type.GetField("instance", BindingFlags.Static | BindingFlags.Public);
		if (fieldInfo != null)
		{
			return fieldInfo.GetValue(null);
		}
		return null;
	}

	private static bool InvokeFirstParameterless(object instance, string[] names)
	{
		Type type = instance.GetType();
		foreach (string name in names)
		{
			try
			{
				MethodInfo method = type.GetMethod(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (method == null || method.GetParameters().Length != 0)
				{
					continue;
				}
				method.Invoke(instance, null);
				return true;
			}
			catch
			{
			}
		}
		return false;
	}

	private static bool IsHostBestEffort()
	{
		try
		{
			Type type = FindType("Unity.Netcode.NetworkManager");
			if (type == null)
			{
				return true;
			}
			object obj = type.GetProperty("Singleton", BindingFlags.Static | BindingFlags.Public)?.GetValue(null, null);
			if (obj == null)
			{
				return true;
			}
			PropertyInfo property = type.GetProperty("IsHost", BindingFlags.Instance | BindingFlags.Public);
			PropertyInfo property2 = type.GetProperty("IsServer", BindingFlags.Instance | BindingFlags.Public);
			bool num = property != null && (bool)property.GetValue(obj, null);
			bool flag = property2 != null && (bool)property2.GetValue(obj, null);
			return num || flag;
		}
		catch
		{
			return true;
		}
	}

	private static Type FindType(string typeName)
	{
		Type type = Type.GetType(typeName, throwOnError: false);
		if (type != null)
		{
			return type;
		}
		Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
		foreach (Assembly assembly in assemblies)
		{
			try
			{
				type = assembly.GetType(typeName, throwOnError: false);
				if (type != null)
				{
					return type;
				}
				Type[] types = assembly.GetTypes();
				foreach (Type type2 in types)
				{
					if (type2 != null && (type2.FullName == typeName || type2.Name == typeName))
					{
						return type2;
					}
				}
			}
			catch
			{
			}
		}
		return null;
	}
}