Decompiled source of LethalPhonesVariableCallTimeout v1.0.0

plugins/LethalPhonesCallTimeout/LethalPhones.CallTimeout.dll

Decompiled a day ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using BepInEx;
using BepInEx.Configuration;
using HarmonyLib;
using UnityEngine;

[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: AssemblyDescription("Configurable fixed or receiving-ringtone-based unanswered call timeouts.")]
[assembly: CompilationRelaxations(8)]
[assembly: AssemblyTitle("LethalPhones Call Timeout")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace LethalPhonesCallTimeout;

internal static class CallTimeoutPatches
{
	internal static Func<TimeoutSettings> SettingsProvider = () => new TimeoutSettings();

	internal static Action<string> Warning = delegate
	{
	};

	private static bool lookupWarningLogged;

	internal static void Install(Harmony patcher)
	{
		//IL_0075: Unknown result type (might be due to invalid IL or missing references)
		//IL_0081: Expected O, but got Unknown
		//IL_0103: Unknown result type (might be due to invalid IL or missing references)
		//IL_0111: Expected O, but got Unknown
		string[] array = new string[2] { "Scoops.misc.PlayerPhone", "Scoops.gameobjects.SwitchboardPhone" };
		foreach (string typeName in array)
		{
			MethodInfo methodInfo = IteratorMoveNext(typeName, "CallTimeoutCoroutine", typeof(short));
			RequireField(methodInfo.DeclaringType, "<>4__this");
			RequireField(methodInfo.DeclaringType, "number");
			patcher.Patch((MethodBase)methodInfo, (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(CallTimeoutPatches), "TimeoutTranspiler", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null);
		}
		string[] array2 = new string[2] { "Scoops.misc.PlayerPhone", "Scoops.misc.PhoneBehavior" };
		foreach (string typeName2 in array2)
		{
			MethodInfo methodInfo2 = IteratorMoveNext(typeName2, "PhoneRingCoroutine", typeof(int));
			RequireField(methodInfo2.DeclaringType, "<>4__this");
			RequireField(methodInfo2.DeclaringType, "repeats");
			patcher.Patch((MethodBase)methodInfo2, new HarmonyMethod(typeof(CallTimeoutPatches), "NoisePrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
		}
	}

	internal static float ResolveOutgoingTimeout(object caller, short number)
	{
		TimeoutSettings timeoutSettings = SettingsProvider();
		float? ringtoneLength = null;
		if (timeoutSettings.Mode == TimeoutMode.RingtoneLength)
		{
			try
			{
				ringtoneLength = LethalPhonesRuntime.OutgoingRingtoneLength(number);
			}
			catch (Exception error)
			{
				WarnAboutFallback(error);
			}
		}
		return TimeoutPolicy.Resolve(timeoutSettings, ringtoneLength);
	}

	private static IEnumerable<CodeInstruction> TimeoutTranspiler(IEnumerable<CodeInstruction> instructions, MethodBase __originalMethod)
	{
		//IL_00e1: Unknown result type (might be due to invalid IL or missing references)
		//IL_00e8: Expected O, but got Unknown
		//IL_0129: Unknown result type (might be due to invalid IL or missing references)
		//IL_0133: Expected O, but got Unknown
		//IL_013a: Unknown result type (might be due to invalid IL or missing references)
		//IL_0144: Expected O, but got Unknown
		//IL_014c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0156: Expected O, but got Unknown
		//IL_0172: Unknown result type (might be due to invalid IL or missing references)
		//IL_017c: Expected O, but got Unknown
		List<CodeInstruction> list = new List<CodeInstruction>(instructions);
		List<CodeInstruction> list2 = new List<CodeInstruction>();
		Type declaringType = __originalMethod.DeclaringType;
		FieldInfo fieldInfo = RequireField(declaringType, "<>4__this");
		FieldInfo fieldInfo2 = RequireField(declaringType, "number");
		int num = 0;
		for (int i = 0; i < list.Count; i++)
		{
			ConstructorInfo constructorInfo = ((i + 1 < list.Count) ? (list[i + 1].operand as ConstructorInfo) : null);
			if (list[i].opcode == OpCodes.Ldc_R4 && (float)list[i].operand == 15f && list[i + 1].opcode == OpCodes.Newobj && constructorInfo != null && constructorInfo.DeclaringType == typeof(WaitForSeconds))
			{
				CodeInstruction val = new CodeInstruction(OpCodes.Ldarg_0, (object)null);
				val.labels.AddRange(list[i].labels);
				val.blocks.AddRange(list[i].blocks);
				list2.Add(val);
				list2.Add(new CodeInstruction(OpCodes.Ldfld, (object)fieldInfo));
				list2.Add(new CodeInstruction(OpCodes.Ldarg_0, (object)null));
				list2.Add(new CodeInstruction(OpCodes.Ldfld, (object)fieldInfo2));
				list2.Add(new CodeInstruction(OpCodes.Call, (object)AccessTools.Method(typeof(CallTimeoutPatches), "ResolveOutgoingTimeout", (Type[])null, (Type[])null)));
				num++;
			}
			else
			{
				list2.Add(list[i]);
			}
		}
		if (num != 1)
		{
			throw new InvalidOperationException("Expected exactly one 15-second call timer; found " + num);
		}
		return list2;
	}

	private static void NoisePrefix(object __instance)
	{
		Type type = __instance.GetType();
		object value = RequireField(type, "<>4__this").GetValue(__instance);
		if (value == null)
		{
			return;
		}
		string fullName = value.GetType().FullName;
		if (fullName != "Scoops.misc.PlayerPhone" && fullName != "Scoops.gameobjects.SwitchboardPhone")
		{
			return;
		}
		TimeoutSettings timeoutSettings = SettingsProvider();
		float? ringtoneLength = null;
		if (timeoutSettings.Mode == TimeoutMode.RingtoneLength)
		{
			try
			{
				ringtoneLength = LethalPhonesRuntime.RingtoneLength(value);
			}
			catch (Exception error)
			{
				WarnAboutFallback(error);
			}
		}
		float num = TimeoutPolicy.Resolve(timeoutSettings, ringtoneLength);
		FieldInfo fieldInfo = RequireField(type, "repeats");
		fieldInfo.SetValue(__instance, Math.Max((int)fieldInfo.GetValue(__instance), (int)Math.Ceiling(num / 4f)));
	}

	private static MethodInfo IteratorMoveNext(string typeName, string method, Type parameter)
	{
		Type type = AccessTools.TypeByName(typeName);
		MethodInfo methodInfo = ((type == null) ? null : AccessTools.DeclaredMethod(type, method, new Type[1] { parameter }, (Type[])null));
		if (methodInfo == null || methodInfo.ReturnType != typeof(IEnumerator))
		{
			throw new MissingMethodException(typeName, method);
		}
		MethodInfo methodInfo2 = AccessTools.EnumeratorMoveNext((MethodBase)methodInfo);
		if (methodInfo2 == null)
		{
			throw new MissingMethodException(typeName, method + ".MoveNext");
		}
		return methodInfo2;
	}

	private static void WarnAboutFallback(Exception error)
	{
		if (!lookupWarningLogged)
		{
			lookupWarningLogged = true;
			Warning("Ringtone lookup failed; using FixedSeconds as fallback. Further identical warnings are suppressed. " + error.Message);
		}
	}

	private static FieldInfo RequireField(Type type, string name)
	{
		FieldInfo fieldInfo = AccessTools.Field(type, name);
		if (fieldInfo == null)
		{
			throw new MissingFieldException(type.FullName, name);
		}
		return fieldInfo;
	}
}
internal static class LethalPhonesRuntime
{
	private static FieldInfo phones;

	private static Type behaviorType;

	private static FieldInfo number;

	private static FieldInfo ringtoneId;

	private static FieldInfo ringtones;

	private static FieldInfo switchboardRingtone;

	private static Type switchboardType;

	private static string defaultRingtone;

	internal static void Initialize()
	{
		Type type = (behaviorType = RequireType("Scoops.misc.PhoneBehavior"));
		phones = RequireField(RequireType("Scoops.service.PhoneNetworkHandler"), "allPhoneBehaviors");
		number = RequireField(type, "phoneNumber");
		ringtoneId = RequireField(type, "phoneRingtoneId");
		Type type2 = RequireType("Scoops.customization.CustomizationManager");
		ringtones = RequireField(type2, "ringtoneCustomizations");
		defaultRingtone = (string)RequireField(type2, "DEFAULT_RINGTONE").GetValue(null);
		switchboardType = RequireType("Scoops.gameobjects.SwitchboardPhone");
		switchboardRingtone = RequireField(RequireType("Scoops.service.PhoneAssetManager"), "switchboardRing");
	}

	internal static float? OutgoingRingtoneLength(short targetNumber)
	{
		IEnumerable all = phones.GetValue(null) as IEnumerable;
		float? result = FindLength(all, targetNumber);
		if (result.HasValue)
		{
			return result;
		}
		if (typeof(Object).IsAssignableFrom(behaviorType))
		{
			return FindLength(Object.FindObjectsOfType(behaviorType), targetNumber);
		}
		return null;
	}

	private static float? FindLength(IEnumerable all, short targetNumber)
	{
		if (all == null)
		{
			return null;
		}
		foreach (object item in all)
		{
			if (!IsMissing(item) && (short)number.GetValue(item) == targetNumber)
			{
				return RingtoneLength(item);
			}
		}
		return null;
	}

	internal static float? RingtoneLength(object phone)
	{
		if (IsMissing(phone))
		{
			return null;
		}
		if (switchboardType.IsInstanceOfType(phone))
		{
			return ClipLength(switchboardRingtone.GetValue(null));
		}
		if (!(ringtones.GetValue(null) is IDictionary dictionary))
		{
			return null;
		}
		object value = ((ringtoneId.GetValue(phone) is string key && dictionary.Contains(key)) ? dictionary[key] : null);
		if (IsMissing(value) && defaultRingtone != null && dictionary.Contains(defaultRingtone))
		{
			value = dictionary[defaultRingtone];
		}
		return ClipLength(value);
	}

	private static float? ClipLength(object value)
	{
		AudioClip val = (AudioClip)((value is AudioClip) ? value : null);
		if ((Object)(object)val == (Object)null)
		{
			return null;
		}
		float length = val.length;
		if (!TimeoutPolicy.ValidPositive(length))
		{
			return null;
		}
		return length;
	}

	private static bool IsMissing(object value)
	{
		if (value == null)
		{
			return true;
		}
		Object val = (Object)((value is Object) ? value : null);
		if (!object.ReferenceEquals(val, null))
		{
			return val == (Object)null;
		}
		return false;
	}

	private static Type RequireType(string name)
	{
		Type type = AccessTools.TypeByName(name);
		if (type == null)
		{
			throw new TypeLoadException(name);
		}
		return type;
	}

	private static FieldInfo RequireField(Type type, string name)
	{
		FieldInfo fieldInfo = AccessTools.Field(type, name);
		if (fieldInfo == null)
		{
			throw new MissingFieldException(type.FullName, name);
		}
		return fieldInfo;
	}
}
[BepInIncompatibility("private.phones.calltimeout")]
[BepInPlugin("lethalphones.calltimeout", "LethalPhones Call Timeout", "1.0.0")]
[BepInDependency(/*Could not decode attribute arguments.*/)]
public sealed class Plugin : BaseUnityPlugin
{
	public const string Id = "lethalphones.calltimeout";

	private ConfigEntry<TimeoutMode> mode;

	private ConfigEntry<float> fixedSeconds;

	private ConfigEntry<float> maximumRingtoneSeconds;

	private ConfigEntry<float> ringtoneGraceSeconds;

	private Harmony harmony;

	private void Awake()
	{
		//IL_0074: Unknown result type (might be due to invalid IL or missing references)
		//IL_007e: Expected O, but got Unknown
		//IL_00b3: Unknown result type (might be due to invalid IL or missing references)
		//IL_00bd: Expected O, but got Unknown
		//IL_00f2: Unknown result type (might be due to invalid IL or missing references)
		//IL_00fc: Expected O, but got Unknown
		//IL_011b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0125: Expected O, but got Unknown
		bool value = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Enable the unanswered-call timer patches.").Value;
		mode = ((BaseUnityPlugin)this).Config.Bind<TimeoutMode>("Calls", "Mode", TimeoutMode.Fixed, "Fixed: use FixedSeconds. RingtoneLength: use the called phone's ringtone length.");
		fixedSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Calls", "FixedSeconds", 45f, new ConfigDescription("Fixed timeout and fallback when a ringtone cannot be resolved (seconds).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 3600f), new object[0]));
		maximumRingtoneSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Calls", "MaximumRingtoneSeconds", 300f, new ConfigDescription("Upper limit for ringtone-based timeouts (seconds).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 3600f), new object[0]));
		ringtoneGraceSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Calls", "RingtoneGraceSeconds", 0f, new ConfigDescription("Optional extra seconds after the ringtone length to allow for network delay.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 10f), new object[0]));
		if (!value)
		{
			((BaseUnityPlugin)this).Logger.LogInfo((object)"Call timeout patches disabled by configuration.");
			return;
		}
		harmony = new Harmony("lethalphones.calltimeout");
		try
		{
			CallTimeoutPatches.SettingsProvider = ReadSettings;
			CallTimeoutPatches.Warning = delegate(string message)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)message);
			};
			LethalPhonesRuntime.Initialize();
			CallTimeoutPatches.Install(harmony);
			((BaseUnityPlugin)this).Logger.LogInfo((object)string.Concat("Call timeout patches active. Mode=", mode.Value, ", fixed/fallback=", fixedSeconds.Value, "s."));
		}
		catch (Exception ex)
		{
			harmony.UnpatchSelf();
			((BaseUnityPlugin)this).Logger.LogError((object)("Call timeout patches disabled because the expected LethalPhones layout was not found: " + ex));
		}
	}

	private TimeoutSettings ReadSettings()
	{
		TimeoutSettings timeoutSettings = new TimeoutSettings();
		timeoutSettings.Mode = mode.Value;
		timeoutSettings.FixedSeconds = fixedSeconds.Value;
		timeoutSettings.MaximumRingtoneSeconds = maximumRingtoneSeconds.Value;
		timeoutSettings.RingtoneGraceSeconds = ringtoneGraceSeconds.Value;
		return timeoutSettings;
	}

	private void OnDestroy()
	{
		if (harmony != null)
		{
			harmony.UnpatchSelf();
		}
	}
}
internal enum TimeoutMode
{
	Fixed,
	RingtoneLength
}
internal sealed class TimeoutSettings
{
	internal TimeoutMode Mode;

	internal float FixedSeconds = 45f;

	internal float MaximumRingtoneSeconds = 300f;

	internal float RingtoneGraceSeconds;
}
internal static class TimeoutPolicy
{
	internal const float MinimumSeconds = 0.1f;

	internal const float MaximumSeconds = 3600f;

	internal static bool ValidPositive(float value)
	{
		if (!float.IsNaN(value) && !float.IsInfinity(value))
		{
			return value > 0f;
		}
		return false;
	}

	internal static float Resolve(TimeoutSettings settings, float? ringtoneLength)
	{
		if (settings == null)
		{
			settings = new TimeoutSettings();
		}
		float result = Sanitize(settings.FixedSeconds, 45f);
		if (settings.Mode != TimeoutMode.RingtoneLength || !ringtoneLength.HasValue || !ValidPositive(ringtoneLength.Value))
		{
			return result;
		}
		float val = Sanitize(settings.MaximumRingtoneSeconds, 300f);
		float num = settings.RingtoneGraceSeconds;
		if (float.IsNaN(num) || float.IsInfinity(num) || num < 0f)
		{
			num = 0f;
		}
		num = Math.Min(num, 10f);
		return Math.Max(0.1f, Math.Min(val, ringtoneLength.Value + num));
	}

	private static float Sanitize(float value, float fallback)
	{
		if (!ValidPositive(value))
		{
			value = fallback;
		}
		return Math.Max(0.1f, Math.Min(3600f, value));
	}
}