Decompiled source of SeedRecoveryFix v1.4.0

BepInEx/plugins/SeedRecoveryFix.dll

Decompiled 2 months ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Configuration;
using UnityEngine;
using UnityEngine.SceneManagement;

[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("daltoncurtis.seedrecoveryfix", "SeedRecoveryFix", "1.4.0")]
public class SeedRecoveryFixPlugin : BaseUnityPlugin
{
	[CompilerGenerated]
	private sealed class <RecoveryRoutine>d__23 : IEnumerator<object>, IDisposable, IEnumerator
	{
		private int <>1__state;

		private object <>2__current;

		public SeedRecoveryFixPlugin <>4__this;

		object IEnumerator<object>.Current
		{
			[DebuggerHidden]
			get
			{
				return <>2__current;
			}
		}

		object IEnumerator.Current
		{
			[DebuggerHidden]
			get
			{
				return <>2__current;
			}
		}

		[DebuggerHidden]
		public <RecoveryRoutine>d__23(int <>1__state)
		{
			this.<>1__state = <>1__state;
		}

		[DebuggerHidden]
		void IDisposable.Dispose()
		{
			<>1__state = -2;
		}

		private bool MoveNext()
		{
			//IL_00b8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c2: Expected O, but got Unknown
			int num = <>1__state;
			SeedRecoveryFixPlugin seedRecoveryFixPlugin = <>4__this;
			switch (num)
			{
			default:
				return false;
			case 0:
				<>1__state = -1;
				seedRecoveryFixPlugin.lastRecoveryAt = Time.realtimeSinceStartup;
				seedRecoveryFixPlugin.recoveryAttempts++;
				if (seedRecoveryFixPlugin.recoveryAttempts <= Mathf.Max(0, seedRecoveryFixPlugin.RetryCountBeforeOrbit.Value))
				{
					seedRecoveryFixPlugin.ChatStatus("Retrying seed generation...");
					((BaseUnityPlugin)seedRecoveryFixPlugin).Logger.LogWarning((object)$"SeedRecoveryFix: STUCK LOAD → RETRY {seedRecoveryFixPlugin.recoveryAttempts}/{seedRecoveryFixPlugin.RetryCountBeforeOrbit.Value}");
					seedRecoveryFixPlugin.TryAbortRoundOnly();
					float num2 = Mathf.Max(0f, seedRecoveryFixPlugin.RestartDelaySeconds.Value);
					if (num2 > 0f)
					{
						<>2__current = (object)new WaitForSecondsRealtime(num2);
						<>1__state = 1;
						return true;
					}
					goto IL_00d2;
				}
				seedRecoveryFixPlugin.ChatStatus("Going to orbit (recovery fallback)");
				((BaseUnityPlugin)seedRecoveryFixPlugin).Logger.LogWarning((object)"SeedRecoveryFix: STUCK LOAD → ORBIT FALLBACK");
				seedRecoveryFixPlugin.TryAbortRoundOnly();
				seedRecoveryFixPlugin.recoveryRoutine = null;
				return false;
			case 1:
				{
					<>1__state = -1;
					goto IL_00d2;
				}
				IL_00d2:
				seedRecoveryFixPlugin.TryRestartRoundOnly();
				seedRecoveryFixPlugin.recoveryRoutine = null;
				return false;
			}
		}

		bool IEnumerator.MoveNext()
		{
			//ILSpy generated this explicit interface implementation from .override directive in MoveNext
			return this.MoveNext();
		}

		[DebuggerHidden]
		void IEnumerator.Reset()
		{
			throw new NotSupportedException();
		}
	}

	public const string ModGuid = "daltoncurtis.seedrecoveryfix";

	public const string ModName = "SeedRecoveryFix";

	public const string ModVersion = "1.4.0";

	private ConfigEntry<bool> OnlyHost;

	private ConfigEntry<int> WaitForPlayersTimeoutSeconds;

	private ConfigEntry<int> ProgressGraceSeconds;

	private ConfigEntry<int> RetryCountBeforeOrbit;

	private ConfigEntry<float> RestartDelaySeconds;

	private ConfigEntry<float> CooldownSeconds;

	private ConfigEntry<string> DisarmScenesCsv;

	private bool watchdogArmed;

	private float armedAt;

	private float lastProgressAt;

	private int recoveryAttempts;

	private bool chatTimerStartedSent;

	private Coroutine recoveryRoutine;

	private float lastRecoveryAt;

	private void Awake()
	{
		//IL_00fc: Unknown result type (might be due to invalid IL or missing references)
		//IL_0106: Expected O, but got Unknown
		OnlyHost = ((BaseUnityPlugin)this).Config.Bind<bool>("Watchdog", "OnlyHost", true, "Only host triggers recovery.");
		WaitForPlayersTimeoutSeconds = ((BaseUnityPlugin)this).Config.Bind<int>("Detection", "WaitForPlayersTimeoutSeconds", 120, "Seconds after 'Waiting for all players to load!' before recovery can trigger.");
		ProgressGraceSeconds = ((BaseUnityPlugin)this).Config.Bind<int>("Detection", "ProgressGraceSeconds", 20, "If any load progress is detected within this window (seconds), do NOT trigger recovery yet.");
		RetryCountBeforeOrbit = ((BaseUnityPlugin)this).Config.Bind<int>("Recovery", "RetryCountBeforeOrbit", 1, "How many retries (abort+restart) before orbit fallback.");
		RestartDelaySeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Recovery", "RestartDelaySeconds", 1.5f, "Delay between abort and restart (seconds). Uses coroutine (safe).");
		CooldownSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Recovery", "CooldownSeconds", 25f, "Minimum time between recovery actions to avoid loops/spam.");
		DisarmScenesCsv = ((BaseUnityPlugin)this).Config.Bind<string>("Detection", "DisarmScenesCsv", "MainMenu,InitScene,CompanyBuilding,ShipBuildModeScene", "Comma-separated scene names that should always disarm the watchdog.");
		Application.logMessageReceived += new LogCallback(OnUnityLog);
		SceneManager.sceneLoaded += OnSceneLoaded;
		Disarm("startup");
		((BaseUnityPlugin)this).Logger.LogInfo((object)"SeedRecoveryFix 1.4.0 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);
		SceneManager.sceneLoaded -= OnSceneLoaded;
	}

	private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
	{
		lastProgressAt = Time.realtimeSinceStartup;
		if (IsDisarmSceneName(((Scene)(ref scene)).name) && watchdogArmed)
		{
			Disarm("scene loaded: " + ((Scene)(ref scene)).name);
		}
	}

	private void Update()
	{
		if (OnlyHost.Value && !IsHostBestEffort())
		{
			return;
		}
		if (IsDisarmSceneActive())
		{
			if (watchdogArmed)
			{
				Disarm("safe scene active");
			}
		}
		else if (!IsNetworkActiveBestEffort())
		{
			if (watchdogArmed)
			{
				Disarm("network inactive");
			}
		}
		else
		{
			if (!watchdogArmed)
			{
				return;
			}
			if (HasShipLandedBestEffort())
			{
				Disarm("ship landed");
			}
			else if (recoveryRoutine == null)
			{
				float realtimeSinceStartup = Time.realtimeSinceStartup;
				if (!(realtimeSinceStartup - lastRecoveryAt < Mathf.Max(0.1f, CooldownSeconds.Value)) && !(realtimeSinceStartup - armedAt < (float)WaitForPlayersTimeoutSeconds.Value) && !(realtimeSinceStartup - lastProgressAt < (float)Mathf.Max(0, ProgressGraceSeconds.Value)))
				{
					watchdogArmed = false;
					recoveryRoutine = ((MonoBehaviour)this).StartCoroutine(RecoveryRoutine());
				}
			}
		}
	}

	private void OnUnityLog(string condition, string stackTrace, LogType type)
	{
		if (string.IsNullOrEmpty(condition))
		{
			return;
		}
		if (condition.IndexOf("Waiting for all players to load", StringComparison.OrdinalIgnoreCase) >= 0)
		{
			watchdogArmed = true;
			armedAt = Time.realtimeSinceStartup;
			lastProgressAt = armedAt;
			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)
		{
			Disarm("generation finished");
		}
		else if (IsProgressLog(condition))
		{
			lastProgressAt = Time.realtimeSinceStartup;
		}
	}

	private bool IsProgressLog(string s)
	{
		string[] array = new string[15]
		{
			"Loading scene", "Scene that began loading", "LOADING GAME", "Getting Random DungeonFlows", "Setting Random DungeonFlows", "ExtendedLevel <-> ExtendedDungeonFlow", "Now listening to dungeon generator status", "Dungeon has finished generating", "Players finished generating", "Spawning synced props on server",
			"Created New Day History Log", "GeneratedFloorPostProcessing", "Fire Exit Patch Report", "Initialized EnemyManager", "Initialized ScrapItemManager"
		};
		for (int i = 0; i < array.Length; i++)
		{
			if (s.IndexOf(array[i], StringComparison.OrdinalIgnoreCase) >= 0)
			{
				return true;
			}
		}
		return false;
	}

	[IteratorStateMachine(typeof(<RecoveryRoutine>d__23))]
	private IEnumerator RecoveryRoutine()
	{
		//yield-return decompiler failed: Unexpected instruction in Iterator.Dispose()
		return new <RecoveryRoutine>d__23(0)
		{
			<>4__this = this
		};
	}

	private void Disarm(string reason)
	{
		watchdogArmed = false;
		armedAt = 0f;
		lastProgressAt = 0f;
		recoveryAttempts = 0;
		chatTimerStartedSent = false;
		if (recoveryRoutine != null)
		{
			((MonoBehaviour)this).StopCoroutine(recoveryRoutine);
			recoveryRoutine = null;
		}
		((BaseUnityPlugin)this).Logger.LogInfo((object)("SeedRecoveryFix: watchdog disarmed (" + reason + ")."));
	}

	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 text = "[SeedRecoveryFix] " + msg;
			string[] array = new string[3] { "AddTextToChatOnServer", "AddTextToChat", "AddChatMessage" };
			for (int i = 0; i < array.Length; i++)
			{
				MethodInfo method = type.GetMethod(array[i], BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (!(method == null))
				{
					ParameterInfo[] parameters = method.GetParameters();
					if (parameters.Length == 1 && parameters[0].ParameterType == typeof(string))
					{
						method.Invoke(singletonFromType, new object[1] { text });
						break;
					}
				}
			}
		}
		catch
		{
		}
	}

	private bool IsDisarmSceneActive()
	{
		//IL_0000: Unknown result type (might be due to invalid IL or missing references)
		//IL_0005: Unknown result type (might be due to invalid IL or missing references)
		try
		{
			Scene activeScene = SceneManager.GetActiveScene();
			string sceneName = ((Scene)(ref activeScene)).name ?? "";
			return IsDisarmSceneName(sceneName);
		}
		catch
		{
		}
		return false;
	}

	private bool IsDisarmSceneName(string sceneName)
	{
		if (string.IsNullOrEmpty(sceneName))
		{
			return false;
		}
		try
		{
			string text = ((DisarmScenesCsv != null) ? (DisarmScenesCsv.Value ?? "") : "");
			if (string.IsNullOrEmpty(text))
			{
				return false;
			}
			string[] array = text.Split(new char[1] { ',' }, StringSplitOptions.RemoveEmptyEntries);
			for (int i = 0; i < array.Length; i++)
			{
				string text2 = array[i].Trim();
				if (text2.Length != 0 && string.Equals(sceneName, text2, StringComparison.OrdinalIgnoreCase))
				{
					return true;
				}
			}
		}
		catch
		{
		}
		return false;
	}

	private static bool IsNetworkActiveBestEffort()
	{
		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 false;
			}
			PropertyInfo property = type.GetProperty("IsClient", 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 bool HasShipLandedBestEffort()
	{
		try
		{
			object singleton = GetSingleton("StartOfRound");
			if (singleton == null)
			{
				return false;
			}
			Type type = singleton.GetType();
			FieldInfo field = type.GetField("shipHasLanded", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			if (field != null && field.FieldType == typeof(bool))
			{
				return (bool)field.GetValue(singleton);
			}
			PropertyInfo property = type.GetProperty("shipHasLanded", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			if (property != null && property.PropertyType == typeof(bool))
			{
				return (bool)property.GetValue(singleton, null);
			}
		}
		catch
		{
		}
		return false;
	}

	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 TryRestartRoundOnly()
	{
		object singleton = GetSingleton("StartOfRound");
		if (singleton == null)
		{
			return false;
		}
		string[] names = new string[4] { "StartGameServerRpc", "StartGame", "BeginGameServerRpc", "BeginGame" };
		return InvokeFirstParameterless(singleton, names);
	}

	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 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 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 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;
				}
			}
			catch
			{
			}
		}
		return null;
	}
}