Decompiled source of ReviveAlliesRevived v0.1.2

ReviveAllies.dll

Decompiled 19 hours ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using ReviveAllies.Components;
using ReviveAllies.Components.States;
using ReviveAllies.E2E;
using Steamworks;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: IgnoresAccessChecksTo("assembly_guiutils")]
[assembly: IgnoresAccessChecksTo("assembly_utils")]
[assembly: IgnoresAccessChecksTo("assembly_valheim")]
[assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
[assembly: AssemblyCompany("ReviveAllies")]
[assembly: AssemblyConfiguration("Debug")]
[assembly: AssemblyFileVersion("0.3.4.0")]
[assembly: AssemblyInformationalVersion("0.3.4")]
[assembly: AssemblyProduct("ReviveAllies")]
[assembly: AssemblyTitle("ReviveAllies")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.3.4.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
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;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace ReviveAllies
{
	public enum ReviveModeType
	{
		Hold,
		Press
	}
	[BepInPlugin("com.andres.reviveallies", "ReviveAllies", "0.3.4-1.0-revived")]
	public class Plugin : BaseUnityPlugin
	{
		public const string PluginGuid = "com.andres.reviveallies";

		public const string PluginName = "ReviveAllies";

		public const string PluginVersion = "0.3.4-1.0-revived";

		internal static ConfigEntry<ReviveModeType> ReviveModeCfg = null;

		internal static ConfigEntry<float> ReviveHoldTimeCfg = null;

		internal static ConfigEntry<float> ReviveWindowCfg = null;

		internal const string RpcConfig = "ReviveAllies_Config";

		private static bool s_hasServerConfig;

		private static float s_srvWindow;

		private static float s_srvHoldTime;

		private static bool s_srvPressMode;

		private static readonly float s_windowEnvOverride = ReadWindowOverride();

		public const float ReviveHealthFraction = 0.25f;

		private Harmony? _harmony;

		internal static ManualLogSource Logger { get; private set; } = null;

		private static bool UseServerConfig => s_hasServerConfig && (Object)(object)ZNet.instance != (Object)null && !ZNet.instance.IsServer();

		public static float ReviveWindow
		{
			get
			{
				if (s_windowEnvOverride > 0f)
				{
					return s_windowEnvOverride;
				}
				return UseServerConfig ? s_srvWindow : (ReviveWindowCfg?.Value ?? 30f);
			}
		}

		public static float ReviveDuration => Mathf.Max(0.1f, UseServerConfig ? s_srvHoldTime : (ReviveHoldTimeCfg?.Value ?? 4f));

		public static bool RevivePressMode => UseServerConfig ? s_srvPressMode : (ReviveModeCfg != null && ReviveModeCfg.Value == ReviveModeType.Press);

		private static float ReadWindowOverride()
		{
			string environmentVariable = Environment.GetEnvironmentVariable("RR_E2E_WINDOW");
			float result;
			return (float.TryParse(environmentVariable, out result) && result > 0f) ? result : 0f;
		}

		internal static void ApplyServerConfig(float window, float holdTime, bool pressMode)
		{
			s_srvWindow = window;
			s_srvHoldTime = holdTime;
			s_srvPressMode = pressMode;
			s_hasServerConfig = true;
			Logger.LogInfo((object)$"Adopted server revive config: window={window:F0}s hold={holdTime:F1}s press={pressMode}");
		}

		internal static void ResetServerConfig()
		{
			s_hasServerConfig = false;
		}

		internal static void BroadcastConfig()
		{
			if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer() && ZRoutedRpc.instance != null)
			{
				ZRoutedRpc instance = ZRoutedRpc.instance;
				long num = 0L;
				object[] obj = new object[3]
				{
					ReviveWindowCfg?.Value ?? 30f,
					Mathf.Max(0.1f, ReviveHoldTimeCfg?.Value ?? 4f),
					null
				};
				ConfigEntry<ReviveModeType> reviveModeCfg = ReviveModeCfg;
				obj[2] = ((reviveModeCfg != null && reviveModeCfg.Value == ReviveModeType.Press) ? 1 : 0);
				instance.InvokeRoutedRPC(num, "ReviveAllies_Config", obj);
			}
		}

		private void Awake()
		{
			//IL_006b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0075: Expected O, but got Unknown
			//IL_00a8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b2: Expected O, but got Unknown
			Logger = ((BaseUnityPlugin)this).Logger;
			Logger.LogInfo((object)"ReviveAllies v0.3.4-1.0-revived loaded!");
			ReviveModeCfg = ((BaseUnityPlugin)this).Config.Bind<ReviveModeType>("Revive", "Mode", ReviveModeType.Hold, "Hold: the interact key must be held for HoldTimeSeconds to revive. Press: a single press revives instantly.");
			ReviveHoldTimeCfg = ((BaseUnityPlugin)this).Config.Bind<float>("Revive", "HoldTimeSeconds", 4f, new ConfigDescription("How long the interact key must be held to complete a revive (Hold mode).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 60f), Array.Empty<object>()));
			ReviveWindowCfg = ((BaseUnityPlugin)this).Config.Bind<float>("Revive", "WindowSeconds", 30f, new ConfigDescription("How long a downed player can be revived before dying for real. Server-authoritative: the host's value governs everyone.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 600f), Array.Empty<object>()));
			ReviveModeCfg.SettingChanged += delegate
			{
				BroadcastConfig();
			};
			ReviveHoldTimeCfg.SettingChanged += delegate
			{
				BroadcastConfig();
			};
			ReviveWindowCfg.SettingChanged += delegate
			{
				BroadcastConfig();
			};
			_harmony = Harmony.CreateAndPatchAll(typeof(Plugin).Assembly, "com.andres.reviveallies");
			if (Environment.GetEnvironmentVariable("RR_E2E") == "1")
			{
				E2ERunner.Bootstrap();
			}
		}

		private void OnDestroy()
		{
			Harmony? harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
		}
	}
}
namespace ReviveAllies.Patches
{
	[HarmonyPatch(typeof(Character), "CheckDeath")]
	internal static class CheckDeathPatch
	{
		private static bool Prefix(Character __instance)
		{
			//IL_00c3: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00cb: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d5: Expected O, but got Unknown
			Player val = (Player)(object)((__instance is Player) ? __instance : null);
			if (val == null)
			{
				return true;
			}
			if (!((Character)val).m_nview.IsOwner())
			{
				return true;
			}
			if (((Character)val).IsDead())
			{
				return true;
			}
			if (((Character)val).GetHealth() > 0f)
			{
				return true;
			}
			if ((Object)(object)((Component)val).GetComponent<DownedStateMachine>() == (Object)null)
			{
				return true;
			}
			if (!val.IsDowned())
			{
				val.EnterDownedState();
				return false;
			}
			if (!((Component)val).GetComponent<ChannelSignal>().IsChanneling && val.IsReviveWindowExpired())
			{
				if (((Character)val).m_lastHit == null)
				{
					((Character)val).m_lastHit = new HitData
					{
						m_hitType = (HitType)14
					};
				}
				val.ExpireDownedState();
				return true;
			}
			return false;
		}
	}
	[HarmonyPatch(typeof(ZNet), "Awake")]
	internal static class ZNetAwakeConfigSyncPatch
	{
		private static void Postfix()
		{
			Plugin.ResetServerConfig();
			ZRoutedRpc instance = ZRoutedRpc.instance;
			if (instance == null)
			{
				return;
			}
			instance.Register<float, float, int>("ReviveAllies_Config", (Action<long, float, float, int>)delegate(long sender, float window, float holdTime, int mode)
			{
				if (!((Object)(object)ZNet.instance != (Object)null) || !ZNet.instance.IsServer())
				{
					Plugin.ApplyServerConfig(window, holdTime, mode != 0);
				}
			});
		}
	}
	[HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")]
	internal static class ZNetPeerInfoConfigSyncPatch
	{
		private static void Postfix(ZNet __instance)
		{
			if (__instance.IsServer())
			{
				Plugin.BroadcastConfig();
			}
		}
	}
	[HarmonyPatch(typeof(BaseAI), "IsEnemy", new Type[]
	{
		typeof(Character),
		typeof(Character)
	})]
	internal static class BaseAIIgnoreDownedPatch
	{
		private static void Postfix(Character a, Character b, ref bool __result)
		{
			if (__result && (IsDownedPlayer(a) || IsDownedPlayer(b)))
			{
				__result = false;
			}
		}

		private static bool IsDownedPlayer(Character c)
		{
			return ((Player?)(object)((c is Player) ? c : null))?.IsDowned() ?? false;
		}
	}
	[HarmonyPatch(typeof(ZNet), "OpenServer")]
	internal static class ZNetOpenServerSocketPatch
	{
		private static bool Prefix(ZNet __instance)
		{
			//IL_0014: Unknown result type (might be due to invalid IL or missing references)
			//IL_001a: Invalid comparison between Unknown and I4
			//IL_0042: Unknown result type (might be due to invalid IL or missing references)
			//IL_0048: Expected O, but got Unknown
			if (!E2EConfig.MultiplayerMode)
			{
				return true;
			}
			if ((int)ZNet.m_onlineBackend != 3)
			{
				return true;
			}
			if (!__instance.IsServer())
			{
				return true;
			}
			int port = E2EConfig.Port;
			ZSocket2 val = new ZSocket2();
			if (!val.StartHost())
			{
				Plugin.Logger.LogError((object)$"E2E: ZSocket2 host failed to bind port {port}");
				return true;
			}
			__instance.m_hostSocket = (ISocket)(object)val;
			ZNet.m_openServer = true;
			E2ELog.Write($"E2E: opened CustomSocket (TCP) host on port {port}");
			return false;
		}
	}
	[HarmonyPatch(typeof(ZNet), "Update")]
	internal static class ZNetUpdatePumpConnectorPatch
	{
		private static void Postfix(ZNet __instance)
		{
			//IL_000f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0015: Invalid comparison between Unknown and I4
			if (E2EConfig.MultiplayerMode && (int)ZNet.m_onlineBackend == 3 && __instance.m_serverConnector != null)
			{
				__instance.UpdateClientConnector(Time.deltaTime);
			}
		}
	}
	[HarmonyPatch(typeof(ZNet), "SendPeerInfo")]
	internal static class ZNetSendPeerInfoSocketPatch
	{
		private static bool Prefix(ZNet __instance, ZRpc rpc, string password)
		{
			//IL_0014: Unknown result type (might be due to invalid IL or missing references)
			//IL_001a: Invalid comparison between Unknown and I4
			//IL_003d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0043: Expected O, but got Unknown
			//IL_0050: Unknown result type (might be due to invalid IL or missing references)
			//IL_0055: Unknown result type (might be due to invalid IL or missing references)
			//IL_0075: Unknown result type (might be due to invalid IL or missing references)
			if (!E2EConfig.MultiplayerMode)
			{
				return true;
			}
			if ((int)ZNet.m_onlineBackend != 3)
			{
				return true;
			}
			if (__instance.IsServer())
			{
				return true;
			}
			ZPackage val = new ZPackage();
			val.Write(ZNet.GetUID());
			val.Write(((object)Version.CurrentVersion/*cast due to .constrained prefix*/).ToString());
			val.Write(36u);
			val.Write(__instance.m_referencePosition);
			val.Write(Game.instance.GetPlayerProfile().GetName());
			val.Write("");
			rpc.Invoke("PeerInfo", new object[1] { val });
			E2ELog.Write("E2E[client]: sent CustomSocket PeerInfo (no steam ticket)");
			return false;
		}
	}
	[HarmonyPatch(typeof(ZSocket2), "Recv")]
	internal static class SocketLatencyPatch
	{
		private static readonly Dictionary<ZSocket2, Queue<(DateTime due, ZPackage pkg)>> s_delayed = new Dictionary<ZSocket2, Queue<(DateTime, ZPackage)>>();

		private static void Postfix(ZSocket2 __instance, ref ZPackage? __result)
		{
			int latencyMs = E2EConfig.LatencyMs;
			if (latencyMs > 0 && E2EConfig.MultiplayerMode)
			{
				if (!s_delayed.TryGetValue(__instance, out Queue<(DateTime, ZPackage)> value))
				{
					value = (s_delayed[__instance] = new Queue<(DateTime, ZPackage)>());
				}
				if (__result != null)
				{
					value.Enqueue((DateTime.UtcNow.AddMilliseconds(latencyMs), __result));
				}
				__result = ((value.Count > 0 && value.Peek().Item1 <= DateTime.UtcNow) ? value.Dequeue().Item2 : null);
			}
		}
	}
	[HarmonyPatch(typeof(Player), "Awake")]
	internal static class PlayerAwakePatch
	{
		private static void Postfix(Player __instance)
		{
			if ((Object)(object)((Component)__instance).GetComponent<GiveUp>() == (Object)null)
			{
				((Component)__instance).gameObject.AddComponent<GiveUp>();
			}
			if ((Object)(object)((Component)__instance).GetComponent<ChannelSignal>() == (Object)null)
			{
				((Component)__instance).gameObject.AddComponent<ChannelSignal>();
			}
			if ((Object)(object)((Component)__instance).GetComponent<DownedStateMachine>() == (Object)null)
			{
				((Component)__instance).gameObject.AddComponent<DownedStateMachine>();
			}
			if ((Object)(object)((Component)__instance).GetComponent<ReviveRequest>() == (Object)null)
			{
				((Component)__instance).gameObject.AddComponent<ReviveRequest>();
			}
			if ((Object)(object)((Component)__instance).GetComponent<ReviverStateMachine>() == (Object)null)
			{
				((Component)__instance).gameObject.AddComponent<ReviverStateMachine>();
			}
			if ((Object)(object)((Component)__instance).GetComponent<ReconnectDeathCheck>() == (Object)null)
			{
				((Component)__instance).gameObject.AddComponent<ReconnectDeathCheck>();
			}
			if ((Object)(object)((Component)__instance).GetComponent<DownedView>() == (Object)null)
			{
				((Component)__instance).gameObject.AddComponent<DownedView>();
			}
		}
	}
	[HarmonyPatch(typeof(Player), "CanMove")]
	internal static class PlayerCanMovePatch
	{
		private static void Postfix(Player __instance, ref bool __result)
		{
			if (__result && __instance.IsDowned())
			{
				__result = false;
			}
		}
	}
	[HarmonyPatch(typeof(Character), "UpdateMotion")]
	internal static class CharacterUpdateMotionPatch
	{
		private static bool Prefix(Character __instance)
		{
			Player val = (Player)(object)((__instance is Player) ? __instance : null);
			if (val != null && val.IsDowned())
			{
				return false;
			}
			return true;
		}
	}
	[HarmonyPatch(typeof(Player), "LateUpdate")]
	internal static class PlayerLateUpdatePatch
	{
		private static void Postfix(Player __instance)
		{
			if (((Character)__instance).IsDead())
			{
				if ((Object)(object)((Character)__instance).m_collider != (Object)null && ((Collider)((Character)__instance).m_collider).enabled)
				{
					((Collider)((Character)__instance).m_collider).enabled = false;
				}
				if ((Object)(object)((Character)__instance).m_nview != (Object)null && ((Character)__instance).m_nview.IsValid() && ((Character)__instance).m_nview.IsOwner() && (Object)(object)((Character)__instance).m_body != (Object)null && !((Character)__instance).m_body.isKinematic)
				{
					((Character)__instance).m_body.isKinematic = true;
				}
			}
		}
	}
	[HarmonyPatch(typeof(Player), "UpdateHover")]
	internal static class PlayerUpdateHoverPatch
	{
		private static bool Prefix(Player __instance)
		{
			if (__instance.IsDowned())
			{
				return false;
			}
			return true;
		}
	}
	[HarmonyPatch(typeof(Player), "OnRespawn")]
	internal static class PlayerOnRespawnPatch
	{
		private static bool Prefix(Player __instance)
		{
			if (__instance.IsDowned())
			{
				return false;
			}
			return true;
		}
	}
	[HarmonyPatch(typeof(EnemyHud), "TestShow")]
	internal static class EnemyHudHideDownedPatch
	{
		private static void Postfix(Character c, ref bool __result)
		{
			if (__result)
			{
				Player val = (Player)(object)((c is Player) ? c : null);
				if (val != null && (val.IsDowned() || ((Character)val).IsDead()))
				{
					__result = false;
				}
			}
		}
	}
	[HarmonyPatch(typeof(ZNetScene), "Awake")]
	internal static class ZNetSceneAwakeRegisterPrefabsPatch
	{
		private static void Postfix(ZNetScene __instance)
		{
			MarkerPrefab.RegisterPrefab(__instance);
		}
	}
	[HarmonyPatch(typeof(TombStone), "Setup")]
	internal static class TombStoneSetupReplacePatch
	{
		private static void Postfix(TombStone __instance, long ownerUID)
		{
			//IL_0078: Unknown result type (might be due to invalid IL or missing references)
			//IL_007d: Unknown result type (might be due to invalid IL or missing references)
			//IL_007e: Unknown result type (might be due to invalid IL or missing references)
			//IL_007f: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a1: Unknown result type (might be due to invalid IL or missing references)
			//IL_0095: Unknown result type (might be due to invalid IL or missing references)
			//IL_009a: Unknown result type (might be due to invalid IL or missing references)
			//IL_00be: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00fb: Unknown result type (might be due to invalid IL or missing references)
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null || (Object)(object)((Character)localPlayer).m_nview == (Object)null || !((Character)localPlayer).m_nview.IsValid() || ownerUID != localPlayer.GetPlayerID())
			{
				return;
			}
			GraveReplaceView graveReplaceView = localPlayer.GraveReplace();
			if (graveReplaceView.Pending)
			{
				graveReplaceView.Pending = false;
				Vector3 val = graveReplaceView.Pos;
				if (val == Vector3.zero)
				{
					val = ((Component)__instance).transform.position;
				}
				((Component)__instance).transform.position = val;
				Rigidbody component = ((Component)__instance).GetComponent<Rigidbody>();
				if ((Object)(object)component != (Object)null)
				{
					component.position = val;
					component.linearVelocity = Vector3.zero;
				}
				GameObject marker = localPlayer.FindDownedMarker() ?? MarkerPrefab.FindFor(localPlayer.GetPlayerID());
				DownedMarker.MarkReplaced(marker);
				DownedStateMachineView downedStateMachineView = localPlayer.State();
				downedStateMachineView.Marker = ZDOID.None;
			}
		}
	}
	[HarmonyPatch(typeof(Player), "OnDeath")]
	internal static class PlayerOnDeathMarkerCrumblePatch
	{
		private static void Postfix(Player __instance)
		{
			//IL_006e: Unknown result type (might be due to invalid IL or missing references)
			ZNetView nview = ((Character)__instance).m_nview;
			if (!((Object)(object)nview == (Object)null) && nview.IsValid() && nview.IsOwner())
			{
				GraveReplaceView graveReplaceView = __instance.GraveReplace();
				if (graveReplaceView.Pending)
				{
					graveReplaceView.Pending = false;
					GameObject marker = __instance.FindDownedMarker() ?? MarkerPrefab.FindFor(__instance.GetPlayerID());
					DownedStateMachineView downedStateMachineView = __instance.State();
					downedStateMachineView.Marker = ZDOID.None;
					DownedMarker.Crumble(marker);
					Plugin.Logger.LogInfo((object)(__instance.GetPlayerName() + " died with no grave to drop; marker crumbled"));
				}
			}
		}
	}
	[HarmonyPatch(typeof(Player), "CreateDeathEffects")]
	internal static class SuppressPlayerRagdollPatch
	{
		private static bool Prefix()
		{
			return false;
		}
	}
}
namespace ReviveAllies.E2E
{
	public static class E2EConfig
	{
		public static bool Enabled => Environment.GetEnvironmentVariable("RR_E2E") == "1";

		public static string Role => (Environment.GetEnvironmentVariable("RR_E2E_ROLE") ?? "").ToLowerInvariant();

		public static bool MultiplayerMode => Role == "host" || Role == "client";

		public static bool IsHost => Role == "host";

		public static bool IsClient => Role == "client";

		public static string Scenario => (Environment.GetEnvironmentVariable("RR_E2E_SCENARIO") ?? "revive").ToLowerInvariant();

		public static bool IsRejoinScenario => Scenario == "rejoin";

		public static bool IsVanishScenario => Scenario == "vanish";

		public static bool IsReviveLoopScenario => Scenario == "reviveloop";

		public static bool IsConfigSyncScenario => Scenario == "configsync";

		public static string LoopDownRole => (Environment.GetEnvironmentVariable("RR_E2E_LOOP_DOWN") ?? "host").ToLowerInvariant();

		public static int LoopCycles
		{
			get
			{
				string environmentVariable = Environment.GetEnvironmentVariable("RR_E2E_LOOP_CYCLES");
				int result;
				return (int.TryParse(environmentVariable, out result) && result > 0) ? result : 15;
			}
		}

		public static string LoopMode => (Environment.GetEnvironmentVariable("RR_E2E_LOOP_MODE") ?? "revive").ToLowerInvariant();

		public static bool IsExpireLoop => LoopMode == "expire";

		public static bool Manual => Environment.GetEnvironmentVariable("RR_E2E_MANUAL") == "1";

		public static int Port
		{
			get
			{
				string environmentVariable = Environment.GetEnvironmentVariable("RR_E2E_PORT");
				int result;
				return int.TryParse(environmentVariable, out result) ? result : 2456;
			}
		}

		public static string WorldName => Environment.GetEnvironmentVariable("RR_E2E_WORLD") ?? "e2e_mp";

		public static string ServerHost => Environment.GetEnvironmentVariable("RR_E2E_HOST") ?? "127.0.0.1";

		public static int LatencyMs
		{
			get
			{
				string environmentVariable = Environment.GetEnvironmentVariable("RR_E2E_LATENCY");
				int result;
				return (int.TryParse(environmentVariable, out result) && result > 0) ? result : 0;
			}
		}
	}
	public static class E2ELog
	{
		private static readonly object s_lock = new object();

		private static string? s_path;

		private static bool s_init;

		private static string? Path
		{
			get
			{
				if (!s_init)
				{
					s_path = Environment.GetEnvironmentVariable("RR_E2E_LOG");
					if (string.IsNullOrEmpty(s_path))
					{
						string environmentVariable = Environment.GetEnvironmentVariable("RR_E2E_RESULTS");
						if (!string.IsNullOrEmpty(environmentVariable))
						{
							s_path = environmentVariable + ".log";
						}
					}
					s_init = true;
					if (!string.IsNullOrEmpty(s_path))
					{
						try
						{
							File.WriteAllText(s_path, "# E2E log role=" + E2EConfig.Role + "\n");
						}
						catch
						{
						}
					}
				}
				return s_path;
			}
		}

		public static void Write(string msg)
		{
			Plugin.Logger.LogInfo((object)msg);
			string path = Path;
			if (string.IsNullOrEmpty(path))
			{
				return;
			}
			try
			{
				lock (s_lock)
				{
					File.AppendAllText(path, msg + "\n");
				}
			}
			catch
			{
			}
		}
	}
	public class E2ERunner : MonoBehaviour
	{
		private const float HardTimeoutSeconds = 360f;

		private readonly List<(string name, bool pass, string detail)> _results = new List<(string, bool, string)>();

		private bool _started;

		private bool _worldStartIssued;

		private float _elapsed;

		private const float LeakGraceSeconds = 8f;

		private int m_loopLeaks;

		private static string ResultPath => Environment.GetEnvironmentVariable("RR_E2E_RESULTS") ?? Path.Combine(Paths.GameRootPath ?? ".", "e2e-results.txt");

		public static void Bootstrap()
		{
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_000c: Expected O, but got Unknown
			GameObject val = new GameObject("RevivalRevived_E2ERunner");
			Object.DontDestroyOnLoad((Object)(object)val);
			val.AddComponent<E2ERunner>();
			Plugin.Logger.LogInfo((object)("E2E: harness bootstrapped (role='" + E2EConfig.Role + "')"));
		}

		private void Start()
		{
			((MonoBehaviour)this).StartCoroutine(RunAll());
		}

		private void Update()
		{
			if (!E2EConfig.Manual)
			{
				_elapsed += Time.unscaledDeltaTime;
				if (_elapsed > 360f && _started)
				{
					Log("E2E: HARD TIMEOUT reached, aborting");
					Finish(ok: false, "hard-timeout");
				}
			}
		}

		private IEnumerator RunAll()
		{
			_started = true;
			Log($"E2E: run starting (role='{E2EConfig.Role}' manual={E2EConfig.Manual})");
			if (E2EConfig.Manual)
			{
				yield return ((MonoBehaviour)this).StartCoroutine(RunManual());
				yield break;
			}
			if (Environment.GetEnvironmentVariable("RR_E2E_DEMO") == "1")
			{
				yield return ((MonoBehaviour)this).StartCoroutine(RunDemo());
			}
			else if (E2EConfig.IsHost)
			{
				yield return ((MonoBehaviour)this).StartCoroutine(RunHost());
			}
			else if (E2EConfig.IsClient)
			{
				yield return ((MonoBehaviour)this).StartCoroutine(RunClient());
			}
			else
			{
				yield return ((MonoBehaviour)this).StartCoroutine(RunSingleProcess());
			}
			bool allPass = _results.Count > 0;
			foreach (var result in _results)
			{
				allPass &= result.pass;
			}
			Finish(allPass, allPass ? "all-pass" : "failures");
		}

		private IEnumerator RunHost()
		{
			yield return ((MonoBehaviour)this).StartCoroutine(AutoStart());
			yield return ((MonoBehaviour)this).StartCoroutine(WaitForPlayerInWorld());
			Player player = Player.m_localPlayer;
			if ((Object)(object)player == (Object)null)
			{
				Record("host_spawn", pass: false, "no local player");
				yield break;
			}
			Record("host_spawn", pass: true, "name=" + player.GetPlayerName());
			if (E2EConfig.IsConfigSyncScenario)
			{
				Plugin.ReviveHoldTimeCfg.Value = 7f;
				Plugin.ReviveModeCfg.Value = ReviveModeType.Press;
				Plugin.BroadcastConfig();
				Log("E2E[host]: configsync -- authoritative hold=7 mode=Press");
			}
			Log("E2E[host]: waiting for a client to connect...");
			float waited = 0f;
			while (waited < 180f)
			{
				int peers = (((Object)(object)ZNet.instance != (Object)null) ? ZNet.instance.GetPeerConnections() : 0);
				int players = Player.GetAllPlayers().Count;
				if (peers >= 1 && players >= 2)
				{
					break;
				}
				waited += Time.unscaledDeltaTime;
				yield return null;
			}
			int peerCount = (((Object)(object)ZNet.instance != (Object)null) ? ZNet.instance.GetPeerConnections() : 0);
			int playerCount = Player.GetAllPlayers().Count;
			bool connected = peerCount >= 1 && playerCount >= 2;
			Record("host_client_connected", connected, $"peers={peerCount} players={playerCount}");
			if (!connected)
			{
				yield break;
			}
			if (E2EConfig.IsRejoinScenario)
			{
				Log("E2E[host]: rejoin scenario -- idling as server");
				yield return (object)new WaitForSecondsRealtime(160f);
				Record("host_idle_complete", pass: true, "server stayed up for client rejoin");
				yield break;
			}
			if (E2EConfig.IsConfigSyncScenario)
			{
				Log("E2E[host]: configsync -- idling while client verifies");
				yield return (object)new WaitForSecondsRealtime(30f);
				Record("host_idle_complete", pass: true, "server stayed up for configsync");
				yield break;
			}
			if (E2EConfig.IsVanishScenario)
			{
				yield return ((MonoBehaviour)this).StartCoroutine(RunHostVanish(player));
				yield break;
			}
			if (E2EConfig.IsReviveLoopScenario)
			{
				if (E2EConfig.LoopDownRole == "host")
				{
					yield return ((MonoBehaviour)this).StartCoroutine(RunLoopVictim(player));
				}
				else
				{
					yield return ((MonoBehaviour)this).StartCoroutine(RunLoopReviver(player));
				}
				yield break;
			}
			yield return (object)new WaitForSecondsRealtime(2f);
			Log("E2E[host]: downing self");
			((Character)player).SetHealth(0f);
			waited = 0f;
			while (waited < 5f && !player.IsDowned())
			{
				waited += Time.unscaledDeltaTime;
				yield return null;
			}
			Record("host_downed", player.IsDowned(), $"downed={player.IsDowned()} dead={((Character)player).IsDead()}");
			if (player.IsDowned())
			{
				Log("E2E[host]: waiting to be revived by client...");
				waited = 0f;
				float maxSeenProgress = 0f;
				while (waited < 28f && player.IsDowned() && !((Character)player).IsDead())
				{
					maxSeenProgress = Mathf.Max(maxSeenProgress, player.GetReviveProgress());
					waited += Time.unscaledDeltaTime;
					yield return null;
				}
				bool revived = !player.IsDowned() && !((Character)player).IsDead() && ((Character)player).GetHealth() > 0f;
				Record("host_revived_by_client", revived, $"downed={player.IsDowned()} dead={((Character)player).IsDead()} hp={((Character)player).GetHealth():F0}");
				Record("host_saw_progress", maxSeenProgress > 0.5f, $"maxSeenProgress={maxSeenProgress:F2}");
				yield return (object)new WaitForSecondsRealtime(6f);
			}
		}

		private IEnumerator RunHostVanish(Player me)
		{
			Log("E2E[host]: vanish scenario -- waiting for client to be downed...");
			Player downed = null;
			float w = 0f;
			while (w < 60f)
			{
				downed = FindDownedRemotePlayer(me);
				if ((Object)(object)downed != (Object)null)
				{
					break;
				}
				w += Time.unscaledDeltaTime;
				yield return null;
			}
			Record("vanish_client_downed", (Object)(object)downed != (Object)null, ((Object)(object)downed != (Object)null) ? ("name=" + downed.GetPlayerName()) : "none");
			if ((Object)(object)downed == (Object)null)
			{
				yield break;
			}
			long downedPid = downed.GetPlayerID();
			yield return (object)new WaitForSecondsRealtime(1f);
			Log("E2E[host]: channeling; client will log out mid-hold...");
			float maxProg = 0f;
			bool vanished = false;
			bool revived = false;
			w = 0f;
			while (w < 40f)
			{
				if ((Object)(object)downed == (Object)null || (Object)(object)((Character)downed).m_nview == (Object)null || !((Character)downed).m_nview.IsValid())
				{
					vanished = true;
					break;
				}
				if (!downed.IsDowned() && ((Character)downed).GetHealth() > 0f)
				{
					revived = true;
					break;
				}
				FindMarkerInteractable(downed)?.Interact((Humanoid)(object)me, hold: true, alt: false);
				maxProg = Mathf.Max(maxProg, downed.GetReviveProgress());
				w += Time.unscaledDeltaTime;
				yield return null;
			}
			Record("vanish_mid_channel", vanished && !revived && maxProg > 0.1f, $"vanished={vanished} revived={revived} maxProg={maxProg:F2}");
			if (!vanished)
			{
				yield break;
			}
			GameObject orphan = MarkerPrefab.FindFor(downedPid);
			Revivable orphanInteractable = (((Object)(object)orphan != (Object)null) ? orphan.GetComponentInChildren<Revivable>() : null);
			w = 0f;
			while (w < 1.5f)
			{
				orphanInteractable?.Interact((Humanoid)(object)me, hold: true, alt: false);
				w += Time.unscaledDeltaTime;
				yield return null;
			}
			yield return (object)new WaitForSecondsRealtime(3f);
			orphan = MarkerPrefab.FindFor(downedPid);
			bool orphanPersists = (Object)(object)orphan != (Object)null;
			string hover = "";
			bool interactInert = true;
			if ((Object)(object)orphan != (Object)null)
			{
				Revivable inter = orphan.GetComponentInChildren<Revivable>();
				if ((Object)(object)inter != (Object)null)
				{
					interactInert = !inter.Interact((Humanoid)(object)me, hold: true, alt: false);
					hover = inter.GetHoverText();
				}
			}
			bool hoverExplains = hover.IndexOf("disconnected", StringComparison.OrdinalIgnoreCase) >= 0;
			bool onlyMe = Player.GetAllPlayers().Count == 1;
			Record("vanish_channel_fizzles", orphanPersists && interactInert && hoverExplains && onlyMe, $"orphanPersists={orphanPersists} interactInert={interactInert} " + string.Format("hoverExplains={0} hover=\"{1}\" onlyMe={2}", hoverExplains, hover.Replace("\n", " | "), onlyMe));
		}

		private IEnumerator RunClient()
		{
			yield return ((MonoBehaviour)this).StartCoroutine(AutoStart());
			yield return ((MonoBehaviour)this).StartCoroutine(WaitForPlayerInWorld());
			Player me = Player.m_localPlayer;
			if ((Object)(object)me == (Object)null)
			{
				Record("client_spawn", pass: false, "no local player");
				yield break;
			}
			Record("client_spawn", pass: true, "name=" + me.GetPlayerName());
			Log("E2E[client]: waiting to see the remote host player...");
			float waited = 0f;
			while (waited < 60f && Player.GetAllPlayers().Count < 2)
			{
				waited += Time.unscaledDeltaTime;
				yield return null;
			}
			bool sawRemote = Player.GetAllPlayers().Count >= 2;
			Record("client_sees_host", sawRemote, $"players={Player.GetAllPlayers().Count}");
			if (!sawRemote)
			{
				yield break;
			}
			if (E2EConfig.IsRejoinScenario)
			{
				yield return ((MonoBehaviour)this).StartCoroutine(RunClientRejoin(me));
				yield break;
			}
			if (E2EConfig.IsVanishScenario)
			{
				yield return ((MonoBehaviour)this).StartCoroutine(RunClientVanish(me));
				yield break;
			}
			if (E2EConfig.IsReviveLoopScenario)
			{
				if (E2EConfig.LoopDownRole == "client")
				{
					yield return ((MonoBehaviour)this).StartCoroutine(RunLoopVictim(me));
				}
				else
				{
					yield return ((MonoBehaviour)this).StartCoroutine(RunLoopReviver(me));
				}
				yield break;
			}
			if (E2EConfig.IsConfigSyncScenario)
			{
				yield return ((MonoBehaviour)this).StartCoroutine(RunClientConfigSync());
				yield break;
			}
			Log("E2E[client]: waiting for remote player to be downed...");
			Player downed = null;
			waited = 0f;
			while (waited < 60f)
			{
				downed = FindDownedRemotePlayer(me);
				if ((Object)(object)downed != (Object)null)
				{
					break;
				}
				waited += Time.unscaledDeltaTime;
				yield return null;
			}
			Record("client_detected_downed", (Object)(object)downed != (Object)null, ((Object)(object)downed != (Object)null) ? ("name=" + downed.GetPlayerName()) : "none");
			if ((Object)(object)downed == (Object)null)
			{
				yield break;
			}
			yield return ((MonoBehaviour)this).StartCoroutine(ValidateMarker(downed));
			Log("E2E[client]: channeling revive on remote player...");
			waited = 0f;
			float interactSeconds = 0f;
			float maxUiFill = 0f;
			float lastProg = 0f;
			float worstRegression = 0f;
			bool uiSeen = false;
			bool sawFull = false;
			bool reboundAfterFull = false;
			while (waited < 20f && downed.IsDowned())
			{
				Revivable interactable = FindInteractableViaHoverRay(downed, me);
				if ((Object)(object)interactable != (Object)null)
				{
					interactable.Interact((Humanoid)(object)me, hold: true, alt: false);
					interactSeconds += Time.unscaledDeltaTime;
				}
				float prog = downed.GetReviveProgress();
				if (prog > 0.02f && prog < lastProg - 0.01f)
				{
					worstRegression = Mathf.Max(worstRegression, lastProg - prog);
				}
				lastProg = prog;
				if (prog >= 0.99f)
				{
					sawFull = true;
				}
				else if (sawFull && prog > 0.05f && prog < 0.9f)
				{
					reboundAfterFull = true;
				}
				if (ProgressUI.Visible)
				{
					uiSeen = true;
					maxUiFill = Mathf.Max(maxUiFill, ProgressUI.Fill);
				}
				waited += Time.unscaledDeltaTime;
				yield return null;
			}
			bool revivedRemote = !downed.IsDowned();
			Record("client_revived_remote", revivedRemote, $"downed={downed.IsDowned()} channelSecs={interactSeconds:F1}");
			bool monotonic = worstRegression < 0.05f;
			Record("client_progress_ui", uiSeen && maxUiFill > 0.3f && monotonic && !reboundAfterFull, $"uiSeen={uiSeen} maxFill={maxUiFill:F2} worstRegression={worstRegression:F2} monotonic={monotonic} " + $"sawFull={sawFull} reboundAfterFull={reboundAfterFull}");
			float vw = 0f;
			bool visibleAgain = false;
			bool solidAgain = false;
			while (vw < 5f && (Object)(object)downed != (Object)null)
			{
				visibleAgain = (Object)(object)((Character)downed).m_visual != (Object)null && ((Character)downed).m_visual.activeSelf;
				solidAgain = (Object)(object)((Character)downed).m_collider != (Object)null && ((Collider)((Character)downed).m_collider).enabled;
				if (visibleAgain && solidAgain)
				{
					break;
				}
				vw += Time.unscaledDeltaTime;
				yield return null;
			}
			Record("client_host_visible_after_revive", visibleAgain && solidAgain, $"visible={visibleAgain} colliderOn={solidAgain} after={vw:F1}s");
			float mw = 0f;
			bool markerDestroyed = false;
			while (mw < 6f)
			{
				markerDestroyed = Object.FindObjectsOfType<DownedMarker>().Length == 0;
				if (markerDestroyed)
				{
					break;
				}
				mw += Time.unscaledDeltaTime;
				yield return null;
			}
			Record("client_marker_gone_after_revive", markerDestroyed, $"markerDestroyed={markerDestroyed} after={mw:F1}s");
		}

		private IEnumerator RunClientVanish(Player me)
		{
			Log("E2E[client]: vanish scenario -- downing self");
			((Character)me).SetHealth(0f);
			float w = 0f;
			while (w < 6f && !me.IsDowned())
			{
				w += Time.unscaledDeltaTime;
				yield return null;
			}
			if (!me.IsDowned())
			{
				Record("vanish_pre_down", pass: false, "could not down self");
				yield break;
			}
			Record("vanish_pre_down", pass: true, "downed=True");
			Log("E2E[client]: waiting for host channel progress before logging out...");
			w = 0f;
			float seenProg = 0f;
			while (w < 30f && me.IsDowned())
			{
				seenProg = Mathf.Max(seenProg, me.GetReviveProgress());
				if (seenProg > 0.25f)
				{
					break;
				}
				w += Time.unscaledDeltaTime;
				yield return null;
			}
			Record("vanish_saw_host_channel", seenProg > 0.25f && me.IsDowned(), $"seenProg={seenProg:F2}");
			if (!(seenProg <= 0.25f))
			{
				Log("E2E[client]: logging out mid-channel");
				Game.instance.Logout(true, true);
				w = 0f;
				while (w < 60f && (Object)(object)FejdStartup.instance == (Object)null)
				{
					w += Time.unscaledDeltaTime;
					yield return null;
				}
				Record("vanish_logged_out", (Object)(object)FejdStartup.instance != (Object)null, "back at menu");
			}
		}

		private IEnumerator RunClientRejoin(Player me)
		{
			Log("E2E[client]: downing self before logout");
			((Character)me).SetHealth(0f);
			float w = 0f;
			while (w < 6f && !me.IsDowned())
			{
				w += Time.unscaledDeltaTime;
				yield return null;
			}
			if (!me.IsDowned())
			{
				Record("rejoin_pre_down", pass: false, "could not down self");
				yield break;
			}
			yield return (object)new WaitForSecondsRealtime(1f);
			long pid = me.GetPlayerID();
			bool hadMarker = (Object)(object)MarkerPrefab.FindFor(pid) != (Object)null;
			Record("rejoin_pre_down", hadMarker, $"downed=True marker={hadMarker}");
			if (!hadMarker)
			{
				yield break;
			}
			Log("E2E[client]: logging out...");
			Game.instance.Logout(true, true);
			w = 0f;
			while (w < 60f && (Object)(object)FejdStartup.instance == (Object)null)
			{
				w += Time.unscaledDeltaTime;
				yield return null;
			}
			if ((Object)(object)FejdStartup.instance == (Object)null)
			{
				Record("rejoin_reconnect", pass: false, "menu did not return");
				yield break;
			}
			Log("E2E[client]: back at menu, reconnecting...");
			_worldStartIssued = false;
			yield return ((MonoBehaviour)this).StartCoroutine(AutoStart());
			yield return ((MonoBehaviour)this).StartCoroutine(WaitForPlayerInWorld());
			Player me2 = Player.m_localPlayer;
			if ((Object)(object)me2 == (Object)null)
			{
				Record("rejoin_reconnect", pass: false, "no local player after reconnect");
				yield break;
			}
			Record("rejoin_reconnect", pass: true, "name=" + me2.GetPlayerName());
			Log("E2E[client]: waiting for disconnect-death on reconnect...");
			bool diedOnReconnect = false;
			bool markerGone = false;
			bool realTombstone = false;
			w = 0f;
			while (w < 20f)
			{
				if ((Object)(object)Player.m_localPlayer != (Object)null && ((Character)Player.m_localPlayer).IsDead())
				{
					diedOnReconnect = true;
				}
				markerGone = (Object)(object)MarkerPrefab.FindFor(pid) == (Object)null;
				TombStone[] array = Object.FindObjectsOfType<TombStone>();
				foreach (TombStone t in array)
				{
					ZNetView nv = ((Component)t).GetComponent<ZNetView>();
					if ((Object)(object)nv != (Object)null && nv.IsValid() && !new DownedMarkerView(nv).IsMarker)
					{
						realTombstone = true;
						break;
					}
				}
				if (diedOnReconnect && markerGone)
				{
					break;
				}
				w += Time.unscaledDeltaTime;
				yield return null;
			}
			bool notDowned = (Object)(object)Player.m_localPlayer == (Object)null || !Player.m_localPlayer.IsDowned();
			Record("reconnect_downed_dies", diedOnReconnect && markerGone && notDowned, $"died={diedOnReconnect} markerGone={markerGone} realTombstone={realTombstone} notDowned={notDowned}");
		}

		private IEnumerator RunClientConfigSync()
		{
			Plugin.ReviveHoldTimeCfg.Value = 2f;
			Plugin.ReviveModeCfg.Value = ReviveModeType.Hold;
			Log("E2E[client]: configsync -- local hold=2 mode=Hold, expecting server hold=7 mode=Press");
			float w = 0f;
			bool adopted = false;
			while (w < 20f && !adopted)
			{
				adopted = Mathf.Abs(Plugin.ReviveDuration - 7f) < 0.01f && Plugin.RevivePressMode;
				w += Time.unscaledDeltaTime;
				yield return null;
			}
			bool durationFromServer = Mathf.Abs(Plugin.ReviveDuration - 7f) < 0.01f;
			bool modeFromServer = Plugin.RevivePressMode;
			Record("client_adopts_server_config", durationFromServer && modeFromServer, $"durationFromServer={durationFromServer} (dur={Plugin.ReviveDuration:F1}, local was 2) " + $"modeFromServer={modeFromServer} (press; local was hold)");
		}

		private static List<ZDO> MarkerZdos()
		{
			List<ZDO> list = new List<ZDO>();
			int stableHashCode = StringExtensionMethods.GetStableHashCode("RevivalRevived_DownedMarker");
			foreach (KeyValuePair<ZDOID, ZDO> item in ZDOMan.instance.m_objectsByID)
			{
				if (item.Value.GetPrefab() == stableHashCode)
				{
					list.Add(item.Value);
				}
			}
			return list;
		}

		private static List<DownedMarker> GhostMarkers()
		{
			//IL_0043: Unknown result type (might be due to invalid IL or missing references)
			List<DownedMarker> list = new List<DownedMarker>();
			DownedMarker[] array = Object.FindObjectsOfType<DownedMarker>();
			foreach (DownedMarker downedMarker in array)
			{
				ZNetView component = ((Component)downedMarker).GetComponent<ZNetView>();
				ZDO val = (((Object)(object)component != (Object)null) ? component.GetZDO() : null);
				ZDO val2 = ((val != null) ? ZDOMan.instance.GetZDO(val.m_uid) : null);
				if (val == null || val2 != val)
				{
					list.Add(downedMarker);
				}
			}
			return list;
		}

		private void DumpLeaks(string context)
		{
			//IL_0025: Unknown result type (might be due to invalid IL or missing references)
			//IL_002a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0040: Unknown result type (might be due to invalid IL or missing references)
			//IL_0068: Unknown result type (might be due to invalid IL or missing references)
			//IL_008e: Unknown result type (might be due to invalid IL or missing references)
			//IL_01dc: Unknown result type (might be due to invalid IL or missing references)
			//IL_0229: Unknown result type (might be due to invalid IL or missing references)
			foreach (ZDO item in MarkerZdos())
			{
				DownedMarkerView downedMarkerView = new DownedMarkerView(item);
				ZDO val = ((downedMarkerView.LinkedPlayer != ZDOID.None) ? ZDOMan.instance.GetZDO(downedMarkerView.LinkedPlayer) : null);
				bool flag = val != null && new DownedStateMachineView(val).Downed;
				GameObject val2 = ZNetScene.instance.FindInstance(item.m_uid);
				Log($"E2E-LEAK[{context}] zdo {item.m_uid}: owner={item.GetOwner()} mine={item.IsOwner()} " + $"ownerRev={item.OwnerRevision} dataRev={item.DataRevision} " + string.Format("replaced={0} playerZdo={1} ", downedMarkerView.ReplacedByGrave, (val != null) ? "alive" : "gone") + $"playerDowned={flag} chan={new ReviveChannelDecayingProgressView(item).Channeling} anchorSec={new ReviveChannelDecayingProgressView(item).AnchorSeconds:F2} " + "instance=" + (((Object)(object)val2 != (Object)null) ? "yes" : "no"));
			}
			foreach (DownedMarker item2 in GhostMarkers())
			{
				ZNetView component = ((Component)item2).GetComponent<ZNetView>();
				ZDO val3 = (((Object)(object)component != (Object)null) ? component.GetZDO() : null);
				Log($"E2E-LEAK[{context}] GHOST instance at {((Component)item2).transform.position}: " + "zdoRef=" + ((val3 != null) ? ((object)Unsafe.As<ZDOID, ZDOID>(ref val3.m_uid)/*cast due to .constrained prefix*/).ToString() : "null") + " registered=" + ((val3 != null && ZDOMan.instance.GetZDO(val3.m_uid) != null) ? "different-object" : "none"));
			}
		}

		private IEnumerator LeakCheck(string context)
		{
			float w = 0f;
			while (w < 8f && (MarkerZdos().Count > 0 || GhostMarkers().Count > 0))
			{
				w += Time.unscaledDeltaTime;
				yield return null;
			}
			int zdos = MarkerZdos().Count;
			int ghosts = GhostMarkers().Count;
			if (zdos > 0 || ghosts > 0)
			{
				m_loopLeaks++;
				DumpLeaks(context);
			}
			Log($"E2E loop [{context}]: zdosLeft={zdos} ghosts={ghosts} leaksSoFar={m_loopLeaks}");
		}

		private IEnumerator RunLoopVictim(Player me)
		{
			if (E2EConfig.IsExpireLoop)
			{
				yield return ((MonoBehaviour)this).StartCoroutine(RunLoopVictimExpire());
				yield break;
			}
			int cycles = E2EConfig.LoopCycles;
			int revives = 0;
			for (int cycle = 1; cycle <= cycles; cycle++)
			{
				((Character)me).SetHealth(((Character)me).GetMaxHealth());
				yield return (object)new WaitForSecondsRealtime(1.2f);
				Log($"E2E loop {cycle}: downing self");
				((Character)me).SetHealth(0f);
				float w = 0f;
				while (w < 5f && !me.IsDowned())
				{
					w += Time.unscaledDeltaTime;
					yield return null;
				}
				if (!me.IsDowned())
				{
					Record("reviveloop_victim", pass: false, $"cycle {cycle}: could not down");
					yield break;
				}
				w = 0f;
				while (w < 40f && me.IsDowned() && !((Character)me).IsDead())
				{
					w += Time.unscaledDeltaTime;
					yield return null;
				}
				if (me.IsDowned() || ((Character)me).IsDead())
				{
					Record("reviveloop_victim", pass: false, $"cycle {cycle}: not revived (dead={((Character)me).IsDead()})");
					yield break;
				}
				revives++;
				yield return ((MonoBehaviour)this).StartCoroutine(LeakCheck($"victim cycle {cycle}"));
			}
			Record("reviveloop_victim", revives == cycles && m_loopLeaks == 0, $"revives={revives}/{cycles} leakCycles={m_loopLeaks}");
			yield return (object)new WaitForSecondsRealtime(8f);
		}

		private IEnumerator RunLoopVictimExpire()
		{
			int cycles = E2EConfig.LoopCycles;
			int deaths = 0;
			for (int cycle = 1; cycle <= cycles; cycle++)
			{
				Player me = Player.m_localPlayer;
				if ((Object)(object)me == (Object)null)
				{
					Record("reviveloop_victim", pass: false, $"cycle {cycle}: no local player");
					yield break;
				}
				((Character)me).SetHealth(((Character)me).GetMaxHealth());
				GiveTestItem(me);
				yield return (object)new WaitForSecondsRealtime(1.2f);
				Log($"E2E loop {cycle}: downing self (expire mode)");
				((Character)me).SetHealth(0f);
				float w = 0f;
				while (w < 5f && !me.IsDowned())
				{
					w += Time.unscaledDeltaTime;
					yield return null;
				}
				if (!me.IsDowned())
				{
					Record("reviveloop_victim", pass: false, $"cycle {cycle}: could not down");
					yield break;
				}
				w = 0f;
				while (w < 30f && me.GetReviveProgress() < 0.15f && me.IsDowned())
				{
					w += Time.unscaledDeltaTime;
					yield return null;
				}
				if (!me.IsDowned())
				{
					Record("reviveloop_victim", pass: false, $"cycle {cycle}: revived before expiry could fire");
					yield break;
				}
				DownedStateMachineView s = new DownedStateMachineView(((Character)me).m_nview)
				{
					DownedTime = (float)ZNet.instance.GetTimeSeconds() - Plugin.ReviveWindow - 5f
				};
				w = 0f;
				while (w < 12f && !((Character)me).IsDead())
				{
					((Character)me).SetHealth(0f);
					w += Time.unscaledDeltaTime;
					yield return null;
				}
				if (!((Character)me).IsDead())
				{
					Record("reviveloop_victim", pass: false, $"cycle {cycle}: expiry did not kill");
					yield break;
				}
				deaths++;
				yield return ((MonoBehaviour)this).StartCoroutine(LeakCheck($"victim expire cycle {cycle}"));
				yield return ((MonoBehaviour)this).StartCoroutine(WaitForAlivePlayer());
			}
			Record("reviveloop_victim", deaths == cycles && m_loopLeaks == 0, $"deaths={deaths}/{cycles} leakCycles={m_loopLeaks}");
			yield return (object)new WaitForSecondsRealtime(8f);
		}

		private IEnumerator RunLoopReviver(Player me)
		{
			if (E2EConfig.IsExpireLoop)
			{
				yield return ((MonoBehaviour)this).StartCoroutine(RunLoopReviverExpire(me));
				yield break;
			}
			int revived = 0;
			while (true)
			{
				Player downed = null;
				float w = 0f;
				while (w < 30f && (Object)(object)downed == (Object)null)
				{
					downed = FindDownedRemotePlayer(me);
					w += Time.unscaledDeltaTime;
					yield return null;
				}
				if ((Object)(object)downed == (Object)null)
				{
					break;
				}
				yield return (object)new WaitForSecondsRealtime(0.5f);
				w = 0f;
				float overshoot = 0f;
				bool jittered = false;
				while (w < 40f && overshoot < 2f)
				{
					if (!jittered && downed.GetReviveProgress() > 0.45f)
					{
						jittered = true;
						yield return (object)new WaitForSecondsRealtime(0.65f);
						w += 0.65f;
						continue;
					}
					(FindInteractableViaHoverRay(downed, me) ?? Object.FindObjectOfType<Revivable>())?.Interact((Humanoid)(object)me, hold: true, alt: false);
					if (!downed.IsDowned())
					{
						overshoot += Time.unscaledDeltaTime;
					}
					w += Time.unscaledDeltaTime;
					yield return null;
				}
				if (downed.IsDowned())
				{
					Record("reviveloop_reviver", pass: false, "revive did not land");
					yield break;
				}
				revived++;
				yield return ((MonoBehaviour)this).StartCoroutine(LeakCheck($"reviver after revive #{revived}"));
			}
			Record("reviveloop_reviver", revived > 0 && m_loopLeaks == 0, $"revived={revived} leakCycles={m_loopLeaks}");
		}

		private IEnumerator RunLoopReviverExpire(Player me)
		{
			int deaths = 0;
			while (true)
			{
				Player downed = null;
				float w = 0f;
				while (w < 30f && (Object)(object)downed == (Object)null)
				{
					downed = FindDownedRemotePlayer(me);
					w += Time.unscaledDeltaTime;
					yield return null;
				}
				if ((Object)(object)downed == (Object)null)
				{
					break;
				}
				yield return (object)new WaitForSecondsRealtime(0.4f);
				w = 0f;
				float overshoot = 0f;
				while (w < 40f && overshoot < 2.5f)
				{
					(FindInteractableViaHoverRay(downed, me) ?? Object.FindObjectOfType<Revivable>())?.Interact((Humanoid)(object)me, hold: true, alt: false);
					if (((Character)downed).IsDead() || !downed.IsDowned())
					{
						overshoot += Time.unscaledDeltaTime;
					}
					w += Time.unscaledDeltaTime;
					yield return null;
				}
				if (!((Character)downed).IsDead())
				{
					Record("reviveloop_reviver", pass: false, "victim did not die mid-channel");
					yield break;
				}
				deaths++;
				yield return ((MonoBehaviour)this).StartCoroutine(LeakCheck($"reviver expire #{deaths}"));
			}
			Record("reviveloop_reviver", deaths > 0 && m_loopLeaks == 0, $"deaths={deaths} leakCycles={m_loopLeaks}");
		}

		private IEnumerator ValidateMarker(Player downed)
		{
			GameObject marker = downed.FindDownedMarker();
			if ((Object)(object)marker == (Object)null)
			{
				float w = 0f;
				while (w < 6f && (Object)(object)marker == (Object)null)
				{
					marker = downed.FindDownedMarker();
					w += Time.unscaledDeltaTime;
					yield return null;
				}
			}
			if ((Object)(object)marker == (Object)null)
			{
				Record("client_marker_sync", pass: false, "marker not found on client");
				yield break;
			}
			ZNetView nview = marker.GetComponent<ZNetView>();
			bool isMarkerFlag = (Object)(object)nview != (Object)null && nview.IsValid() && new DownedMarkerView(nview).IsMarker;
			DownedMarker dm = marker.GetComponent<DownedMarker>();
			bool green = (Object)(object)dm != (Object)null && dm.IsGreen();
			bool hasInteractable = (Object)(object)marker.GetComponentInChildren<Revivable>() != (Object)null;
			bool noTombScript = (Object)(object)marker.GetComponent<TombStone>() == (Object)null;
			float maxPlayerDist = 0f;
			float maxFrameJump = 0f;
			Vector3 prev = marker.transform.position;
			int samples = 0;
			float t = 0f;
			while (t < 2.5f && downed.IsDowned())
			{
				Vector3 pos = marker.transform.position;
				maxPlayerDist = Mathf.Max(maxPlayerDist, Vector3.Distance(pos, ((Component)downed).transform.position));
				maxFrameJump = Mathf.Max(maxFrameJump, Vector3.Distance(pos, prev));
				prev = pos;
				samples++;
				t += Time.unscaledDeltaTime;
				yield return null;
			}
			bool noRagdolls = Object.FindObjectsOfType<Ragdoll>().Length == 0;
			bool corpseColliderOff = (Object)(object)((Character)downed).m_collider == (Object)null || !((Collider)((Character)downed).m_collider).enabled;
			Player me = Player.m_localPlayer;
			bool hoverRayHitsMarker = false;
			bool hoverBlockedByCorpse = false;
			bool hoverTextOk = false;
			Vector3 mpos = marker.transform.position + Vector3.up * 0.3f;
			Vector3 origin = mpos + new Vector3(1.8f, 1.2f, 0f);
			Vector3 val = mpos - origin;
			RaycastHit[] hits = Physics.RaycastAll(origin, ((Vector3)(ref val)).normalized, 6f, me.m_interactMask);
			Array.Sort(hits, (RaycastHit a, RaycastHit b) => ((RaycastHit)(ref a)).distance.CompareTo(((RaycastHit)(ref b)).distance));
			if (hits.Length != 0)
			{
				Collider first = ((RaycastHit)(ref hits[0])).collider;
				hoverRayHitsMarker = (Object)(object)((Component)first).GetComponentInParent<Revivable>() != (Object)null;
				hoverBlockedByCorpse = (Object)(object)((Component)first).GetComponentInParent<Player>() == (Object)(object)downed;
				Revivable inter = ((Component)first).GetComponentInParent<Revivable>();
				if ((Object)(object)inter != (Object)null)
				{
					string hover = inter.GetHoverText();
					hoverTextOk = !string.IsNullOrEmpty(hover) && hover.IndexOf("Revive", StringComparison.OrdinalIgnoreCase) >= 0;
				}
			}
			bool hudHidden = true;
			if ((Object)(object)EnemyHud.instance != (Object)null)
			{
				IDictionary huds = Traverse.Create((object)EnemyHud.instance).Field("m_huds").GetValue<IDictionary>();
				hudHidden = huds == null || !huds.Contains(downed);
			}
			bool pass = samples > 5 && isMarkerFlag && green && hasInteractable && noTombScript && maxPlayerDist < 5f && maxFrameJump < 2f && noRagdolls && corpseColliderOff && hoverRayHitsMarker && !hoverBlockedByCorpse && hoverTextOk && hudHidden;
			Record("client_marker_sync", pass, $"samples={samples} markerFlag={isMarkerFlag} green={green} interactable={hasInteractable} " + $"noTombScript={noTombScript} maxPlayerDist={maxPlayerDist:F2} maxFrameJump={maxFrameJump:F2} noRagdolls={noRagdolls} " + $"corpseColliderOff={corpseColliderOff} hoverRayHitsMarker={hoverRayHitsMarker} " + $"hoverBlockedByCorpse={hoverBlockedByCorpse} hoverTextOk={hoverTextOk} hudHidden={hudHidden}");
		}

		private IEnumerator RunManual()
		{
			yield return ((MonoBehaviour)this).StartCoroutine(AutoStart());
			yield return ((MonoBehaviour)this).StartCoroutine(WaitForPlayerInWorld());
			Log("E2E: manual play mode (" + E2EConfig.Role + ") -- no tests, game is yours");
			while (true)
			{
				yield return null;
			}
		}

		private IEnumerator RunDemo()
		{
			yield return ((MonoBehaviour)this).StartCoroutine(AutoStart());
			yield return ((MonoBehaviour)this).StartCoroutine(WaitForPlayerInWorld());
			Player player = Player.m_localPlayer;
			if ((Object)(object)player == (Object)null)
			{
				Record("demo", pass: false, "no player");
				yield break;
			}
			Log("E2E: local player ready (demo)");
			((MonoBehaviour)this).StartCoroutine(DespawnRavens());
			yield return (object)new WaitForSecondsRealtime(3f);
			if ((Object)(object)GameCamera.instance != (Object)null)
			{
				GameCamera.instance.m_maxDistance = 9f;
				Traverse.Create((object)GameCamera.instance).Field("m_distance").SetValue((object)8f);
			}
			yield return (object)new WaitForSecondsRealtime(2f);
			Log("DEMO: downing");
			GiveTestItem(player);
			((Character)player).SetHealth(0f);
			float w = 0f;
			while (w < 5f && !player.IsDowned())
			{
				w += Time.unscaledDeltaTime;
				yield return null;
			}
			yield return (object)new WaitForSecondsRealtime(3.5f);
			Log("DEMO: channeling revive");
			w = 0f;
			while (w < 12f && player.IsDowned())
			{
				FindMarkerInteractable(player)?.SimulateHold();
				w += Time.unscaledDeltaTime;
				yield return null;
			}
			yield return (object)new WaitForSecondsRealtime(3f);
			Log("DEMO: downing again, letting window run out");
			((Character)player).SetHealth(0f);
			w = 0f;
			while (w < 5f && !player.IsDowned())
			{
				w += Time.unscaledDeltaTime;
				yield return null;
			}
			w = 0f;
			while (w < Plugin.ReviveWindow + 10f && !((Character)player).IsDead())
			{
				w += Time.unscaledDeltaTime;
				yield return null;
			}
			yield return (object)new WaitForSecondsRealtime(5f);
			Record("demo", pass: true, "sequence complete");
		}

		private IEnumerator DespawnRavens()
		{
			while (true)
			{
				Raven[] array = Object.FindObjectsOfType<Raven>();
				foreach (Raven raven in array)
				{
					if ((Object)(object)raven != (Object)null && (Object)(object)ZNetScene.instance != (Object)null)
					{
						ZNetScene.instance.Destroy(((Component)raven).gameObject);
					}
				}
				yield return (object)new WaitForSecondsRealtime(0.5f);
			}
		}

		private IEnumerator RunSingleProcess()
		{
			yield return ((MonoBehaviour)this).StartCoroutine(AutoStart());
			yield return ((MonoBehaviour)this).StartCoroutine(WaitForPlayerInWorld());
			Player player = Player.m_localPlayer;
			if ((Object)(object)player == (Object)null)
			{
				Record("local_player", pass: false, "no local player");
				yield break;
			}
			Log($"E2E: local player ready: {player.GetPlayerName()} hp={((Character)player).GetHealth()}/{((Character)player).GetMaxHealth()}");
			yield return (object)new WaitForSecondsRealtime(2f);
			yield return ((MonoBehaviour)this).StartCoroutine(Test_LethalDamageDowns());
			yield return ((MonoBehaviour)this).StartCoroutine(Test_DownedConstraints());
			yield return ((MonoBehaviour)this).StartCoroutine(Test_MarkerColorGradient());
			yield return ((MonoBehaviour)this).StartCoroutine(Test_HoldProgressAndUI());
			yield return ((MonoBehaviour)this).StartCoroutine(Test_ReviveNoNetworkTick());
			yield return ((MonoBehaviour)this).StartCoroutine(Test_ReviveRestores());
			yield return ((MonoBehaviour)this).StartCoroutine(Test_PressModeRevives());
			yield return ((MonoBehaviour)this).StartCoroutine(Test_DisconnectDeath());
			yield return ((MonoBehaviour)this).StartCoroutine(WaitForAlivePlayer());
			yield return ((MonoBehaviour)this).StartCoroutine(Test_ExpiryKills());
			yield return ((MonoBehaviour)this).StartCoroutine(WaitForAlivePlayer());
			yield return ((MonoBehaviour)this).StartCoroutine(Test_EmptyInventoryCrumbles());
			yield return ((MonoBehaviour)this).StartCoroutine(WaitForAlivePlayer());
			yield return ((MonoBehaviour)this).StartCoroutine(Test_GiveUp());
		}

		private IEnumerator Test_GiveUp()
		{
			Player player = Player.m_localPlayer;
			if ((Object)(object)player == (Object)null || ((Character)player).IsDead())
			{
				Record("give_up_kills", pass: false, "no alive player");
				yield break;
			}
			((Character)player).SetHealth(((Character)player).GetMaxHealth());
			GiveTestItem(player);
			yield return null;
			((Character)player).SetHealth(0f);
			float w = 0f;
			while (w < 5f && !player.IsDowned())
			{
				w += Time.unscaledDeltaTime;
				yield return null;
			}
			if (!player.IsDowned())
			{
				Record("give_up_kills", pass: false, "could not down");
				yield break;
			}
			yield return (object)new WaitForSecondsRealtime(0.5f);
			w = 0f;
			float maxFrac = 0f;
			bool sawRed = false;
			while (w < 6f && !((Character)player).IsDead())
			{
				((Component)player).GetComponent<GiveUp>()?.SimulateHold();
				maxFrac = Mathf.Max(maxFrac, GiveUp.LocalFraction);
				if (ProgressUI.Visible && ProgressUI.GivingUp)
				{
					sawRed = true;
				}
				w += Time.unscaledDeltaTime;
				yield return null;
			}
			yield return (object)new WaitForSecondsRealtime(0.5f);
			bool dead = ((Character)player).IsDead();
			bool notDowned = !player.IsDowned();
			bool realTombstone = (Object)(object)FindRealGraveNear(((Component)player).transform.position, 999f) != (Object)null;
			bool fractionReset = GiveUp.LocalFraction <= 0.01f;
			Record("give_up_kills", dead && notDowned && sawRed && maxFrac > 0.5f && realTombstone && fractionReset, $"dead={dead} notDowned={notDowned} sawRed={sawRed} maxFrac={maxFrac:F2} " + $"realTombstone={realTombstone} fractionReset={fractionReset}");
		}

		private IEnumerator Test_LethalDamageDowns()
		{
			Player player = Player.m_localPlayer;
			((Character)player).SetHealth(0f);
			float waited = 0f;
			while (waited < 5f && !player.IsDowned())
			{
				waited += Time.unscaledDeltaTime;
				yield return null;
			}
			float popVelY = MarkerPrefab.LastPopVelY;
			bool popped = popVelY > 3f;
			yield return (object)new WaitForSecondsRealtime(0.5f);
			bool downed = player.IsDowned();
			bool notDead = !((Character)player).IsDead();
			bool hasController = (Object)(object)((Component)player).GetComponent<DownedStateMachine>() != (Object)null;
			GameObject marker = player.FindDownedMarker();
			bool markerExists = (Object)(object)marker != (Object)null;
			bool markerIsOurPrefab = (Object)(object)marker != (Object)null && (Object)(object)marker.GetComponent<ZNetView>() != (Object)null && ((Object)marker).name.StartsWith("RevivalRevived_DownedMarker") && (Object)(object)marker.GetComponent<TombStone>() == (Object)null;
			bool noRagdolls = Object.FindObjectsOfType<Ragdoll>().Length == 0;
			bool visualHidden = (Object)(object)((Character)player).m_visual != (Object)null && !((Character)player).m_visual.activeSelf;
			bool poofPlayed = PlayerDownedExtensions.LastPoofCount > 0;
			bool poofSmall = PlayerDownedExtensions.LastPoofSourceName.IndexOf("Greyling", StringComparison.OrdinalIgnoreCase) >= 0;
			Record("lethal_damage_downs", downed && notDead && hasController && markerExists && markerIsOurPrefab && noRagdolls && poofPlayed && poofSmall && popped, $"downed={downed} notDead={notDead} controller={hasController} marker={markerExists} " + $"ourPrefab={markerIsOurPrefab} noRagdolls={noRagdolls} visualHidden={visualHidden} " + $"poof={PlayerDownedExtensions.LastPoofCount} poofSrc={PlayerDownedExtensions.LastPoofSourceName} popVelY={popVelY:F1}");
		}

		private IEnumerator Test_MarkerColorGradient()
		{
			Player player = Player.m_localPlayer;
			GameObject marker = player.FindDownedMarker();
			DownedMarker dm = (((Object)(object)marker != (Object)null) ? marker.GetComponent<DownedMarker>() : null);
			if ((Object)(object)dm == (Object)null)
			{
				Record("marker_color_gradient", pass: false, "no DownedMarker");
				yield break;
			}
			float blend0 = dm.CurrentBlend;
			DownedStateMachineView pstate = new DownedStateMachineView(((Character)player).m_nview);
			pstate.DownedTime -= Plugin.ReviveWindow * 0.5f;
			yield return null;
			yield return null;
			float blend1 = dm.CurrentBlend;
			bool startedGreen = blend0 < 0.25f;
			bool progressed = blend1 > blend0 + 0.25f;
			Record("marker_color_gradient", startedGreen && progressed, $"blend0={blend0:F2} blend1={blend1:F2}");
		}

		private IEnumerator Test_DisconnectDeath()
		{
			Player player = Player.m_localPlayer;
			((Character)player).SetHealth(((Character)player).GetMaxHealth());
			GiveTestItem(player);
			yield return null;
			((Character)player).SetHealth(0f);
			float w = 0f;
			while (w < 5f && !player.IsDowned())
			{
				w += Time.unscaledDeltaTime;
				yield return null;
			}
			yield return (object)new WaitForSecondsRealtime(0.5f);
			GameObject m1 = player.FindDownedMarker();
			if ((Object)(object)m1 == (Object)null)
			{
				Record("disconnect_kills_on_reconnect", pass: false, "no marker after down");
				yield break;
			}
			DownedStateMachineView s = new DownedStateMachineView(((Character)player).m_nview)
			{
				Downed = false,
				Marker = ZDOID.None
			};
			((Character)player).SetHealth(((Character)player).GetMaxHealth());
			((Collider)((Character)player).m_collider).enabled = true;
			((Character)player).m_body.isKinematic = false;
			if ((Object)(object)((Character)player).m_visual != (Object)null)
			{
				((Character)player).m_visual.SetActive(true);
			}
			yield return null;
			((Component)player).GetComponent<ReconnectDeathCheck>().RunReconnectCheck();
			w = 0f;
			while (w < 8f && !((Character)player).IsDead())
			{
				w += Time.unscaledDeltaTime;
				yield return null;
			}
			yield return (object)new WaitForSecondsRealtime(0.5f);
			bool dead = ((Character)player).IsDead();
			bool markerGone = (Object)(object)MarkerPrefab.FindFor(player.GetPlayerID()) == (Object)null;
			bool realTombstone = false;
			TombStone[] array = Object.FindObjectsOfType<TombStone>();
			foreach (TombStone t in array)
			{
				ZNetView nv = ((Component)t).GetComponent<ZNetView>();
				if ((Object)(object)nv != (Object)null && nv.IsValid() && !new DownedMarkerView(nv).IsMarker)
				{
					realTombstone = true;
					break;
				}
			}
			bool noRagdolls = Object.FindObjectsOfType<Ragdoll>().Length == 0;
			Record("disconnect_kills_on_reconnect", dead && markerGone && realTombstone && noRagdolls, $"dead={dead} markerGone={markerGone} realTombstone={realTombstone} noRagdolls={noRagdolls}");
			yield return (object)new WaitForSecondsRealtime(0.5f);
		}

		private IEnumerator Test_DownedConstraints()
		{
			Player player = Player.m_localPlayer;
			if (!player.IsDowned())
			{
				Record("downed_constraints", pass: false, "precondition: not downed");
				yield break;
			}
			bool cannotMove = !((Character)player).CanMove();
			bool kinematic = (Object)(object)((Character)player).m_body != (Object)null && ((Character)player).m_body.isKinematic;
			GameObject marker = player.FindDownedMarker();
			bool interactableFound = false;
			bool hoverOk = false;
			bool green = false;
			bool stripped = false;
			bool noEmbers = false;
			bool nameOk = false;
			int disabledFx = 0;
			if ((Object)(object)marker != (Object)null)
			{
				TMP_Text worldText = marker.GetComponentInChildren<TMP_Text>(true);
				nameOk = (Object)(object)worldText != (Object)null && worldText.text == player.GetPlayerName();
				Revivable interactable = marker.GetComponentInChildren<Revivable>();
				interactableFound = (Object)(object)interactable != (Object)null;
				DownedMarker dm = marker.GetComponent<DownedMarker>();
				green = (Object)(object)dm != (Object)null && dm.IsGreen();
				disabledFx = MarkerPrefab.TemplateEffectsRemoved;
				stripped = (Object)(object)marker.GetComponent<TombStone>() == (Object)null && (Object)(object)marker.GetComponent<Container>() == (Object)null;
				noEmbers = disabledFx > 0 && marker.GetComponentsInChildren<ParticleSystem>(true).Length == 0;
				if ((Object)(object)interactable != (Object)null)
				{
					string hover = interactable.GetHoverText();
					hoverOk = !string.IsNullOrEmpty(hover) && hover.IndexOf("Revive", StringComparison.OrdinalIgnoreCase) >= 0;
					Log("E2E[downed_constraints]: hover = \"" + hover.Replace("\n", " | ") + "\"");
				}
			}
			Record("downed_constraints", cannotMove && kinematic && interactableFound && hoverOk && green && stripped && noEmbers && nameOk, $"cannotMove={cannotMove} kinematic={kinematic} interactable={interactableFound} hoverOk={hoverOk} " + $"green={green} stripped={stripped} noEmbers={noEmbers} disabledFx={disabledFx} nameOk={nameOk}");
		}

		private IEnumerator Test_HoldProgressAndUI()
		{
			Player player = Player.m_localPlayer;
			if (!player.IsDowned())
			{
				Record("hold_progress_and_ui", pass: false, "precondition: not downed");
				yield break;
			}
			Revivable interactable = FindMarkerInteractable(player);
			if ((Object)(object)interactable == (Object)null)
			{
				Record("hold_progress_and_ui", pass: false, "no marker interactable");
				yield break;
			}
			float remainingBefore = player.GetDownedRemainingTime();
			float t = 0f;
			float maxProg = 0f;
			float maxFill = 0f;
			float firstProgressAt = -1f;
			bool uiSeen = false;
			while (t < 1.5f)
			{
				interactable.SimulateHold();
				float prog = player.GetReviveProgress();
				if (prog > 0.05f && firstProgressAt < 0f)
				{
					firstProgressAt = t;
				}
				maxProg = Mathf.Max(maxProg, prog);
				if (ProgressUI.Visible)
				{
					uiSeen = true;
					maxFill = Mathf.Max(maxFill, ProgressUI.Fill);
				}
				t += Time.unscaledDeltaTime;
				yield return null;
			}
			bool responsive = firstProgressAt >= 0f && firstProgressAt < 0.6f;
			float remainingAfter = player.GetDownedRemainingTime();
			bool windowPaused = remainingBefore - remainingAfter < 0.6f;
			bool partial = maxProg > 0.05f && maxProg < 0.9f;
			bool stillDowned = player.IsDowned();
			t = 0f;
			while (t < 3f)
			{
				t += Time.unscaledDeltaTime;
				yield return null;
			}
			bool decayed = player.GetReviveProgress() <= 0.01f;
			bool uiHidden = !ProgressUI.Visible;
			Record("hold_progress_and_ui", partial && stillDowned && uiSeen && decayed && uiHidden && windowPaused && responsive, $"maxProg={maxProg:F2} partial={partial} stillDowned={stillDowned} uiSeen={uiSeen} " + $"maxFill={maxFill:F2} decayed={decayed} uiHidden={uiHidden} " + $"windowPaused={windowPaused} remBefore={remainingBefore:F1} remAfter={remainingAfter:F1} " + $"responsive={responsive} firstProgressAt={firstProgressAt:F2}");
		}

		private IEnumerator Test_ReviveNoNetworkTick()
		{
			Player player = Player.m_localPlayer;
			if (!player.IsDowned())
			{
				Record("revive_no_network_tick", pass: false, "precondition: not downed");
				yield break;
			}
			Revivable interactable = FindMarkerInteractable(player);
			GameObject marker = player.FindDownedMarker();
			ZNetView nview = (((Object)(object)marker != (Object)null) ? marker.GetComponent<ZNetView>() : null);
			if ((Object)(object)interactable == (Object)null || (Object)(object)nview == (Object)null || !nview.IsValid())
			{
				Record("revive_no_network_tick", pass: false, "no marker/interactable");
				yield break;
			}
			float t = 0f;
			while (t < 0.6f)
			{
				interactable.SimulateHold();
				t += Time.unscaledDeltaTime;
				yield return null;
			}
			ReviveChannelDecayingProgressView anchor0 = new ReviveChannelDecayingProgressView(nview);
			float anchorTime0 = anchor0.AnchorTime;
			float anchorSeconds0 = anchor0.AnchorSeconds;
			float progAtBaseline = player.GetReviveProgress();
			int frames = 0;
			t = 0f;
			while (t < 1.5f)
			{
				interactable.SimulateHold();
				frames++;
				t += Time.unscaledDeltaTime;
				yield return null;
			}
			ReviveChannelDecayingProgressView anchor1 = new ReviveChannelDecayingProgressView(nview);
			float progAfter = player.GetReviveProgress();
			bool progressRose = progAfter > progAtBaseline + 0.1f;
			bool anchorStable = Mathf.Abs(anchor1.AnchorTime - anchorTime0) < 0.01f && Mathf.Abs(anchor1.AnchorSeconds - anchorSeconds0) < 0.01f;
			bool stillDowned = player.IsDowned();
			Record("revive_no_network_tick", progressRose && anchorStable && stillDowned, $"progressRose={progressRose} ({progAtBaseline:F2}->{progAfter:F2}) " + $"anchorStable={anchorStable} (t {anchorTime0:F2}->{anchor1.AnchorTime:F2}) frames={frames} stillDowned={stillDowned}");
		}

		private IEnumerator Test_PressModeRevives()
		{
			Player player = Player.m_localPlayer;
			Plugin.ReviveModeCfg.Value = ReviveModeType.Press;
			((Character)player).SetHealth(((Character)player).GetMaxHealth());
			yield return null;
			((Character)player).SetHealth(0f);
			float w = 0f;
			while (w < 5f && !player.IsDowned())
			{
				w += Time.unscaledDeltaTime;
				yield return null;
			}
			if (!player.IsDowned())
			{
				Plugin.ReviveModeCfg.Value = ReviveModeType.Hold;
				Record("press_mode_revives", pass: false, "could not down");
				yield break;
			}
			yield return (object)new WaitForSecondsRealtime(0.5f);
			FindMarkerInteractable(player)?.SimulateHold();
			w = 0f;
			while (w < 3f && player.IsDowned())
			{
				w += Time.unscaledDeltaTime;
				yield return null;
			}
			yield return (object)new WaitForSecondsRealtime(0.5f);
			bool revived = !player.IsDowned() && !((Character)player).IsDead() && ((Character)player).GetHealth() > 0f;
			bool markerGone = (Object)(object)MarkerPrefab.FindFor(player.GetPlayerID()) == (Object)null;
			Plugin.ReviveModeCfg.Value = ReviveModeType.Hold;
			Record("press_mode_revives", revived && markerGone, $"revived={revived} hp={((Character)player).GetHealth():F0} markerGone={markerGone}");
		}

		private IEnumerator Test_ReviveRestores()
		{
			Player player = Player.m_localPlayer;
			if (!player.IsDowned())
			{
				Record("revive_restores", pass: false, "precondition: not downed");
				yield break;
			}
			GameObject markerBefore = player.FindDownedMarker();
			int crumblesBefore = DownedMarker.CrumbleEvents;
			player.ReviveFromDowned(0L);
			yield return (object)new WaitForSecondsRealtime(0.5f);
			bool canMove = false;
			float waitMove = 0f;
			while (waitMove < 3f)
			{
				canMove = ((Character)player).CanMove();
				if (canMove)
				{
					break;
				}
				waitMove += Time.unscaledDeltaTime;
				yield return null;
			}
			bool notDowned = !player.IsDowned();
			bool healthy = ((Character)player).GetHealth() > 0f;
			bool visualBack = (Object)(object)((Character)player).m_visual != (Object)null && ((Character)player).m_visual.activeSelf;
			bool collider = (Object)(object)((Character)player).m_collider != (Object)null && ((Collider)((Character)player).m_collider).enabled;
			bool notKinematic = (Object)(object)((Character)player).m_body != (Object)null && !((Character)player).m_body.isKinematic;
			yield return (object)new WaitForSecondsRealtime(0.5f);
			bool markerGone = (Object)(object)markerBefore == (Object)null || !Object.op_Implicit((Object)(object)markerBefore);
			bool crumbled = DownedMarker.CrumbleEvents > crumblesBefore;
			Record("revive_restores", notDowned && healthy && canMove && visualBack && collider && notKinematic && markerGone && crumbled, $"notDowned={notDowned} hp={((Character)player).GetHealth():F0} canMove={canMove} " + $"visualBack={visualBack} collider={collider} notKinematic={notKinematic} markerGone={markerGone} crumbled={crumbled}");
		}

		private IEnumerator Test_ExpiryKills()
		{
			Player player = Player.m_localPlayer;
			if (((Character)player).IsDead())
			{
				Record("expiry_kills_real_tombstone", pass: false, "already dead");
				yield break;
			}
			((Character)player).SetHealth(((Character)player).GetMaxHealth());
			GiveTestItem(player);
			yield return null;
			((Character)player).SetHealth(0f);
			float waited = 0f;
			while (waited < 5f && !player.IsDowned())
			{
				waited += Time.unscaledDeltaTime;
				yield return null;
			}
			if (!player.IsDowned())
			{
				Record("expiry_kills_real_tombstone", pass: false, "could not re-down");
				yield break;
			}
			yield return (object)new WaitForSecondsRealtime(0.5f);
			GameObject markerBefore = player.FindDownedMarker();
			Vector3 markerPos = (((Object)(object)markerBefore != (Object)null) ? markerBefore.transform.position : Vector3.zero);
			DownedStateMachineView s = new DownedStateMachineView(((Character)player).m_nview)
			{
				DownedTime = (float)ZNet.instance.GetTimeSeconds() - Plugin.ReviveWindow - 5f
			};
			waited = 0f;
			while (waited < 10f && !((Character)player).IsDead())
			{
				((Character)player).SetHealth(0f);
				waited += Time.unscaledDeltaTime;
				yield return null;
			}
			bool dead = ((Character)player).IsDead();
			bool notDowned = !player.IsDowned();
			bool corpseInert = dead && (Object)(object)((Character)player).m_collider != (Object)null && !((Collider)((Character)player).m_collider).enabled;
			bool gapless = true;
			bool markerGone = (Object)(object)markerBefore == (Object)null || !Object.op_Implicit((Object)(object)markerBefore);
			float hw = 0f;
			while (hw < 9f && !markerGone)
			{
				bool visible = false;
				Renderer[] componentsInChildren = markerBefore.GetComponentsInChildren<Renderer>();
				foreach (Renderer r in componentsInChildren)
				{
					if (r.enabled)
					{
						visible = true;
						break;
					}
				}
				if (!visible && (Object)(object)FindRealGraveNear(markerPos, 3f) == (Object)null)
				{
					gapless = false;
				}
				hw += Time.unscaledDeltaTime;
				yield return null;
				markerGone = (Object)(object)markerBefore == (Object)null || !Object.op_Implicit((Object)(object)markerBefore);
			}
			bool realTombstone = false;
			bool inPlace = false;
			bool noPop = false;
			bool embersOnGrave = false;
			TombStone grave = FindRealGraveNear(markerPos, 999f);
			if ((Object)(object)grave != (Object)null)
			{
				realTombstone = true;
				inPlace = markerPos != Vector3.zero && Vector3.Distance(((Component)grave).transform.position, markerPos) < 2f;
				Rigidbody rb = ((Component)grave).GetComponent<Rigidbody>();
				noPop = (Object)(object)rb == (Object)null || rb.linearVelocity.y < 1f;
				embersOnGrave = ((Component)grave).GetComponentsInChildren<ParticleSystem>(false).Length != 0;
			}
			bool noRagdolls = Object.FindObjectsOfType<Ragdoll>().Length == 0;
			Record("expiry_kills_real_tombstone", dead && notDowned && corpseInert && markerGone && gapless && realTombstone && inPlace && noPop && embersOnGrave && noRagdolls, $"dead={dead} notDowned={notDowned} corpseInert={corpseInert} markerGone={markerGone} gapless={gapless} " + $"realTombstone={realTombstone} inPlace={inPlace} noPop={noPop} embersOnGrave={embersOnGrave} noRagdolls={noRagdolls}");
		}

		private static TombStone? FindRealGraveNear(Vector3 pos, float maxDist)
		{
			//IL_0076: Unknown result type (might be due to invalid IL or missing references)
			//IL_007b: Unknown result type (might be due to invalid IL or missing references)
			TombStone result = null;
			float num = maxDist;
			TombStone[] array = Object.FindObjectsOfType<TombStone>();
			foreach (TombStone val in array)
			{
				ZNetView component = ((Component)val).GetComponent<ZNetView>();
				if (!((Object)(object)component == (Object)null) && component.IsValid() && (!((Object)(object)component != (Object)null) || !component.IsValid() || !new DownedMarkerView(component).IsMarker))
				{
					float num2 = Vector3.Distance(((Component)val).transform.position, pos);
					if (num2 <= num)
					{
						num = num2;
						result = val;
					}
				}
			}
			return result;
		}

		private IEnumerator Test_EmptyInventoryCrumbles()
		{
			Player player = Player.m_localPlayer;
			if ((Object)(object)player == (Object)null || ((Character)player).IsDead())
			{
				Record("empty_death_crumbles_marker", pass: false, "no alive player");
				yield break;
			}
			((Character)player).SetHealth(((Character)player).GetMaxHealth());
			((Humanoid)player).GetInventory().RemoveAll();
			yield return null;
			((Character)player).SetHealth(0f);
			float w = 0f;
			while (w < 5f && !player.IsDowned())
			{
				w += Time.unscaledDeltaTime;
				yield return null;
			}
			if (!player.IsDowned())
			{
				Record("empty_death_crumbles_marker", pass: false, "could not down");
				yield break;
			}
			yield return (object)new WaitForSecondsRealtime(0.5f);
			GameObject marker = player.FindDownedMarker();
			if ((Object)(object)marker == (Object)null)
			{
				Record("empty_death_crumbles_marker", pass: false, "no marker");
				yield break;
			}
			_ = marker.transform.position;
			int gravesBefore = CountRealGraves();
			DownedStateMachineView s = new DownedStateMachineView(((Character)player).m_nview)
			{
				DownedTime = (float)ZNet.instance.GetTimeSeconds() - Plugin.ReviveWindow - 5f
			};
			w = 0f;
			while (w < 10f && !((Character)player).IsDead())
			{
				((Character)player).SetHealth(0f);
				w += Time.unscaledDeltaTime;
				yield return null;
			}
			yield return (object)new WaitForSecondsRealtime(1f);
			bool dead = ((Character)player).IsDead();
			bool markerGone = (Object)(object)marker == (Object)null || !Object.op_Implicit((Object)(object)marker);
			bool crumbled = DownedMarker.LastCrumbleEffectCount > 0;
			bool noNewGrave = CountRealGraves() == gravesBefore;
			bool pendingCleared = !new GraveReplaceView(((Character)player).m_nview).Pending;
			Record("empty_death_crumbles_marker", dead && markerGone && crumbled && noNewGrave && pendingCleared, $"dead={dead} markerGone={markerGone} crumbleEffects={DownedMarker.LastCrumbleEffectCount} " + $"noNewGrave={noNewGrave} (before={gravesBefore}) pendingCleared={pendingCleared}");
		}

		private static int CountRealGraves()
		{
			int num = 0;
			TombStone[] array = Object.FindObjectsOfType<TombStone>();
			foreach (TombStone val in array)
			{
				ZNetView component = ((Component)val).GetComponent<ZNetView>();
				if (!((Object)(object)component == (Object)null) && component.IsValid() && (!((Object)(object)component != (Object)null) || !component.IsValid() || !new DownedMarkerView(component).IsMarker))
				{
					num++;
				}
			}
			return num;
		}

		private static void GiveTestItem(Player player)
		{
			try
			{
				if (!((Object)(object)ObjectDB.instance == (Object)null))
				{
					GameObject itemPrefab = ObjectDB.instance.GetItemPrefab("Wood");
					if (!((Object)(object)itemPrefab == (Object)null))
					{
						((Humanoid)player).GetInventory().AddItem(itemPrefab, 1);
					}
				}
			}
			catch (Exception ex)
			{
				Log("E2E: GiveTestItem failed: " + ex.Message);
			}
		}

		private IEnumerator AutoStart()
		{
			Log("E2E: waiting for FejdStartup (main menu)...");
			float waited = 0f;
			while ((Object)(object)FejdStartup.instance == (Object)null && (Object)(object)Game.instance == (Object)null)
			{
				waited += Time.unscaledDeltaTime;
				if (waited > 120f)
				{
					Log("E2E: FejdStartup never appeared");
					yield break;
				}
				yield return null;
			}
			if ((Object)(object)Game.instance != (Object)null)
			{
				Log("E2E: Game already running");
				yield break;
			}
			yield return (object)new WaitForSecondsRealtime(3f);
			if (_worldStartIssued)
			{
				yield break;
			}
			_worldStartIssued = true;
			Exception err = null;
			try
			{
				FejdStartup fejd = FejdStartup.instance;
				if (E2EConfig.IsHost)
				{
					PlayerProfile profile = GetOrCreateProfile(Environment.GetEnvironmentVariable("RR_E2E_PROFILE") ?? "e2e_host", Environment.GetEnvironmentVariable("RR_E2E_CHARNAME") ?? "E2EHost");
					Game.SetProfile(profile.GetFilename(), profile.m_fileSource);
					World world = World.GetCreateWorld(E2EConfig.WorldName, (FileSource)2);
					ZNet.m_onlineBackend = (OnlineBackendType)3;
					ZNet.SetServer(true, true, false, world.m_name, "", world);
					ZNet.ResetServerHost();
					Log($"E2E[host]: world '{world.m_name}', CustomSocket port {E2EConfig.Port}, loading scene");
				}
				else if (E2EConfig.IsClient)
				{
					PlayerProfile profile2 = GetOrCreateProfile(Environment.GetEnvironmentVariable("RR_E2E_PROFILE") ?? "e2e_client", Environment.GetEnvironmentVariable("RR_E2E_CHARNAME") ?? "E2EClient");
					Game.SetProfile(profile2.GetFilename(), profile2.m_fileSource);
					ZNet.m_onlineBackend = (OnlineBackendType)3;
					ZNet.SetServer(false, false, false, "", "", (World)null);
					ZNet.m_serverHost = E2EConfig.ServerHost;
					ZNet.m_serverHostPort = E2EConfig.Port;
					Log($"E2E[client]: connecting to {E2EConfig.ServerHost}:{E2EConfig.Port}, loading scene");
				}
				else
				{
					PlayerProfile profile3 = GetOrCreateProfile("e2e_solo", "E2ESolo");
					Game.SetProfile(profile3.GetFilename(), profile3.m_fileSource);
					string soloWorldName = Environment.GetEnvironmentVariable("RR_E2E_WORLD") ?? "e2e_world";
					World world2 = World.GetCreateWorld(soloWorldName, (FileSource)2);
					ZNet.m_onlineBackend = (OnlineBackendType)0;
					ZNet.SetServer(true, false, false, world2.m_name, "", world2);
					ZNet.ResetServerHost();
					Log("E2E[solo]: world '" + world2.m_name + "', loading scene");
				}
				Traverse.Create((object)fejd).Field("m_startingWorld").SetValue((object)true);
				Traverse.Create((object)fejd).Method("LoadMainScene", Array.Empty<object>()).GetValue();
			}
			catch (Exception ex)
			{
				Exception e = ex;
				err = e;
			}
			if (err != null)
			{
				Log("E2E: AutoStart error: " + err);
			}
		}

		private static PlayerProfile GetOrCreateProfile(string filename, string charName)
		{
			//IL_0059: Unknown result type (might be due to invalid IL or missing references)
			//IL_005f: Expected O, but got Unknown
			foreach (PlayerProfile allPlayerProfile in SaveSystem.GetAllPlayerProfiles())
			{
				if (allPlayerProfile.GetFilename() == filename)
				{
					allPlayerProfile.m_firstSpawn = false;
					allPlayerProfile.Save();
					return allPlayerProfile;
				}
			}
			PlayerProfile val = new PlayerProfile(filename, (FileSource)2);
			val.SetName(charName);
			val.m_firstSpawn = false;
			val.Save();
			return val;
		}

		private IEnumerator WaitForAlivePlayer()
		{
			float w = 0f;
			while (w < 30f)
			{
				Player p = Player.m_localPlayer;
				if ((Object)(object)p != (Object)null && (Object)(object)((Character)p).m_nview != (Object)null && ((Character)p).m_nview.IsValid() && !((Character)p).IsDead() && ((Character)p).GetHealth() > 0f)
				{
					yield return (object)new WaitForSecondsRealtime(0.5f);
					yield break;
				}
				w += Time.unscaledDeltaTime;
				yield return null;
			}
			Log("E2E: WaitForAlivePlayer timed out");
		}

		private IEnumerator WaitForPlayerInWorld()
		{
			Log("E2E: waiting for player to spawn in world...");
			float waited = 0f;
			while (true)
			{
				Player p = Player.m_localPlayer;
				if ((Object)(object)p != (Object)null && (Object)(object)((Component)p).GetComponent<ZNetView>() != (Object)null && ((Component)p).GetComponent<ZNetView>().IsValid() && (Object)(object)ZNetScene.instance != (Object)null)
				{
					yield return null;
					yield break;
				}
				waited += Time.unscaledDeltaTime;
				if (waited > 240f)
				{
					break;
				}
				yield return null;
			}
			Log("E2E: timed out waiting for player spawn");
		}

		private static Revivable? FindInteractableViaHoverRay(Player downed, Player me)
		{
			//IL_0023: Unknown result type (might be due to invalid IL or missing references)
			//IL_0028: Unknown result type (might be due to invalid IL or missing references)
			//IL_0032: Unknown result type (might be due to invalid IL or missing references)
			//IL_0037: Unknown result type (might be due to invalid IL or missing references)
			//IL_003c: Unknown result type (might be due to invalid IL or missing references)
			//IL_003d: Unknown result type (might be due to invalid IL or missing references)
			//IL_004d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0052: Unknown result type (might be due to invalid IL or missing references)
			//IL_0057: Unknown result type (might be due to invalid IL or missing references)
			//IL_0058: Unknown result type (might be due to invalid IL or missing references)
			//IL_0059: Unknown result type (might be due to invalid IL or missing references)
			//IL_005a: Unknown result type (might be due to invalid IL or missing references)
			//IL_005b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0060: Unknown result type (might be due to invalid IL or missing references)
			//IL_0064: Unknown result type (might be due to invalid IL or missing references)
			GameObject val = downed.FindDownedMarker();
			if ((Object)(object)val == (Object)null)
			{
				return null;
			}
			Vector3 val2 = val.transform.position + Vector3.up * 0.3f;
			Vector3 val3 = val2 + new Vector3(1.8f, 1.2f, 0f);
			Vector3 val4 = val2 - val3;
			RaycastHit[] array = Physics.RaycastAll(val3, ((Vector3)(ref val4)).normalized, 6f, me.m_interactMask);
			if (array.Length == 0)
			{
				return null;
			}
			Array.Sort(array, (RaycastHit a, RaycastHit b) => ((RaycastHit)(ref a)).distance.CompareTo(((RaycastHit)(ref b)).distance));
			return ((Component)((RaycastHit)(ref array[0])).collider).GetComponentInParent<Revivable>();
		}

		private static Revivable? FindMarkerInteractable(Player downed)
		{
			GameObject val = downed.FindDownedMarker();
			return ((Object)(object)val != (Object)null) ? val.GetComponentInChildren<Revivable>() : null;
		}

		private static Player? FindDownedRemotePlayer(Player me)
		{
			foreach (Player allPlayer in Player.GetAllPlayers())
			{
				if ((Object)(object)allPlayer == (Object)null || (Object)(object)allPlayer == (Object)(object)me || !allPlayer.IsDowned())
				{
					continue;
				}
				return allPlayer;
			}
			return null;
		}

		private void Record(string name, bool pass, string detail)
		{
			_results.Add((name, pass, detail));
			Log("E2E_RESULT: " + (pass ? "PASS" : "FAIL") + " " + name + " -- " + detail);
		}

		private static void Log(string msg)
		{
			E2ELog.Write(msg);
		}

		private void Finish(bool ok, string reason)
		{
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.AppendLine("overall=" + (ok ? "PASS" : "FAIL") + " role=" + E2EConfig.Role + " reason=" + reason);
			foreach (var result in _results)
			{
				stringBuilder.AppendLine((result.pass ? "PASS" : "FAIL") + "\t" + result.name + "\t" + result.detail);
			}
			string text = stringBuilder.ToString();
			try
			{
				File.WriteAllText(ResultPath, text);
				Log("E2E: results written to " + ResultPath);
			}
			catch (Exception ex)
			{
				Log("E2E: failed to write results: " + ex);
			}
			Log("E2E_SUMMARY_BEGIN\n" + text + "E2E_SUMMARY_END");
			Log("E2E: DONE overall=" + (ok ? "PASS" : "FAIL") + " -- quitting");
			((MonoBehaviour)this).StartCoroutine(QuitSoon());
		}

		private IEnumerator QuitSoon()
		{
			yield return (object)new WaitForSecondsRealtime(1f);
			Application.Quit();
			yield return (object)new WaitForSecondsRealtime(2f);
			Process.GetCurrentProcess().Kill();
		}
	}
	[HarmonyPatch(typeof(SteamUtils), "IsSteamRunningOnSteamDeck")]
	internal static class SteamStartupPatch
	{
		private static bool Prefix(ref bool __result)
		{
			__result = false;
			return false;
		}
	}
}
namespace ReviveAllies.Components
{
	public static class DownedKeys
	{
		public const string RpcChannel = "RevivalRevived_Channel";
	}
	public class DownedMarker : MonoBehaviour
	{
		private struct TintedMaterial
		{
			public Material Mat;

			public bool HasEmission;

			public Color OrigEmission;

			public float EmissionIntensity;

			public bool HasColor;

			public Color OrigColor;
		}

		public static readonly Color ReviveGreen = new Color(0.25f, 1f, 0.35f);

		private static EffectList? s_crumbleEffect;

		private static readonly int EmissionColorId = Shader.PropertyToID("_EmissionColor");

		private static readonly int ColorId = Shader.PropertyToID("_Color");

		private readonly List<TintedMaterial> m_tinted = new List<TintedMaterial>();

		private readonly List<(Light light, Color orig)> m_lights = new List<(Light, Color)>();

		private ZNetView? m_nview;

		private float m_replacedSince = -1f;

		private bool m_hiddenForReplace;

		private const float ReplaceHideTimeout = 4f;

		private const float ReplaceDestroyDelay = 5f;

		private const float ReplaceDestroyFailsafe = 10f;

		public static int LastCrumbleEffectCount { get; private set; }

		public static int CrumbleEvents { get; private set; }

		public int TintedLights => m_lights.Count;

		public int TintedMaterials => m_tinted.Count;

		public float CurrentBlend { get; private set; }

		public bool HiddenForReplace => m_hiddenForReplace;

		public static void MarkReplaced(GameObject? marker)
		{
			ZNetView val = Owned(marker);
			if (!((Object)(object)val == (Object)null))
			{
				DownedMarkerView downedMarkerView = new DownedMarkerView(val);
				downedMarkerView.ReplacedByGrave = true;
			}
		}

		public static void Crumble(GameObject? marker)
		{
			//IL_0024: Unknown result type (might be due to invalid IL or missing references)
			//IL_002f: Unknown result type (might be due to invalid IL or missing references)
			//IL_003d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0043: Unknown result type (might be due to invalid IL or missing references)
			if (!((Object)(object)marker == (Object)null))
			{
				EffectList val = GraveCrumbleEffect();
				if (val != null)
				{
					GameObject[] array = val.Create(marker.transform.position, marker.transform.rotation, (Transform)null, 1f, -1, default(ZDOID));
					LastCrumbleEffectCount = ((array != null) ? array.Length : 0);
					CrumbleEvents++;
				}
				DestroyMarker(marker);
			}
		}

		public static void CrumbleLinked(ZNetView playerNview)
		{
			//IL_000a: Unknown result type (might be due to invalid IL or missing references)
			//IL_000f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0010: Unknown result type (might be due to invalid IL or missing references)
			//IL_0011: Unknown result type (might be due to invalid IL or missing references)
			//IL_002b: Unknown result type (might be due to invalid IL or missing references)
			//IL_003b: Unknown result type (might be due to invalid IL or missing references)
			ZDOID marker = new DownedStateMachineView(playerNview).Marker;
			if (!(marker == ZDOID.None))
			{
				DownedStateMachineView downedStateMachineView = new DownedStateMachineView(playerNview);
				downedStateMachineView.Marker = ZDOID.None;
				Crumble(ZNetScene.instance.FindInstance(marker));
			}
		}

		public static void DestroyMarker(GameObject? marker)
		{
			ZNetView val = Owned(marker);
			if (val != null)
			{
				val.Destroy();
			}
		}

		private static ZNetView? Owned(GameObject? marker)
		{
			ZNetView val = (((Object)(object)marker != (Object)null) ? marker.GetComponent<ZNetView>() : null);
			if ((Object)(object)val == (Object)null || !val.IsValid())
			{
				return null;
			}
			if (!val.IsOwner())
			{
				val.ClaimOwnership();
			}
			return val;
		}

		private static EffectList? GraveCrumbleEffect()
		{
			if (s_crumbleEffect != null)
			{
				return s_crumbleEffect;
			}
			GameObject val = (((Object)(object)ZNetScene.instance != (Object)null) ? MarkerPrefab.FindTombstonePrefab(ZNetScene.instance) : null);
			TombStone val2 = (((Object)(object)val != (Object)null) ? val.GetComponent<TombStone>() : null);
			if ((Object)(object)val2 != (Object)null && val2.m_removeEffect != null && val2.m_removeEffect.m_effectPrefabs.Length != 0)
			{
				s_crumbleEffect = val2.m_removeEffect;
			}
			else
			{
				Plugin.Logger.LogWarning((object)"DownedMarker: no grave crumble effect found");
			}
			return s_crumbleEffect;
		}

		private void Awake()
		{
			m_nview = ((Component)this).GetComponent<ZNetView>();
			CaptureAccents();
			ApplyBlend(0f);
		}

		private void Start()
		{
			if ((Object)(object)m_nview == (Object)null || !m_nview.IsValid())
			{
				return;
			}
			string ownerName = new DownedMarkerView(m_nview).OwnerName;
			if (!string.IsNullOrEmpty(ownerName))
			{
				TMP_Text componentInChildren = ((Component)this).GetComponentInChildren<TMP_Text>(true);
				if ((Object)(object)componentInChildren != (Object)null)
				{
					componentInChildren.text = ownerName;
				}
			}
		}

		private void Update()
		{
			//IL_0051: Unknown result type (might be due to invalid IL or missing references)
			//IL_0056: Unknown result type (might be due to invalid IL or missing references)
			//IL_0057: Unknown result type (might be due to invalid IL or missing references)
			//IL_0058: Unknown result type (might be due to invalid IL or missing references)
			//IL_006c: Unknown result type (might be due to invalid IL or missing references)
			if (!((Object)(object)m_nview == (Object)null) && m_nview.IsValid())
			{
				DownedMarkerView downedMarkerView = new DownedMarkerView(m_nview);
				if (downedMarkerView.ReplacedByGrave)
				{
					UpdateReplaced();
					return;
				}
				ZDOID linkedPlayer = downedMarkerView.LinkedPlayer;
				ZDO val = ((linkedPlayer != ZDOID.None) ? ZDOMan.instance.GetZDO(linkedPlayer) : null);
				float num = ((val != null) ? new DownedStateMachineView(val).DownedTime : downedMarkerView.DownedTime);
				float num2 = (float)ZNet.instance.GetTimeSeconds() - num;
				ApplyBlend(Mathf.Clamp01(num2 / Plugin.ReviveWindow));
			}
		}

		private void CaptureAccents()
		{
			//IL_001b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0097: Unknown result type (might be due to invalid IL or missing references)
			//IL_009c: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ee: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f3: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f5: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bd: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bf: Unknown result type (might be due to invalid IL or missing references)
			//IL_0103: Unknown result type (might be due to invalid IL or missing references)
			//IL_010a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0119: Unknown result type (might be due to invalid IL or missing references)
			//IL_0120: Unknown result type (might be due to invalid IL or missing references)
			//IL_0143: Unknown result type (might be due to invalid IL or missing references)
			//IL_0145: Unknown result type (might be due to invalid IL or missing references)
			Light[] componentsInChildren = ((Component)this).GetComponentsInChildren<Light>(true);
			foreach (Light val in componentsInChildren)
			{
				m_lights.Add((val, val.color));
			}
			Renderer[] componentsInChildren2 = ((Component)this).GetComponentsInChildren<Renderer>(true);
			foreach (Renderer val2 in componentsInChildren2)
			{
				Material[] materials = val2.materials;
				foreach (Material val3 in materials)
				{
					TintedMaterial item = new TintedMaterial
					{
						Mat = val3
					};
					if (val3.HasProperty(EmissionColorId))
					{
						Color color = val3.GetColor(EmissionColorId);
						if (((Color)(ref color)).maxColorComponent > 0.05f)
						{
							item.HasEmission = true;
							item.OrigEmission = color;
							item.EmissionIntensity = ((Color)(ref color)).maxColorComponent;
						}
					}
					if (val3.HasProperty(ColorId))
					{
						Color color2 = val3.GetColor(ColorId);
						if (color2.r > 0.4f && color2.r > color2.g * 1.5f && color2.r > color2.b * 1.5f)
						{
							item.HasColor = true;
							item.OrigColor = color2;
						}
					}
					if (item.HasEmission || item.HasColor)
					{
						m_tinted.Add(item);
					}
				}
			}
		}

		private void ApplyBlend(float blend)
		{
			//IL_0041: Unknown result type (might be due to invalid IL or missing references)
			//IL_004c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0051: Unknown result type (might be due to invalid IL or missing references)
			//IL_005e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0061: Unknown result type (might be due to invalid IL or missing references)
			//IL_0067: Unknown result type (might be due to invalid IL or missing references)
			//IL_009c: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f6: Unknown result type (might be due to invalid IL or missing references)
			//IL_0109: Unknown result type (might be due to invalid IL or missing references)
			//IL_010e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0111: Unknown result type (might be due to invalid IL or missing references)
			CurrentBlend = blend;
			foreach (TintedMaterial item in m_tinted)
			{
				if (!((Object)(object)item.Mat == (Object)null))
				{
					if (item.HasEmission)
					{
						Color val = ReviveGreen * item.EmissionIntensity;
						item.Mat.SetColor(EmissionColorId, Color.Lerp(val, item.OrigEmission, blend));
						item.Mat.EnableKeyword("_EMISSION");
					}
					if (item.HasColor)
					{
						item.Mat.SetColor(ColorId, Color.Lerp(ReviveGreen, item.OrigColor, blend));
					}
				}
			}
			foreach (var (val2, val3) in m_lights)
			{
				if ((Object)(object)val2 != (Object)null)
				{
					val2.color = Color.Lerp(ReviveGreen, val3, blend);
				}
			}
		}

		public bool IsGreen()
		{
			return (TintedLights > 0 || TintedMaterials > 0) && CurrentBlend < 0.5f;
		}

		private void UpdateReplaced()
		{
			if (m_replacedSince < 0f)
			{
				m_replacedSince = Time.time;
			}
			float num = Time.time - m_replacedSince;
			if (!m_hiddenForReplace && (num > 4f || GraveNearby()))
			{
				HideLocally();
			}
			if (m_nview.IsOwner())
			{
				if (num > 5f)
				{
					m_nview.Destroy();
				}
			}
			else if (num > 10f)
			{
				m_nview.ClaimOwnership();
				m_nview.Destroy();
			}
		}

		private bool GraveNearby()
		{
			//IL_0017: Unknown result type (might be due to invalid IL or missing references)
			//IL_0022: Unknown result type (might b