using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using ExitGames.Client.Photon; using HarmonyLib; using Photon.Pun; using Photon.Realtime; using UnityEngine; [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: AssemblyVersion("0.0.0.0")] namespace HellRevival; [BepInPlugin("AngelcoMilk.HellRevival", "HellRevival", "0.1.6")] public sealed class Plugin : BaseUnityPlugin { public const string PluginGuid = "AngelcoMilk.HellRevival"; public const string PluginName = "HellRevival"; public const string PluginVersion = "0.1.6"; internal static Plugin Instance; internal static ManualLogSource Log; private Harmony _harmony; private float _nextCapabilityPublishTime; private void Awake() { //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; ModConfig.Bind(((BaseUnityPlugin)this).Config); InstantReviveController.Reset(); ClientReviveProtection.Reset(); MultiplayerCapabilitySync.PublishLocalCapabilities(); _harmony = new Harmony("AngelcoMilk.HellRevival"); _harmony.PatchAll(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"HellRevival 0.1.6 loaded."); } private void Update() { if (!(Time.time < _nextCapabilityPublishTime)) { _nextCapabilityPublishTime = Time.time + 10f; MultiplayerCapabilitySync.PublishLocalCapabilities(); } } private void OnDestroy() { InstantReviveController.Reset(); ClientReviveProtection.Reset(); MultiplayerCapabilitySync.Clear(); if (_harmony != null) { _harmony.UnpatchSelf(); _harmony = null; } if ((Object)(object)Instance == (Object)(object)this) { Instance = null; } } internal Coroutine StartReviveRoutine(IEnumerator routine) { return ((MonoBehaviour)this).StartCoroutine(routine); } internal Coroutine StartProtectionRoutine(PlayerAvatar player) { return ((MonoBehaviour)this).StartCoroutine(ClientReviveProtection.Run(player)); } internal Coroutine StartProtectionRoutine(PlayerAvatar player, bool hasExpectedPosition, Vector3 expectedPosition, string reason) { //IL_0003: Unknown result type (might be due to invalid IL or missing references) return ((MonoBehaviour)this).StartCoroutine(ClientReviveProtection.Run(player, hasExpectedPosition, expectedPosition, reason)); } } internal enum ClientProtectionMode { Auto, Always, Off } internal enum ReviveTimingPolicy { Auto, Instant, StableDelayed } internal enum UnknownClientPolicy { HostLocal, StableDelayed } internal static class ModConfig { private const float FixedStableDelayedReviveDelay = 0.75f; private const float FixedProtectionWindow = 0.75f; internal static ConfigEntry EnableInstantExtractionRevive; internal static ConfigEntry ReviveHealth; internal static void Bind(ConfigFile config) { //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Expected O, but got Unknown EnableInstantExtractionRevive = config.Bind("General", "Enable Automatic Revive", true, "Host/singleplayer only. Automatically revives a player when their death head enters the extraction point."); ReviveHealth = config.Bind("General", "Revive Health", 20, new ConfigDescription("Health after an extraction revive. Vanilla extraction revive gives 1 health; 20 matches More-Revive-HP's default +19 behavior.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 100), new object[0])); } internal static float SafeProtectionWindow() { return 0.75f; } internal static float SafeStableDelayedReviveDelay() { return 0.75f; } internal static ReviveTimingPolicy SafeReviveTimingPolicy() { return ReviveTimingPolicy.Auto; } internal static UnknownClientPolicy SafeUnknownClientPolicy() { return UnknownClientPolicy.HostLocal; } internal static bool ShouldOverrideReviveHealth() { return true; } internal static int SafeMinimumReviveHealth(int maxHealth) { int num = ((ReviveHealth == null) ? 20 : ReviveHealth.Value); return Mathf.Clamp(num, 1, Mathf.Max(1, maxHealth)); } internal static bool IsDebugLoggingEnabled() { return false; } internal static ReviveTimingPolicy EffectiveReviveTimingPolicy(PlayerAvatar targetPlayer, out string reason) { ReviveTimingPolicy reviveTimingPolicy = SafeReviveTimingPolicy(); if (reviveTimingPolicy != 0) { reason = "forced=" + reviveTimingPolicy; return reviveTimingPolicy; } return MultiplayerCapabilitySync.ResolveReviveTimingFor(targetPlayer, out reason); } internal static ReviveTimingPolicy EffectiveReviveTimingPolicy() { string reason; return EffectiveReviveTimingPolicy(null, out reason); } } internal static class InstantReviveController { private sealed class ReviveState { internal bool ReviveCompleted; internal bool WasTriggered; internal bool ReviveQueued; internal float FirstInsideTime; internal float LastAttemptTime; internal float NextDebugLogTime; } private const float RetryIntervalSeconds = 0.12f; private const float DebugLogIntervalSeconds = 0.75f; private static readonly Dictionary States = new Dictionary(); private static readonly Collider[] OverlapBuffer = (Collider[])(object)new Collider[32]; private static readonly FieldInfo PlayerDeathHeadTriggeredField = typeof(PlayerDeathHead).GetField("triggered", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo PlayerDeathHeadRoomVolumeCheckField = typeof(PlayerDeathHead).GetField("roomVolumeCheck", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo PlayerDeathHeadInExtractionPointField = typeof(PlayerDeathHead).GetField("inExtractionPoint", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo PlayerDeathHeadPhysGrabObjectField = typeof(PlayerDeathHead).GetField("physGrabObject", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo PlayerAvatarDeadSetField = typeof(PlayerAvatar).GetField("deadSet", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo PlayerAvatarIsDisabledField = typeof(PlayerAvatar).GetField("isDisabled", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo RoomVolumeCheckInExtractionPointField = typeof(RoomVolumeCheck).GetField("inExtractionPoint", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo RoomVolumeCheckMaskField = typeof(RoomVolumeCheck).GetField("Mask", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo RoundDirectorExtractionPointCurrentField = typeof(RoundDirector).GetField("extractionPointCurrent", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo RoundDirectorExtractionPointListField = typeof(RoundDirector).GetField("extractionPointList", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); internal static void Reset() { States.Clear(); } internal static void ProcessHead(PlayerDeathHead head) { if (!IsHostOrSingleplayer() || ModConfig.EnableInstantExtractionRevive == null || !ModConfig.EnableInstantExtractionRevive.Value || (Object)(object)head == (Object)null) { return; } int instanceID = ((Object)head).GetInstanceID(); if (!States.TryGetValue(instanceID, out var value)) { value = new ReviveState(); States[instanceID] = value; } if (!IsTriggered(head)) { value.ReviveCompleted = false; value.WasTriggered = false; value.ReviveQueued = false; value.FirstInsideTime = 0f; value.LastAttemptTime = 0f; return; } if (!value.WasTriggered) { value.ReviveCompleted = false; value.WasTriggered = true; value.ReviveQueued = false; value.FirstInsideTime = 0f; value.LastAttemptTime = 0f; } if (value.ReviveCompleted || value.ReviveQueued) { return; } if (!IsInsideExtractionPoint(head, out var roomCheckInside, out var headInside, out var fallbackInside)) { value.FirstInsideTime = 0f; DebugLogState(value, "Revive scan: host=True, triggered=True, roomCheck=" + roomCheckInside + ", headField=" + headInside + ", fallback=" + fallbackInside + ", inside=False."); return; } if (value.FirstInsideTime <= 0f) { value.FirstInsideTime = Time.time; } if (value.LastAttemptTime > 0f && Time.time - value.LastAttemptTime < 0.12f) { return; } if (value.LastAttemptTime <= 0f) { value.NextDebugLogTime = 0f; } value.LastAttemptTime = Time.time; string reason; ReviveTimingPolicy reviveTimingPolicy = ModConfig.EffectiveReviveTimingPolicy(head.playerAvatar, out reason); DebugLogState(value, string.Concat("Revive timing selected: policy=", reviveTimingPolicy, ", reason=", reason, ", target=", MultiplayerCapabilitySync.DescribePlayer(head.playerAvatar), ", firstInsideAge=", (Time.time - value.FirstInsideTime).ToString("F3"), ".")); if (reviveTimingPolicy == ReviveTimingPolicy.Instant || (Object)(object)Plugin.Instance == (Object)null) { ExecuteReviveAttempt(head, value, roomCheckInside, headInside, fallbackInside, "instant revive", requirePreflight: false, reviveTimingPolicy, reason); return; } value.ReviveQueued = true; DebugLogState(value, string.Concat("queued stable delayed revive. policy=", reviveTimingPolicy, ", repoFidelityLoaded=", ClientReviveProtection.IsREPOFidelityLoaded(), ", delay=", ModConfig.SafeStableDelayedReviveDelay().ToString("F2"), ", roomCheck=", roomCheckInside, ", headField=", headInside, ", fallback=", fallbackInside, ".")); try { Plugin.Instance.StartReviveRoutine(RunStableDelayedRevive(head, value)); } catch (Exception ex) { value.ReviveQueued = false; LogException("queue stable delayed revive", ex); } } private static IEnumerator RunStableDelayedRevive(PlayerDeathHead head, ReviveState state) { float reviveAllowedAt = Time.time + ModConfig.SafeStableDelayedReviveDelay(); bool roomCheckInside; bool headInside; bool fallbackInside; while (true) { if ((Object)(object)head == (Object)null || ModConfig.EnableInstantExtractionRevive == null || !ModConfig.EnableInstantExtractionRevive.Value || !IsHostOrSingleplayer() || !IsTriggered(head) || (Object)(object)head.playerAvatar == (Object)null) { if (state != null) { state.ReviveQueued = false; } DebugLog("stable delayed revive aborted before attempt; death head/player state changed."); yield break; } if (!IsInsideExtractionPoint(head, out roomCheckInside, out headInside, out fallbackInside)) { if (state != null) { state.ReviveQueued = false; state.FirstInsideTime = 0f; } DebugLogState(state, "stable delayed revive aborted; head left extraction point. roomCheck=" + roomCheckInside + ", headField=" + headInside + ", fallback=" + fallbackInside + "."); yield break; } string currentPolicyReason; ReviveTimingPolicy currentPolicy = ModConfig.EffectiveReviveTimingPolicy(head.playerAvatar, out currentPolicyReason); if (currentPolicy == ReviveTimingPolicy.Instant) { if (state != null) { state.ReviveQueued = false; } DebugLogState(state, "stable delayed revive switched to instant after policy refresh. reason=" + currentPolicyReason + "."); ExecuteReviveAttempt(head, state, roomCheckInside, headInside, fallbackInside, "instant revive after policy refresh", requirePreflight: false, currentPolicy, currentPolicyReason); yield break; } if (Time.time < reviveAllowedAt) { DebugLogState(state, "stable delayed revive waiting for delay. remaining=" + Mathf.Max(0f, reviveAllowedAt - Time.time).ToString("F2") + ", roomCheck=" + roomCheckInside + ", headField=" + headInside + "."); yield return null; } else { if (ReviveDiagnostics.AreCriticalDependenciesReadyNoLog(head.playerAvatar)) { break; } DebugLogState(state, "stable delayed revive waiting; vanilla ReviveRPC dependencies are not ready. " + ReviveDiagnostics.BuildReadinessReport(head.playerAvatar, ready: false)); yield return null; } } if (state != null) { state.ReviveQueued = false; } ExecuteReviveAttempt(policy: ModConfig.EffectiveReviveTimingPolicy(head.playerAvatar, out var policyReason), head: head, state: state, roomCheckInside: roomCheckInside, headInside: headInside, fallbackInside: fallbackInside, stage: "stable delayed revive", requirePreflight: false, policyReason: policyReason); } private static void ExecuteReviveAttempt(PlayerDeathHead head, ReviveState state, bool roomCheckInside, bool headInside, bool fallbackInside, string stage, bool requirePreflight, ReviveTimingPolicy policy, string policyReason) { //IL_0063: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)head == (Object)null || (Object)(object)head.playerAvatar == (Object)null) { return; } PlayerAvatar playerAvatar = head.playerAvatar; Vector3 position; bool hasExpectedPosition = TryGetExpectedRevivePosition(head, out position); if (requirePreflight && !ReviveDiagnostics.AreCriticalDependenciesReady(playerAvatar, stage + " preflight")) { DebugLogState(state, stage + ": revive delayed; vanilla ReviveRPC dependencies are not ready."); return; } SetExtractionState(head, value: true); if (TryRevive(head, state, roomCheckInside, headInside, fallbackInside, stage, hasExpectedPosition, position, policy, policyReason, out var hadException, out var _) && !hadException && state != null) { state.ReviveCompleted = true; } if (hadException) { DebugLogState(state, stage + ": skipped post revive protection because vanilla ReviveRPC threw."); } } private static bool TryRevive(PlayerDeathHead head, ReviveState state, bool roomCheckInside, bool headInside, bool fallbackInside, string stage, bool hasExpectedPosition, Vector3 expectedPosition, ReviveTimingPolicy policy, string policyReason, out bool hadException, out bool successObserved) { //IL_00e0: Unknown result type (might be due to invalid IL or missing references) hadException = false; successObserved = false; try { float num = ((state == null || state.FirstInsideTime <= 0f) ? 0f : (Time.time - state.FirstInsideTime)); DebugLogState(state, string.Concat(stage, ": revive attempt. effectivePolicy=", policy, ", policyReason=", policyReason, ", target=", MultiplayerCapabilitySync.DescribePlayer(head.playerAvatar), ", firstInsideAge=", num.ToString("F3"), ", roomCheck=", roomCheckInside, ", headFieldBefore=", headInside, ", fallback=", fallbackInside, ", expectedPosition=", FormatExpectedPosition(hasExpectedPosition, expectedPosition), ", repoFidelityProtection=", ClientReviveProtection.WouldRunFor(head.playerAvatar), ".")); head.Revive(); successObserved = HasReviveClearlySucceeded(head); bool flag = IsMultiplayer(); DebugLogState(state, stage + ": revive returned. multiplayer=" + flag + ", headFieldAfter=" + GetBool(PlayerDeathHeadInExtractionPointField, head, defaultValue: false, "PlayerDeathHead.inExtractionPoint") + ", triggeredAfter=" + IsTriggered(head) + ", playerDeadSet=" + GetPlayerBool(head, PlayerAvatarDeadSetField, "PlayerAvatar.deadSet") + ", playerIsDisabled=" + GetPlayerBool(head, PlayerAvatarIsDisabledField, "PlayerAvatar.isDisabled") + ", successObserved=" + successObserved + "."); return successObserved || flag; } catch (Exception ex) { hadException = true; LogException(stage + " revive attempt", ex); successObserved = HasReviveClearlySucceeded(head); DebugLogState(state, stage + ": revive threw. triggeredAfter=" + IsTriggered(head) + ", playerDeadSet=" + GetPlayerBool(head, PlayerAvatarDeadSetField, "PlayerAvatar.deadSet") + ", playerIsDisabled=" + GetPlayerBool(head, PlayerAvatarIsDisabledField, "PlayerAvatar.isDisabled") + ", successObservedAfterException=" + successObserved + "."); return successObserved; } } private static bool IsTriggered(PlayerDeathHead head) { return GetBool(PlayerDeathHeadTriggeredField, head, defaultValue: false, "PlayerDeathHead.triggered"); } private static bool IsInsideExtractionPoint(PlayerDeathHead head, out bool roomCheckInside, out bool headInside, out bool fallbackInside) { roomCheckInside = false; headInside = GetBool(PlayerDeathHeadInExtractionPointField, head, defaultValue: false, "PlayerDeathHead.inExtractionPoint"); fallbackInside = false; RoomVolumeCheck roomVolumeCheck = GetRoomVolumeCheck(head); if ((Object)(object)roomVolumeCheck != (Object)null && RoomVolumeCheckInExtractionPointField != null) { roomCheckInside = GetBool(RoomVolumeCheckInExtractionPointField, roomVolumeCheck, defaultValue: false, "RoomVolumeCheck.inExtractionPoint"); if (roomCheckInside) { return true; } } if (IsFallbackExtractionDetectionEnabled()) { fallbackInside = IsIndependentlyInsideExtractionPoint(head, roomVolumeCheck); } if (!headInside) { return fallbackInside; } return true; } private static bool HasReviveClearlySucceeded(PlayerDeathHead head) { if ((Object)(object)head == (Object)null) { return true; } PlayerAvatar playerAvatar = head.playerAvatar; if ((Object)(object)playerAvatar == (Object)null) { return !IsTriggered(head); } bool @bool = GetBool(PlayerAvatarDeadSetField, playerAvatar, defaultValue: true, "PlayerAvatar.deadSet"); bool bool2 = GetBool(PlayerAvatarIsDisabledField, playerAvatar, defaultValue: true, "PlayerAvatar.isDisabled"); if (!@bool) { return !bool2; } return false; } private static RoomVolumeCheck GetRoomVolumeCheck(PlayerDeathHead head) { if (PlayerDeathHeadRoomVolumeCheckField == null || (Object)(object)head == (Object)null) { return null; } try { object? value = PlayerDeathHeadRoomVolumeCheckField.GetValue(head); return (RoomVolumeCheck)((value is RoomVolumeCheck) ? value : null); } catch (Exception ex) { DebugLog("PlayerDeathHead.roomVolumeCheck read failed: " + ex.Message); return null; } } private static bool IsIndependentlyInsideExtractionPoint(PlayerDeathHead head, RoomVolumeCheck check) { //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)check != (Object)null) { Vector3 val = check.currentSize; if (val == Vector3.zero) { val = ((Component)check).transform.localScale; } LayerMask roomVolumeMask = GetRoomVolumeMask(check); Vector3 val2 = ((Component)check).transform.position + ((Component)check).transform.rotation * check.CheckPosition; int count = Physics.OverlapBoxNonAlloc(val2, val * 0.5f, OverlapBuffer, ((Component)check).transform.rotation, LayerMask.op_Implicit(roomVolumeMask), (QueryTriggerInteraction)2); if (AnyExtractionRoomVolume(count)) { return IsNearKnownExtractionPoint(((Component)head).transform.position); } return false; } int count2 = Physics.OverlapSphereNonAlloc(((Component)head).transform.position, 0.35f, OverlapBuffer, -1, (QueryTriggerInteraction)2); if (AnyExtractionRoomVolume(count2)) { return IsNearKnownExtractionPoint(((Component)head).transform.position); } return false; } private static bool IsFallbackExtractionDetectionEnabled() { return false; } private static bool IsNearKnownExtractionPoint(Vector3 position) { //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) try { if ((Object)(object)RoundDirector.instance == (Object)null) { return false; } ExtractionPoint field = GetField(RoundDirectorExtractionPointCurrentField, RoundDirector.instance); if ((Object)(object)field != (Object)null && Vector3.Distance(position, ((Component)field).transform.position) <= 12f) { return true; } List field2 = GetField>(RoundDirectorExtractionPointListField, RoundDirector.instance); if (field2 == null) { return false; } for (int i = 0; i < field2.Count; i++) { GameObject val = field2[i]; if ((Object)(object)val != (Object)null && Vector3.Distance(position, val.transform.position) <= 12f) { return true; } } } catch (Exception ex) { DebugLog("Known extraction point fallback check failed: " + ex.Message); } return false; } private static LayerMask GetRoomVolumeMask(RoomVolumeCheck check) { //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)check != (Object)null && RoomVolumeCheckMaskField != null) { object value = RoomVolumeCheckMaskField.GetValue(check); if (value is LayerMask) { return (LayerMask)value; } } return LayerMask.op_Implicit(-1); } private static bool AnyExtractionRoomVolume(int count) { for (int i = 0; i < count; i++) { Collider val = OverlapBuffer[i]; if (!((Object)(object)val == (Object)null)) { RoomVolume val2 = ((Component)val).GetComponent(); if ((Object)(object)val2 == (Object)null) { val2 = ((Component)val).GetComponentInParent(); } if ((Object)(object)val2 != (Object)null && val2.Extraction) { return true; } } } return false; } private static void SetExtractionState(PlayerDeathHead head, bool value) { SetBool(PlayerDeathHeadInExtractionPointField, head, value, "PlayerDeathHead.inExtractionPoint"); RoomVolumeCheck roomVolumeCheck = GetRoomVolumeCheck(head); if ((Object)(object)roomVolumeCheck != (Object)null) { SetBool(RoomVolumeCheckInExtractionPointField, roomVolumeCheck, value, "RoomVolumeCheck.inExtractionPoint"); } } private static bool TryGetExpectedRevivePosition(PlayerDeathHead head, out Vector3 position) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) position = Vector3.zero; if ((Object)(object)head == (Object)null) { return false; } try { PhysGrabObject physGrabObject = GetPhysGrabObject(head); if ((Object)(object)physGrabObject != (Object)null) { position = physGrabObject.centerPoint - Vector3.up * 0.25f; return true; } } catch (Exception ex) { DebugLog("Expected revive position from death head PhysGrabObject failed: " + ex.Message); } position = ((Component)head).transform.position; return true; } private static PhysGrabObject GetPhysGrabObject(PlayerDeathHead head) { if (PlayerDeathHeadPhysGrabObjectField == null || (Object)(object)head == (Object)null) { return null; } try { object? value = PlayerDeathHeadPhysGrabObjectField.GetValue(head); return (PhysGrabObject)((value is PhysGrabObject) ? value : null); } catch (Exception ex) { DebugLog("PlayerDeathHead.physGrabObject read failed: " + ex.Message); return null; } } private static bool IsHostOrSingleplayer() { try { return SemiFunc.IsMasterClientOrSingleplayer(); } catch { return true; } } private static bool IsMultiplayer() { try { return GameManager.Multiplayer(); } catch { return false; } } private static bool GetBool(FieldInfo field, object instance, bool defaultValue, string name) { if (field == null || instance == null) { return defaultValue; } try { object value = field.GetValue(instance); if (value is bool) { return (bool)value; } } catch (Exception ex) { DebugLog(name + " read failed: " + ex.Message); } return defaultValue; } internal static T GetField(FieldInfo field, object instance) where T : class { if (field == null || instance == null) { return null; } try { return field.GetValue(instance) as T; } catch (Exception ex) { DebugLog("Field read failed: " + ex.Message); return null; } } private static string GetPlayerBool(PlayerDeathHead head, FieldInfo field, string name) { PlayerAvatar val = (((Object)(object)head == (Object)null) ? null : head.playerAvatar); if (field == null || (Object)(object)val == (Object)null) { return "unknown"; } try { object value = field.GetValue(val); if (value is bool) { return ((bool)value).ToString(); } } catch (Exception ex) { DebugLog(name + " read failed: " + ex.Message); } return "unknown"; } private static void SetBool(FieldInfo field, object instance, bool value, string name) { if (field == null || instance == null) { DebugLog(name + " is not available; vanilla Revive may no-op if the game field is still false."); return; } try { field.SetValue(instance, value); } catch (Exception ex) { LogException(name + " write", ex); } } private static void DebugLogState(ReviveState state, string message) { if (ModConfig.IsDebugLoggingEnabled() && state != null && !(Time.time < state.NextDebugLogTime)) { state.NextDebugLogTime = Time.time + 0.75f; Plugin.Log.LogInfo((object)message); } } private static void DebugLog(string message) { if (ModConfig.IsDebugLoggingEnabled()) { Plugin.Log.LogInfo((object)message); } } private static string FormatExpectedPosition(bool hasExpectedPosition, Vector3 expectedPosition) { if (!hasExpectedPosition) { return "unknown"; } return "(" + expectedPosition.x.ToString("F2") + ", " + expectedPosition.y.ToString("F2") + ", " + expectedPosition.z.ToString("F2") + ")"; } internal static void LogException(string stage, Exception ex) { if (Plugin.Log == null) { return; } Exception ex2 = ex; if (ex is TargetInvocationException && ex.InnerException != null) { ex2 = ex.InnerException; } Plugin.Log.LogWarning((object)(stage + " failed: " + ex2.GetType().Name + ": " + ex2.Message)); if (ModConfig.IsDebugLoggingEnabled()) { if (ex2.StackTrace != null) { Plugin.Log.LogWarning((object)(stage + " stack: " + ex2.StackTrace)); } if (!object.ReferenceEquals(ex2, ex)) { Plugin.Log.LogWarning((object)(stage + " wrapper: " + ex.GetType().Name + ": " + ex.Message)); } } } } internal static class ClientReviveProtection { internal const string REPOFidelityGuid = "Vippy.REPOFidelity"; private static readonly FieldInfo SpectateCameraMainCameraField = typeof(SpectateCamera).GetField("MainCamera", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo SpectateCameraParentObjectField = typeof(SpectateCamera).GetField("ParentObject", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo SpectateCameraPreviousParentField = typeof(SpectateCamera).GetField("PreviousParent", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo AudioManagerAudioListenerField = typeof(AudioManager).GetField("AudioListener", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo PlayerAvatarIsLocalField = typeof(PlayerAvatar).GetField("isLocal", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); internal static void Reset() { } internal static void OnReviveRpc(PlayerAvatar player) { //IL_001d: Unknown result type (might be due to invalid IL or missing references) if (WouldRunFor(player) && !((Object)(object)Plugin.Instance == (Object)null)) { Plugin.Instance.StartProtectionRoutine(player, hasExpectedPosition: false, Vector3.zero, "post revive rpc protection"); } } internal static IEnumerator Run(PlayerAvatar player) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) return Run(player, hasExpectedPosition: false, Vector3.zero, "post revive protection"); } internal static IEnumerator Run(PlayerAvatar player, bool hasExpectedPosition, Vector3 expectedPosition, string reason) { if (WouldRunFor(player)) { float endTime = Time.time + ModConfig.SafeProtectionWindow(); DebugLog("Starting local " + reason + "."); yield return null; while (Time.time <= endTime) { ApplyLocalFixes(player); yield return null; } ApplyLocalFixes(player); DebugLog("Finished local " + reason + "."); } } internal static bool WouldRunFor(PlayerAvatar player) { if ((Object)(object)player == (Object)null || !IsLocalPlayer(player)) { return false; } return IsREPOFidelityLoaded(); } private static bool IsLocalPlayer(PlayerAvatar player) { try { if (PlayerAvatarIsLocalField != null) { object value = PlayerAvatarIsLocalField.GetValue(player); if (value is bool) { return (bool)value; } } } catch { } return (Object)(object)player == (Object)(object)PlayerAvatar.instance; } internal static bool IsREPOFidelityLoaded() { try { return Chainloader.PluginInfos.ContainsKey("Vippy.REPOFidelity"); } catch { return false; } } private static void ApplyLocalFixes(PlayerAvatar player) { if ((Object)(object)player == (Object)null) { player = PlayerAvatar.instance; } InspectSpectateCamera(SpectateCamera.instance); if ((Object)(object)player != (Object)null && (Object)(object)player.localCamera != (Object)null) { SafeCall((Action)player.localCamera.Teleported, "PlayerLocalCamera.Teleported"); } EnsureObject("CameraZoom.Instance", (Object)(object)CameraZoom.Instance); EnsureObject("PostProcessing.Instance", (Object)(object)PostProcessing.Instance); EnsureObject("AudioManager.instance", (Object)(object)AudioManager.instance); if ((Object)(object)AudioManager.instance != (Object)null) { EnsureObject("AudioManager.AudioListener", (Object)(object)GetField(AudioManagerAudioListenerField, AudioManager.instance)); } } private static void InspectSpectateCamera(SpectateCamera spectate) { if ((Object)(object)spectate == (Object)null) { DebugLog("SpectateCamera.instance is not ready during local revive protection."); return; } EnsureObject("SpectateCamera.MainCamera", (Object)(object)GetField(SpectateCameraMainCameraField, spectate)); EnsureObject("SpectateCamera.ParentObject", (Object)(object)GetField(SpectateCameraParentObjectField, spectate)); EnsureObject("SpectateCamera.PreviousParent", (Object)(object)GetField(SpectateCameraPreviousParentField, spectate)); EnsureObject("SpectateCamera.normalTransformPivot", (Object)(object)spectate.normalTransformPivot); } private static void SafeCall(Action action, string name) { try { action(); } catch (Exception ex) { DebugLog(name + " failed: " + ex.Message); } } private static T GetField(FieldInfo field, object instance) where T : class { if (field == null || instance == null) { return null; } try { return field.GetValue(instance) as T; } catch (Exception ex) { DebugLog("Field read failed: " + ex.Message); return null; } } private static void EnsureObject(string name, Object obj) { if (obj == (Object)null) { DebugLog(name + " is not ready during local revive protection."); } } private static void DebugLog(string message) { if (ModConfig.IsDebugLoggingEnabled()) { Plugin.Log.LogInfo((object)message); } } } internal static class ReviveHealthController { private static readonly FieldInfo PlayerHealthHealthField = typeof(PlayerHealth).GetField("health", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo PlayerHealthMaxHealthField = typeof(PlayerHealth).GetField("maxHealth", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); internal static void OnReviveRpc(PlayerAvatar player, bool revivedByTruck) { if (!ModConfig.ShouldOverrideReviveHealth() || (Object)(object)player == (Object)null || (Object)(object)player.playerHealth == (Object)null || !IsHostOrSingleplayer()) { return; } if (revivedByTruck) { DebugLog("Skipped revive health override for truck revive."); return; } int num = Mathf.Max(1, GetPlayerHealthInt(PlayerHealthMaxHealthField, player.playerHealth, 100, "PlayerHealth.maxHealth")); int num2 = ModConfig.SafeMinimumReviveHealth(num); int num3 = Mathf.Max(0, num2 - 1); if (num3 <= 0) { DebugLog("Skipped revive health override because configured target is vanilla revive health."); return; } int playerHealthInt = GetPlayerHealthInt(PlayerHealthHealthField, player.playerHealth, 0, "PlayerHealth.health"); if (IsOwnedByLocalClient(player) && playerHealthInt >= num2) { DebugLog("Skipped revive health override because local owner health is already " + playerHealthInt + "."); return; } try { player.playerHealth.HealOther(num3, true); DebugLog("Applied revive health override. target=" + num2 + ", extra=" + num3 + ", max=" + num + ", player=" + MultiplayerCapabilitySync.DescribePlayer(player) + "."); } catch (Exception ex) { Plugin.Log.LogWarning((object)("HellRevival revive health override failed: " + ex.Message)); } } private static int GetPlayerHealthInt(FieldInfo field, PlayerHealth playerHealth, int defaultValue, string name) { if (field == null || (Object)(object)playerHealth == (Object)null) { return defaultValue; } try { object value = field.GetValue(playerHealth); if (value is int) { return (int)value; } } catch (Exception ex) { DebugLog(name + " read failed: " + ex.Message); } return defaultValue; } private static bool IsHostOrSingleplayer() { try { return SemiFunc.IsMasterClientOrSingleplayer(); } catch { try { return !GameManager.Multiplayer() || PhotonNetwork.IsMasterClient; } catch { return true; } } } private static bool IsOwnedByLocalClient(PlayerAvatar player) { try { return (Object)(object)player.photonView == (Object)null || player.photonView.IsMine; } catch { return false; } } private static void DebugLog(string message) { if (ModConfig.IsDebugLoggingEnabled()) { Plugin.Log.LogInfo((object)message); } } } internal static class MultiplayerCapabilitySync { private const byte HasModKey = 201; private const byte ModVersionKey = 202; private const byte HasREPOFidelityKey = 203; internal static void Clear() { } internal static void PublishLocalCapabilities() { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Expected O, but got Unknown try { if (GameManager.Multiplayer() && PhotonNetwork.LocalPlayer != null) { Hashtable val = new Hashtable(); val[(byte)201] = true; val[(byte)202] = "0.1.6"; val[(byte)203] = ClientReviveProtection.IsREPOFidelityLoaded(); PhotonNetwork.LocalPlayer.SetCustomProperties(val, (Hashtable)null, (WebFlags)null); DebugLog("Published multiplayer capabilities: hasMod=True, version=0.1.6, hasREPOFidelity=" + ClientReviveProtection.IsREPOFidelityLoaded() + "."); } } catch (Exception ex) { DebugLog("Publish multiplayer capabilities failed: " + ex.Message); } } internal static ReviveTimingPolicy ResolveReviveTimingFor(PlayerAvatar targetPlayer, out string reason) { Player photonPlayer = GetPhotonPlayer(targetPlayer); if (photonPlayer == null) { ReviveTimingPolicy reviveTimingPolicy = LocalPolicy(); reason = string.Concat("noPhotonPlayer; localPolicy=", reviveTimingPolicy, ", localRepoFidelity=", ClientReviveProtection.IsREPOFidelityLoaded()); return reviveTimingPolicy; } bool value; bool known = TryGetBool(photonPlayer, 201, out value); bool value2; bool flag = TryGetBool(photonPlayer, 203, out value2); string @string = GetString(photonPlayer, 202); if (flag && value2) { reason = "targetRepoFidelity=True, targetHasMod=" + FormatKnownBool(known, value) + ", targetVersion=" + FormatString(@string); return ReviveTimingPolicy.StableDelayed; } if (flag && !value2) { reason = "targetRepoFidelity=False, targetHasMod=" + FormatKnownBool(known, value) + ", targetVersion=" + FormatString(@string); return ReviveTimingPolicy.Instant; } if (photonPlayer.IsLocal) { ReviveTimingPolicy reviveTimingPolicy2 = LocalPolicy(); reason = string.Concat("targetLocalNoReport; localPolicy=", reviveTimingPolicy2, ", localRepoFidelity=", ClientReviveProtection.IsREPOFidelityLoaded()); return reviveTimingPolicy2; } if (ModConfig.SafeUnknownClientPolicy() == UnknownClientPolicy.StableDelayed) { reason = "targetReportUnknown; unknownClientPolicy=StableDelayed, targetHasMod=" + FormatKnownBool(known, value) + ", targetVersion=" + FormatString(@string); return ReviveTimingPolicy.StableDelayed; } ReviveTimingPolicy reviveTimingPolicy3 = LocalPolicy(); reason = string.Concat("targetReportUnknown; unknownClientPolicy=HostLocal, hostPolicy=", reviveTimingPolicy3, ", targetHasMod=", FormatKnownBool(known, value), ", targetVersion=", FormatString(@string)); return reviveTimingPolicy3; } internal static string DescribePlayer(PlayerAvatar targetPlayer) { Player photonPlayer = GetPhotonPlayer(targetPlayer); if (photonPlayer == null) { return "unknown"; } return "actor=" + photonPlayer.ActorNumber + ", nick=" + FormatString(photonPlayer.NickName); } private static ReviveTimingPolicy LocalPolicy() { if (!ClientReviveProtection.IsREPOFidelityLoaded()) { return ReviveTimingPolicy.Instant; } return ReviveTimingPolicy.StableDelayed; } private static Player GetPhotonPlayer(PlayerAvatar targetPlayer) { try { if (!GameManager.Multiplayer()) { return PhotonNetwork.LocalPlayer; } if ((Object)(object)targetPlayer == (Object)null) { return PhotonNetwork.LocalPlayer; } if ((Object)(object)targetPlayer.photonView != (Object)null && targetPlayer.photonView.Owner != null) { return targetPlayer.photonView.Owner; } if ((Object)(object)targetPlayer == (Object)(object)PlayerAvatar.instance) { return PhotonNetwork.LocalPlayer; } } catch (Exception ex) { DebugLog("Resolve target Photon player failed: " + ex.Message); } return null; } private static bool TryGetBool(Player player, byte key, out bool value) { value = false; try { if (player == null || player.CustomProperties == null || !player.CustomProperties.ContainsKey(key)) { return false; } object obj = player.CustomProperties[key]; if (obj is bool) { value = (bool)obj; return true; } } catch (Exception ex) { DebugLog("Read multiplayer capability bool failed: " + ex.Message); } return false; } private static string GetString(Player player, byte key) { try { if (player == null || player.CustomProperties == null || !player.CustomProperties.ContainsKey(key)) { return null; } return player.CustomProperties[key] as string; } catch (Exception ex) { DebugLog("Read multiplayer capability string failed: " + ex.Message); return null; } } private static string FormatKnownBool(bool known, bool value) { if (!known) { return "unknown"; } return value.ToString(); } private static string FormatString(string value) { if (!string.IsNullOrEmpty(value)) { return value; } return "unknown"; } private static void DebugLog(string message) { if (ModConfig.IsDebugLoggingEnabled()) { Plugin.Log.LogInfo((object)message); } } } internal static class ReviveDiagnostics { private static readonly FieldInfo PlayerAvatarPlayerDeathHeadField = typeof(PlayerAvatar).GetField("playerDeathHead", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo PlayerAvatarPlayerAvatarCollisionField = typeof(PlayerAvatar).GetField("playerAvatarCollision", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo PlayerAvatarIsLocalField = typeof(PlayerAvatar).GetField("isLocal", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo PlayerDeathHeadPhysGrabObjectField = typeof(PlayerDeathHead).GetField("physGrabObject", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); internal static bool AreCriticalDependenciesReady(PlayerAvatar player, string reason) { PlayerDeathHead deathHead = GetDeathHead(player); bool flag = (Object)(object)player != (Object)null && (Object)(object)deathHead != (Object)null && (Object)(object)GetPhysGrabObject(deathHead) != (Object)null && (Object)(object)player.playerHealth != (Object)null && (Object)(object)player.playerAvatarVisuals != (Object)null && (Object)(object)player.playerDeathEffects != (Object)null && (Object)(object)player.playerReviveEffects != (Object)null && (Object)(object)GetAvatarCollision(player) != (Object)null && (Object)(object)player.RoomVolumeCheck != (Object)null; if ((Object)(object)player != (Object)null && IsLocalPlayer(player)) { flag = flag && (Object)(object)player.playerTransform != (Object)null && (Object)(object)player.playerTransform.parent != (Object)null && (Object)(object)CameraAim.Instance != (Object)null && (Object)(object)CameraPosition.instance != (Object)null && (Object)(object)GameDirector.instance != (Object)null && (Object)(object)SpectateCamera.instance != (Object)null && (Object)(object)PlayerController.instance != (Object)null && (Object)(object)CameraGlitch.Instance != (Object)null; } if (ModConfig.IsDebugLoggingEnabled()) { Plugin.Log.LogInfo((object)(reason + ": " + BuildReadinessReport(player, flag))); } return flag; } internal static void LogReviveRpcPrefix(PlayerAvatar player) { if (ModConfig.IsDebugLoggingEnabled()) { Plugin.Log.LogInfo((object)("ReviveRPC prefix: " + BuildReadinessReport(player, AreCriticalDependenciesReadyNoLog(player)))); } } internal static bool AreCriticalDependenciesReadyNoLog(PlayerAvatar player) { if ((Object)(object)player == (Object)null || (Object)(object)player.playerHealth == (Object)null || (Object)(object)player.playerAvatarVisuals == (Object)null || (Object)(object)player.playerDeathEffects == (Object)null || (Object)(object)player.playerReviveEffects == (Object)null || (Object)(object)player.RoomVolumeCheck == (Object)null) { return false; } PlayerDeathHead deathHead = GetDeathHead(player); if ((Object)(object)deathHead == (Object)null || (Object)(object)GetPhysGrabObject(deathHead) == (Object)null || (Object)(object)GetAvatarCollision(player) == (Object)null) { return false; } if (!IsLocalPlayer(player)) { return true; } if ((Object)(object)player.playerTransform != (Object)null && (Object)(object)player.playerTransform.parent != (Object)null && (Object)(object)CameraAim.Instance != (Object)null && (Object)(object)CameraPosition.instance != (Object)null && (Object)(object)GameDirector.instance != (Object)null && (Object)(object)SpectateCamera.instance != (Object)null && (Object)(object)PlayerController.instance != (Object)null) { return (Object)(object)CameraGlitch.Instance != (Object)null; } return false; } internal static string BuildReadinessReport(PlayerAvatar player, bool ready) { if ((Object)(object)player == (Object)null) { return "ready=False, playerAvatar=null."; } PlayerDeathHead deathHead = GetDeathHead(player); bool flag = IsLocalPlayer(player); string text = (((Object)(object)player.playerTransform == (Object)null) ? "unknown" : FormatReady((Object)(object)player.playerTransform.parent != (Object)null)); return "ready=" + ready + ", isLocal=" + flag + ", playerDeathHead=" + FormatReady((Object)(object)deathHead != (Object)null) + ", physGrabObject=" + FormatReady((Object)(object)deathHead != (Object)null && (Object)(object)GetPhysGrabObject(deathHead) != (Object)null) + ", playerHealth=" + FormatReady((Object)(object)player.playerHealth != (Object)null) + ", playerAvatarVisuals=" + FormatReady((Object)(object)player.playerAvatarVisuals != (Object)null) + ", playerDeathEffects=" + FormatReady((Object)(object)player.playerDeathEffects != (Object)null) + ", playerReviveEffects=" + FormatReady((Object)(object)player.playerReviveEffects != (Object)null) + ", playerAvatarCollision=" + FormatReady((Object)(object)GetAvatarCollision(player) != (Object)null) + ", playerTransform=" + FormatReady((Object)(object)player.playerTransform != (Object)null) + ", playerTransform.parent=" + text + ", CameraAim.Instance=" + FormatReady((Object)(object)CameraAim.Instance != (Object)null) + ", CameraPosition.instance=" + FormatReady((Object)(object)CameraPosition.instance != (Object)null) + ", GameDirector.instance=" + FormatReady((Object)(object)GameDirector.instance != (Object)null) + ", SpectateCamera.instance=" + FormatReady((Object)(object)SpectateCamera.instance != (Object)null) + ", PlayerController.instance=" + FormatReady((Object)(object)PlayerController.instance != (Object)null) + ", CameraGlitch.Instance=" + FormatReady((Object)(object)CameraGlitch.Instance != (Object)null) + ", RoomVolumeCheck=" + FormatReady((Object)(object)player.RoomVolumeCheck != (Object)null) + "."; } private static bool IsLocalPlayer(PlayerAvatar player) { if ((Object)(object)player == (Object)null) { return false; } try { if (PlayerAvatarIsLocalField != null) { object value = PlayerAvatarIsLocalField.GetValue(player); if (value is bool) { return (bool)value; } } } catch { } return (Object)(object)player == (Object)(object)PlayerAvatar.instance; } private static PlayerDeathHead GetDeathHead(PlayerAvatar player) { return InstantReviveController.GetField(PlayerAvatarPlayerDeathHeadField, player); } private static PhysGrabObject GetPhysGrabObject(PlayerDeathHead head) { return InstantReviveController.GetField(PlayerDeathHeadPhysGrabObjectField, head); } private static PlayerAvatarCollision GetAvatarCollision(PlayerAvatar player) { return InstantReviveController.GetField(PlayerAvatarPlayerAvatarCollisionField, player); } private static string FormatReady(bool ready) { if (!ready) { return "null"; } return "ok"; } } [HarmonyPatch(typeof(PlayerDeathHead), "Update")] internal static class PlayerDeathHeadUpdatePatch { private static void Postfix(PlayerDeathHead __instance) { InstantReviveController.ProcessHead(__instance); } } [HarmonyPatch(typeof(PlayerAvatar), "ReviveRPC")] internal static class PlayerAvatarReviveRpcPatch { private static void Prefix(PlayerAvatar __instance) { ReviveDiagnostics.LogReviveRpcPrefix(__instance); } private static void Postfix(PlayerAvatar __instance, bool _revivedByTruck) { ReviveHealthController.OnReviveRpc(__instance, _revivedByTruck); ClientReviveProtection.OnReviveRpc(__instance); } } [HarmonyPatch(typeof(RunManager), "ChangeLevel")] internal static class RunManagerChangeLevelPatch { private static void Prefix() { InstantReviveController.Reset(); ClientReviveProtection.Reset(); MultiplayerCapabilitySync.PublishLocalCapabilities(); } }