Decompiled source of TimeOutLimit v1.0.1

plugins/TimeOutLimit.dll

Decompiled a day ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: AssemblyTitle("TimeOutLimit")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Radamanto")]
[assembly: AssemblyProduct("TimeOutLimit")]
[assembly: AssemblyCopyright("Copyright ©  2026 Radamanto")]
[assembly: AssemblyTrademark("")]
[assembly: ComVisible(false)]
[assembly: Guid("8DF044BD-3BA5-4E54-8C7D-9D4CF8D4314C")]
[assembly: AssemblyFileVersion("1.0.1")]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.0.1.0")]
[module: UnverifiableCode]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
}
namespace radamanto.TimeOutLimit
{
	[BepInPlugin("radamanto.TimeOutLimit", "TimeOutLimit", "1.0.1")]
	public class TimeOutLimitPlugin : BaseUnityPlugin
	{
		[HarmonyPatch(typeof(ZRpc), "SetLongTimeout", new Type[] { typeof(bool) })]
		private static class ZRpcSetLongTimeoutPatch
		{
			private static void Postfix()
			{
				ApplyZRpcTimeout();
				Log.LogDebug((object)$"ZRpc timeout set to {ZRpc.m_timeout:0}s");
			}
		}

		public const string ModGUID = "radamanto.TimeOutLimit";

		public const string ModName = "TimeOutLimit";

		public const string ModVersion = "1.0.1";

		private const float MinTimeout = 30f;

		private const float MaxTimeout = 600f;

		private readonly Harmony _harmony = new Harmony("radamanto.TimeOutLimit");

		internal static ManualLogSource Log = null;

		internal static ConfigEntry<float> Timeout = null;

		private const string WaitForQueueMarker = "waitForQueue";

		private const string VanillaDisconnectMessage = "Disconnecting {0} after 30 seconds config sending timeout";

		private const float VanillaConfigSyncTimeout = 30f;

		private static readonly MethodInfo TimeGetter = AccessTools.PropertyGetter(typeof(Time), "time");

		private static readonly MethodInfo CurrentTimeoutMethod = SymbolExtensions.GetMethodInfo((Expression<Action>)(() => CurrentTimeout()));

		private static readonly MethodInfo DisconnectMessageMethod = SymbolExtensions.GetMethodInfo((Expression<Action>)(() => DisconnectMessageFormat(string.Empty)));

		private static readonly HashSet<string> PatchedTimeouts = new HashSet<string>();

		private static readonly HashSet<string> PatchedMessages = new HashSet<string>();

		private const string JotunnGUID = "com.jotunn.jotunn";

		private static readonly Version JotunnMinVersion = new Version(2, 24, 0);

		private static FieldInfo? _jotunnTimeoutField;

		private static bool _jotunnResolved;

		public static float CurrentTimeout()
		{
			return Timeout.Value;
		}

		private void Awake()
		{
			//IL_003a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0044: Expected O, but got Unknown
			Log = ((BaseUnityPlugin)this).Logger;
			Timeout = ((BaseUnityPlugin)this).Config.Bind<float>("General", "Timeout", 120f, new ConfigDescription("Timeout in seconds for the vanilla ZRpc ping, ServerSync config sending and Jotunn custom RPCs. Vanilla is 30.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(30f, 600f), Array.Empty<object>()));
			Timeout.SettingChanged += delegate
			{
				ApplyRuntimeTimeouts();
			};
			_harmony.PatchAll();
			ApplyZRpcTimeout();
		}

		private void Start()
		{
			ApplyJotunnTimeout();
			PatchServerSyncTimeouts();
		}

		private static void ApplyRuntimeTimeouts()
		{
			ApplyZRpcTimeout();
			ApplyJotunnTimeout();
			Log.LogInfo((object)$"Timeout changed to {CurrentTimeout():0}s");
		}

		private static void ApplyZRpcTimeout()
		{
			ZRpc.m_timeout = CurrentTimeout();
		}

		public static string DisconnectMessageFormat(string source)
		{
			return $"[{source}] Disconnecting {{0}} after {CurrentTimeout():0} seconds config sending timeout";
		}

		private void PatchServerSyncTimeouts()
		{
			//IL_0072: Unknown result type (might be due to invalid IL or missing references)
			//IL_0077: Unknown result type (might be due to invalid IL or missing references)
			//IL_007f: Expected O, but got Unknown
			HashSet<Assembly> hashSet = new HashSet<Assembly>();
			hashSet.Add(typeof(TimeOutLimitPlugin).Assembly);
			HashSet<Assembly> hashSet2 = hashSet;
			HarmonyMethod val = new HarmonyMethod(SymbolExtensions.GetMethodInfo((Expression<Action>)(() => WaitForQueueTranspiler(null, null))))
			{
				priority = 0
			};
			int num = 0;
			List<string> list = new List<string>();
			foreach (PluginInfo value in Chainloader.PluginInfos.Values)
			{
				if ((Object)(object)((value != null) ? value.Instance : null) == (Object)null || value.Metadata.GUID == "com.jotunn.jotunn")
				{
					continue;
				}
				Assembly assembly = ((object)value.Instance).GetType().Assembly;
				if (!hashSet2.Add(assembly))
				{
					continue;
				}
				foreach (MethodInfo item in FindWaitForQueueMethods(assembly))
				{
					num++;
					if (!CanRewrite(item))
					{
						list.Add(value.Metadata.Name);
						continue;
					}
					try
					{
						_harmony.Patch((MethodBase)item, (HarmonyMethod)null, (HarmonyMethod)null, val, (HarmonyMethod)null, (HarmonyMethod)null);
					}
					catch (Exception ex)
					{
						list.Add(value.Metadata.Name);
						Log.LogWarning((object)("Failed to patch " + value.Metadata.Name + " [" + value.Metadata.GUID + "] " + item.DeclaringType?.FullName + ": " + ex.GetBaseException().Message));
						RemovePatch(item);
					}
				}
			}
			Log.LogInfo((object)$"ServerSync config sending timeout: {PatchedTimeouts.Count}/{num} methods patched to {CurrentTimeout():0}s, {PatchedMessages.Count} disconnect messages tagged");
			if (list.Count > 0)
			{
				Log.LogWarning((object)("ServerSync config sending timeout kept at 30s (IL cannot be rewritten, obfuscated): " + string.Join(", ", list.Distinct())));
			}
		}

		private static bool CanRewrite(MethodInfo method)
		{
			try
			{
				foreach (CodeInstruction originalInstruction in PatchProcessor.GetOriginalInstructions((MethodBase)method, (ILGenerator)null))
				{
					if (originalInstruction.operand == null && RequiresOperand(originalInstruction.opcode.OperandType))
					{
						return false;
					}
				}
				return true;
			}
			catch (Exception)
			{
				return false;
			}
		}

		private static bool RequiresOperand(OperandType type)
		{
			switch (type)
			{
			case OperandType.InlineField:
			case OperandType.InlineMethod:
			case OperandType.InlineSig:
			case OperandType.InlineString:
			case OperandType.InlineTok:
			case OperandType.InlineType:
				return true;
			default:
				return false;
			}
		}

		private void RemovePatch(MethodInfo method)
		{
			try
			{
				_harmony.Unpatch((MethodBase)method, (HarmonyPatchType)0, "radamanto.TimeOutLimit");
			}
			catch (Exception)
			{
			}
		}

		private static IEnumerable<MethodInfo> FindWaitForQueueMethods(Assembly assembly)
		{
			foreach (Type type in GetLoadableTypes(assembly))
			{
				MethodInfo moveNext = GetWaitForQueueMoveNext(type);
				if (moveNext != null)
				{
					yield return moveNext;
				}
			}
		}

		private static MethodInfo? GetWaitForQueueMoveNext(Type type)
		{
			try
			{
				if (!type.Name.Contains("waitForQueue") || !type.IsClass || !type.IsNested)
				{
					return null;
				}
				MethodInfo method = type.GetMethod("MoveNext", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null);
				return (method != null && method.ReturnType == typeof(bool)) ? method : null;
			}
			catch (Exception)
			{
				return null;
			}
		}

		private static IEnumerable<Type> GetLoadableTypes(Assembly assembly)
		{
			try
			{
				return assembly.GetTypes();
			}
			catch (ReflectionTypeLoadException ex)
			{
				return ex.Types.Where((Type t) => t != null);
			}
			catch (Exception)
			{
				return Array.Empty<Type>();
			}
		}

		private static IEnumerable<CodeInstruction> WaitForQueueTranspiler(IEnumerable<CodeInstruction> instructions, MethodBase original)
		{
			//IL_0102: Unknown result type (might be due to invalid IL or missing references)
			//IL_010c: Expected O, but got Unknown
			List<CodeInstruction> list = instructions.ToList();
			string text = original.DeclaringType?.Assembly.GetName().Name ?? "unknown";
			string item = text + ":" + original.DeclaringType?.FullName;
			int num = FindTimeoutConstant(list);
			if (num < 0)
			{
				Log.LogWarning((object)("[" + text + "] config sending timeout pattern not found, leaving it unchanged"));
				return list;
			}
			list[num].opcode = OpCodes.Call;
			list[num].operand = CurrentTimeoutMethod;
			PatchedTimeouts.Add(item);
			int num2 = list.FindIndex((CodeInstruction c) => c.opcode == OpCodes.Ldstr && c.operand is string text2 && text2 == "Disconnecting {0} after 30 seconds config sending timeout");
			if (num2 >= 0)
			{
				list[num2].operand = text;
				list.Insert(num2 + 1, new CodeInstruction(OpCodes.Call, (object)DisconnectMessageMethod));
				PatchedMessages.Add(item);
			}
			return list;
		}

		private static int FindTimeoutConstant(List<CodeInstruction> codes)
		{
			for (int i = 0; i + 2 < codes.Count; i++)
			{
				if (CodeInstructionExtensions.Calls(codes[i], TimeGetter) && codes[i + 1].opcode == OpCodes.Ldc_R4 && codes[i + 1].operand is float num && Mathf.Approximately(num, 30f) && codes[i + 2].opcode == OpCodes.Add)
				{
					return i + 1;
				}
			}
			return -1;
		}

		private static void ApplyJotunnTimeout()
		{
			FieldInfo fieldInfo = ResolveJotunnTimeoutField();
			if (!(fieldInfo == null))
			{
				fieldInfo.SetValue(null, CurrentTimeout());
			}
		}

		private static FieldInfo? ResolveJotunnTimeoutField()
		{
			if (_jotunnResolved)
			{
				return _jotunnTimeoutField;
			}
			_jotunnResolved = true;
			if (!Chainloader.PluginInfos.TryGetValue("com.jotunn.jotunn", out var value) || (Object)(object)((value != null) ? value.Instance : null) == (Object)null)
			{
				return null;
			}
			if (value.Metadata.Version < JotunnMinVersion)
			{
				Log.LogInfo((object)$"Skipping Jotunn {value.Metadata.Version}: custom RPC timeout requires {JotunnMinVersion} or newer");
				return null;
			}
			Type type = ((object)value.Instance).GetType().Assembly.GetType("Jotunn.Entities.CustomRPC");
			_jotunnTimeoutField = ((type == null) ? null : AccessTools.Field(type, "Timeout"));
			if (_jotunnTimeoutField == null || !_jotunnTimeoutField.IsStatic || _jotunnTimeoutField.FieldType != typeof(float))
			{
				_jotunnTimeoutField = null;
				Log.LogWarning((object)$"Jotunn {value.Metadata.Version}: CustomRPC.Timeout not found, leaving it unchanged");
				return null;
			}
			Log.LogInfo((object)$"Patching Jotunn {value.Metadata.Version} custom RPC timeout");
			return _jotunnTimeoutField;
		}
	}
}