Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of BetterReviveExperience v0.2.11
BepInEx/plugins/BetterReviveExperience/BetterReviveExperience.dll
Decompiled 12 hours agousing 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"); } } }