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.3.5
plugins/BetterReviveExperience/BetterReviveExperience.dll
Decompiled a month 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.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<bool> KeepItemsOnDeath { get; private set; } public static ConfigEntry<bool> ProtectHeldItems { get; private set; } public static ConfigEntry<bool> SwapHeldItemOnOccupiedSlot { get; private set; } public static ConfigEntry<bool> ReturnHeldItemOnDeath { 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_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<bool>("Inventory", "KeepItemsOnDeath", true, "Keep inventory-slot items when a player dies."); ProtectHeldItems = ((BaseUnityPlugin)this).Config.Bind<bool>("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<bool>("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<bool>("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<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.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<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.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<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); 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<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}"; } 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<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>(); } 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<string, HeldWeaponRecord> LastWeaponByPlayer = new Dictionary<string, HeldWeaponRecord>(); private static readonly Dictionary<int, string> LastOwnerByWeapon = new Dictionary<int, string>(); private static readonly Dictionary<string, HeldWeaponRecord> DeathCandidates = new Dictionary<string, HeldWeaponRecord>(); private static readonly Dictionary<string, PendingWeaponReturn> PendingReturns = new Dictionary<string, PendingWeaponReturn>(); private static readonly Dictionary<string, PendingForcedDropRecovery> PendingForcedDropRecoveries = new Dictionary<string, PendingForcedDropRecovery>(); private static readonly Dictionary<string, PendingInventorySwap> PendingInventorySwaps = new Dictionary<string, PendingInventorySwap>(); private static readonly Dictionary<int, float> LastProtectionLogAt = new Dictionary<int, float>(); private static bool AutoHoldWarningLogged; public static bool ValidateGameApi(ICollection<string> 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<ItemEquippable>(); 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<PhysGrabber>(); } 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<PhysGrabObject>() : 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<PhotonView>(); 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<ItemAttributes>(); 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"); } } }