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.3.5.0")] [assembly: AssemblyInformationalVersion("0.3.5+541bb47aa5d00ac3b166b99c8031dfe7a7c07d19")] [assembly: AssemblyProduct("BetterReviveExperience")] [assembly: AssemblyTitle("BetterReviveExperience")] [assembly: AssemblyVersion("0.3.5.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.3.5")] [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.3.5"; 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 ProtectHeldItems { get; private set; } public static ConfigEntry SwapHeldItemOnOccupiedSlot { get; private set; } public static ConfigEntry ReturnHeldItemOnDeath { 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_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Expected O, but got Unknown //IL_0105: Unknown result type (might be due to invalid IL or missing references) //IL_010f: Expected O, but got Unknown //IL_0138: Unknown result type (might be due to invalid IL or missing references) //IL_0142: Expected O, but got Unknown //IL_0190: Unknown result type (might be due to invalid IL or missing references) //IL_019a: Expected O, but got Unknown //IL_0210: Unknown result type (might be due to invalid IL or missing references) //IL_021a: Expected O, but got Unknown //IL_0356: 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."); ProtectHeldItems = ((BaseUnityPlugin)this).Config.Bind("Inventory", "ProtectHeldItems", true, "Return a storable held item to its original or first free vanilla inventory slot after an impact or tumble. If all three slots are occupied, the local player keeps holding it."); SwapHeldItemOnOccupiedSlot = ((BaseUnityPlugin)this).Config.Bind("Inventory", "SwapHeldItemOnOccupiedSlot", true, "When an occupied inventory hotkey is pressed while holding another storable item, store the held item in the vacated slot instead of dropping it."); ReturnHeldItemOnDeath = ((BaseUnityPlugin)this).Config.Bind("Inventory", "ReturnHeldItemOnDeath", true, "Return the storable item held at death to the first free vanilla inventory slot (slots 1-3). If all three are occupied, place it near the death head instead."); 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.3.5 loaded"); Log.LogInfo((object)($"[BRE] patches={num}, keepItems={KeepItemsOnDeath.Value}, " + $"protectHeldItems={ProtectHeldItems.Value}, swapHeldItems={SwapHeldItemOnOccupiedSlot.Value}, " + $"returnDeathItem={ReturnHeldItemOnDeath.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"); if (Chainloader.PluginInfos.ContainsKey("Mistyck.NoForcedDropMod")) { Log.LogWarning((object)"[BRE] NoForcedDropMod detected. Disable its overlapping forced-drop hook while BRE held-item protection is enabled."); } } private void RegisterPatches() { RegisterPostfix(typeof(PlayerAvatar), "Update", typeof(PlayerAvatarUpdatePatch), "Postfix"); RegisterPrefix(typeof(PlayerAvatar), "PlayerDeathRPC", typeof(PlayerDeathPatch), "Prefix", 800); 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(PhysGrabber), "ReleaseObjectRPC", typeof(ForcedGrabReleaseReceivePatch), "Prefix", 800); 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(MethodInfo target, Type patchType, string patchName, int priority = 400) { //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.Method(patchType, patchName, (Type[])null, (Type[])null); if (target == null || methodInfo == null) { throw new MissingMethodException("[BRE] Cannot register prefix for " + target?.DeclaringType?.FullName + "." + target?.Name); } _harmony.Patch((MethodBase)target, new HarmonyMethod(methodInfo) { priority = priority }, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } 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.3.5"; } 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); WeaponProtectionController.ValidateGameApi(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}"; } internal static bool IsDead(PlayerAvatar player) { if (Object.op_Implicit((Object)(object)player) && DeadSetField != null) { return (bool)DeadSetField.GetValue(player); } return false; } internal 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(); } internal 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) }); } } internal static int GetInventorySpotIndex(ItemEquippable item) { if (!Object.op_Implicit((Object)(object)item) || !(InventorySpotIndexField != null)) { return -1; } return (int)InventorySpotIndexField.GetValue(item); } 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 } internal static class WeaponProtectionController { private sealed class HeldWeaponRecord { public PlayerAvatar Player; public PhysGrabObject Physical; public ItemEquippable Item; public int OwnerViewId; public int PreferredSlot; public float LastSeenAt; } private sealed class PendingWeaponReturn { public HeldWeaponRecord Weapon; public float ReadyAt; public float GiveUpAt; } private sealed class PendingForcedDropRecovery { public HeldWeaponRecord Item; public float ReadyAt; public float GiveUpAt; } private sealed class PendingInventorySwap { public HeldWeaponRecord HeldItem; public ItemEquippable OutgoingItem; public int TargetSlot; public float ReadyAt; public float GiveUpAt; } private const float RecentHoldWindowSeconds = 0.75f; private const float DeathCleanupDelaySeconds = 0.6f; private const float ReturnTimeoutSeconds = 4f; private const float ForcedDropRecoveryDelaySeconds = 0.05f; private const float ForcedDropRecoveryTimeoutSeconds = 2f; private const float InventorySwapDelaySeconds = 0.25f; private const float InventorySwapTimeoutSeconds = 2f; private const float ImpactReleaseMinimumDisableSeconds = 0.95f; private const float ImpactReleaseMaximumDisableSeconds = 2.05f; private static readonly FieldInfo GrabbedPhysObjectField = AccessTools.Field(typeof(PhysGrabber), "grabbedPhysGrabObject"); private static readonly FieldInfo ForceGrabTimerField = AccessTools.Field(typeof(ItemEquippable), "forceGrabTimer"); private static readonly FieldInfo ItemUnequipAutoHoldField = AccessTools.Field(typeof(GameplayManager), "itemUnequipAutoHold"); private static readonly Dictionary LastWeaponByPlayer = new Dictionary(); private static readonly Dictionary LastOwnerByWeapon = new Dictionary(); private static readonly Dictionary DeathCandidates = new Dictionary(); private static readonly Dictionary PendingReturns = new Dictionary(); private static readonly Dictionary PendingForcedDropRecoveries = new Dictionary(); private static readonly Dictionary PendingInventorySwaps = new Dictionary(); private static readonly Dictionary LastProtectionLogAt = new Dictionary(); private static bool AutoHoldWarningLogged; public static bool ValidateGameApi(ICollection missing) { if (GrabbedPhysObjectField == null) { missing.Add("PhysGrabber.grabbedPhysGrabObject"); } if (ForceGrabTimerField == null) { missing.Add("ItemEquippable.forceGrabTimer"); } if (GrabbedPhysObjectField != null) { return ForceGrabTimerField != null; } return false; } public static void ObservePlayer(PlayerAvatar player) { if (ReviveController.IsHost() && Object.op_Implicit((Object)(object)player) && !ReviveController.IsDead(player)) { WarnIfNativeAutoHoldDisabled(player); PhysGrabObject heldPhysical = GetHeldPhysical(player.physGrabber); if (IsStorableItem(heldPhysical, out var item) && IsLatestActiveHolder(player.physGrabber, heldPhysical)) { RecordHolder(player, heldPhysical, item); } } } public static void CaptureBeforeDeath(PlayerAvatar player) { if (Plugin.ReturnHeldItemOnDeath.Value && ReviveController.IsHost() && Object.op_Implicit((Object)(object)player)) { string playerId = ReviveController.GetPlayerId(player); PhysGrabObject heldPhysical = GetHeldPhysical(player.physGrabber); HeldWeaponRecord heldWeaponRecord = null; HeldWeaponRecord value; if (IsStorableItem(heldPhysical, out var item) && IsLatestActiveHolder(player.physGrabber, heldPhysical)) { heldWeaponRecord = RecordHolder(player, heldPhysical, item); } else if (LastWeaponByPlayer.TryGetValue(playerId, out value) && Time.time - value.LastSeenAt <= 0.75f && IsStillLastOwner(playerId, value.Physical)) { heldWeaponRecord = value; } if (heldWeaponRecord != null) { DeathCandidates[playerId] = heldWeaponRecord; Plugin.Debug("[BRE] death item captured: player=" + playerId + ", item=" + ItemName(heldWeaponRecord.Item)); } } } public static void ConfirmDeath(PlayerAvatar player) { if (Plugin.ReturnHeldItemOnDeath.Value && ReviveController.IsHost() && Object.op_Implicit((Object)(object)player) && ReviveController.IsDead(player)) { string playerId = ReviveController.GetPlayerId(player); PendingForcedDropRecoveries.Remove(playerId); PendingInventorySwaps.Remove(playerId); if (DeathCandidates.TryGetValue(playerId, out var value)) { DeathCandidates.Remove(playerId); PendingReturns[playerId] = new PendingWeaponReturn { Weapon = value, ReadyAt = Time.time + 0.6f, GiveUpAt = Time.time + 4f }; Plugin.Log.LogInfo((object)("[BRE] held item queued for death return: player=" + playerId + ", item=" + ItemName(value.Item))); } } } public static void ProcessPendingReturn(PlayerAvatar player, PlayerDeathHead deathHead = null) { //IL_01c5: Unknown result type (might be due to invalid IL or missing references) //IL_01ca: Unknown result type (might be due to invalid IL or missing references) //IL_01d2: Unknown result type (might be due to invalid IL or missing references) //IL_01df: Unknown result type (might be due to invalid IL or missing references) //IL_0206: Unknown result type (might be due to invalid IL or missing references) //IL_021b: Unknown result type (might be due to invalid IL or missing references) if (!ReviveController.IsHost() || !Object.op_Implicit((Object)(object)player)) { return; } string playerId = ReviveController.GetPlayerId(player); if (!PendingReturns.TryGetValue(playerId, out var value) || Time.time < value.ReadyAt) { return; } HeldWeaponRecord weapon = value.Weapon; if (weapon == null || !Object.op_Implicit((Object)(object)weapon.Item) || !Object.op_Implicit((Object)(object)weapon.Physical)) { PendingReturns.Remove(playerId); Plugin.Log.LogWarning((object)("[BRE] held item return cancelled: player=" + playerId + ", item no longer exists")); } else if (!IsStillLastOwner(playerId, weapon.Physical) || IsHeldByAnotherPlayer(playerId, weapon.Physical)) { PendingReturns.Remove(playerId); Plugin.Log.LogInfo((object)("[BRE] held item return cancelled: player=" + playerId + ", item=" + ItemName(weapon.Item) + ", reason=new-holder")); } else if (weapon.Item.IsEquipped()) { PendingReturns.Remove(playerId); Plugin.Debug("[BRE] held item already equipped: player=" + playerId + ", item=" + ItemName(weapon.Item)); } else { if (weapon.Physical.playerGrabbing.Count > 0 && Time.time < value.GiveUpAt) { return; } int num = FindFreeVanillaSlot(player, playerId, weapon.PreferredSlot); if (num >= 0) { ReviveController.RestoreEquippedState(weapon.Item, num, weapon.OwnerViewId); PendingReturns.Remove(playerId); Plugin.Log.LogInfo((object)("[BRE] held item returned to inventory: player=" + playerId + ", " + $"item={ItemName(weapon.Item)}, slot={num}")); } else if (!((Object)(object)StatsManager.instance == (Object)null) || !(Time.time < value.GiveUpAt)) { Vector3 fallbackPosition = GetFallbackPosition(player, deathHead); weapon.Physical.Teleport(fallbackPosition, ((Component)weapon.Physical).transform.rotation); if (Object.op_Implicit((Object)(object)weapon.Physical.rb)) { weapon.Physical.rb.velocity = Vector3.zero; weapon.Physical.rb.angularVelocity = Vector3.zero; } PendingReturns.Remove(playerId); Plugin.Log.LogInfo((object)("[BRE] held item returned nearby: player=" + playerId + ", item=" + ItemName(weapon.Item) + ", reason=no-free-vanilla-slot")); } } } public static bool AllowForcedRelease(PhysGrabber grabber, bool physGrabEnded, float disableTimer, int releaseObjectViewId, PhotonMessageInfo info) { //IL_01c4: Unknown result type (might be due to invalid IL or missing references) if (!Plugin.ProtectHeldItems.Value || !ReviveController.IsHost() || !Object.op_Implicit((Object)(object)grabber)) { return true; } if (!IsImpactOrTumbleRelease(disableTimer, releaseObjectViewId)) { return true; } PlayerAvatar playerAvatar = grabber.playerAvatar; if (!Object.op_Implicit((Object)(object)playerAvatar) || ReviveController.IsDead(playerAvatar)) { return true; } PhysGrabObject heldPhysical = GetHeldPhysical(grabber); if (!IsStorableItem(heldPhysical, out var item)) { return true; } if (!IsLatestActiveHolder(grabber, heldPhysical)) { return true; } HeldWeaponRecord heldWeaponRecord = RecordHolder(playerAvatar, heldPhysical, item); string playerId = ReviveController.GetPlayerId(playerAvatar); bool flag = IsLocalOwner(playerAvatar); int num = FindFreeVanillaSlot(playerAvatar, playerId, heldWeaponRecord.PreferredSlot); if (flag && num < 0) { int weaponKey = GetWeaponKey(heldPhysical); if (!LastProtectionLogAt.TryGetValue(weaponKey, out var value) || Time.time - value >= 1f) { LastProtectionLogAt[weaponKey] = Time.time; Plugin.Log.LogInfo((object)("[BRE] kept forced-drop item in hand: player=" + playerId + ", item=" + ItemName(item) + ", reason=no-free-vanilla-slot")); } return false; } if (flag && ForceGrabTimerField != null) { ForceGrabTimerField.SetValue(item, 0f); } PendingForcedDropRecoveries[playerId] = new PendingForcedDropRecovery { Item = heldWeaponRecord, ReadyAt = Time.time + 0.05f, GiveUpAt = Time.time + 2f }; Plugin.Log.LogInfo((object)("[BRE] forced item release queued for inventory: player=" + playerId + ", " + $"item={ItemName(item)}, preferredSlot={heldWeaponRecord.PreferredSlot}, " + $"disableTimer={disableTimer:0.##}, physGrabEnded={physGrabEnded}, " + $"sender={GetSenderActorNumber(info)}, singleplayer={!SemiFunc.IsMultiplayer()}")); return true; } public static void ProcessForcedDropRecovery(PlayerAvatar player) { //IL_01b1: Unknown result type (might be due to invalid IL or missing references) //IL_01bc: Unknown result type (might be due to invalid IL or missing references) //IL_01c6: Unknown result type (might be due to invalid IL or missing references) //IL_01cb: Unknown result type (might be due to invalid IL or missing references) //IL_01d0: Unknown result type (might be due to invalid IL or missing references) //IL_01da: Unknown result type (might be due to invalid IL or missing references) //IL_01df: Unknown result type (might be due to invalid IL or missing references) //IL_01e4: Unknown result type (might be due to invalid IL or missing references) //IL_01ec: Unknown result type (might be due to invalid IL or missing references) //IL_01f9: Unknown result type (might be due to invalid IL or missing references) //IL_0220: Unknown result type (might be due to invalid IL or missing references) //IL_0235: Unknown result type (might be due to invalid IL or missing references) if (!ReviveController.IsHost() || !Object.op_Implicit((Object)(object)player) || ReviveController.IsDead(player)) { return; } string playerId = ReviveController.GetPlayerId(player); if (!PendingForcedDropRecoveries.TryGetValue(playerId, out var value) || Time.time < value.ReadyAt) { return; } HeldWeaponRecord item = value.Item; if (item == null || !Object.op_Implicit((Object)(object)item.Item) || !Object.op_Implicit((Object)(object)item.Physical)) { PendingForcedDropRecoveries.Remove(playerId); return; } PhysGrabObject heldPhysical = GetHeldPhysical(player.physGrabber); if (item.Item.IsEquipped()) { PendingForcedDropRecoveries.Remove(playerId); } else if ((Object)(object)heldPhysical == (Object)(object)item.Physical) { if (!(Time.time < value.GiveUpAt)) { PendingForcedDropRecoveries.Remove(playerId); Plugin.Debug("[BRE] forced-drop recovery ended with item still held: player=" + playerId + ", item=" + ItemName(item.Item)); } } else if (!IsStillLastOwner(playerId, item.Physical) || IsHeldByAnotherPlayer(playerId, item.Physical)) { PendingForcedDropRecoveries.Remove(playerId); } else { if (item.Physical.playerGrabbing.Count > 0 && Time.time < value.GiveUpAt) { return; } int num = FindFreeVanillaSlot(player, playerId, item.PreferredSlot); if (num >= 0) { ReviveController.RestoreEquippedState(item.Item, num, item.OwnerViewId); PendingForcedDropRecoveries.Remove(playerId); Plugin.Log.LogInfo((object)("[BRE] forced-drop item recovered to inventory: player=" + playerId + ", " + $"item={ItemName(item.Item)}, slot={num}")); } else if (!((Object)(object)StatsManager.instance == (Object)null) || !(Time.time < value.GiveUpAt)) { Vector3 val = ((Component)player).transform.position + ((Component)player).transform.forward * 0.75f + Vector3.up * 0.5f; item.Physical.Teleport(val, ((Component)item.Physical).transform.rotation); if (Object.op_Implicit((Object)(object)item.Physical.rb)) { item.Physical.rb.velocity = Vector3.zero; item.Physical.rb.angularVelocity = Vector3.zero; } PendingForcedDropRecoveries.Remove(playerId); Plugin.Log.LogInfo((object)("[BRE] forced-drop item recovered nearby: player=" + playerId + ", item=" + ItemName(item.Item) + ", reason=no-free-vanilla-slot")); } } } public static void CaptureInventorySwap(ItemEquippable outgoingItem, int physGrabberViewId, bool isForceUnequip) { if (!Plugin.SwapHeldItemOnOccupiedSlot.Value || isForceUnequip || !ReviveController.IsHost() || !Object.op_Implicit((Object)(object)outgoingItem)) { return; } int inventorySpotIndex = ReviveController.GetInventorySpotIndex(outgoingItem); if (inventorySpotIndex < 0 || inventorySpotIndex > 2) { Plugin.Debug("[BRE] inventory swap skipped: outgoing=" + ItemName(outgoingItem) + ", " + $"reason=invalid-slot, slot={inventorySpotIndex}"); return; } Plugin.Debug("[BRE] inventory swap candidate: outgoing=" + ItemName(outgoingItem) + ", " + $"slot={inventorySpotIndex}, grabberView={physGrabberViewId}"); PhysGrabber val = ResolveGrabber(physGrabberViewId); PlayerAvatar val2 = (Object.op_Implicit((Object)(object)val) ? val.playerAvatar : null); if (!Object.op_Implicit((Object)(object)val2) || ReviveController.IsDead(val2)) { Plugin.Debug("[BRE] inventory swap skipped: outgoing=" + ItemName(outgoingItem) + ", reason=player-missing-or-dead"); return; } PhysGrabObject heldPhysical = GetHeldPhysical(val); if (!IsStorableItem(heldPhysical, out var item) || (Object)(object)item == (Object)(object)outgoingItem || !IsLatestActiveHolder(val, heldPhysical)) { Plugin.Debug("[BRE] inventory swap skipped: player=" + ReviveController.GetPlayerId(val2) + ", outgoing=" + ItemName(outgoingItem) + ", reason=no-valid-held-item"); return; } HeldWeaponRecord heldItem = RecordHolder(val2, heldPhysical, item); string playerId = ReviveController.GetPlayerId(val2); PendingForcedDropRecoveries.Remove(playerId); PendingInventorySwaps[playerId] = new PendingInventorySwap { HeldItem = heldItem, OutgoingItem = outgoingItem, TargetSlot = inventorySpotIndex, ReadyAt = Time.time + 0.25f, GiveUpAt = Time.time + 2f }; Plugin.Log.LogInfo((object)("[BRE] inventory swap queued: player=" + playerId + ", held=" + ItemName(item) + ", outgoing=" + ItemName(outgoingItem) + ", " + $"slot={inventorySpotIndex}")); } public static void ProcessPendingInventorySwap(PlayerAvatar player) { if (!ReviveController.IsHost() || !Object.op_Implicit((Object)(object)player)) { return; } string playerId = ReviveController.GetPlayerId(player); if (!PendingInventorySwaps.TryGetValue(playerId, out var value) || Time.time < value.ReadyAt) { return; } if (ReviveController.IsDead(player)) { CancelInventorySwap(playerId, value, "player-dead"); return; } HeldWeaponRecord heldItem = value.HeldItem; if (heldItem == null || !Object.op_Implicit((Object)(object)heldItem.Item) || !Object.op_Implicit((Object)(object)heldItem.Physical) || !Object.op_Implicit((Object)(object)value.OutgoingItem)) { CancelInventorySwap(playerId, value, "item-missing"); } else if (heldItem.Item.IsEquipped()) { if (ReviveController.GetInventorySpotIndex(heldItem.Item) == value.TargetSlot) { PendingInventorySwaps.Remove(playerId); Plugin.Log.LogInfo((object)("[BRE] inventory swap already completed: player=" + playerId + ", " + $"item={ItemName(heldItem.Item)}, slot={value.TargetSlot}")); } else { CancelInventorySwap(playerId, value, "held-item-equipped-elsewhere"); } } else if (!IsStillLastOwner(playerId, heldItem.Physical) || IsHeldByAnotherPlayer(playerId, heldItem.Physical)) { CancelInventorySwap(playerId, value, "new-holder"); } else if (value.OutgoingItem.IsEquipped()) { if (!(Time.time < value.GiveUpAt)) { CancelInventorySwap(playerId, value, "outgoing-item-still-equipped"); } } else { if (heldItem.Physical.playerGrabbing.Count > 0 && Time.time < value.GiveUpAt) { return; } if (!IsVanillaSlotFree(player, playerId, value.TargetSlot)) { if (!(Time.time < value.GiveUpAt)) { CancelInventorySwap(playerId, value, "target-slot-occupied"); } return; } ReviveController.RestoreEquippedState(heldItem.Item, value.TargetSlot, heldItem.OwnerViewId); PendingInventorySwaps.Remove(playerId); Plugin.Log.LogInfo((object)("[BRE] inventory swap completed: player=" + playerId + ", stored=" + ItemName(heldItem.Item) + ", held=" + ItemName(value.OutgoingItem) + ", " + $"slot={value.TargetSlot}")); } } private static HeldWeaponRecord RecordHolder(PlayerAvatar player, PhysGrabObject physical, ItemEquippable item) { string playerId = ReviveController.GetPlayerId(player); int weaponKey = GetWeaponKey(physical); if (LastOwnerByWeapon.TryGetValue(weaponKey, out var value) && value != playerId && LastWeaponByPlayer.TryGetValue(value, out var value2) && (Object)(object)value2.Physical == (Object)(object)physical) { LastWeaponByPlayer.Remove(value); } HeldWeaponRecord value3; bool num = !LastWeaponByPlayer.TryGetValue(playerId, out value3) || (Object)(object)value3.Physical != (Object)(object)physical; value3 = new HeldWeaponRecord { Player = player, Physical = physical, Item = item, OwnerViewId = ((SemiFunc.IsMultiplayer() && Object.op_Implicit((Object)(object)player.physGrabber) && Object.op_Implicit((Object)(object)player.physGrabber.photonView)) ? player.physGrabber.photonView.ViewID : (-1)), PreferredSlot = ReviveController.GetInventorySpotIndex(item), LastSeenAt = Time.time }; LastOwnerByWeapon[weaponKey] = playerId; LastWeaponByPlayer[playerId] = value3; if (num) { Plugin.Debug("[BRE] storable item holder recorded: player=" + playerId + ", item=" + ItemName(item)); } return value3; } private static PhysGrabObject GetHeldPhysical(PhysGrabber grabber) { if (!Object.op_Implicit((Object)(object)grabber) || !grabber.grabbed || GrabbedPhysObjectField == null) { return null; } object? value = GrabbedPhysObjectField.GetValue(grabber); PhysGrabObject val = (PhysGrabObject)((value is PhysGrabObject) ? value : null); if (!Object.op_Implicit((Object)(object)val) || !val.playerGrabbing.Contains(grabber)) { return null; } return val; } private static bool IsLatestActiveHolder(PhysGrabber grabber, PhysGrabObject physical) { if (!Object.op_Implicit((Object)(object)grabber) || !Object.op_Implicit((Object)(object)physical)) { return false; } for (int num = physical.playerGrabbing.Count - 1; num >= 0; num--) { PhysGrabber val = physical.playerGrabbing[num]; if (Object.op_Implicit((Object)(object)val) && val.grabbed && !(GrabbedPhysObjectField == null) && (Object)/*isinst with value type is only supported in some contexts*/ == (Object)(object)physical) { return (Object)(object)val == (Object)(object)grabber; } } return false; } private static bool IsImpactOrTumbleRelease(float disableTimer, int releaseObjectViewId) { if (releaseObjectViewId == -1 && disableTimer >= 0.95f) { return disableTimer <= 2.05f; } return false; } private static int GetSenderActorNumber(PhotonMessageInfo info) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) if (info.Sender == null) { return -1; } return info.Sender.ActorNumber; } private static void WarnIfNativeAutoHoldDisabled(PlayerAvatar player) { if (!AutoHoldWarningLogged && IsLocalOwner(player) && !((Object)(object)GameplayManager.instance == (Object)null) && !(ItemUnequipAutoHoldField == null)) { object value = ItemUnequipAutoHoldField.GetValue(GameplayManager.instance); if (value is bool && !(bool)value) { AutoHoldWarningLogged = true; Plugin.Log.LogWarning((object)"[BRE] The native ItemUnequipAutoHold setting is disabled. Items taken from inventory will be released after the game's temporary hold expires. Enable the game's auto-hold setting; ProtectHeldItems only handles impact and tumble releases."); } } } private static bool IsStorableItem(PhysGrabObject physical, out ItemEquippable item) { item = null; if (!Object.op_Implicit((Object)(object)physical) || physical.dead) { return false; } item = ((Component)physical).GetComponent(); return Object.op_Implicit((Object)(object)item); } 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 bool IsStillLastOwner(string playerId, PhysGrabObject physical) { if (Object.op_Implicit((Object)(object)physical) && LastOwnerByWeapon.TryGetValue(GetWeaponKey(physical), out var value)) { return value == playerId; } return false; } private static bool IsHeldByAnotherPlayer(string playerId, PhysGrabObject physical) { foreach (PhysGrabber item in physical.playerGrabbing) { if (Object.op_Implicit((Object)(object)item) && Object.op_Implicit((Object)(object)item.playerAvatar) && ReviveController.GetPlayerId(item.playerAvatar) != playerId) { return true; } } return false; } private static int FindFreeVanillaSlot(PlayerAvatar player, string playerId, int preferredSlot = -1) { StatsManager instance = StatsManager.instance; bool flag = IsLocalOwner(player) && (Object)(object)Inventory.instance != (Object)null; if (!flag && ((Object)(object)instance == (Object)null || string.IsNullOrEmpty(playerId))) { return -1; } if (preferredSlot >= 0 && preferredSlot <= 2 && !IsVanillaSlotTaken(instance, playerId, preferredSlot, flag)) { return preferredSlot; } for (int i = 0; i <= 2; i++) { if (!IsVanillaSlotTaken(instance, playerId, i, flag)) { return i; } } return -1; } private static bool IsVanillaSlotFree(PlayerAvatar player, string playerId, int slot) { if (slot < 0 || slot > 2) { return false; } StatsManager instance = StatsManager.instance; bool flag = IsLocalOwner(player) && (Object)(object)Inventory.instance != (Object)null; if (!flag && ((Object)(object)instance == (Object)null || string.IsNullOrEmpty(playerId))) { return false; } return !IsVanillaSlotTaken(instance, playerId, slot, flag); } private static bool IsVanillaSlotTaken(StatsManager stats, string playerId, int slot, bool useLocalInventory) { if (useLocalInventory) { InventorySpot spotByIndex = Inventory.instance.GetSpotByIndex(slot); if ((Object)(object)spotByIndex != (Object)null) { return spotByIndex.IsOccupied(); } return false; } return slot switch { 0 => stats.playerInventorySpot1.ContainsKey(playerId), 1 => stats.playerInventorySpot2.ContainsKey(playerId), _ => stats.playerInventorySpot3.ContainsKey(playerId), }; } private static bool IsLocalOwner(PlayerAvatar player) { if (Object.op_Implicit((Object)(object)player)) { if (SemiFunc.IsMultiplayer()) { if (Object.op_Implicit((Object)(object)player.photonView)) { return player.photonView.IsMine; } return false; } return true; } return false; } private static Vector3 GetFallbackPosition(PlayerAvatar player, PlayerDeathHead deathHead) { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) PhysGrabObject val = (Object.op_Implicit((Object)(object)deathHead) ? ((Component)deathHead).GetComponent() : null); if (Object.op_Implicit((Object)(object)val)) { return val.centerPoint + Vector3.up * 0.5f; } if (Object.op_Implicit((Object)(object)deathHead)) { return ((Component)deathHead).transform.position + Vector3.up * 0.5f; } return ((Component)player).transform.position + Vector3.up * 0.75f; } private static int GetWeaponKey(PhysGrabObject physical) { PhotonView component = ((Component)physical).GetComponent(); if (!Object.op_Implicit((Object)(object)component) || component.ViewID == 0) { return ((Object)physical).GetInstanceID(); } return component.ViewID; } private static string ItemName(ItemEquippable item) { if (!Object.op_Implicit((Object)(object)item)) { return "unknown"; } ItemAttributes component = ((Component)item).GetComponent(); if (!Object.op_Implicit((Object)(object)component) || !Object.op_Implicit((Object)(object)component.item)) { return ((Object)((Component)item).gameObject).name; } return component.item.itemName; } private static void CancelInventorySwap(string playerId, PendingInventorySwap pending, string reason) { PendingInventorySwaps.Remove(playerId); Plugin.Log.LogInfo((object)("[BRE] inventory swap cancelled: player=" + playerId + ", item=" + ItemName(pending?.HeldItem?.Item) + ", reason=" + reason)); } public static void Reset() { LastWeaponByPlayer.Clear(); LastOwnerByWeapon.Clear(); DeathCandidates.Clear(); PendingReturns.Clear(); PendingForcedDropRecoveries.Clear(); PendingInventorySwaps.Clear(); LastProtectionLogAt.Clear(); } } } namespace BetterReviveExperience.Patches { internal static class PlayerAvatarUpdatePatch { private static void Postfix(PlayerAvatar __instance) { WeaponProtectionController.ObservePlayer(__instance); WeaponProtectionController.ProcessPendingInventorySwap(__instance); WeaponProtectionController.ProcessForcedDropRecovery(__instance); ReviveController.OnPlayerAvatarUpdated(__instance); WeaponProtectionController.ProcessPendingReturn(__instance); } } [HarmonyPatch(typeof(PlayerAvatar), "PlayerDeathRPC")] internal static class PlayerDeathPatch { [HarmonyPrefix] [HarmonyPriority(800)] private static void Prefix(PlayerAvatar __instance) { WeaponProtectionController.CaptureBeforeDeath(__instance); } [HarmonyPostfix] [HarmonyPriority(0)] private static void Postfix(PlayerAvatar __instance) { ReviveController.OnPlayerDeath(__instance); WeaponProtectionController.ConfirmDeath(__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) { WeaponProtectionController.ProcessPendingReturn(__instance.playerAvatar, __instance); ReviveController.OnDeathHeadUpdated(__instance); } } internal static class ForcedGrabReleaseReceivePatch { private static bool Prefix(PhysGrabber __instance, bool physGrabEnded, float _disableTimer, int _releaseObjectViewID, PhotonMessageInfo _info) { //IL_0004: Unknown result type (might be due to invalid IL or missing references) return WeaponProtectionController.AllowForcedRelease(__instance, physGrabEnded, _disableTimer, _releaseObjectViewID, _info); } } [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) { WeaponProtectionController.CaptureInventorySwap(__instance, physGrabberPhotonViewID, isForceUnequip); return ReviveController.AllowForcedUnequip(__instance, physGrabberPhotonViewID, isForceUnequip); } } [HarmonyPatch(typeof(RunManager), "ChangeLevel")] internal static class LevelChangePatch { [HarmonyPrefix] private static void Prefix() { ReviveController.Reset(refundPending: true); WeaponProtectionController.Reset(); Plugin.Debug("[BRE] level change: state reset"); } } [HarmonyPatch(typeof(RoundDirector), "Start")] internal static class RoundStartPatch { [HarmonyPostfix] private static void Postfix() { ReviveController.Reset(); WeaponProtectionController.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); WeaponProtectionController.Reset(); Plugin.Debug("[BRE] main menu: state reset"); } } }