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 KeepItemsOnDeath { get; private set; } public static ConfigEntry ReviveTrigger { get; private set; } public static ConfigEntry ReviveCost { get; private set; } public static ConfigEntry ReviveHealthPercent { get; private set; } public static ConfigEntry EnableHeldHeadRevive { get; private set; } public static ConfigEntry HeldHeadReviveKey { get; private set; } public static ConfigEntry EnableCartRevive { get; private set; } public static ConfigEntry EnableShopRevive { get; private set; } public static ConfigEntry 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(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(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("Inventory", "KeepItemsOnDeath", true, "Keep inventory-slot items when a player dies. Held items still drop."); ReviveTrigger = ((BaseUnityPlugin)this).Config.Bind("Revive", "Mode", "ExtractionOrTruck", new ConfigDescription("Disabled, extraction machine activated, or direct extraction/truck revive.", (AcceptableValueBase)(object)new AcceptableValueList(new string[3] { "Disabled", "ExtractionMachineActivated", "ExtractionOrTruck" }), Array.Empty())); ReviveCost = ((BaseUnityPlugin)this).Config.Bind("Revive", "Cost", "0", new ConfigDescription("Shared team currency consumed per revive. Selectable in 1,000-currency steps.", (AcceptableValueBase)(object)new AcceptableValueList(ReviveCostOptions), Array.Empty())); ReviveHealthPercent = ((BaseUnityPlugin)this).Config.Bind("Revive", "HealthPercent", 25, new ConfigDescription("Health after revive, from 1 to 100 percent.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 100), Array.Empty())); EnableHeldHeadRevive = ((BaseUnityPlugin)this).Config.Bind("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("Revive", "HeldHeadReviveKey", "H", new ConfigDescription("Key used for held-head revive.", (AcceptableValueBase)(object)new AcceptableValueList(HeldHeadReviveKeyOptions), Array.Empty())); EnableCartRevive = ((BaseUnityPlugin)this).Config.Bind("Revive", "EnableCartRevive", true, "Immediately revive a player when their death head is placed inside a cart."); EnableShopRevive = ((BaseUnityPlugin)this).Config.Bind("Revive", "EnableShopRevive", true, "Immediately revive players killed in the shop."); DebugLogging = ((BaseUnityPlugin)this).Config.Bind("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 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 PendingRevives = new Dictionary(); private static readonly Dictionary PendingHealthSyncs = new Dictionary(); private static readonly Dictionary LastHeadStates = new Dictionary(); private static readonly HashSet InsufficientFundsLogged = new HashSet(); private static readonly HashSet ObservedPlayerUpdates = new HashSet(); private static readonly HashSet ObservedDeathHeadUpdates = new HashSet(); public static bool ValidateGameApi() { List list = new List(); 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 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(); if (!Object.op_Implicit((Object)(object)val)) { val = controller.physGrabObject.GetComponentInParent(); } 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(); 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(); } 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"); } } }