Decompiled source of BetterReviveExperience v0.2.11

BepInEx/plugins/BetterReviveExperience/BetterReviveExperience.dll

Decompiled 12 hours ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using BetterReviveExperience.Patches;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Photon.Pun;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("BetterReviveExperience")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.2.11.0")]
[assembly: AssemblyInformationalVersion("0.2.11+66adfeef35dac47a63cec6a402c0791b90ba9c85")]
[assembly: AssemblyProduct("BetterReviveExperience")]
[assembly: AssemblyTitle("BetterReviveExperience")]
[assembly: AssemblyVersion("0.2.11.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[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 BetterReviveExperience
{
	[BepInPlugin("com.mods.betterreviveexperience", "BetterReviveExperience", "0.2.11")]
	[BepInDependency("nickklmao.repoconfig", "1.2.6")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string PLUGIN_GUID = "com.mods.betterreviveexperience";

		public const string PLUGIN_NAME = "BetterReviveExperience";

		public const string PLUGIN_VERSION = "0.2.11";

		private const int ReviveCostStep = 1000;

		private const int ReviveCostMaximum = 100000;

		private static readonly string[] HeldHeadReviveKeyOptions = new string[4] { "H", "R", "Y", "F" };

		private static readonly string[] ReviveCostOptions = BuildReviveCostOptions();

		private Harmony _harmony;

		public static ManualLogSource Log { get; private set; }

		public static ConfigEntry<bool> KeepItemsOnDeath { get; private set; }

		public static ConfigEntry<string> ReviveTrigger { get; private set; }

		public static ConfigEntry<string> ReviveCost { get; private set; }

		public static ConfigEntry<int> ReviveHealthPercent { get; private set; }

		public static ConfigEntry<bool> EnableHeldHeadRevive { get; private set; }

		public static ConfigEntry<string> HeldHeadReviveKey { get; private set; }

		public static ConfigEntry<bool> EnableCartRevive { get; private set; }

		public static ConfigEntry<bool> EnableShopRevive { get; private set; }

		public static ConfigEntry<bool> DebugLogging { get; private set; }

		public static int ReviveCostAmount
		{
			get
			{
				if (!int.TryParse(ReviveCost.Value, out var result))
				{
					return 0;
				}
				return Mathf.Clamp(result, 0, 100000);
			}
		}

		public static ReviveMode CurrentReviveMode
		{
			get
			{
				if (!Enum.TryParse<ReviveMode>(ReviveTrigger.Value, ignoreCase: true, out var result))
				{
					return ReviveMode.Disabled;
				}
				return result;
			}
		}

		public static KeyCode HeldHeadReviveKeyCode
		{
			get
			{
				//IL_005a: Unknown result type (might be due to invalid IL or missing references)
				string value = HeldHeadReviveKey.Value;
				if (value != null && value.Length == 1 && value[0] >= '0' && value[0] <= '9')
				{
					return (KeyCode)(48 + (value[0] - 48));
				}
				if (string.Equals(value, "Enter", StringComparison.OrdinalIgnoreCase))
				{
					return (KeyCode)13;
				}
				if (!Enum.TryParse<KeyCode>(value, ignoreCase: true, out KeyCode result))
				{
					return (KeyCode)104;
				}
				return result;
			}
		}

		private void Awake()
		{
			//IL_006d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0077: Expected O, but got Unknown
			//IL_00a5: Unknown result type (might be due to invalid IL or missing references)
			//IL_00af: Expected O, but got Unknown
			//IL_00d8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e2: Expected O, but got Unknown
			//IL_0130: Unknown result type (might be due to invalid IL or missing references)
			//IL_013a: Expected O, but got Unknown
			//IL_01b0: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ba: Expected O, but got Unknown
			//IL_02af: Unknown result type (might be due to invalid IL or missing references)
			Log = ((BaseUnityPlugin)this).Logger;
			KeepItemsOnDeath = ((BaseUnityPlugin)this).Config.Bind<bool>("Inventory", "KeepItemsOnDeath", true, "Keep inventory-slot items when a player dies. Held items still drop.");
			ReviveTrigger = ((BaseUnityPlugin)this).Config.Bind<string>("Revive", "Mode", "ExtractionOrTruck", new ConfigDescription("Disabled, extraction machine activated, or direct extraction/truck revive.", (AcceptableValueBase)(object)new AcceptableValueList<string>(new string[3] { "Disabled", "ExtractionMachineActivated", "ExtractionOrTruck" }), Array.Empty<object>()));
			ReviveCost = ((BaseUnityPlugin)this).Config.Bind<string>("Revive", "Cost", "0", new ConfigDescription("Shared team currency consumed per revive. Selectable in 1,000-currency steps.", (AcceptableValueBase)(object)new AcceptableValueList<string>(ReviveCostOptions), Array.Empty<object>()));
			ReviveHealthPercent = ((BaseUnityPlugin)this).Config.Bind<int>("Revive", "HealthPercent", 25, new ConfigDescription("Health after revive, from 1 to 100 percent.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 100), Array.Empty<object>()));
			EnableHeldHeadRevive = ((BaseUnityPlugin)this).Config.Bind<bool>("Revive", "EnableHeldHeadRevive", true, "Allow the host to revive the death head currently held with the physics grabber by pressing H.");
			HeldHeadReviveKey = ((BaseUnityPlugin)this).Config.Bind<string>("Revive", "HeldHeadReviveKey", "H", new ConfigDescription("Key used for held-head revive.", (AcceptableValueBase)(object)new AcceptableValueList<string>(HeldHeadReviveKeyOptions), Array.Empty<object>()));
			EnableCartRevive = ((BaseUnityPlugin)this).Config.Bind<bool>("Revive", "EnableCartRevive", true, "Immediately revive a player when their death head is placed inside a cart.");
			EnableShopRevive = ((BaseUnityPlugin)this).Config.Bind<bool>("Revive", "EnableShopRevive", true, "Immediately revive players killed in the shop.");
			DebugLogging = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "DebugLogging", true, "Write detailed death-head, inventory, and Harmony diagnostics to the BepInEx log. Enabled by default during development.");
			NormalizeConfig();
			ReviveController.ValidateGameApi();
			_harmony = new Harmony("com.mods.betterreviveexperience");
			RegisterPatches();
			int num = 0;
			foreach (MethodBase patchedMethod in _harmony.GetPatchedMethods())
			{
				num++;
				Debug("[BRE] patched: " + patchedMethod.DeclaringType?.FullName + "." + patchedMethod.Name);
			}
			Log.LogInfo((object)"BetterReviveExperience v0.2.11 loaded");
			Log.LogInfo((object)($"[BRE] patches={num}, keepItems={KeepItemsOnDeath.Value}, " + $"mode={CurrentReviveMode}, cost={ReviveCostAmount}, " + $"health={ReviveHealthPercent.Value}%, heldHead={EnableHeldHeadRevive.Value}/" + $"{HeldHeadReviveKeyCode}, " + $"cart={EnableCartRevive.Value}, shop={EnableShopRevive.Value}"));
			if (Chainloader.PluginInfos.ContainsKey("zichen.gametools"))
			{
				Log.LogWarning((object)"[BRE] GameTools detected. Disable its automatic death-head revive options to keep BetterReviveExperience cost, health, and trigger rules authoritative.");
			}
			WarnOverlappingReviveMod("Hypn.ReviveHeadInTruckOrExtractionPoint", "ReviveHeadInTruckOrExtractionPoint");
			WarnOverlappingReviveMod("Kai.Revive_at_Cart", "CartRevive");
			WarnOverlappingReviveMod("endersaltz.LetMeShop", "LetMeShop");
			WarnOverlappingReviveMod("com.yuniverse.reviveplayer", "UltimateReviveNew");
		}

		private void RegisterPatches()
		{
			RegisterPostfix(typeof(PlayerAvatar), "Update", typeof(PlayerAvatarUpdatePatch), "Postfix");
			RegisterPostfix(typeof(PlayerAvatar), "PlayerDeathRPC", typeof(PlayerDeathPatch), "Postfix", 0);
			RegisterPostfix(typeof(PlayerDeathHead), "Trigger", typeof(DeathHeadTriggerPatch), "Postfix");
			RegisterPostfix(typeof(PlayerDeathHead), "Update", typeof(DeathHeadUpdatePatch), "Postfix", 400, new string[1] { "zichen.gametools" });
			RegisterPostfix(typeof(PlayerAvatar), "ReviveRPC", typeof(PlayerRevivePatch), "Postfix", 0);
			RegisterPostfix(typeof(PhysGrabCart), "Update", typeof(CartUpdatePatch), "Postfix");
			RegisterPrefix(typeof(ItemEquippable), "RPC_CompleteUnequip", typeof(ForcedUnequipPatch), "Prefix", 800);
			RegisterPrefix(typeof(RunManager), "ChangeLevel", typeof(LevelChangePatch), "Prefix");
			RegisterPostfix(typeof(RoundDirector), "Start", typeof(RoundStartPatch), "Postfix");
			RegisterPostfix(typeof(MainMenuOpen), "Start", typeof(MainMenuPatch), "Postfix");
		}

		private void RegisterPrefix(Type targetType, string targetName, Type patchType, string patchName, int priority = 400)
		{
			RegisterPatch(targetType, targetName, patchType, patchName, isPrefix: true, priority, null);
		}

		private void RegisterPostfix(Type targetType, string targetName, Type patchType, string patchName, int priority = 400, string[] after = null)
		{
			RegisterPatch(targetType, targetName, patchType, patchName, isPrefix: false, priority, after);
		}

		private void RegisterPatch(Type targetType, string targetName, Type patchType, string patchName, bool isPrefix, int priority, string[] after)
		{
			//IL_0074: Unknown result type (might be due to invalid IL or missing references)
			//IL_0079: Unknown result type (might be due to invalid IL or missing references)
			//IL_0081: Unknown result type (might be due to invalid IL or missing references)
			//IL_008a: Expected O, but got Unknown
			MethodInfo methodInfo = AccessTools.Method(targetType, targetName, (Type[])null, (Type[])null);
			MethodInfo methodInfo2 = AccessTools.Method(patchType, patchName, (Type[])null, (Type[])null);
			if (methodInfo == null || methodInfo2 == null)
			{
				throw new MissingMethodException("[BRE] Cannot patch " + targetType.FullName + "." + targetName + " with " + patchType.FullName + "." + patchName);
			}
			HarmonyMethod val = new HarmonyMethod(methodInfo2)
			{
				priority = priority,
				after = after
			};
			_harmony.Patch((MethodBase)methodInfo, isPrefix ? val : null, isPrefix ? null : val, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
		}

		private static void NormalizeConfig()
		{
			ReviveHealthPercent.Value = Mathf.Clamp(ReviveHealthPercent.Value, 1, 100);
		}

		private static string[] BuildReviveCostOptions()
		{
			string[] array = new string[101];
			for (int i = 0; i <= 100000; i += 1000)
			{
				array[i / 1000] = i.ToString();
			}
			return array;
		}

		public static void Debug(string message)
		{
			ConfigEntry<bool> debugLogging = DebugLogging;
			if (debugLogging != null && debugLogging.Value)
			{
				ManualLogSource log = Log;
				if (log != null)
				{
					log.LogInfo((object)message);
				}
			}
		}

		private static void WarnOverlappingReviveMod(string guid, string name)
		{
			if (Chainloader.PluginInfos.ContainsKey(guid))
			{
				Log.LogWarning((object)("[BRE] " + name + " detected. Disable it while BRE manages the same revive trigger to prevent duplicate ReviveRPC calls, health changes, or charges."));
			}
		}

		private void OnDestroy()
		{
			ManualLogSource log = Log;
			if (log != null)
			{
				log.LogWarning((object)"[BRE] plugin component destroyed; keeping session patches installed");
			}
		}
	}
	public static class PluginInfo
	{
		public const string PLUGIN_GUID = "com.mods.betterreviveexperience";

		public const string PLUGIN_NAME = "BetterReviveExperience";

		public const string PLUGIN_VERSION = "0.2.11";
	}
	internal static class ReviveController
	{
		private sealed class PendingRevive
		{
			public int Cost;

			public int TargetHealth;

			public float StartedAt;

			public string Trigger;
		}

		private sealed class PendingHealth
		{
			public PlayerAvatar Player;

			public int TargetHealth;

			public float ApplyAt;
		}

		private const float ReviveTimeoutSeconds = 5f;

		private const float HealthSyncDelaySeconds = 0.35f;

		private static readonly FieldInfo SteamIdField = AccessTools.Field(typeof(PlayerAvatar), "steamID");

		private static readonly FieldInfo DeadSetField = AccessTools.Field(typeof(PlayerAvatar), "deadSet");

		private static readonly FieldInfo PlayerDeathHeadField = AccessTools.Field(typeof(PlayerAvatar), "playerDeathHead");

		private static readonly FieldInfo TriggeredField = AccessTools.Field(typeof(PlayerDeathHead), "triggered");

		private static readonly FieldInfo TriggeredTimerField = AccessTools.Field(typeof(PlayerDeathHead), "triggeredTimer");

		private static readonly FieldInfo DeathHeadPhysGrabObjectField = AccessTools.Field(typeof(PlayerDeathHead), "physGrabObject");

		private static readonly FieldInfo InExtractionPointField = AccessTools.Field(typeof(PlayerDeathHead), "inExtractionPoint");

		private static readonly FieldInfo DeathHeadRoomVolumeField = AccessTools.Field(typeof(PlayerDeathHead), "roomVolumeCheck");

		private static readonly FieldInfo RoomInTruckField = AccessTools.Field(typeof(RoomVolumeCheck), "inTruck");

		private static readonly FieldInfo RoomInExtractionPointField = AccessTools.Field(typeof(RoomVolumeCheck), "inExtractionPoint");

		private static readonly FieldInfo CartItemsField = AccessTools.Field(typeof(PhysGrabCart), "itemsInCart");

		private static readonly FieldInfo ExtractionPointActiveField = AccessTools.Field(typeof(RoundDirector), "extractionPointActive");

		private static readonly FieldInfo MaxHealthField = AccessTools.Field(typeof(PlayerHealth), "maxHealth");

		private static readonly FieldInfo InventorySpotIndexField = AccessTools.Field(typeof(ItemEquippable), "inventorySpotIndex");

		private static readonly MethodInfo UpdateItemStateMethod = AccessTools.Method(typeof(ItemEquippable), "RPC_UpdateItemState", (Type[])null, (Type[])null);

		private static readonly MethodInfo UpdateHealthMethod = AccessTools.Method(typeof(PlayerHealth), "UpdateHealthRPC", (Type[])null, (Type[])null);

		private static readonly MethodInfo StatGetRunCurrencyMethod = AccessTools.Method(typeof(SemiFunc), "StatGetRunCurrency", (Type[])null, (Type[])null);

		private static readonly MethodInfo StatSetRunCurrencyMethod = AccessTools.Method(typeof(SemiFunc), "StatSetRunCurrency", (Type[])null, (Type[])null);

		private static readonly Dictionary<string, PendingRevive> PendingRevives = new Dictionary<string, PendingRevive>();

		private static readonly Dictionary<string, PendingHealth> PendingHealthSyncs = new Dictionary<string, PendingHealth>();

		private static readonly Dictionary<string, string> LastHeadStates = new Dictionary<string, string>();

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

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

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

		public static bool ValidateGameApi()
		{
			List<string> list = new List<string>();
			Require(SteamIdField, "PlayerAvatar.steamID", list);
			Require(DeadSetField, "PlayerAvatar.deadSet", list);
			Require(PlayerDeathHeadField, "PlayerAvatar.playerDeathHead", list);
			Require(TriggeredField, "PlayerDeathHead.triggered", list);
			Require(TriggeredTimerField, "PlayerDeathHead.triggeredTimer", list);
			Require(DeathHeadPhysGrabObjectField, "PlayerDeathHead.physGrabObject", list);
			Require(InExtractionPointField, "PlayerDeathHead.inExtractionPoint", list);
			Require(DeathHeadRoomVolumeField, "PlayerDeathHead.roomVolumeCheck", list);
			Require(RoomInTruckField, "RoomVolumeCheck.inTruck", list);
			Require(RoomInExtractionPointField, "RoomVolumeCheck.inExtractionPoint", list);
			Require(CartItemsField, "PhysGrabCart.itemsInCart", list);
			Require(ExtractionPointActiveField, "RoundDirector.extractionPointActive", list);
			Require(MaxHealthField, "PlayerHealth.maxHealth", list);
			Require(InventorySpotIndexField, "ItemEquippable.inventorySpotIndex", list);
			Require(UpdateItemStateMethod, "ItemEquippable.RPC_UpdateItemState", list);
			Require(UpdateHealthMethod, "PlayerHealth.UpdateHealthRPC", list);
			Require(StatGetRunCurrencyMethod, "SemiFunc.StatGetRunCurrency", list);
			Require(StatSetRunCurrencyMethod, "SemiFunc.StatSetRunCurrency", list);
			if (list.Count == 0)
			{
				Plugin.Debug("[BRE] game API validation passed");
				return true;
			}
			Plugin.Log.LogError((object)("[BRE] game API validation failed: " + string.Join(", ", list)));
			return false;
		}

		private static void Require(MemberInfo member, string name, ICollection<string> missing)
		{
			if (member == null)
			{
				missing.Add(name);
			}
		}

		public static string GetPlayerId(PlayerAvatar player)
		{
			if (!Object.op_Implicit((Object)(object)player))
			{
				return string.Empty;
			}
			string text = SteamIdField?.GetValue(player) as string;
			if (!string.IsNullOrEmpty(text))
			{
				return text;
			}
			if (!Object.op_Implicit((Object)(object)player.photonView))
			{
				return $"instance:{((Object)player).GetInstanceID()}";
			}
			return $"view:{player.photonView.ViewID}";
		}

		private static bool IsDead(PlayerAvatar player)
		{
			if (Object.op_Implicit((Object)(object)player) && DeadSetField != null)
			{
				return (bool)DeadSetField.GetValue(player);
			}
			return false;
		}

		private static bool IsHost()
		{
			if ((Object)(object)GameManager.instance == (Object)null)
			{
				return false;
			}
			if (!SemiFunc.IsMultiplayer())
			{
				return SemiFunc.IsMasterClientOrSingleplayer();
			}
			if (PhotonNetwork.InRoom && PhotonNetwork.IsMasterClient)
			{
				return SemiFunc.IsMasterClientOrSingleplayer();
			}
			return false;
		}

		private static bool IsHeadReady(PlayerDeathHead deathHead)
		{
			if (!Object.op_Implicit((Object)(object)deathHead) || TriggeredField == null || !(bool)TriggeredField.GetValue(deathHead))
			{
				return false;
			}
			if (!(TriggeredTimerField == null))
			{
				return (float)TriggeredTimerField.GetValue(deathHead) <= 0f;
			}
			return true;
		}

		private static PlayerDeathHead GetDeathHead(PlayerAvatar player)
		{
			if (!Object.op_Implicit((Object)(object)player) || !(PlayerDeathHeadField != null))
			{
				return null;
			}
			object? value = PlayerDeathHeadField.GetValue(player);
			return (PlayerDeathHead)((value is PlayerDeathHead) ? value : null);
		}

		public static void OnPlayerDeath(PlayerAvatar player)
		{
			if (Object.op_Implicit((Object)(object)player) && IsHost())
			{
				string playerId = GetPlayerId(player);
				bool flag = IsDead(player);
				Plugin.Debug($"[BRE] PlayerDeathRPC observed: player={playerId}, host=true, deadSet={flag}");
				if (flag)
				{
					PendingHealthSyncs.Remove(playerId);
					InsufficientFundsLogged.Remove(playerId);
					Plugin.Debug("[BRE] death confirmed: player=" + playerId);
				}
			}
		}

		public static void OnPlayerAvatarUpdated(PlayerAvatar player)
		{
			if (Object.op_Implicit((Object)(object)player) && IsHost())
			{
				string playerId = GetPlayerId(player);
				if (ObservedPlayerUpdates.Add(playerId))
				{
					Plugin.Debug("[BRE] runtime probe: PlayerAvatar.Update reached, player=" + playerId + ", host=true");
				}
				ApplyPendingHealth(playerId);
				if (!IsDead(player) && PendingRevives.TryGetValue(playerId, out var value))
				{
					CompleteRevive(playerId, player, value);
				}
				PlayerController instance = PlayerController.instance;
				if (Object.op_Implicit((Object)(object)instance) && (Object)(object)instance.playerAvatarScript == (Object)(object)player)
				{
					TryHeldHeadRevive(instance);
				}
			}
		}

		private static void TryHeldHeadRevive(PlayerController controller)
		{
			//IL_000c: Unknown result type (might be due to invalid IL or missing references)
			if (Plugin.EnableHeldHeadRevive.Value && Input.GetKeyDown(Plugin.HeldHeadReviveKeyCode) && controller.physGrabActive && Object.op_Implicit((Object)(object)controller.physGrabObject))
			{
				PlayerDeathHead val = controller.physGrabObject.GetComponent<PlayerDeathHead>();
				if (!Object.op_Implicit((Object)(object)val))
				{
					val = controller.physGrabObject.GetComponentInParent<PlayerDeathHead>();
				}
				if (!Object.op_Implicit((Object)(object)val) || !Object.op_Implicit((Object)(object)val.playerAvatar) || !IsDead(val.playerAvatar) || !IsHeadReady(val))
				{
					Plugin.Debug("[BRE] held-head revive ignored: grabbed object is not a ready death head");
				}
				else
				{
					BeginRevive(val, revivedByTruck: false, "held-head-" + Plugin.HeldHeadReviveKey.Value);
				}
			}
		}

		public static void OnDeathHeadTriggered(PlayerDeathHead deathHead)
		{
			if (Object.op_Implicit((Object)(object)deathHead) && IsHost() && Object.op_Implicit((Object)(object)deathHead.playerAvatar))
			{
				Plugin.Debug("[BRE] death head triggered: player=" + GetPlayerId(deathHead.playerAvatar) + ", host=true");
			}
		}

		public static void OnDeathHeadUpdated(PlayerDeathHead deathHead)
		{
			if (!Object.op_Implicit((Object)(object)deathHead) || !IsHost() || !Object.op_Implicit((Object)(object)deathHead.playerAvatar))
			{
				return;
			}
			PlayerAvatar playerAvatar = deathHead.playerAvatar;
			string playerId = GetPlayerId(playerAvatar);
			if (ObservedDeathHeadUpdates.Add(playerId))
			{
				Plugin.Debug("[BRE] runtime probe: PlayerDeathHead.Update reached, player=" + playerId + ", host=true");
			}
			ApplyPendingHealth(playerId);
			if (!IsDead(playerAvatar))
			{
				return;
			}
			if (PendingRevives.TryGetValue(playerId, out var value))
			{
				if (Time.time - value.StartedAt < 5f)
				{
					return;
				}
				PendingRevives.Remove(playerId);
				Refund(value.Cost);
				Plugin.Log.LogWarning((object)("[BRE] revive timed out and refunded: player=" + playerId + ", " + $"trigger={value.Trigger}, cost={value.Cost}"));
			}
			if (!IsHeadReady(deathHead))
			{
				return;
			}
			if (Plugin.EnableShopRevive.Value && SemiFunc.RunIsShop())
			{
				BeginRevive(deathHead, revivedByTruck: false, "shop");
			}
			else
			{
				if (Plugin.CurrentReviveMode == ReviveMode.Disabled)
				{
					return;
				}
				object? obj = DeathHeadRoomVolumeField?.GetValue(deathHead);
				RoomVolumeCheck val = (RoomVolumeCheck)((obj is RoomVolumeCheck) ? obj : null);
				bool flag = InExtractionPointField != null && (bool)InExtractionPointField.GetValue(deathHead);
				bool flag2 = ReadBool(val, RoomInExtractionPointField);
				bool flag3 = flag || flag2;
				bool flag4 = ReadBool(val, RoomInTruckField);
				bool flag5 = (Object)(object)RoundDirector.instance != (Object)null && ExtractionPointActiveField != null && (bool)ExtractionPointActiveField.GetValue(RoundDirector.instance);
				string text = $"roomCheck={Object.op_Implicit((Object)(object)val)}, headExtraction={flag}, " + $"roomExtraction={flag2}, truck={flag4}, machine={flag5}";
				if (!LastHeadStates.TryGetValue(playerId, out var value2) || value2 != text)
				{
					LastHeadStates[playerId] = text;
					Plugin.Debug("[BRE] death head: player=" + playerId + ", " + text);
				}
				if (Plugin.CurrentReviveMode == ReviveMode.ExtractionMachineActivated)
				{
					if (flag3 && flag5)
					{
						BeginRevive(deathHead, revivedByTruck: false, "extraction-machine");
					}
				}
				else if (flag4)
				{
					BeginRevive(deathHead, revivedByTruck: true, "truck");
				}
				else if (flag3)
				{
					BeginRevive(deathHead, revivedByTruck: false, "extraction");
				}
			}
		}

		public static void OnCartUpdated(PhysGrabCart cart)
		{
			if (!Plugin.EnableCartRevive.Value || !IsHost() || !Object.op_Implicit((Object)(object)cart) || (Object)(object)GameDirector.instance == (Object)null || CartItemsField == null || !(CartItemsField.GetValue(cart) is IEnumerable items))
			{
				return;
			}
			foreach (PlayerAvatar player in GameDirector.instance.PlayerList)
			{
				if (!Object.op_Implicit((Object)(object)player) || !IsDead(player))
				{
					continue;
				}
				PlayerDeathHead deathHead = GetDeathHead(player);
				if (IsHeadReady(deathHead))
				{
					object? obj = DeathHeadPhysGrabObjectField?.GetValue(deathHead);
					PhysGrabObject val = (PhysGrabObject)((obj is PhysGrabObject) ? obj : null);
					if (Object.op_Implicit((Object)(object)val) && Contains(items, val))
					{
						BeginRevive(deathHead, revivedByTruck: false, "cart");
					}
				}
			}
		}

		private static bool Contains(IEnumerable items, PhysGrabObject target)
		{
			foreach (object item in items)
			{
				PhysGrabObject val = (PhysGrabObject)((item is PhysGrabObject) ? item : null);
				if (val != null && (Object)(object)val == (Object)(object)target)
				{
					return true;
				}
			}
			return false;
		}

		private static bool ReadBool(object instance, FieldInfo field)
		{
			if (instance != null && field != null)
			{
				return (bool)field.GetValue(instance);
			}
			return false;
		}

		private static void BeginRevive(PlayerDeathHead deathHead, bool revivedByTruck, string trigger)
		{
			if (!IsHost())
			{
				return;
			}
			PlayerAvatar val = (Object.op_Implicit((Object)(object)deathHead) ? deathHead.playerAvatar : null);
			if (!Object.op_Implicit((Object)(object)val) || !IsDead(val) || PendingRevives.ContainsKey(GetPlayerId(val)))
			{
				return;
			}
			if ((Object)(object)StatsManager.instance == (Object)null || (Object)(object)PunManager.instance == (Object)null)
			{
				Plugin.Log.LogWarning((object)("[BRE] revive postponed: currency managers are not ready, trigger=" + trigger));
				return;
			}
			string playerId = GetPlayerId(val);
			int reviveCostAmount = Plugin.ReviveCostAmount;
			int num = SemiFunc.StatGetRunCurrency();
			if (num < reviveCostAmount)
			{
				if (InsufficientFundsLogged.Add(playerId))
				{
					Plugin.Log.LogInfo((object)("[BRE] revive denied: player=" + playerId + ", trigger=" + trigger + ", " + $"cost={reviveCostAmount}, currency={num}"));
				}
				return;
			}
			int maxHealth = GetMaxHealth(val.playerHealth);
			int num2 = Mathf.Clamp(Mathf.CeilToInt((float)(maxHealth * Plugin.ReviveHealthPercent.Value) / 100f), 1, maxHealth);
			if (reviveCostAmount > 0)
			{
				SemiFunc.StatSetRunCurrency(num - reviveCostAmount);
			}
			PendingRevives[playerId] = new PendingRevive
			{
				Cost = reviveCostAmount,
				TargetHealth = num2,
				StartedAt = Time.time,
				Trigger = trigger
			};
			Plugin.Log.LogInfo((object)("[BRE] revive request: player=" + playerId + ", trigger=" + trigger + ", " + $"cost={reviveCostAmount}, health={num2}/{maxHealth}"));
			try
			{
				val.Revive(revivedByTruck);
			}
			catch (Exception ex)
			{
				if (!IsDead(val))
				{
					Plugin.Log.LogWarning((object)("[BRE] native revive threw after clearing death state; " + $"continuing health sync: player={playerId}, trigger={trigger}, {ex}"));
					if (PendingRevives.TryGetValue(playerId, out var value))
					{
						CompleteRevive(playerId, val, value);
					}
				}
				else
				{
					PendingRevives.Remove(playerId);
					Refund(reviveCostAmount);
					Plugin.Log.LogError((object)("[BRE] revive call failed and refunded: player=" + playerId + ", " + $"trigger={trigger}, {ex}"));
				}
			}
		}

		public static void OnPlayerRevived(PlayerAvatar player)
		{
			if (IsHost() && Object.op_Implicit((Object)(object)player) && !IsDead(player))
			{
				string playerId = GetPlayerId(player);
				if (!PendingRevives.TryGetValue(playerId, out var value))
				{
					Plugin.Debug("[BRE] vanilla/other revive observed: player=" + playerId);
				}
				else
				{
					CompleteRevive(playerId, player, value);
				}
			}
		}

		private static void CompleteRevive(string playerId, PlayerAvatar player, PendingRevive pending)
		{
			PendingRevives.Remove(playerId);
			InsufficientFundsLogged.Remove(playerId);
			LastHeadStates.Remove(playerId);
			PendingHealthSyncs[playerId] = new PendingHealth
			{
				Player = player,
				TargetHealth = pending.TargetHealth,
				ApplyAt = Time.time + 0.35f
			};
			Plugin.Log.LogInfo((object)$"[BRE] revive confirmed: player={playerId}, trigger={pending.Trigger}, cost={pending.Cost}");
		}

		private static void ApplyPendingHealth(string playerId)
		{
			//IL_00e0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e6: Unknown result type (might be due to invalid IL or missing references)
			if (!IsHost() || !PendingHealthSyncs.TryGetValue(playerId, out var value) || Time.time < value.ApplyAt)
			{
				return;
			}
			PendingHealthSyncs.Remove(playerId);
			PlayerAvatar player = value.Player;
			if (!Object.op_Implicit((Object)(object)player) || !Object.op_Implicit((Object)(object)player.playerHealth))
			{
				return;
			}
			PlayerHealth playerHealth = player.playerHealth;
			int maxHealth = GetMaxHealth(playerHealth);
			int num = Mathf.Clamp(value.TargetHealth, 1, maxHealth);
			if (SemiFunc.IsMultiplayer())
			{
				PhotonView component = ((Component)playerHealth).GetComponent<PhotonView>();
				if (!Object.op_Implicit((Object)(object)component))
				{
					Plugin.Log.LogWarning((object)("[BRE] revive health sync failed: player=" + playerId + ", PhotonView missing"));
					return;
				}
				component.RPC("UpdateHealthRPC", (RpcTarget)0, new object[4] { num, maxHealth, true, false });
			}
			else
			{
				playerHealth.UpdateHealthRPC(num, maxHealth, true, false, default(PhotonMessageInfo));
			}
			Plugin.Log.LogInfo((object)$"[BRE] revive health synchronized: player={playerId}, health={num}/{maxHealth}");
		}

		private static int GetMaxHealth(PlayerHealth health)
		{
			if (!Object.op_Implicit((Object)(object)health) || !(MaxHealthField != null))
			{
				return 100;
			}
			return (int)MaxHealthField.GetValue(health);
		}

		public static bool AllowForcedUnequip(ItemEquippable item, int physGrabberPhotonViewId, bool isForceUnequip)
		{
			if (!isForceUnequip || !Plugin.KeepItemsOnDeath.Value || !IsHost())
			{
				return true;
			}
			PhysGrabber val = ResolveGrabber(physGrabberPhotonViewId);
			PlayerAvatar val2 = (Object.op_Implicit((Object)(object)val) ? val.playerAvatar : null);
			if (!Object.op_Implicit((Object)(object)val2) || !IsDead(val2))
			{
				return true;
			}
			int num = ((Object.op_Implicit((Object)(object)item) && InventorySpotIndexField != null) ? ((int)InventorySpotIndexField.GetValue(item)) : (-1));
			if (!Object.op_Implicit((Object)(object)item) || num < 0)
			{
				Plugin.Log.LogWarning((object)$"[BRE] could not preserve item: player={GetPlayerId(val2)}, slot={num}");
				return true;
			}
			RestoreEquippedState(item, num, physGrabberPhotonViewId);
			Plugin.Debug($"[BRE] inventory item preserved: player={GetPlayerId(val2)}, slot={num}");
			return false;
		}

		private static PhysGrabber ResolveGrabber(int photonViewId)
		{
			if (!SemiFunc.IsMultiplayer())
			{
				return PhysGrabber.instance;
			}
			PhotonView val = PhotonView.Find(photonViewId);
			if (!Object.op_Implicit((Object)(object)val))
			{
				return null;
			}
			return ((Component)val).GetComponent<PhysGrabber>();
		}

		private static void RestoreEquippedState(ItemEquippable item, int spot, int ownerId)
		{
			//IL_0097: Unknown result type (might be due to invalid IL or missing references)
			//IL_009d: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d5: Unknown result type (might be due to invalid IL or missing references)
			//IL_00db: Unknown result type (might be due to invalid IL or missing references)
			if (SemiFunc.IsMultiplayer())
			{
				((MonoBehaviourPun)item).photonView.RPC("RPC_UpdateItemState", (RpcTarget)0, new object[3] { 3, spot, ownerId });
				((MonoBehaviourPun)item).photonView.RPC("RPC_UpdateItemState", (RpcTarget)0, new object[3] { 2, spot, ownerId });
			}
			else
			{
				UpdateItemStateMethod.Invoke(item, new object[4]
				{
					3,
					spot,
					ownerId,
					(object)default(PhotonMessageInfo)
				});
				UpdateItemStateMethod.Invoke(item, new object[4]
				{
					2,
					spot,
					ownerId,
					(object)default(PhotonMessageInfo)
				});
			}
		}

		private static void Refund(int cost)
		{
			if (IsHost() && cost > 0 && !((Object)(object)StatsManager.instance == (Object)null) && !((Object)(object)PunManager.instance == (Object)null))
			{
				SemiFunc.StatSetRunCurrency(SemiFunc.StatGetRunCurrency() + cost);
			}
		}

		public static void Reset(bool refundPending = false)
		{
			if (refundPending)
			{
				foreach (PendingRevive value in PendingRevives.Values)
				{
					Refund(value.Cost);
				}
			}
			PendingRevives.Clear();
			PendingHealthSyncs.Clear();
			LastHeadStates.Clear();
			InsufficientFundsLogged.Clear();
			ObservedPlayerUpdates.Clear();
			ObservedDeathHeadUpdates.Clear();
		}
	}
	public enum ReviveMode
	{
		Disabled,
		ExtractionMachineActivated,
		ExtractionOrTruck
	}
}
namespace BetterReviveExperience.Patches
{
	internal static class PlayerAvatarUpdatePatch
	{
		private static void Postfix(PlayerAvatar __instance)
		{
			ReviveController.OnPlayerAvatarUpdated(__instance);
		}
	}
	[HarmonyPatch(typeof(PlayerAvatar), "PlayerDeathRPC")]
	internal static class PlayerDeathPatch
	{
		[HarmonyPostfix]
		[HarmonyPriority(0)]
		private static void Postfix(PlayerAvatar __instance)
		{
			ReviveController.OnPlayerDeath(__instance);
		}
	}
	[HarmonyPatch(typeof(PlayerDeathHead), "Trigger")]
	internal static class DeathHeadTriggerPatch
	{
		[HarmonyPostfix]
		private static void Postfix(PlayerDeathHead __instance)
		{
			ReviveController.OnDeathHeadTriggered(__instance);
		}
	}
	[HarmonyPatch(typeof(PlayerDeathHead), "Update")]
	internal static class DeathHeadUpdatePatch
	{
		[HarmonyPostfix]
		private static void Postfix(PlayerDeathHead __instance)
		{
			ReviveController.OnDeathHeadUpdated(__instance);
		}
	}
	[HarmonyPatch(typeof(PlayerAvatar), "ReviveRPC")]
	internal static class PlayerRevivePatch
	{
		[HarmonyPostfix]
		[HarmonyPriority(0)]
		private static void Postfix(PlayerAvatar __instance)
		{
			ReviveController.OnPlayerRevived(__instance);
		}
	}
	[HarmonyPatch(typeof(PhysGrabCart), "Update")]
	internal static class CartUpdatePatch
	{
		[HarmonyPostfix]
		private static void Postfix(PhysGrabCart __instance)
		{
			ReviveController.OnCartUpdated(__instance);
		}
	}
	[HarmonyPatch(typeof(ItemEquippable), "RPC_CompleteUnequip")]
	internal static class ForcedUnequipPatch
	{
		[HarmonyPrefix]
		[HarmonyPriority(800)]
		private static bool Prefix(ItemEquippable __instance, int physGrabberPhotonViewID, bool isForceUnequip)
		{
			return ReviveController.AllowForcedUnequip(__instance, physGrabberPhotonViewID, isForceUnequip);
		}
	}
	[HarmonyPatch(typeof(RunManager), "ChangeLevel")]
	internal static class LevelChangePatch
	{
		[HarmonyPrefix]
		private static void Prefix()
		{
			ReviveController.Reset(refundPending: true);
			Plugin.Debug("[BRE] level change: state reset");
		}
	}
	[HarmonyPatch(typeof(RoundDirector), "Start")]
	internal static class RoundStartPatch
	{
		[HarmonyPostfix]
		private static void Postfix()
		{
			ReviveController.Reset();
			Plugin.Debug("[BRE] round start: state reset");
		}
	}
	[HarmonyPatch(typeof(MainMenuOpen), "Start")]
	internal static class MainMenuPatch
	{
		[HarmonyPostfix]
		private static void Postfix()
		{
			ReviveController.Reset(refundPending: true);
			Plugin.Debug("[BRE] main menu: state reset");
		}
	}
}