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;
}
}