using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using ReviveAllies.Components; using ReviveAllies.Components.States; using ReviveAllies.E2E; using Steamworks; using TMPro; using UnityEngine; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: IgnoresAccessChecksTo("assembly_guiutils")] [assembly: IgnoresAccessChecksTo("assembly_utils")] [assembly: IgnoresAccessChecksTo("assembly_valheim")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("ReviveAllies")] [assembly: AssemblyConfiguration("Debug")] [assembly: AssemblyFileVersion("0.3.4.0")] [assembly: AssemblyInformationalVersion("0.3.4")] [assembly: AssemblyProduct("ReviveAllies")] [assembly: AssemblyTitle("ReviveAllies")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.3.4.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [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 ReviveAllies { public enum ReviveModeType { Hold, Press } [BepInPlugin("com.andres.reviveallies", "ReviveAllies", "0.3.4-1.0-revived")] public class Plugin : BaseUnityPlugin { public const string PluginGuid = "com.andres.reviveallies"; public const string PluginName = "ReviveAllies"; public const string PluginVersion = "0.3.4-1.0-revived"; internal static ConfigEntry ReviveModeCfg = null; internal static ConfigEntry ReviveHoldTimeCfg = null; internal static ConfigEntry ReviveWindowCfg = null; internal const string RpcConfig = "ReviveAllies_Config"; private static bool s_hasServerConfig; private static float s_srvWindow; private static float s_srvHoldTime; private static bool s_srvPressMode; private static readonly float s_windowEnvOverride = ReadWindowOverride(); public const float ReviveHealthFraction = 0.25f; private Harmony? _harmony; internal static ManualLogSource Logger { get; private set; } = null; private static bool UseServerConfig => s_hasServerConfig && (Object)(object)ZNet.instance != (Object)null && !ZNet.instance.IsServer(); public static float ReviveWindow { get { if (s_windowEnvOverride > 0f) { return s_windowEnvOverride; } return UseServerConfig ? s_srvWindow : (ReviveWindowCfg?.Value ?? 30f); } } public static float ReviveDuration => Mathf.Max(0.1f, UseServerConfig ? s_srvHoldTime : (ReviveHoldTimeCfg?.Value ?? 4f)); public static bool RevivePressMode => UseServerConfig ? s_srvPressMode : (ReviveModeCfg != null && ReviveModeCfg.Value == ReviveModeType.Press); private static float ReadWindowOverride() { string environmentVariable = Environment.GetEnvironmentVariable("RR_E2E_WINDOW"); float result; return (float.TryParse(environmentVariable, out result) && result > 0f) ? result : 0f; } internal static void ApplyServerConfig(float window, float holdTime, bool pressMode) { s_srvWindow = window; s_srvHoldTime = holdTime; s_srvPressMode = pressMode; s_hasServerConfig = true; Logger.LogInfo((object)$"Adopted server revive config: window={window:F0}s hold={holdTime:F1}s press={pressMode}"); } internal static void ResetServerConfig() { s_hasServerConfig = false; } internal static void BroadcastConfig() { if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer() && ZRoutedRpc.instance != null) { ZRoutedRpc instance = ZRoutedRpc.instance; long num = 0L; object[] obj = new object[3] { ReviveWindowCfg?.Value ?? 30f, Mathf.Max(0.1f, ReviveHoldTimeCfg?.Value ?? 4f), null }; ConfigEntry reviveModeCfg = ReviveModeCfg; obj[2] = ((reviveModeCfg != null && reviveModeCfg.Value == ReviveModeType.Press) ? 1 : 0); instance.InvokeRoutedRPC(num, "ReviveAllies_Config", obj); } } private void Awake() { //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Expected O, but got Unknown //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Expected O, but got Unknown Logger = ((BaseUnityPlugin)this).Logger; Logger.LogInfo((object)"ReviveAllies v0.3.4-1.0-revived loaded!"); ReviveModeCfg = ((BaseUnityPlugin)this).Config.Bind("Revive", "Mode", ReviveModeType.Hold, "Hold: the interact key must be held for HoldTimeSeconds to revive. Press: a single press revives instantly."); ReviveHoldTimeCfg = ((BaseUnityPlugin)this).Config.Bind("Revive", "HoldTimeSeconds", 4f, new ConfigDescription("How long the interact key must be held to complete a revive (Hold mode).", (AcceptableValueBase)(object)new AcceptableValueRange(0.1f, 60f), Array.Empty())); ReviveWindowCfg = ((BaseUnityPlugin)this).Config.Bind("Revive", "WindowSeconds", 30f, new ConfigDescription("How long a downed player can be revived before dying for real. Server-authoritative: the host's value governs everyone.", (AcceptableValueBase)(object)new AcceptableValueRange(5f, 600f), Array.Empty())); ReviveModeCfg.SettingChanged += delegate { BroadcastConfig(); }; ReviveHoldTimeCfg.SettingChanged += delegate { BroadcastConfig(); }; ReviveWindowCfg.SettingChanged += delegate { BroadcastConfig(); }; _harmony = Harmony.CreateAndPatchAll(typeof(Plugin).Assembly, "com.andres.reviveallies"); if (Environment.GetEnvironmentVariable("RR_E2E") == "1") { E2ERunner.Bootstrap(); } } private void OnDestroy() { Harmony? harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } } } namespace ReviveAllies.Patches { [HarmonyPatch(typeof(Character), "CheckDeath")] internal static class CheckDeathPatch { private static bool Prefix(Character __instance) { //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: 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_00d5: Expected O, but got Unknown Player val = (Player)(object)((__instance is Player) ? __instance : null); if (val == null) { return true; } if (!((Character)val).m_nview.IsOwner()) { return true; } if (((Character)val).IsDead()) { return true; } if (((Character)val).GetHealth() > 0f) { return true; } if ((Object)(object)((Component)val).GetComponent() == (Object)null) { return true; } if (!val.IsDowned()) { val.EnterDownedState(); return false; } if (!((Component)val).GetComponent().IsChanneling && val.IsReviveWindowExpired()) { if (((Character)val).m_lastHit == null) { ((Character)val).m_lastHit = new HitData { m_hitType = (HitType)14 }; } val.ExpireDownedState(); return true; } return false; } } [HarmonyPatch(typeof(ZNet), "Awake")] internal static class ZNetAwakeConfigSyncPatch { private static void Postfix() { Plugin.ResetServerConfig(); ZRoutedRpc instance = ZRoutedRpc.instance; if (instance == null) { return; } instance.Register("ReviveAllies_Config", (Action)delegate(long sender, float window, float holdTime, int mode) { if (!((Object)(object)ZNet.instance != (Object)null) || !ZNet.instance.IsServer()) { Plugin.ApplyServerConfig(window, holdTime, mode != 0); } }); } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] internal static class ZNetPeerInfoConfigSyncPatch { private static void Postfix(ZNet __instance) { if (__instance.IsServer()) { Plugin.BroadcastConfig(); } } } [HarmonyPatch(typeof(BaseAI), "IsEnemy", new Type[] { typeof(Character), typeof(Character) })] internal static class BaseAIIgnoreDownedPatch { private static void Postfix(Character a, Character b, ref bool __result) { if (__result && (IsDownedPlayer(a) || IsDownedPlayer(b))) { __result = false; } } private static bool IsDownedPlayer(Character c) { return ((Player?)(object)((c is Player) ? c : null))?.IsDowned() ?? false; } } [HarmonyPatch(typeof(ZNet), "OpenServer")] internal static class ZNetOpenServerSocketPatch { private static bool Prefix(ZNet __instance) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Invalid comparison between Unknown and I4 //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Expected O, but got Unknown if (!E2EConfig.MultiplayerMode) { return true; } if ((int)ZNet.m_onlineBackend != 3) { return true; } if (!__instance.IsServer()) { return true; } int port = E2EConfig.Port; ZSocket2 val = new ZSocket2(); if (!val.StartHost()) { Plugin.Logger.LogError((object)$"E2E: ZSocket2 host failed to bind port {port}"); return true; } __instance.m_hostSocket = (ISocket)(object)val; ZNet.m_openServer = true; E2ELog.Write($"E2E: opened CustomSocket (TCP) host on port {port}"); return false; } } [HarmonyPatch(typeof(ZNet), "Update")] internal static class ZNetUpdatePumpConnectorPatch { private static void Postfix(ZNet __instance) { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Invalid comparison between Unknown and I4 if (E2EConfig.MultiplayerMode && (int)ZNet.m_onlineBackend == 3 && __instance.m_serverConnector != null) { __instance.UpdateClientConnector(Time.deltaTime); } } } [HarmonyPatch(typeof(ZNet), "SendPeerInfo")] internal static class ZNetSendPeerInfoSocketPatch { private static bool Prefix(ZNet __instance, ZRpc rpc, string password) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Invalid comparison between Unknown and I4 //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Expected O, but got Unknown //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) if (!E2EConfig.MultiplayerMode) { return true; } if ((int)ZNet.m_onlineBackend != 3) { return true; } if (__instance.IsServer()) { return true; } ZPackage val = new ZPackage(); val.Write(ZNet.GetUID()); val.Write(((object)Version.CurrentVersion/*cast due to .constrained prefix*/).ToString()); val.Write(36u); val.Write(__instance.m_referencePosition); val.Write(Game.instance.GetPlayerProfile().GetName()); val.Write(""); rpc.Invoke("PeerInfo", new object[1] { val }); E2ELog.Write("E2E[client]: sent CustomSocket PeerInfo (no steam ticket)"); return false; } } [HarmonyPatch(typeof(ZSocket2), "Recv")] internal static class SocketLatencyPatch { private static readonly Dictionary> s_delayed = new Dictionary>(); private static void Postfix(ZSocket2 __instance, ref ZPackage? __result) { int latencyMs = E2EConfig.LatencyMs; if (latencyMs > 0 && E2EConfig.MultiplayerMode) { if (!s_delayed.TryGetValue(__instance, out Queue<(DateTime, ZPackage)> value)) { value = (s_delayed[__instance] = new Queue<(DateTime, ZPackage)>()); } if (__result != null) { value.Enqueue((DateTime.UtcNow.AddMilliseconds(latencyMs), __result)); } __result = ((value.Count > 0 && value.Peek().Item1 <= DateTime.UtcNow) ? value.Dequeue().Item2 : null); } } } [HarmonyPatch(typeof(Player), "Awake")] internal static class PlayerAwakePatch { private static void Postfix(Player __instance) { if ((Object)(object)((Component)__instance).GetComponent() == (Object)null) { ((Component)__instance).gameObject.AddComponent(); } if ((Object)(object)((Component)__instance).GetComponent() == (Object)null) { ((Component)__instance).gameObject.AddComponent(); } if ((Object)(object)((Component)__instance).GetComponent() == (Object)null) { ((Component)__instance).gameObject.AddComponent(); } if ((Object)(object)((Component)__instance).GetComponent() == (Object)null) { ((Component)__instance).gameObject.AddComponent(); } if ((Object)(object)((Component)__instance).GetComponent() == (Object)null) { ((Component)__instance).gameObject.AddComponent(); } if ((Object)(object)((Component)__instance).GetComponent() == (Object)null) { ((Component)__instance).gameObject.AddComponent(); } if ((Object)(object)((Component)__instance).GetComponent() == (Object)null) { ((Component)__instance).gameObject.AddComponent(); } } } [HarmonyPatch(typeof(Player), "CanMove")] internal static class PlayerCanMovePatch { private static void Postfix(Player __instance, ref bool __result) { if (__result && __instance.IsDowned()) { __result = false; } } } [HarmonyPatch(typeof(Character), "UpdateMotion")] internal static class CharacterUpdateMotionPatch { private static bool Prefix(Character __instance) { Player val = (Player)(object)((__instance is Player) ? __instance : null); if (val != null && val.IsDowned()) { return false; } return true; } } [HarmonyPatch(typeof(Player), "LateUpdate")] internal static class PlayerLateUpdatePatch { private static void Postfix(Player __instance) { if (((Character)__instance).IsDead()) { if ((Object)(object)((Character)__instance).m_collider != (Object)null && ((Collider)((Character)__instance).m_collider).enabled) { ((Collider)((Character)__instance).m_collider).enabled = false; } if ((Object)(object)((Character)__instance).m_nview != (Object)null && ((Character)__instance).m_nview.IsValid() && ((Character)__instance).m_nview.IsOwner() && (Object)(object)((Character)__instance).m_body != (Object)null && !((Character)__instance).m_body.isKinematic) { ((Character)__instance).m_body.isKinematic = true; } } } } [HarmonyPatch(typeof(Player), "UpdateHover")] internal static class PlayerUpdateHoverPatch { private static bool Prefix(Player __instance) { if (__instance.IsDowned()) { return false; } return true; } } [HarmonyPatch(typeof(Player), "OnRespawn")] internal static class PlayerOnRespawnPatch { private static bool Prefix(Player __instance) { if (__instance.IsDowned()) { return false; } return true; } } [HarmonyPatch(typeof(EnemyHud), "TestShow")] internal static class EnemyHudHideDownedPatch { private static void Postfix(Character c, ref bool __result) { if (__result) { Player val = (Player)(object)((c is Player) ? c : null); if (val != null && (val.IsDowned() || ((Character)val).IsDead())) { __result = false; } } } } [HarmonyPatch(typeof(ZNetScene), "Awake")] internal static class ZNetSceneAwakeRegisterPrefabsPatch { private static void Postfix(ZNetScene __instance) { MarkerPrefab.RegisterPrefab(__instance); } } [HarmonyPatch(typeof(TombStone), "Setup")] internal static class TombStoneSetupReplacePatch { private static void Postfix(TombStone __instance, long ownerUID) { //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_007e: 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) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_009a: Unknown result type (might be due to invalid IL or missing references) //IL_00be: Unknown result type (might be due to invalid IL or missing references) //IL_00c6: Unknown result type (might be due to invalid IL or missing references) //IL_00fb: Unknown result type (might be due to invalid IL or missing references) Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null || (Object)(object)((Character)localPlayer).m_nview == (Object)null || !((Character)localPlayer).m_nview.IsValid() || ownerUID != localPlayer.GetPlayerID()) { return; } GraveReplaceView graveReplaceView = localPlayer.GraveReplace(); if (graveReplaceView.Pending) { graveReplaceView.Pending = false; Vector3 val = graveReplaceView.Pos; if (val == Vector3.zero) { val = ((Component)__instance).transform.position; } ((Component)__instance).transform.position = val; Rigidbody component = ((Component)__instance).GetComponent(); if ((Object)(object)component != (Object)null) { component.position = val; component.linearVelocity = Vector3.zero; } GameObject marker = localPlayer.FindDownedMarker() ?? MarkerPrefab.FindFor(localPlayer.GetPlayerID()); DownedMarker.MarkReplaced(marker); DownedStateMachineView downedStateMachineView = localPlayer.State(); downedStateMachineView.Marker = ZDOID.None; } } } [HarmonyPatch(typeof(Player), "OnDeath")] internal static class PlayerOnDeathMarkerCrumblePatch { private static void Postfix(Player __instance) { //IL_006e: Unknown result type (might be due to invalid IL or missing references) ZNetView nview = ((Character)__instance).m_nview; if (!((Object)(object)nview == (Object)null) && nview.IsValid() && nview.IsOwner()) { GraveReplaceView graveReplaceView = __instance.GraveReplace(); if (graveReplaceView.Pending) { graveReplaceView.Pending = false; GameObject marker = __instance.FindDownedMarker() ?? MarkerPrefab.FindFor(__instance.GetPlayerID()); DownedStateMachineView downedStateMachineView = __instance.State(); downedStateMachineView.Marker = ZDOID.None; DownedMarker.Crumble(marker); Plugin.Logger.LogInfo((object)(__instance.GetPlayerName() + " died with no grave to drop; marker crumbled")); } } } } [HarmonyPatch(typeof(Player), "CreateDeathEffects")] internal static class SuppressPlayerRagdollPatch { private static bool Prefix() { return false; } } } namespace ReviveAllies.E2E { public static class E2EConfig { public static bool Enabled => Environment.GetEnvironmentVariable("RR_E2E") == "1"; public static string Role => (Environment.GetEnvironmentVariable("RR_E2E_ROLE") ?? "").ToLowerInvariant(); public static bool MultiplayerMode => Role == "host" || Role == "client"; public static bool IsHost => Role == "host"; public static bool IsClient => Role == "client"; public static string Scenario => (Environment.GetEnvironmentVariable("RR_E2E_SCENARIO") ?? "revive").ToLowerInvariant(); public static bool IsRejoinScenario => Scenario == "rejoin"; public static bool IsVanishScenario => Scenario == "vanish"; public static bool IsReviveLoopScenario => Scenario == "reviveloop"; public static bool IsConfigSyncScenario => Scenario == "configsync"; public static string LoopDownRole => (Environment.GetEnvironmentVariable("RR_E2E_LOOP_DOWN") ?? "host").ToLowerInvariant(); public static int LoopCycles { get { string environmentVariable = Environment.GetEnvironmentVariable("RR_E2E_LOOP_CYCLES"); int result; return (int.TryParse(environmentVariable, out result) && result > 0) ? result : 15; } } public static string LoopMode => (Environment.GetEnvironmentVariable("RR_E2E_LOOP_MODE") ?? "revive").ToLowerInvariant(); public static bool IsExpireLoop => LoopMode == "expire"; public static bool Manual => Environment.GetEnvironmentVariable("RR_E2E_MANUAL") == "1"; public static int Port { get { string environmentVariable = Environment.GetEnvironmentVariable("RR_E2E_PORT"); int result; return int.TryParse(environmentVariable, out result) ? result : 2456; } } public static string WorldName => Environment.GetEnvironmentVariable("RR_E2E_WORLD") ?? "e2e_mp"; public static string ServerHost => Environment.GetEnvironmentVariable("RR_E2E_HOST") ?? "127.0.0.1"; public static int LatencyMs { get { string environmentVariable = Environment.GetEnvironmentVariable("RR_E2E_LATENCY"); int result; return (int.TryParse(environmentVariable, out result) && result > 0) ? result : 0; } } } public static class E2ELog { private static readonly object s_lock = new object(); private static string? s_path; private static bool s_init; private static string? Path { get { if (!s_init) { s_path = Environment.GetEnvironmentVariable("RR_E2E_LOG"); if (string.IsNullOrEmpty(s_path)) { string environmentVariable = Environment.GetEnvironmentVariable("RR_E2E_RESULTS"); if (!string.IsNullOrEmpty(environmentVariable)) { s_path = environmentVariable + ".log"; } } s_init = true; if (!string.IsNullOrEmpty(s_path)) { try { File.WriteAllText(s_path, "# E2E log role=" + E2EConfig.Role + "\n"); } catch { } } } return s_path; } } public static void Write(string msg) { Plugin.Logger.LogInfo((object)msg); string path = Path; if (string.IsNullOrEmpty(path)) { return; } try { lock (s_lock) { File.AppendAllText(path, msg + "\n"); } } catch { } } } public class E2ERunner : MonoBehaviour { private const float HardTimeoutSeconds = 360f; private readonly List<(string name, bool pass, string detail)> _results = new List<(string, bool, string)>(); private bool _started; private bool _worldStartIssued; private float _elapsed; private const float LeakGraceSeconds = 8f; private int m_loopLeaks; private static string ResultPath => Environment.GetEnvironmentVariable("RR_E2E_RESULTS") ?? Path.Combine(Paths.GameRootPath ?? ".", "e2e-results.txt"); public static void Bootstrap() { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Expected O, but got Unknown GameObject val = new GameObject("RevivalRevived_E2ERunner"); Object.DontDestroyOnLoad((Object)(object)val); val.AddComponent(); Plugin.Logger.LogInfo((object)("E2E: harness bootstrapped (role='" + E2EConfig.Role + "')")); } private void Start() { ((MonoBehaviour)this).StartCoroutine(RunAll()); } private void Update() { if (!E2EConfig.Manual) { _elapsed += Time.unscaledDeltaTime; if (_elapsed > 360f && _started) { Log("E2E: HARD TIMEOUT reached, aborting"); Finish(ok: false, "hard-timeout"); } } } private IEnumerator RunAll() { _started = true; Log($"E2E: run starting (role='{E2EConfig.Role}' manual={E2EConfig.Manual})"); if (E2EConfig.Manual) { yield return ((MonoBehaviour)this).StartCoroutine(RunManual()); yield break; } if (Environment.GetEnvironmentVariable("RR_E2E_DEMO") == "1") { yield return ((MonoBehaviour)this).StartCoroutine(RunDemo()); } else if (E2EConfig.IsHost) { yield return ((MonoBehaviour)this).StartCoroutine(RunHost()); } else if (E2EConfig.IsClient) { yield return ((MonoBehaviour)this).StartCoroutine(RunClient()); } else { yield return ((MonoBehaviour)this).StartCoroutine(RunSingleProcess()); } bool allPass = _results.Count > 0; foreach (var result in _results) { allPass &= result.pass; } Finish(allPass, allPass ? "all-pass" : "failures"); } private IEnumerator RunHost() { yield return ((MonoBehaviour)this).StartCoroutine(AutoStart()); yield return ((MonoBehaviour)this).StartCoroutine(WaitForPlayerInWorld()); Player player = Player.m_localPlayer; if ((Object)(object)player == (Object)null) { Record("host_spawn", pass: false, "no local player"); yield break; } Record("host_spawn", pass: true, "name=" + player.GetPlayerName()); if (E2EConfig.IsConfigSyncScenario) { Plugin.ReviveHoldTimeCfg.Value = 7f; Plugin.ReviveModeCfg.Value = ReviveModeType.Press; Plugin.BroadcastConfig(); Log("E2E[host]: configsync -- authoritative hold=7 mode=Press"); } Log("E2E[host]: waiting for a client to connect..."); float waited = 0f; while (waited < 180f) { int peers = (((Object)(object)ZNet.instance != (Object)null) ? ZNet.instance.GetPeerConnections() : 0); int players = Player.GetAllPlayers().Count; if (peers >= 1 && players >= 2) { break; } waited += Time.unscaledDeltaTime; yield return null; } int peerCount = (((Object)(object)ZNet.instance != (Object)null) ? ZNet.instance.GetPeerConnections() : 0); int playerCount = Player.GetAllPlayers().Count; bool connected = peerCount >= 1 && playerCount >= 2; Record("host_client_connected", connected, $"peers={peerCount} players={playerCount}"); if (!connected) { yield break; } if (E2EConfig.IsRejoinScenario) { Log("E2E[host]: rejoin scenario -- idling as server"); yield return (object)new WaitForSecondsRealtime(160f); Record("host_idle_complete", pass: true, "server stayed up for client rejoin"); yield break; } if (E2EConfig.IsConfigSyncScenario) { Log("E2E[host]: configsync -- idling while client verifies"); yield return (object)new WaitForSecondsRealtime(30f); Record("host_idle_complete", pass: true, "server stayed up for configsync"); yield break; } if (E2EConfig.IsVanishScenario) { yield return ((MonoBehaviour)this).StartCoroutine(RunHostVanish(player)); yield break; } if (E2EConfig.IsReviveLoopScenario) { if (E2EConfig.LoopDownRole == "host") { yield return ((MonoBehaviour)this).StartCoroutine(RunLoopVictim(player)); } else { yield return ((MonoBehaviour)this).StartCoroutine(RunLoopReviver(player)); } yield break; } yield return (object)new WaitForSecondsRealtime(2f); Log("E2E[host]: downing self"); ((Character)player).SetHealth(0f); waited = 0f; while (waited < 5f && !player.IsDowned()) { waited += Time.unscaledDeltaTime; yield return null; } Record("host_downed", player.IsDowned(), $"downed={player.IsDowned()} dead={((Character)player).IsDead()}"); if (player.IsDowned()) { Log("E2E[host]: waiting to be revived by client..."); waited = 0f; float maxSeenProgress = 0f; while (waited < 28f && player.IsDowned() && !((Character)player).IsDead()) { maxSeenProgress = Mathf.Max(maxSeenProgress, player.GetReviveProgress()); waited += Time.unscaledDeltaTime; yield return null; } bool revived = !player.IsDowned() && !((Character)player).IsDead() && ((Character)player).GetHealth() > 0f; Record("host_revived_by_client", revived, $"downed={player.IsDowned()} dead={((Character)player).IsDead()} hp={((Character)player).GetHealth():F0}"); Record("host_saw_progress", maxSeenProgress > 0.5f, $"maxSeenProgress={maxSeenProgress:F2}"); yield return (object)new WaitForSecondsRealtime(6f); } } private IEnumerator RunHostVanish(Player me) { Log("E2E[host]: vanish scenario -- waiting for client to be downed..."); Player downed = null; float w = 0f; while (w < 60f) { downed = FindDownedRemotePlayer(me); if ((Object)(object)downed != (Object)null) { break; } w += Time.unscaledDeltaTime; yield return null; } Record("vanish_client_downed", (Object)(object)downed != (Object)null, ((Object)(object)downed != (Object)null) ? ("name=" + downed.GetPlayerName()) : "none"); if ((Object)(object)downed == (Object)null) { yield break; } long downedPid = downed.GetPlayerID(); yield return (object)new WaitForSecondsRealtime(1f); Log("E2E[host]: channeling; client will log out mid-hold..."); float maxProg = 0f; bool vanished = false; bool revived = false; w = 0f; while (w < 40f) { if ((Object)(object)downed == (Object)null || (Object)(object)((Character)downed).m_nview == (Object)null || !((Character)downed).m_nview.IsValid()) { vanished = true; break; } if (!downed.IsDowned() && ((Character)downed).GetHealth() > 0f) { revived = true; break; } FindMarkerInteractable(downed)?.Interact((Humanoid)(object)me, hold: true, alt: false); maxProg = Mathf.Max(maxProg, downed.GetReviveProgress()); w += Time.unscaledDeltaTime; yield return null; } Record("vanish_mid_channel", vanished && !revived && maxProg > 0.1f, $"vanished={vanished} revived={revived} maxProg={maxProg:F2}"); if (!vanished) { yield break; } GameObject orphan = MarkerPrefab.FindFor(downedPid); Revivable orphanInteractable = (((Object)(object)orphan != (Object)null) ? orphan.GetComponentInChildren() : null); w = 0f; while (w < 1.5f) { orphanInteractable?.Interact((Humanoid)(object)me, hold: true, alt: false); w += Time.unscaledDeltaTime; yield return null; } yield return (object)new WaitForSecondsRealtime(3f); orphan = MarkerPrefab.FindFor(downedPid); bool orphanPersists = (Object)(object)orphan != (Object)null; string hover = ""; bool interactInert = true; if ((Object)(object)orphan != (Object)null) { Revivable inter = orphan.GetComponentInChildren(); if ((Object)(object)inter != (Object)null) { interactInert = !inter.Interact((Humanoid)(object)me, hold: true, alt: false); hover = inter.GetHoverText(); } } bool hoverExplains = hover.IndexOf("disconnected", StringComparison.OrdinalIgnoreCase) >= 0; bool onlyMe = Player.GetAllPlayers().Count == 1; Record("vanish_channel_fizzles", orphanPersists && interactInert && hoverExplains && onlyMe, $"orphanPersists={orphanPersists} interactInert={interactInert} " + string.Format("hoverExplains={0} hover=\"{1}\" onlyMe={2}", hoverExplains, hover.Replace("\n", " | "), onlyMe)); } private IEnumerator RunClient() { yield return ((MonoBehaviour)this).StartCoroutine(AutoStart()); yield return ((MonoBehaviour)this).StartCoroutine(WaitForPlayerInWorld()); Player me = Player.m_localPlayer; if ((Object)(object)me == (Object)null) { Record("client_spawn", pass: false, "no local player"); yield break; } Record("client_spawn", pass: true, "name=" + me.GetPlayerName()); Log("E2E[client]: waiting to see the remote host player..."); float waited = 0f; while (waited < 60f && Player.GetAllPlayers().Count < 2) { waited += Time.unscaledDeltaTime; yield return null; } bool sawRemote = Player.GetAllPlayers().Count >= 2; Record("client_sees_host", sawRemote, $"players={Player.GetAllPlayers().Count}"); if (!sawRemote) { yield break; } if (E2EConfig.IsRejoinScenario) { yield return ((MonoBehaviour)this).StartCoroutine(RunClientRejoin(me)); yield break; } if (E2EConfig.IsVanishScenario) { yield return ((MonoBehaviour)this).StartCoroutine(RunClientVanish(me)); yield break; } if (E2EConfig.IsReviveLoopScenario) { if (E2EConfig.LoopDownRole == "client") { yield return ((MonoBehaviour)this).StartCoroutine(RunLoopVictim(me)); } else { yield return ((MonoBehaviour)this).StartCoroutine(RunLoopReviver(me)); } yield break; } if (E2EConfig.IsConfigSyncScenario) { yield return ((MonoBehaviour)this).StartCoroutine(RunClientConfigSync()); yield break; } Log("E2E[client]: waiting for remote player to be downed..."); Player downed = null; waited = 0f; while (waited < 60f) { downed = FindDownedRemotePlayer(me); if ((Object)(object)downed != (Object)null) { break; } waited += Time.unscaledDeltaTime; yield return null; } Record("client_detected_downed", (Object)(object)downed != (Object)null, ((Object)(object)downed != (Object)null) ? ("name=" + downed.GetPlayerName()) : "none"); if ((Object)(object)downed == (Object)null) { yield break; } yield return ((MonoBehaviour)this).StartCoroutine(ValidateMarker(downed)); Log("E2E[client]: channeling revive on remote player..."); waited = 0f; float interactSeconds = 0f; float maxUiFill = 0f; float lastProg = 0f; float worstRegression = 0f; bool uiSeen = false; bool sawFull = false; bool reboundAfterFull = false; while (waited < 20f && downed.IsDowned()) { Revivable interactable = FindInteractableViaHoverRay(downed, me); if ((Object)(object)interactable != (Object)null) { interactable.Interact((Humanoid)(object)me, hold: true, alt: false); interactSeconds += Time.unscaledDeltaTime; } float prog = downed.GetReviveProgress(); if (prog > 0.02f && prog < lastProg - 0.01f) { worstRegression = Mathf.Max(worstRegression, lastProg - prog); } lastProg = prog; if (prog >= 0.99f) { sawFull = true; } else if (sawFull && prog > 0.05f && prog < 0.9f) { reboundAfterFull = true; } if (ProgressUI.Visible) { uiSeen = true; maxUiFill = Mathf.Max(maxUiFill, ProgressUI.Fill); } waited += Time.unscaledDeltaTime; yield return null; } bool revivedRemote = !downed.IsDowned(); Record("client_revived_remote", revivedRemote, $"downed={downed.IsDowned()} channelSecs={interactSeconds:F1}"); bool monotonic = worstRegression < 0.05f; Record("client_progress_ui", uiSeen && maxUiFill > 0.3f && monotonic && !reboundAfterFull, $"uiSeen={uiSeen} maxFill={maxUiFill:F2} worstRegression={worstRegression:F2} monotonic={monotonic} " + $"sawFull={sawFull} reboundAfterFull={reboundAfterFull}"); float vw = 0f; bool visibleAgain = false; bool solidAgain = false; while (vw < 5f && (Object)(object)downed != (Object)null) { visibleAgain = (Object)(object)((Character)downed).m_visual != (Object)null && ((Character)downed).m_visual.activeSelf; solidAgain = (Object)(object)((Character)downed).m_collider != (Object)null && ((Collider)((Character)downed).m_collider).enabled; if (visibleAgain && solidAgain) { break; } vw += Time.unscaledDeltaTime; yield return null; } Record("client_host_visible_after_revive", visibleAgain && solidAgain, $"visible={visibleAgain} colliderOn={solidAgain} after={vw:F1}s"); float mw = 0f; bool markerDestroyed = false; while (mw < 6f) { markerDestroyed = Object.FindObjectsOfType().Length == 0; if (markerDestroyed) { break; } mw += Time.unscaledDeltaTime; yield return null; } Record("client_marker_gone_after_revive", markerDestroyed, $"markerDestroyed={markerDestroyed} after={mw:F1}s"); } private IEnumerator RunClientVanish(Player me) { Log("E2E[client]: vanish scenario -- downing self"); ((Character)me).SetHealth(0f); float w = 0f; while (w < 6f && !me.IsDowned()) { w += Time.unscaledDeltaTime; yield return null; } if (!me.IsDowned()) { Record("vanish_pre_down", pass: false, "could not down self"); yield break; } Record("vanish_pre_down", pass: true, "downed=True"); Log("E2E[client]: waiting for host channel progress before logging out..."); w = 0f; float seenProg = 0f; while (w < 30f && me.IsDowned()) { seenProg = Mathf.Max(seenProg, me.GetReviveProgress()); if (seenProg > 0.25f) { break; } w += Time.unscaledDeltaTime; yield return null; } Record("vanish_saw_host_channel", seenProg > 0.25f && me.IsDowned(), $"seenProg={seenProg:F2}"); if (!(seenProg <= 0.25f)) { Log("E2E[client]: logging out mid-channel"); Game.instance.Logout(true, true); w = 0f; while (w < 60f && (Object)(object)FejdStartup.instance == (Object)null) { w += Time.unscaledDeltaTime; yield return null; } Record("vanish_logged_out", (Object)(object)FejdStartup.instance != (Object)null, "back at menu"); } } private IEnumerator RunClientRejoin(Player me) { Log("E2E[client]: downing self before logout"); ((Character)me).SetHealth(0f); float w = 0f; while (w < 6f && !me.IsDowned()) { w += Time.unscaledDeltaTime; yield return null; } if (!me.IsDowned()) { Record("rejoin_pre_down", pass: false, "could not down self"); yield break; } yield return (object)new WaitForSecondsRealtime(1f); long pid = me.GetPlayerID(); bool hadMarker = (Object)(object)MarkerPrefab.FindFor(pid) != (Object)null; Record("rejoin_pre_down", hadMarker, $"downed=True marker={hadMarker}"); if (!hadMarker) { yield break; } Log("E2E[client]: logging out..."); Game.instance.Logout(true, true); w = 0f; while (w < 60f && (Object)(object)FejdStartup.instance == (Object)null) { w += Time.unscaledDeltaTime; yield return null; } if ((Object)(object)FejdStartup.instance == (Object)null) { Record("rejoin_reconnect", pass: false, "menu did not return"); yield break; } Log("E2E[client]: back at menu, reconnecting..."); _worldStartIssued = false; yield return ((MonoBehaviour)this).StartCoroutine(AutoStart()); yield return ((MonoBehaviour)this).StartCoroutine(WaitForPlayerInWorld()); Player me2 = Player.m_localPlayer; if ((Object)(object)me2 == (Object)null) { Record("rejoin_reconnect", pass: false, "no local player after reconnect"); yield break; } Record("rejoin_reconnect", pass: true, "name=" + me2.GetPlayerName()); Log("E2E[client]: waiting for disconnect-death on reconnect..."); bool diedOnReconnect = false; bool markerGone = false; bool realTombstone = false; w = 0f; while (w < 20f) { if ((Object)(object)Player.m_localPlayer != (Object)null && ((Character)Player.m_localPlayer).IsDead()) { diedOnReconnect = true; } markerGone = (Object)(object)MarkerPrefab.FindFor(pid) == (Object)null; TombStone[] array = Object.FindObjectsOfType(); foreach (TombStone t in array) { ZNetView nv = ((Component)t).GetComponent(); if ((Object)(object)nv != (Object)null && nv.IsValid() && !new DownedMarkerView(nv).IsMarker) { realTombstone = true; break; } } if (diedOnReconnect && markerGone) { break; } w += Time.unscaledDeltaTime; yield return null; } bool notDowned = (Object)(object)Player.m_localPlayer == (Object)null || !Player.m_localPlayer.IsDowned(); Record("reconnect_downed_dies", diedOnReconnect && markerGone && notDowned, $"died={diedOnReconnect} markerGone={markerGone} realTombstone={realTombstone} notDowned={notDowned}"); } private IEnumerator RunClientConfigSync() { Plugin.ReviveHoldTimeCfg.Value = 2f; Plugin.ReviveModeCfg.Value = ReviveModeType.Hold; Log("E2E[client]: configsync -- local hold=2 mode=Hold, expecting server hold=7 mode=Press"); float w = 0f; bool adopted = false; while (w < 20f && !adopted) { adopted = Mathf.Abs(Plugin.ReviveDuration - 7f) < 0.01f && Plugin.RevivePressMode; w += Time.unscaledDeltaTime; yield return null; } bool durationFromServer = Mathf.Abs(Plugin.ReviveDuration - 7f) < 0.01f; bool modeFromServer = Plugin.RevivePressMode; Record("client_adopts_server_config", durationFromServer && modeFromServer, $"durationFromServer={durationFromServer} (dur={Plugin.ReviveDuration:F1}, local was 2) " + $"modeFromServer={modeFromServer} (press; local was hold)"); } private static List MarkerZdos() { List list = new List(); int stableHashCode = StringExtensionMethods.GetStableHashCode("RevivalRevived_DownedMarker"); foreach (KeyValuePair item in ZDOMan.instance.m_objectsByID) { if (item.Value.GetPrefab() == stableHashCode) { list.Add(item.Value); } } return list; } private static List GhostMarkers() { //IL_0043: Unknown result type (might be due to invalid IL or missing references) List list = new List(); DownedMarker[] array = Object.FindObjectsOfType(); foreach (DownedMarker downedMarker in array) { ZNetView component = ((Component)downedMarker).GetComponent(); ZDO val = (((Object)(object)component != (Object)null) ? component.GetZDO() : null); ZDO val2 = ((val != null) ? ZDOMan.instance.GetZDO(val.m_uid) : null); if (val == null || val2 != val) { list.Add(downedMarker); } } return list; } private void DumpLeaks(string context) { //IL_0025: 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_0040: 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_008e: Unknown result type (might be due to invalid IL or missing references) //IL_01dc: Unknown result type (might be due to invalid IL or missing references) //IL_0229: Unknown result type (might be due to invalid IL or missing references) foreach (ZDO item in MarkerZdos()) { DownedMarkerView downedMarkerView = new DownedMarkerView(item); ZDO val = ((downedMarkerView.LinkedPlayer != ZDOID.None) ? ZDOMan.instance.GetZDO(downedMarkerView.LinkedPlayer) : null); bool flag = val != null && new DownedStateMachineView(val).Downed; GameObject val2 = ZNetScene.instance.FindInstance(item.m_uid); Log($"E2E-LEAK[{context}] zdo {item.m_uid}: owner={item.GetOwner()} mine={item.IsOwner()} " + $"ownerRev={item.OwnerRevision} dataRev={item.DataRevision} " + string.Format("replaced={0} playerZdo={1} ", downedMarkerView.ReplacedByGrave, (val != null) ? "alive" : "gone") + $"playerDowned={flag} chan={new ReviveChannelDecayingProgressView(item).Channeling} anchorSec={new ReviveChannelDecayingProgressView(item).AnchorSeconds:F2} " + "instance=" + (((Object)(object)val2 != (Object)null) ? "yes" : "no")); } foreach (DownedMarker item2 in GhostMarkers()) { ZNetView component = ((Component)item2).GetComponent(); ZDO val3 = (((Object)(object)component != (Object)null) ? component.GetZDO() : null); Log($"E2E-LEAK[{context}] GHOST instance at {((Component)item2).transform.position}: " + "zdoRef=" + ((val3 != null) ? ((object)Unsafe.As(ref val3.m_uid)/*cast due to .constrained prefix*/).ToString() : "null") + " registered=" + ((val3 != null && ZDOMan.instance.GetZDO(val3.m_uid) != null) ? "different-object" : "none")); } } private IEnumerator LeakCheck(string context) { float w = 0f; while (w < 8f && (MarkerZdos().Count > 0 || GhostMarkers().Count > 0)) { w += Time.unscaledDeltaTime; yield return null; } int zdos = MarkerZdos().Count; int ghosts = GhostMarkers().Count; if (zdos > 0 || ghosts > 0) { m_loopLeaks++; DumpLeaks(context); } Log($"E2E loop [{context}]: zdosLeft={zdos} ghosts={ghosts} leaksSoFar={m_loopLeaks}"); } private IEnumerator RunLoopVictim(Player me) { if (E2EConfig.IsExpireLoop) { yield return ((MonoBehaviour)this).StartCoroutine(RunLoopVictimExpire()); yield break; } int cycles = E2EConfig.LoopCycles; int revives = 0; for (int cycle = 1; cycle <= cycles; cycle++) { ((Character)me).SetHealth(((Character)me).GetMaxHealth()); yield return (object)new WaitForSecondsRealtime(1.2f); Log($"E2E loop {cycle}: downing self"); ((Character)me).SetHealth(0f); float w = 0f; while (w < 5f && !me.IsDowned()) { w += Time.unscaledDeltaTime; yield return null; } if (!me.IsDowned()) { Record("reviveloop_victim", pass: false, $"cycle {cycle}: could not down"); yield break; } w = 0f; while (w < 40f && me.IsDowned() && !((Character)me).IsDead()) { w += Time.unscaledDeltaTime; yield return null; } if (me.IsDowned() || ((Character)me).IsDead()) { Record("reviveloop_victim", pass: false, $"cycle {cycle}: not revived (dead={((Character)me).IsDead()})"); yield break; } revives++; yield return ((MonoBehaviour)this).StartCoroutine(LeakCheck($"victim cycle {cycle}")); } Record("reviveloop_victim", revives == cycles && m_loopLeaks == 0, $"revives={revives}/{cycles} leakCycles={m_loopLeaks}"); yield return (object)new WaitForSecondsRealtime(8f); } private IEnumerator RunLoopVictimExpire() { int cycles = E2EConfig.LoopCycles; int deaths = 0; for (int cycle = 1; cycle <= cycles; cycle++) { Player me = Player.m_localPlayer; if ((Object)(object)me == (Object)null) { Record("reviveloop_victim", pass: false, $"cycle {cycle}: no local player"); yield break; } ((Character)me).SetHealth(((Character)me).GetMaxHealth()); GiveTestItem(me); yield return (object)new WaitForSecondsRealtime(1.2f); Log($"E2E loop {cycle}: downing self (expire mode)"); ((Character)me).SetHealth(0f); float w = 0f; while (w < 5f && !me.IsDowned()) { w += Time.unscaledDeltaTime; yield return null; } if (!me.IsDowned()) { Record("reviveloop_victim", pass: false, $"cycle {cycle}: could not down"); yield break; } w = 0f; while (w < 30f && me.GetReviveProgress() < 0.15f && me.IsDowned()) { w += Time.unscaledDeltaTime; yield return null; } if (!me.IsDowned()) { Record("reviveloop_victim", pass: false, $"cycle {cycle}: revived before expiry could fire"); yield break; } DownedStateMachineView s = new DownedStateMachineView(((Character)me).m_nview) { DownedTime = (float)ZNet.instance.GetTimeSeconds() - Plugin.ReviveWindow - 5f }; w = 0f; while (w < 12f && !((Character)me).IsDead()) { ((Character)me).SetHealth(0f); w += Time.unscaledDeltaTime; yield return null; } if (!((Character)me).IsDead()) { Record("reviveloop_victim", pass: false, $"cycle {cycle}: expiry did not kill"); yield break; } deaths++; yield return ((MonoBehaviour)this).StartCoroutine(LeakCheck($"victim expire cycle {cycle}")); yield return ((MonoBehaviour)this).StartCoroutine(WaitForAlivePlayer()); } Record("reviveloop_victim", deaths == cycles && m_loopLeaks == 0, $"deaths={deaths}/{cycles} leakCycles={m_loopLeaks}"); yield return (object)new WaitForSecondsRealtime(8f); } private IEnumerator RunLoopReviver(Player me) { if (E2EConfig.IsExpireLoop) { yield return ((MonoBehaviour)this).StartCoroutine(RunLoopReviverExpire(me)); yield break; } int revived = 0; while (true) { Player downed = null; float w = 0f; while (w < 30f && (Object)(object)downed == (Object)null) { downed = FindDownedRemotePlayer(me); w += Time.unscaledDeltaTime; yield return null; } if ((Object)(object)downed == (Object)null) { break; } yield return (object)new WaitForSecondsRealtime(0.5f); w = 0f; float overshoot = 0f; bool jittered = false; while (w < 40f && overshoot < 2f) { if (!jittered && downed.GetReviveProgress() > 0.45f) { jittered = true; yield return (object)new WaitForSecondsRealtime(0.65f); w += 0.65f; continue; } (FindInteractableViaHoverRay(downed, me) ?? Object.FindObjectOfType())?.Interact((Humanoid)(object)me, hold: true, alt: false); if (!downed.IsDowned()) { overshoot += Time.unscaledDeltaTime; } w += Time.unscaledDeltaTime; yield return null; } if (downed.IsDowned()) { Record("reviveloop_reviver", pass: false, "revive did not land"); yield break; } revived++; yield return ((MonoBehaviour)this).StartCoroutine(LeakCheck($"reviver after revive #{revived}")); } Record("reviveloop_reviver", revived > 0 && m_loopLeaks == 0, $"revived={revived} leakCycles={m_loopLeaks}"); } private IEnumerator RunLoopReviverExpire(Player me) { int deaths = 0; while (true) { Player downed = null; float w = 0f; while (w < 30f && (Object)(object)downed == (Object)null) { downed = FindDownedRemotePlayer(me); w += Time.unscaledDeltaTime; yield return null; } if ((Object)(object)downed == (Object)null) { break; } yield return (object)new WaitForSecondsRealtime(0.4f); w = 0f; float overshoot = 0f; while (w < 40f && overshoot < 2.5f) { (FindInteractableViaHoverRay(downed, me) ?? Object.FindObjectOfType())?.Interact((Humanoid)(object)me, hold: true, alt: false); if (((Character)downed).IsDead() || !downed.IsDowned()) { overshoot += Time.unscaledDeltaTime; } w += Time.unscaledDeltaTime; yield return null; } if (!((Character)downed).IsDead()) { Record("reviveloop_reviver", pass: false, "victim did not die mid-channel"); yield break; } deaths++; yield return ((MonoBehaviour)this).StartCoroutine(LeakCheck($"reviver expire #{deaths}")); } Record("reviveloop_reviver", deaths > 0 && m_loopLeaks == 0, $"deaths={deaths} leakCycles={m_loopLeaks}"); } private IEnumerator ValidateMarker(Player downed) { GameObject marker = downed.FindDownedMarker(); if ((Object)(object)marker == (Object)null) { float w = 0f; while (w < 6f && (Object)(object)marker == (Object)null) { marker = downed.FindDownedMarker(); w += Time.unscaledDeltaTime; yield return null; } } if ((Object)(object)marker == (Object)null) { Record("client_marker_sync", pass: false, "marker not found on client"); yield break; } ZNetView nview = marker.GetComponent(); bool isMarkerFlag = (Object)(object)nview != (Object)null && nview.IsValid() && new DownedMarkerView(nview).IsMarker; DownedMarker dm = marker.GetComponent(); bool green = (Object)(object)dm != (Object)null && dm.IsGreen(); bool hasInteractable = (Object)(object)marker.GetComponentInChildren() != (Object)null; bool noTombScript = (Object)(object)marker.GetComponent() == (Object)null; float maxPlayerDist = 0f; float maxFrameJump = 0f; Vector3 prev = marker.transform.position; int samples = 0; float t = 0f; while (t < 2.5f && downed.IsDowned()) { Vector3 pos = marker.transform.position; maxPlayerDist = Mathf.Max(maxPlayerDist, Vector3.Distance(pos, ((Component)downed).transform.position)); maxFrameJump = Mathf.Max(maxFrameJump, Vector3.Distance(pos, prev)); prev = pos; samples++; t += Time.unscaledDeltaTime; yield return null; } bool noRagdolls = Object.FindObjectsOfType().Length == 0; bool corpseColliderOff = (Object)(object)((Character)downed).m_collider == (Object)null || !((Collider)((Character)downed).m_collider).enabled; Player me = Player.m_localPlayer; bool hoverRayHitsMarker = false; bool hoverBlockedByCorpse = false; bool hoverTextOk = false; Vector3 mpos = marker.transform.position + Vector3.up * 0.3f; Vector3 origin = mpos + new Vector3(1.8f, 1.2f, 0f); Vector3 val = mpos - origin; RaycastHit[] hits = Physics.RaycastAll(origin, ((Vector3)(ref val)).normalized, 6f, me.m_interactMask); Array.Sort(hits, (RaycastHit a, RaycastHit b) => ((RaycastHit)(ref a)).distance.CompareTo(((RaycastHit)(ref b)).distance)); if (hits.Length != 0) { Collider first = ((RaycastHit)(ref hits[0])).collider; hoverRayHitsMarker = (Object)(object)((Component)first).GetComponentInParent() != (Object)null; hoverBlockedByCorpse = (Object)(object)((Component)first).GetComponentInParent() == (Object)(object)downed; Revivable inter = ((Component)first).GetComponentInParent(); if ((Object)(object)inter != (Object)null) { string hover = inter.GetHoverText(); hoverTextOk = !string.IsNullOrEmpty(hover) && hover.IndexOf("Revive", StringComparison.OrdinalIgnoreCase) >= 0; } } bool hudHidden = true; if ((Object)(object)EnemyHud.instance != (Object)null) { IDictionary huds = Traverse.Create((object)EnemyHud.instance).Field("m_huds").GetValue(); hudHidden = huds == null || !huds.Contains(downed); } bool pass = samples > 5 && isMarkerFlag && green && hasInteractable && noTombScript && maxPlayerDist < 5f && maxFrameJump < 2f && noRagdolls && corpseColliderOff && hoverRayHitsMarker && !hoverBlockedByCorpse && hoverTextOk && hudHidden; Record("client_marker_sync", pass, $"samples={samples} markerFlag={isMarkerFlag} green={green} interactable={hasInteractable} " + $"noTombScript={noTombScript} maxPlayerDist={maxPlayerDist:F2} maxFrameJump={maxFrameJump:F2} noRagdolls={noRagdolls} " + $"corpseColliderOff={corpseColliderOff} hoverRayHitsMarker={hoverRayHitsMarker} " + $"hoverBlockedByCorpse={hoverBlockedByCorpse} hoverTextOk={hoverTextOk} hudHidden={hudHidden}"); } private IEnumerator RunManual() { yield return ((MonoBehaviour)this).StartCoroutine(AutoStart()); yield return ((MonoBehaviour)this).StartCoroutine(WaitForPlayerInWorld()); Log("E2E: manual play mode (" + E2EConfig.Role + ") -- no tests, game is yours"); while (true) { yield return null; } } private IEnumerator RunDemo() { yield return ((MonoBehaviour)this).StartCoroutine(AutoStart()); yield return ((MonoBehaviour)this).StartCoroutine(WaitForPlayerInWorld()); Player player = Player.m_localPlayer; if ((Object)(object)player == (Object)null) { Record("demo", pass: false, "no player"); yield break; } Log("E2E: local player ready (demo)"); ((MonoBehaviour)this).StartCoroutine(DespawnRavens()); yield return (object)new WaitForSecondsRealtime(3f); if ((Object)(object)GameCamera.instance != (Object)null) { GameCamera.instance.m_maxDistance = 9f; Traverse.Create((object)GameCamera.instance).Field("m_distance").SetValue((object)8f); } yield return (object)new WaitForSecondsRealtime(2f); Log("DEMO: downing"); GiveTestItem(player); ((Character)player).SetHealth(0f); float w = 0f; while (w < 5f && !player.IsDowned()) { w += Time.unscaledDeltaTime; yield return null; } yield return (object)new WaitForSecondsRealtime(3.5f); Log("DEMO: channeling revive"); w = 0f; while (w < 12f && player.IsDowned()) { FindMarkerInteractable(player)?.SimulateHold(); w += Time.unscaledDeltaTime; yield return null; } yield return (object)new WaitForSecondsRealtime(3f); Log("DEMO: downing again, letting window run out"); ((Character)player).SetHealth(0f); w = 0f; while (w < 5f && !player.IsDowned()) { w += Time.unscaledDeltaTime; yield return null; } w = 0f; while (w < Plugin.ReviveWindow + 10f && !((Character)player).IsDead()) { w += Time.unscaledDeltaTime; yield return null; } yield return (object)new WaitForSecondsRealtime(5f); Record("demo", pass: true, "sequence complete"); } private IEnumerator DespawnRavens() { while (true) { Raven[] array = Object.FindObjectsOfType(); foreach (Raven raven in array) { if ((Object)(object)raven != (Object)null && (Object)(object)ZNetScene.instance != (Object)null) { ZNetScene.instance.Destroy(((Component)raven).gameObject); } } yield return (object)new WaitForSecondsRealtime(0.5f); } } private IEnumerator RunSingleProcess() { yield return ((MonoBehaviour)this).StartCoroutine(AutoStart()); yield return ((MonoBehaviour)this).StartCoroutine(WaitForPlayerInWorld()); Player player = Player.m_localPlayer; if ((Object)(object)player == (Object)null) { Record("local_player", pass: false, "no local player"); yield break; } Log($"E2E: local player ready: {player.GetPlayerName()} hp={((Character)player).GetHealth()}/{((Character)player).GetMaxHealth()}"); yield return (object)new WaitForSecondsRealtime(2f); yield return ((MonoBehaviour)this).StartCoroutine(Test_LethalDamageDowns()); yield return ((MonoBehaviour)this).StartCoroutine(Test_DownedConstraints()); yield return ((MonoBehaviour)this).StartCoroutine(Test_MarkerColorGradient()); yield return ((MonoBehaviour)this).StartCoroutine(Test_HoldProgressAndUI()); yield return ((MonoBehaviour)this).StartCoroutine(Test_ReviveNoNetworkTick()); yield return ((MonoBehaviour)this).StartCoroutine(Test_ReviveRestores()); yield return ((MonoBehaviour)this).StartCoroutine(Test_PressModeRevives()); yield return ((MonoBehaviour)this).StartCoroutine(Test_DisconnectDeath()); yield return ((MonoBehaviour)this).StartCoroutine(WaitForAlivePlayer()); yield return ((MonoBehaviour)this).StartCoroutine(Test_ExpiryKills()); yield return ((MonoBehaviour)this).StartCoroutine(WaitForAlivePlayer()); yield return ((MonoBehaviour)this).StartCoroutine(Test_EmptyInventoryCrumbles()); yield return ((MonoBehaviour)this).StartCoroutine(WaitForAlivePlayer()); yield return ((MonoBehaviour)this).StartCoroutine(Test_GiveUp()); } private IEnumerator Test_GiveUp() { Player player = Player.m_localPlayer; if ((Object)(object)player == (Object)null || ((Character)player).IsDead()) { Record("give_up_kills", pass: false, "no alive player"); yield break; } ((Character)player).SetHealth(((Character)player).GetMaxHealth()); GiveTestItem(player); yield return null; ((Character)player).SetHealth(0f); float w = 0f; while (w < 5f && !player.IsDowned()) { w += Time.unscaledDeltaTime; yield return null; } if (!player.IsDowned()) { Record("give_up_kills", pass: false, "could not down"); yield break; } yield return (object)new WaitForSecondsRealtime(0.5f); w = 0f; float maxFrac = 0f; bool sawRed = false; while (w < 6f && !((Character)player).IsDead()) { ((Component)player).GetComponent()?.SimulateHold(); maxFrac = Mathf.Max(maxFrac, GiveUp.LocalFraction); if (ProgressUI.Visible && ProgressUI.GivingUp) { sawRed = true; } w += Time.unscaledDeltaTime; yield return null; } yield return (object)new WaitForSecondsRealtime(0.5f); bool dead = ((Character)player).IsDead(); bool notDowned = !player.IsDowned(); bool realTombstone = (Object)(object)FindRealGraveNear(((Component)player).transform.position, 999f) != (Object)null; bool fractionReset = GiveUp.LocalFraction <= 0.01f; Record("give_up_kills", dead && notDowned && sawRed && maxFrac > 0.5f && realTombstone && fractionReset, $"dead={dead} notDowned={notDowned} sawRed={sawRed} maxFrac={maxFrac:F2} " + $"realTombstone={realTombstone} fractionReset={fractionReset}"); } private IEnumerator Test_LethalDamageDowns() { Player player = Player.m_localPlayer; ((Character)player).SetHealth(0f); float waited = 0f; while (waited < 5f && !player.IsDowned()) { waited += Time.unscaledDeltaTime; yield return null; } float popVelY = MarkerPrefab.LastPopVelY; bool popped = popVelY > 3f; yield return (object)new WaitForSecondsRealtime(0.5f); bool downed = player.IsDowned(); bool notDead = !((Character)player).IsDead(); bool hasController = (Object)(object)((Component)player).GetComponent() != (Object)null; GameObject marker = player.FindDownedMarker(); bool markerExists = (Object)(object)marker != (Object)null; bool markerIsOurPrefab = (Object)(object)marker != (Object)null && (Object)(object)marker.GetComponent() != (Object)null && ((Object)marker).name.StartsWith("RevivalRevived_DownedMarker") && (Object)(object)marker.GetComponent() == (Object)null; bool noRagdolls = Object.FindObjectsOfType().Length == 0; bool visualHidden = (Object)(object)((Character)player).m_visual != (Object)null && !((Character)player).m_visual.activeSelf; bool poofPlayed = PlayerDownedExtensions.LastPoofCount > 0; bool poofSmall = PlayerDownedExtensions.LastPoofSourceName.IndexOf("Greyling", StringComparison.OrdinalIgnoreCase) >= 0; Record("lethal_damage_downs", downed && notDead && hasController && markerExists && markerIsOurPrefab && noRagdolls && poofPlayed && poofSmall && popped, $"downed={downed} notDead={notDead} controller={hasController} marker={markerExists} " + $"ourPrefab={markerIsOurPrefab} noRagdolls={noRagdolls} visualHidden={visualHidden} " + $"poof={PlayerDownedExtensions.LastPoofCount} poofSrc={PlayerDownedExtensions.LastPoofSourceName} popVelY={popVelY:F1}"); } private IEnumerator Test_MarkerColorGradient() { Player player = Player.m_localPlayer; GameObject marker = player.FindDownedMarker(); DownedMarker dm = (((Object)(object)marker != (Object)null) ? marker.GetComponent() : null); if ((Object)(object)dm == (Object)null) { Record("marker_color_gradient", pass: false, "no DownedMarker"); yield break; } float blend0 = dm.CurrentBlend; DownedStateMachineView pstate = new DownedStateMachineView(((Character)player).m_nview); pstate.DownedTime -= Plugin.ReviveWindow * 0.5f; yield return null; yield return null; float blend1 = dm.CurrentBlend; bool startedGreen = blend0 < 0.25f; bool progressed = blend1 > blend0 + 0.25f; Record("marker_color_gradient", startedGreen && progressed, $"blend0={blend0:F2} blend1={blend1:F2}"); } private IEnumerator Test_DisconnectDeath() { Player player = Player.m_localPlayer; ((Character)player).SetHealth(((Character)player).GetMaxHealth()); GiveTestItem(player); yield return null; ((Character)player).SetHealth(0f); float w = 0f; while (w < 5f && !player.IsDowned()) { w += Time.unscaledDeltaTime; yield return null; } yield return (object)new WaitForSecondsRealtime(0.5f); GameObject m1 = player.FindDownedMarker(); if ((Object)(object)m1 == (Object)null) { Record("disconnect_kills_on_reconnect", pass: false, "no marker after down"); yield break; } DownedStateMachineView s = new DownedStateMachineView(((Character)player).m_nview) { Downed = false, Marker = ZDOID.None }; ((Character)player).SetHealth(((Character)player).GetMaxHealth()); ((Collider)((Character)player).m_collider).enabled = true; ((Character)player).m_body.isKinematic = false; if ((Object)(object)((Character)player).m_visual != (Object)null) { ((Character)player).m_visual.SetActive(true); } yield return null; ((Component)player).GetComponent().RunReconnectCheck(); w = 0f; while (w < 8f && !((Character)player).IsDead()) { w += Time.unscaledDeltaTime; yield return null; } yield return (object)new WaitForSecondsRealtime(0.5f); bool dead = ((Character)player).IsDead(); bool markerGone = (Object)(object)MarkerPrefab.FindFor(player.GetPlayerID()) == (Object)null; bool realTombstone = false; TombStone[] array = Object.FindObjectsOfType(); foreach (TombStone t in array) { ZNetView nv = ((Component)t).GetComponent(); if ((Object)(object)nv != (Object)null && nv.IsValid() && !new DownedMarkerView(nv).IsMarker) { realTombstone = true; break; } } bool noRagdolls = Object.FindObjectsOfType().Length == 0; Record("disconnect_kills_on_reconnect", dead && markerGone && realTombstone && noRagdolls, $"dead={dead} markerGone={markerGone} realTombstone={realTombstone} noRagdolls={noRagdolls}"); yield return (object)new WaitForSecondsRealtime(0.5f); } private IEnumerator Test_DownedConstraints() { Player player = Player.m_localPlayer; if (!player.IsDowned()) { Record("downed_constraints", pass: false, "precondition: not downed"); yield break; } bool cannotMove = !((Character)player).CanMove(); bool kinematic = (Object)(object)((Character)player).m_body != (Object)null && ((Character)player).m_body.isKinematic; GameObject marker = player.FindDownedMarker(); bool interactableFound = false; bool hoverOk = false; bool green = false; bool stripped = false; bool noEmbers = false; bool nameOk = false; int disabledFx = 0; if ((Object)(object)marker != (Object)null) { TMP_Text worldText = marker.GetComponentInChildren(true); nameOk = (Object)(object)worldText != (Object)null && worldText.text == player.GetPlayerName(); Revivable interactable = marker.GetComponentInChildren(); interactableFound = (Object)(object)interactable != (Object)null; DownedMarker dm = marker.GetComponent(); green = (Object)(object)dm != (Object)null && dm.IsGreen(); disabledFx = MarkerPrefab.TemplateEffectsRemoved; stripped = (Object)(object)marker.GetComponent() == (Object)null && (Object)(object)marker.GetComponent() == (Object)null; noEmbers = disabledFx > 0 && marker.GetComponentsInChildren(true).Length == 0; if ((Object)(object)interactable != (Object)null) { string hover = interactable.GetHoverText(); hoverOk = !string.IsNullOrEmpty(hover) && hover.IndexOf("Revive", StringComparison.OrdinalIgnoreCase) >= 0; Log("E2E[downed_constraints]: hover = \"" + hover.Replace("\n", " | ") + "\""); } } Record("downed_constraints", cannotMove && kinematic && interactableFound && hoverOk && green && stripped && noEmbers && nameOk, $"cannotMove={cannotMove} kinematic={kinematic} interactable={interactableFound} hoverOk={hoverOk} " + $"green={green} stripped={stripped} noEmbers={noEmbers} disabledFx={disabledFx} nameOk={nameOk}"); } private IEnumerator Test_HoldProgressAndUI() { Player player = Player.m_localPlayer; if (!player.IsDowned()) { Record("hold_progress_and_ui", pass: false, "precondition: not downed"); yield break; } Revivable interactable = FindMarkerInteractable(player); if ((Object)(object)interactable == (Object)null) { Record("hold_progress_and_ui", pass: false, "no marker interactable"); yield break; } float remainingBefore = player.GetDownedRemainingTime(); float t = 0f; float maxProg = 0f; float maxFill = 0f; float firstProgressAt = -1f; bool uiSeen = false; while (t < 1.5f) { interactable.SimulateHold(); float prog = player.GetReviveProgress(); if (prog > 0.05f && firstProgressAt < 0f) { firstProgressAt = t; } maxProg = Mathf.Max(maxProg, prog); if (ProgressUI.Visible) { uiSeen = true; maxFill = Mathf.Max(maxFill, ProgressUI.Fill); } t += Time.unscaledDeltaTime; yield return null; } bool responsive = firstProgressAt >= 0f && firstProgressAt < 0.6f; float remainingAfter = player.GetDownedRemainingTime(); bool windowPaused = remainingBefore - remainingAfter < 0.6f; bool partial = maxProg > 0.05f && maxProg < 0.9f; bool stillDowned = player.IsDowned(); t = 0f; while (t < 3f) { t += Time.unscaledDeltaTime; yield return null; } bool decayed = player.GetReviveProgress() <= 0.01f; bool uiHidden = !ProgressUI.Visible; Record("hold_progress_and_ui", partial && stillDowned && uiSeen && decayed && uiHidden && windowPaused && responsive, $"maxProg={maxProg:F2} partial={partial} stillDowned={stillDowned} uiSeen={uiSeen} " + $"maxFill={maxFill:F2} decayed={decayed} uiHidden={uiHidden} " + $"windowPaused={windowPaused} remBefore={remainingBefore:F1} remAfter={remainingAfter:F1} " + $"responsive={responsive} firstProgressAt={firstProgressAt:F2}"); } private IEnumerator Test_ReviveNoNetworkTick() { Player player = Player.m_localPlayer; if (!player.IsDowned()) { Record("revive_no_network_tick", pass: false, "precondition: not downed"); yield break; } Revivable interactable = FindMarkerInteractable(player); GameObject marker = player.FindDownedMarker(); ZNetView nview = (((Object)(object)marker != (Object)null) ? marker.GetComponent() : null); if ((Object)(object)interactable == (Object)null || (Object)(object)nview == (Object)null || !nview.IsValid()) { Record("revive_no_network_tick", pass: false, "no marker/interactable"); yield break; } float t = 0f; while (t < 0.6f) { interactable.SimulateHold(); t += Time.unscaledDeltaTime; yield return null; } ReviveChannelDecayingProgressView anchor0 = new ReviveChannelDecayingProgressView(nview); float anchorTime0 = anchor0.AnchorTime; float anchorSeconds0 = anchor0.AnchorSeconds; float progAtBaseline = player.GetReviveProgress(); int frames = 0; t = 0f; while (t < 1.5f) { interactable.SimulateHold(); frames++; t += Time.unscaledDeltaTime; yield return null; } ReviveChannelDecayingProgressView anchor1 = new ReviveChannelDecayingProgressView(nview); float progAfter = player.GetReviveProgress(); bool progressRose = progAfter > progAtBaseline + 0.1f; bool anchorStable = Mathf.Abs(anchor1.AnchorTime - anchorTime0) < 0.01f && Mathf.Abs(anchor1.AnchorSeconds - anchorSeconds0) < 0.01f; bool stillDowned = player.IsDowned(); Record("revive_no_network_tick", progressRose && anchorStable && stillDowned, $"progressRose={progressRose} ({progAtBaseline:F2}->{progAfter:F2}) " + $"anchorStable={anchorStable} (t {anchorTime0:F2}->{anchor1.AnchorTime:F2}) frames={frames} stillDowned={stillDowned}"); } private IEnumerator Test_PressModeRevives() { Player player = Player.m_localPlayer; Plugin.ReviveModeCfg.Value = ReviveModeType.Press; ((Character)player).SetHealth(((Character)player).GetMaxHealth()); yield return null; ((Character)player).SetHealth(0f); float w = 0f; while (w < 5f && !player.IsDowned()) { w += Time.unscaledDeltaTime; yield return null; } if (!player.IsDowned()) { Plugin.ReviveModeCfg.Value = ReviveModeType.Hold; Record("press_mode_revives", pass: false, "could not down"); yield break; } yield return (object)new WaitForSecondsRealtime(0.5f); FindMarkerInteractable(player)?.SimulateHold(); w = 0f; while (w < 3f && player.IsDowned()) { w += Time.unscaledDeltaTime; yield return null; } yield return (object)new WaitForSecondsRealtime(0.5f); bool revived = !player.IsDowned() && !((Character)player).IsDead() && ((Character)player).GetHealth() > 0f; bool markerGone = (Object)(object)MarkerPrefab.FindFor(player.GetPlayerID()) == (Object)null; Plugin.ReviveModeCfg.Value = ReviveModeType.Hold; Record("press_mode_revives", revived && markerGone, $"revived={revived} hp={((Character)player).GetHealth():F0} markerGone={markerGone}"); } private IEnumerator Test_ReviveRestores() { Player player = Player.m_localPlayer; if (!player.IsDowned()) { Record("revive_restores", pass: false, "precondition: not downed"); yield break; } GameObject markerBefore = player.FindDownedMarker(); int crumblesBefore = DownedMarker.CrumbleEvents; player.ReviveFromDowned(0L); yield return (object)new WaitForSecondsRealtime(0.5f); bool canMove = false; float waitMove = 0f; while (waitMove < 3f) { canMove = ((Character)player).CanMove(); if (canMove) { break; } waitMove += Time.unscaledDeltaTime; yield return null; } bool notDowned = !player.IsDowned(); bool healthy = ((Character)player).GetHealth() > 0f; bool visualBack = (Object)(object)((Character)player).m_visual != (Object)null && ((Character)player).m_visual.activeSelf; bool collider = (Object)(object)((Character)player).m_collider != (Object)null && ((Collider)((Character)player).m_collider).enabled; bool notKinematic = (Object)(object)((Character)player).m_body != (Object)null && !((Character)player).m_body.isKinematic; yield return (object)new WaitForSecondsRealtime(0.5f); bool markerGone = (Object)(object)markerBefore == (Object)null || !Object.op_Implicit((Object)(object)markerBefore); bool crumbled = DownedMarker.CrumbleEvents > crumblesBefore; Record("revive_restores", notDowned && healthy && canMove && visualBack && collider && notKinematic && markerGone && crumbled, $"notDowned={notDowned} hp={((Character)player).GetHealth():F0} canMove={canMove} " + $"visualBack={visualBack} collider={collider} notKinematic={notKinematic} markerGone={markerGone} crumbled={crumbled}"); } private IEnumerator Test_ExpiryKills() { Player player = Player.m_localPlayer; if (((Character)player).IsDead()) { Record("expiry_kills_real_tombstone", pass: false, "already dead"); yield break; } ((Character)player).SetHealth(((Character)player).GetMaxHealth()); GiveTestItem(player); yield return null; ((Character)player).SetHealth(0f); float waited = 0f; while (waited < 5f && !player.IsDowned()) { waited += Time.unscaledDeltaTime; yield return null; } if (!player.IsDowned()) { Record("expiry_kills_real_tombstone", pass: false, "could not re-down"); yield break; } yield return (object)new WaitForSecondsRealtime(0.5f); GameObject markerBefore = player.FindDownedMarker(); Vector3 markerPos = (((Object)(object)markerBefore != (Object)null) ? markerBefore.transform.position : Vector3.zero); DownedStateMachineView s = new DownedStateMachineView(((Character)player).m_nview) { DownedTime = (float)ZNet.instance.GetTimeSeconds() - Plugin.ReviveWindow - 5f }; waited = 0f; while (waited < 10f && !((Character)player).IsDead()) { ((Character)player).SetHealth(0f); waited += Time.unscaledDeltaTime; yield return null; } bool dead = ((Character)player).IsDead(); bool notDowned = !player.IsDowned(); bool corpseInert = dead && (Object)(object)((Character)player).m_collider != (Object)null && !((Collider)((Character)player).m_collider).enabled; bool gapless = true; bool markerGone = (Object)(object)markerBefore == (Object)null || !Object.op_Implicit((Object)(object)markerBefore); float hw = 0f; while (hw < 9f && !markerGone) { bool visible = false; Renderer[] componentsInChildren = markerBefore.GetComponentsInChildren(); foreach (Renderer r in componentsInChildren) { if (r.enabled) { visible = true; break; } } if (!visible && (Object)(object)FindRealGraveNear(markerPos, 3f) == (Object)null) { gapless = false; } hw += Time.unscaledDeltaTime; yield return null; markerGone = (Object)(object)markerBefore == (Object)null || !Object.op_Implicit((Object)(object)markerBefore); } bool realTombstone = false; bool inPlace = false; bool noPop = false; bool embersOnGrave = false; TombStone grave = FindRealGraveNear(markerPos, 999f); if ((Object)(object)grave != (Object)null) { realTombstone = true; inPlace = markerPos != Vector3.zero && Vector3.Distance(((Component)grave).transform.position, markerPos) < 2f; Rigidbody rb = ((Component)grave).GetComponent(); noPop = (Object)(object)rb == (Object)null || rb.linearVelocity.y < 1f; embersOnGrave = ((Component)grave).GetComponentsInChildren(false).Length != 0; } bool noRagdolls = Object.FindObjectsOfType().Length == 0; Record("expiry_kills_real_tombstone", dead && notDowned && corpseInert && markerGone && gapless && realTombstone && inPlace && noPop && embersOnGrave && noRagdolls, $"dead={dead} notDowned={notDowned} corpseInert={corpseInert} markerGone={markerGone} gapless={gapless} " + $"realTombstone={realTombstone} inPlace={inPlace} noPop={noPop} embersOnGrave={embersOnGrave} noRagdolls={noRagdolls}"); } private static TombStone? FindRealGraveNear(Vector3 pos, float maxDist) { //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) TombStone result = null; float num = maxDist; TombStone[] array = Object.FindObjectsOfType(); foreach (TombStone val in array) { ZNetView component = ((Component)val).GetComponent(); if (!((Object)(object)component == (Object)null) && component.IsValid() && (!((Object)(object)component != (Object)null) || !component.IsValid() || !new DownedMarkerView(component).IsMarker)) { float num2 = Vector3.Distance(((Component)val).transform.position, pos); if (num2 <= num) { num = num2; result = val; } } } return result; } private IEnumerator Test_EmptyInventoryCrumbles() { Player player = Player.m_localPlayer; if ((Object)(object)player == (Object)null || ((Character)player).IsDead()) { Record("empty_death_crumbles_marker", pass: false, "no alive player"); yield break; } ((Character)player).SetHealth(((Character)player).GetMaxHealth()); ((Humanoid)player).GetInventory().RemoveAll(); yield return null; ((Character)player).SetHealth(0f); float w = 0f; while (w < 5f && !player.IsDowned()) { w += Time.unscaledDeltaTime; yield return null; } if (!player.IsDowned()) { Record("empty_death_crumbles_marker", pass: false, "could not down"); yield break; } yield return (object)new WaitForSecondsRealtime(0.5f); GameObject marker = player.FindDownedMarker(); if ((Object)(object)marker == (Object)null) { Record("empty_death_crumbles_marker", pass: false, "no marker"); yield break; } _ = marker.transform.position; int gravesBefore = CountRealGraves(); DownedStateMachineView s = new DownedStateMachineView(((Character)player).m_nview) { DownedTime = (float)ZNet.instance.GetTimeSeconds() - Plugin.ReviveWindow - 5f }; w = 0f; while (w < 10f && !((Character)player).IsDead()) { ((Character)player).SetHealth(0f); w += Time.unscaledDeltaTime; yield return null; } yield return (object)new WaitForSecondsRealtime(1f); bool dead = ((Character)player).IsDead(); bool markerGone = (Object)(object)marker == (Object)null || !Object.op_Implicit((Object)(object)marker); bool crumbled = DownedMarker.LastCrumbleEffectCount > 0; bool noNewGrave = CountRealGraves() == gravesBefore; bool pendingCleared = !new GraveReplaceView(((Character)player).m_nview).Pending; Record("empty_death_crumbles_marker", dead && markerGone && crumbled && noNewGrave && pendingCleared, $"dead={dead} markerGone={markerGone} crumbleEffects={DownedMarker.LastCrumbleEffectCount} " + $"noNewGrave={noNewGrave} (before={gravesBefore}) pendingCleared={pendingCleared}"); } private static int CountRealGraves() { int num = 0; TombStone[] array = Object.FindObjectsOfType(); foreach (TombStone val in array) { ZNetView component = ((Component)val).GetComponent(); if (!((Object)(object)component == (Object)null) && component.IsValid() && (!((Object)(object)component != (Object)null) || !component.IsValid() || !new DownedMarkerView(component).IsMarker)) { num++; } } return num; } private static void GiveTestItem(Player player) { try { if (!((Object)(object)ObjectDB.instance == (Object)null)) { GameObject itemPrefab = ObjectDB.instance.GetItemPrefab("Wood"); if (!((Object)(object)itemPrefab == (Object)null)) { ((Humanoid)player).GetInventory().AddItem(itemPrefab, 1); } } } catch (Exception ex) { Log("E2E: GiveTestItem failed: " + ex.Message); } } private IEnumerator AutoStart() { Log("E2E: waiting for FejdStartup (main menu)..."); float waited = 0f; while ((Object)(object)FejdStartup.instance == (Object)null && (Object)(object)Game.instance == (Object)null) { waited += Time.unscaledDeltaTime; if (waited > 120f) { Log("E2E: FejdStartup never appeared"); yield break; } yield return null; } if ((Object)(object)Game.instance != (Object)null) { Log("E2E: Game already running"); yield break; } yield return (object)new WaitForSecondsRealtime(3f); if (_worldStartIssued) { yield break; } _worldStartIssued = true; Exception err = null; try { FejdStartup fejd = FejdStartup.instance; if (E2EConfig.IsHost) { PlayerProfile profile = GetOrCreateProfile(Environment.GetEnvironmentVariable("RR_E2E_PROFILE") ?? "e2e_host", Environment.GetEnvironmentVariable("RR_E2E_CHARNAME") ?? "E2EHost"); Game.SetProfile(profile.GetFilename(), profile.m_fileSource); World world = World.GetCreateWorld(E2EConfig.WorldName, (FileSource)2); ZNet.m_onlineBackend = (OnlineBackendType)3; ZNet.SetServer(true, true, false, world.m_name, "", world); ZNet.ResetServerHost(); Log($"E2E[host]: world '{world.m_name}', CustomSocket port {E2EConfig.Port}, loading scene"); } else if (E2EConfig.IsClient) { PlayerProfile profile2 = GetOrCreateProfile(Environment.GetEnvironmentVariable("RR_E2E_PROFILE") ?? "e2e_client", Environment.GetEnvironmentVariable("RR_E2E_CHARNAME") ?? "E2EClient"); Game.SetProfile(profile2.GetFilename(), profile2.m_fileSource); ZNet.m_onlineBackend = (OnlineBackendType)3; ZNet.SetServer(false, false, false, "", "", (World)null); ZNet.m_serverHost = E2EConfig.ServerHost; ZNet.m_serverHostPort = E2EConfig.Port; Log($"E2E[client]: connecting to {E2EConfig.ServerHost}:{E2EConfig.Port}, loading scene"); } else { PlayerProfile profile3 = GetOrCreateProfile("e2e_solo", "E2ESolo"); Game.SetProfile(profile3.GetFilename(), profile3.m_fileSource); string soloWorldName = Environment.GetEnvironmentVariable("RR_E2E_WORLD") ?? "e2e_world"; World world2 = World.GetCreateWorld(soloWorldName, (FileSource)2); ZNet.m_onlineBackend = (OnlineBackendType)0; ZNet.SetServer(true, false, false, world2.m_name, "", world2); ZNet.ResetServerHost(); Log("E2E[solo]: world '" + world2.m_name + "', loading scene"); } Traverse.Create((object)fejd).Field("m_startingWorld").SetValue((object)true); Traverse.Create((object)fejd).Method("LoadMainScene", Array.Empty()).GetValue(); } catch (Exception ex) { Exception e = ex; err = e; } if (err != null) { Log("E2E: AutoStart error: " + err); } } private static PlayerProfile GetOrCreateProfile(string filename, string charName) { //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Expected O, but got Unknown foreach (PlayerProfile allPlayerProfile in SaveSystem.GetAllPlayerProfiles()) { if (allPlayerProfile.GetFilename() == filename) { allPlayerProfile.m_firstSpawn = false; allPlayerProfile.Save(); return allPlayerProfile; } } PlayerProfile val = new PlayerProfile(filename, (FileSource)2); val.SetName(charName); val.m_firstSpawn = false; val.Save(); return val; } private IEnumerator WaitForAlivePlayer() { float w = 0f; while (w < 30f) { Player p = Player.m_localPlayer; if ((Object)(object)p != (Object)null && (Object)(object)((Character)p).m_nview != (Object)null && ((Character)p).m_nview.IsValid() && !((Character)p).IsDead() && ((Character)p).GetHealth() > 0f) { yield return (object)new WaitForSecondsRealtime(0.5f); yield break; } w += Time.unscaledDeltaTime; yield return null; } Log("E2E: WaitForAlivePlayer timed out"); } private IEnumerator WaitForPlayerInWorld() { Log("E2E: waiting for player to spawn in world..."); float waited = 0f; while (true) { Player p = Player.m_localPlayer; if ((Object)(object)p != (Object)null && (Object)(object)((Component)p).GetComponent() != (Object)null && ((Component)p).GetComponent().IsValid() && (Object)(object)ZNetScene.instance != (Object)null) { yield return null; yield break; } waited += Time.unscaledDeltaTime; if (waited > 240f) { break; } yield return null; } Log("E2E: timed out waiting for player spawn"); } private static Revivable? FindInteractableViaHoverRay(Player downed, Player me) { //IL_0023: 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_0032: 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_003d: Unknown result type (might be due to invalid IL or missing references) //IL_004d: 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) //IL_0058: 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_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) GameObject val = downed.FindDownedMarker(); if ((Object)(object)val == (Object)null) { return null; } Vector3 val2 = val.transform.position + Vector3.up * 0.3f; Vector3 val3 = val2 + new Vector3(1.8f, 1.2f, 0f); Vector3 val4 = val2 - val3; RaycastHit[] array = Physics.RaycastAll(val3, ((Vector3)(ref val4)).normalized, 6f, me.m_interactMask); if (array.Length == 0) { return null; } Array.Sort(array, (RaycastHit a, RaycastHit b) => ((RaycastHit)(ref a)).distance.CompareTo(((RaycastHit)(ref b)).distance)); return ((Component)((RaycastHit)(ref array[0])).collider).GetComponentInParent(); } private static Revivable? FindMarkerInteractable(Player downed) { GameObject val = downed.FindDownedMarker(); return ((Object)(object)val != (Object)null) ? val.GetComponentInChildren() : null; } private static Player? FindDownedRemotePlayer(Player me) { foreach (Player allPlayer in Player.GetAllPlayers()) { if ((Object)(object)allPlayer == (Object)null || (Object)(object)allPlayer == (Object)(object)me || !allPlayer.IsDowned()) { continue; } return allPlayer; } return null; } private void Record(string name, bool pass, string detail) { _results.Add((name, pass, detail)); Log("E2E_RESULT: " + (pass ? "PASS" : "FAIL") + " " + name + " -- " + detail); } private static void Log(string msg) { E2ELog.Write(msg); } private void Finish(bool ok, string reason) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("overall=" + (ok ? "PASS" : "FAIL") + " role=" + E2EConfig.Role + " reason=" + reason); foreach (var result in _results) { stringBuilder.AppendLine((result.pass ? "PASS" : "FAIL") + "\t" + result.name + "\t" + result.detail); } string text = stringBuilder.ToString(); try { File.WriteAllText(ResultPath, text); Log("E2E: results written to " + ResultPath); } catch (Exception ex) { Log("E2E: failed to write results: " + ex); } Log("E2E_SUMMARY_BEGIN\n" + text + "E2E_SUMMARY_END"); Log("E2E: DONE overall=" + (ok ? "PASS" : "FAIL") + " -- quitting"); ((MonoBehaviour)this).StartCoroutine(QuitSoon()); } private IEnumerator QuitSoon() { yield return (object)new WaitForSecondsRealtime(1f); Application.Quit(); yield return (object)new WaitForSecondsRealtime(2f); Process.GetCurrentProcess().Kill(); } } [HarmonyPatch(typeof(SteamUtils), "IsSteamRunningOnSteamDeck")] internal static class SteamStartupPatch { private static bool Prefix(ref bool __result) { __result = false; return false; } } } namespace ReviveAllies.Components { public static class DownedKeys { public const string RpcChannel = "RevivalRevived_Channel"; } public class DownedMarker : MonoBehaviour { private struct TintedMaterial { public Material Mat; public bool HasEmission; public Color OrigEmission; public float EmissionIntensity; public bool HasColor; public Color OrigColor; } public static readonly Color ReviveGreen = new Color(0.25f, 1f, 0.35f); private static EffectList? s_crumbleEffect; private static readonly int EmissionColorId = Shader.PropertyToID("_EmissionColor"); private static readonly int ColorId = Shader.PropertyToID("_Color"); private readonly List m_tinted = new List(); private readonly List<(Light light, Color orig)> m_lights = new List<(Light, Color)>(); private ZNetView? m_nview; private float m_replacedSince = -1f; private bool m_hiddenForReplace; private const float ReplaceHideTimeout = 4f; private const float ReplaceDestroyDelay = 5f; private const float ReplaceDestroyFailsafe = 10f; public static int LastCrumbleEffectCount { get; private set; } public static int CrumbleEvents { get; private set; } public int TintedLights => m_lights.Count; public int TintedMaterials => m_tinted.Count; public float CurrentBlend { get; private set; } public bool HiddenForReplace => m_hiddenForReplace; public static void MarkReplaced(GameObject? marker) { ZNetView val = Owned(marker); if (!((Object)(object)val == (Object)null)) { DownedMarkerView downedMarkerView = new DownedMarkerView(val); downedMarkerView.ReplacedByGrave = true; } } public static void Crumble(GameObject? marker) { //IL_0024: 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_003d: 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) if (!((Object)(object)marker == (Object)null)) { EffectList val = GraveCrumbleEffect(); if (val != null) { GameObject[] array = val.Create(marker.transform.position, marker.transform.rotation, (Transform)null, 1f, -1, default(ZDOID)); LastCrumbleEffectCount = ((array != null) ? array.Length : 0); CrumbleEvents++; } DestroyMarker(marker); } } public static void CrumbleLinked(ZNetView playerNview) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0011: 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_003b: Unknown result type (might be due to invalid IL or missing references) ZDOID marker = new DownedStateMachineView(playerNview).Marker; if (!(marker == ZDOID.None)) { DownedStateMachineView downedStateMachineView = new DownedStateMachineView(playerNview); downedStateMachineView.Marker = ZDOID.None; Crumble(ZNetScene.instance.FindInstance(marker)); } } public static void DestroyMarker(GameObject? marker) { ZNetView val = Owned(marker); if (val != null) { val.Destroy(); } } private static ZNetView? Owned(GameObject? marker) { ZNetView val = (((Object)(object)marker != (Object)null) ? marker.GetComponent() : null); if ((Object)(object)val == (Object)null || !val.IsValid()) { return null; } if (!val.IsOwner()) { val.ClaimOwnership(); } return val; } private static EffectList? GraveCrumbleEffect() { if (s_crumbleEffect != null) { return s_crumbleEffect; } GameObject val = (((Object)(object)ZNetScene.instance != (Object)null) ? MarkerPrefab.FindTombstonePrefab(ZNetScene.instance) : null); TombStone val2 = (((Object)(object)val != (Object)null) ? val.GetComponent() : null); if ((Object)(object)val2 != (Object)null && val2.m_removeEffect != null && val2.m_removeEffect.m_effectPrefabs.Length != 0) { s_crumbleEffect = val2.m_removeEffect; } else { Plugin.Logger.LogWarning((object)"DownedMarker: no grave crumble effect found"); } return s_crumbleEffect; } private void Awake() { m_nview = ((Component)this).GetComponent(); CaptureAccents(); ApplyBlend(0f); } private void Start() { if ((Object)(object)m_nview == (Object)null || !m_nview.IsValid()) { return; } string ownerName = new DownedMarkerView(m_nview).OwnerName; if (!string.IsNullOrEmpty(ownerName)) { TMP_Text componentInChildren = ((Component)this).GetComponentInChildren(true); if ((Object)(object)componentInChildren != (Object)null) { componentInChildren.text = ownerName; } } } private void Update() { //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0056: 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) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)m_nview == (Object)null) && m_nview.IsValid()) { DownedMarkerView downedMarkerView = new DownedMarkerView(m_nview); if (downedMarkerView.ReplacedByGrave) { UpdateReplaced(); return; } ZDOID linkedPlayer = downedMarkerView.LinkedPlayer; ZDO val = ((linkedPlayer != ZDOID.None) ? ZDOMan.instance.GetZDO(linkedPlayer) : null); float num = ((val != null) ? new DownedStateMachineView(val).DownedTime : downedMarkerView.DownedTime); float num2 = (float)ZNet.instance.GetTimeSeconds() - num; ApplyBlend(Mathf.Clamp01(num2 / Plugin.ReviveWindow)); } } private void CaptureAccents() { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00ee: Unknown result type (might be due to invalid IL or missing references) //IL_00f3: Unknown result type (might be due to invalid IL or missing references) //IL_00f5: Unknown result type (might be due to invalid IL or missing references) //IL_00bd: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Unknown result type (might be due to invalid IL or missing references) //IL_0103: Unknown result type (might be due to invalid IL or missing references) //IL_010a: Unknown result type (might be due to invalid IL or missing references) //IL_0119: Unknown result type (might be due to invalid IL or missing references) //IL_0120: Unknown result type (might be due to invalid IL or missing references) //IL_0143: Unknown result type (might be due to invalid IL or missing references) //IL_0145: Unknown result type (might be due to invalid IL or missing references) Light[] componentsInChildren = ((Component)this).GetComponentsInChildren(true); foreach (Light val in componentsInChildren) { m_lights.Add((val, val.color)); } Renderer[] componentsInChildren2 = ((Component)this).GetComponentsInChildren(true); foreach (Renderer val2 in componentsInChildren2) { Material[] materials = val2.materials; foreach (Material val3 in materials) { TintedMaterial item = new TintedMaterial { Mat = val3 }; if (val3.HasProperty(EmissionColorId)) { Color color = val3.GetColor(EmissionColorId); if (((Color)(ref color)).maxColorComponent > 0.05f) { item.HasEmission = true; item.OrigEmission = color; item.EmissionIntensity = ((Color)(ref color)).maxColorComponent; } } if (val3.HasProperty(ColorId)) { Color color2 = val3.GetColor(ColorId); if (color2.r > 0.4f && color2.r > color2.g * 1.5f && color2.r > color2.b * 1.5f) { item.HasColor = true; item.OrigColor = color2; } } if (item.HasEmission || item.HasColor) { m_tinted.Add(item); } } } } private void ApplyBlend(float blend) { //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_005e: 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_0067: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00a2: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00f1: Unknown result type (might be due to invalid IL or missing references) //IL_00f6: Unknown result type (might be due to invalid IL or missing references) //IL_0109: Unknown result type (might be due to invalid IL or missing references) //IL_010e: Unknown result type (might be due to invalid IL or missing references) //IL_0111: Unknown result type (might be due to invalid IL or missing references) CurrentBlend = blend; foreach (TintedMaterial item in m_tinted) { if (!((Object)(object)item.Mat == (Object)null)) { if (item.HasEmission) { Color val = ReviveGreen * item.EmissionIntensity; item.Mat.SetColor(EmissionColorId, Color.Lerp(val, item.OrigEmission, blend)); item.Mat.EnableKeyword("_EMISSION"); } if (item.HasColor) { item.Mat.SetColor(ColorId, Color.Lerp(ReviveGreen, item.OrigColor, blend)); } } } foreach (var (val2, val3) in m_lights) { if ((Object)(object)val2 != (Object)null) { val2.color = Color.Lerp(ReviveGreen, val3, blend); } } } public bool IsGreen() { return (TintedLights > 0 || TintedMaterials > 0) && CurrentBlend < 0.5f; } private void UpdateReplaced() { if (m_replacedSince < 0f) { m_replacedSince = Time.time; } float num = Time.time - m_replacedSince; if (!m_hiddenForReplace && (num > 4f || GraveNearby())) { HideLocally(); } if (m_nview.IsOwner()) { if (num > 5f) { m_nview.Destroy(); } } else if (num > 10f) { m_nview.ClaimOwnership(); m_nview.Destroy(); } } private bool GraveNearby() { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) TombStone[] array = Object.FindObjectsOfType(); foreach (TombStone val in array) { Vector3 val2 = ((Component)val).transform.position - ((Component)this).transform.position; if (((Vector3)(ref val2)).sqrMagnitude < 9f) { return true; } } return false; } private void HideLocally() { m_hiddenForReplace = true; Renderer[] componentsInChildren = ((Component)this).GetComponentsInChildren(); foreach (Renderer val in componentsInChildren) { val.enabled = false; } Collider[] componentsInChildren2 = ((Component)this).GetComponentsInChildren(); foreach (Collider val2 in componentsInChildren2) { val2.enabled = false; } Light[] componentsInChildren3 = ((Component)this).GetComponentsInChildren(); foreach (Light val3 in componentsInChildren3) { ((Behaviour)val3).enabled = false; } } } public struct DownedMarkerView { private static readonly int kIsMarker = StringExtensionMethods.GetStableHashCode("RevivalRevived_isDownedMarker"); private static readonly int kOwnerPlayerId = StringExtensionMethods.GetStableHashCode("RevivalRevived_ownerPlayerID"); private static readonly int kReplacedByGrave = StringExtensionMethods.GetStableHashCode("RevivalRevived_replacedByGrave"); private static readonly int kDownedTime = StringExtensionMethods.GetStableHashCode("RevivalRevived_downedTime"); private static readonly int kOwnerName = ZDOVars.s_ownerName; private static readonly KeyValuePair kPlayer = ZDO.GetHashZDOID("RevivalRevived_playerZDOID"); private readonly ZDO _z; public bool IsMarker { get { return _z.GetBool(kIsMarker, false); } set { _z.Set(kIsMarker, value); } } public string OwnerName { get { return _z.GetString(kOwnerName, ""); } set { _z.Set(kOwnerName, value); } } public long OwnerPlayerId { get { return _z.GetLong(kOwnerPlayerId, 0L); } set { _z.Set(kOwnerPlayerId, value); } } public ZDOID LinkedPlayer { get { //IL_000b: Unknown result type (might be due to invalid IL or missing references) return _z.GetZDOID(kPlayer); } set { //IL_000b: Unknown result type (might be due to invalid IL or missing references) _z.Set(kPlayer, value); } } public float DownedTime { get { return _z.GetFloat(kDownedTime, 0f); } set { _z.Set(kDownedTime, value); } } public bool ReplacedByGrave { get { return _z.GetBool(kReplacedByGrave, false); } set { _z.Set(kReplacedByGrave, value); } } public DownedMarkerView(ZNetView nview) : this(nview.GetZDO()) { } public DownedMarkerView(ZDO zdo) { _z = zdo; } } public class ChannelSignal : MonoBehaviour { private ZNetView m_nview = null; private bool m_channeling; private long m_channeler; private ZDOID m_channelerZdo = ZDOID.None; public bool IsChanneling => m_channeling && m_channelerZdo != ZDOID.None && ZDOMan.instance != null && ZDOMan.instance.GetZDO(m_channelerZdo) != null; public long LastChanneler => m_channeler; private void Awake() { m_nview = ((Component)this).GetComponent(); m_nview.Register("RevivalRevived_Channel", (Action)delegate(long sender, bool channeling, ZDOID channelerZdo) { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001a: 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_0036: Unknown result type (might be due to invalid IL or missing references) if (m_nview.IsOwner() && (channeling || !(channelerZdo != m_channelerZdo))) { m_channeler = sender; m_channelerZdo = channelerZdo; m_channeling = channeling; } }); } } public class DownedStateMachine : StateMachine { private ZNetView m_nview = null; protected override void Awake() { m_nview = ((Component)this).GetComponent(); base.Awake(); } protected override IState CreateInitialState() { return new AliveState(((Component)this).GetComponent()); } private void Update() { if (m_nview.IsValid() && m_nview.IsOwner()) { Tick(); } } } public struct DownedStateMachineView { private static readonly int kDowned = StringExtensionMethods.GetStableHashCode("RevivalRevived_downed"); private static readonly int kDownedTime = StringExtensionMethods.GetStableHashCode("RevivalRevived_downedTime"); private static readonly KeyValuePair kMarker = ZDO.GetHashZDOID("RevivalRevived_markerZDOID"); private readonly ZDO _z; public bool Downed { get { return _z.GetBool(kDowned, false); } set { _z.Set(kDowned, value); } } public float DownedTime { get { return _z.GetFloat(kDownedTime, 0f); } set { _z.Set(kDownedTime, value); } } public ZDOID Marker { get { //IL_000b: Unknown result type (might be due to invalid IL or missing references) return _z.GetZDOID(kMarker); } set { //IL_000b: Unknown result type (might be due to invalid IL or missing references) _z.Set(kMarker, value); } } public DownedStateMachineView(ZNetView nview) : this(nview.GetZDO()) { } public DownedStateMachineView(ZDO zdo) { _z = zdo; } } public class GiveUp : MonoBehaviour { public const float Duration = 2f; private Player m_player = null; private readonly GiveUpDecayingProgress m_timer = new GiveUpDecayingProgress(); private ProgressUI? m_ui; private bool m_simulate; public static float LocalFraction { get; private set; } private void Awake() { m_player = ((Component)this).GetComponent(); } private void OnDestroy() { m_ui?.Close(); if ((Object)(object)m_player == (Object)(object)Player.m_localPlayer) { LocalFraction = 0f; } } public bool Held() { if (m_simulate) { m_simulate = false; return true; } if ((Object)(object)Chat.instance != (Object)null && Chat.instance.HasFocus()) { return false; } if (Console.IsVisible() || Menu.IsVisible() || InventoryGui.IsVisible() || TextInput.IsVisible()) { return false; } return ZInput.GetButton("Use") || ZInput.GetButton("JoyUse"); } public void SimulateHold() { m_simulate = true; } public void ShowUI() { //IL_0018: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)m_ui == (Object)null) { m_ui = ProgressUI.Create(m_timer, ProgressUI.GiveUpRed, isGiveUp: true); } } public bool Channel(float dt) { m_timer.Channel(dt); Mirror(); return m_timer.Full; } public void Decay(float dt) { m_timer.Decay(dt); Mirror(); } public void Reset() { m_timer.Reset(); Mirror(); } private void Mirror() { if ((Object)(object)m_player == (Object)(object)Player.m_localPlayer) { LocalFraction = m_timer.Fraction; } } } public struct GraveReplaceView { private static readonly int kPending = StringExtensionMethods.GetStableHashCode("RevivalRevived_graveReplacePending"); private static readonly int kPos = StringExtensionMethods.GetStableHashCode("RevivalRevived_graveReplacePos"); private readonly ZDO _z; public bool Pending { get { return _z.GetBool(kPending, false); } set { _z.Set(kPending, value); } } public Vector3 Pos { get { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) return _z.GetVec3(kPos, Vector3.zero); } set { //IL_000b: Unknown result type (might be due to invalid IL or missing references) _z.Set(kPos, value); } } public GraveReplaceView(ZNetView nview) : this(nview.GetZDO()) { } public GraveReplaceView(ZDO zdo) { _z = zdo; } } public class ReconnectDeathCheck : MonoBehaviour { private Player m_player = null; private ZNetView m_nview = null; private void Awake() { m_player = ((Component)this).GetComponent(); m_nview = ((Component)this).GetComponent(); } private void Start() { if (m_nview.IsValid() && m_nview.IsOwner()) { ((MonoBehaviour)this).StartCoroutine(Check()); } } public void RunReconnectCheck() { ((MonoBehaviour)this).StartCoroutine(Check()); } private IEnumerator Check() { long pid = m_player.GetPlayerID(); float t = 0f; while (t < 12f && !((Object)(object)m_player == (Object)null) && m_nview.IsValid() && !m_player.IsDowned()) { GameObject orphan = MarkerPrefab.FindFor(pid); if ((Object)(object)orphan != (Object)null) { Plugin.Logger.LogInfo((object)(m_player.GetPlayerName() + " reconnected with an orphaned downed marker -> dying")); Kill(); break; } t += 0.5f; yield return (object)new WaitForSecondsRealtime(0.5f); } } private void Kill() { //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_00ec: Unknown result type (might be due to invalid IL or missing references) //IL_00f1: Unknown result type (might be due to invalid IL or missing references) //IL_00f4: Unknown result type (might be due to invalid IL or missing references) //IL_00fe: Expected O, but got Unknown if (m_nview.IsValid() && m_nview.IsOwner()) { DownedStateMachineView downedStateMachineView = m_player.State(); downedStateMachineView.Downed = false; GameObject val = MarkerPrefab.FindFor(m_player.GetPlayerID()); GameObject val2 = m_player.FindDownedMarker(); Vector3? val3 = (((Object)(object)val != (Object)null) ? new Vector3?(val.transform.position) : (((Object)(object)val2 != (Object)null) ? new Vector3?(val2.transform.position) : ((Vector3?)null))); if (val3.HasValue) { GraveReplaceView graveReplaceView = m_player.GraveReplace(); graveReplaceView.Pending = true; graveReplaceView.Pos = val3.Value; } if (((Character)m_player).m_lastHit == null) { ((Character)m_player).m_lastHit = new HitData { m_hitType = (HitType)14 }; } ((Character)m_player).SetHealth(0f); Traverse.Create((object)m_player).Method("OnDeath", Array.Empty()).GetValue(); Plugin.Logger.LogInfo((object)(m_player.GetPlayerName() + " died from being downed at disconnect")); } } } public class DownedView : MonoBehaviour { private Player m_player = null; private ZNetView m_nview = null; private bool m_wasDowned; private void Awake() { m_player = ((Component)this).GetComponent(); m_nview = ((Component)this).GetComponent(); } private void Start() { m_wasDowned = m_player.IsDowned(); } private void Update() { if (!m_nview.IsValid()) { return; } bool flag = m_player.IsDowned(); if (flag) { if (!m_wasDowned) { m_player.PlayDownedPoof(); } if ((Object)(object)((Character)m_player).m_visual != (Object)null && ((Character)m_player).m_visual.activeSelf) { ((Character)m_player).m_visual.SetActive(false); } if ((Object)(object)((Character)m_player).m_collider != (Object)null && ((Collider)((Character)m_player).m_collider).enabled) { ((Collider)((Character)m_player).m_collider).enabled = false; } if (m_nview.IsOwner() && (Object)(object)((Character)m_player).m_body != (Object)null) { ((Character)m_player).m_body.isKinematic = true; } } else if (m_wasDowned) { Restore(); } m_wasDowned = flag; } private void Restore() { if (!((Character)m_player).IsDead()) { if ((Object)(object)((Character)m_player).m_visual != (Object)null) { ((Character)m_player).m_visual.SetActive(true); } if ((Object)(object)((Character)m_player).m_collider != (Object)null) { ((Collider)((Character)m_player).m_collider).enabled = true; } if (m_nview.IsOwner() && (Object)(object)((Character)m_player).m_body != (Object)null) { ((Character)m_player).m_body.isKinematic = false; } } } } public class GiveUpDecayingProgress : IDecayingProgress { private const float DecayRate = 2f; private float m_seconds; private bool m_active; public float Fraction => Mathf.Clamp01(m_seconds / 2f); public bool Full => m_seconds >= 2f; public event Action? Finished; public void Channel(float dt) { m_seconds += dt; if (m_seconds > 0.01f) { m_active = true; } } public void Decay(float dt) { if (!(m_seconds <= 0f)) { m_seconds = Mathf.Max(0f, m_seconds - dt * 2f); if (m_seconds <= 0f) { Empty(); } } } public void Reset() { m_seconds = 0f; m_active = false; this.Finished?.Invoke(); } private void Empty() { if (m_active) { m_active = false; this.Finished?.Invoke(); } } } public interface IDecayingProgress { float Fraction { get; } event Action Finished; } public static class MarkerPrefab { public const string PrefabName = "RevivalRevived_DownedMarker"; private static GameObject? s_prefabHolder; private static GameObject? s_prefabTemplate; public static int TemplateEffectsRemoved { get; private set; } public static float LastPopVelY { get; private set; } public static void RegisterPrefab(ZNetScene scene) { //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Expected O, but got Unknown if ((Object)(object)s_prefabTemplate == (Object)null) { GameObject val = FindTombstonePrefab(scene); if ((Object)(object)val == (Object)null) { Plugin.Logger.LogError((object)"MarkerPrefab: no TombStone prefab found to derive the marker from"); return; } s_prefabHolder = new GameObject("RevivalRevived_Prefabs"); s_prefabHolder.SetActive(false); Object.DontDestroyOnLoad((Object)(object)s_prefabHolder); GameObject val2 = Object.Instantiate(val, s_prefabHolder.transform); ((Object)val2).name = "RevivalRevived_DownedMarker"; TombStone component = val2.GetComponent(); if ((Object)(object)component != (Object)null) { Object.DestroyImmediate((Object)(object)component); } Container component2 = val2.GetComponent(); if ((Object)(object)component2 != (Object)null) { Object.DestroyImmediate((Object)(object)component2); } RemoveGraveEffects(val2); val2.AddComponent(); val2.AddComponent(); val2.AddComponent(); s_prefabTemplate = val2; Plugin.Logger.LogInfo((object)("MarkerPrefab: built prefab 'RevivalRevived_DownedMarker' from '" + ((Object)val).name + "'")); } int stableHashCode = StringExtensionMethods.GetStableHashCode("RevivalRevived_DownedMarker"); if (!scene.m_namedPrefabs.ContainsKey(stableHashCode)) { scene.m_prefabs.Add(s_prefabTemplate); scene.m_namedPrefabs.Add(stableHashCode, s_prefabTemplate); Plugin.Logger.LogInfo((object)"MarkerPrefab: registered prefab 'RevivalRevived_DownedMarker' with ZNetScene"); } } internal static GameObject? FindTombstonePrefab(ZNetScene scene) { GameObject prefab = scene.GetPrefab("Player_tombstone"); if ((Object)(object)prefab != (Object)null && (Object)(object)prefab.GetComponent() != (Object)null) { return prefab; } foreach (GameObject prefab2 in scene.m_prefabs) { if ((Object)(object)prefab2 != (Object)null && (Object)(object)prefab2.GetComponent() != (Object)null) { return prefab2; } } return null; } private static void RemoveGraveEffects(GameObject template) { ParticleSystem[] componentsInChildren = template.GetComponentsInChildren(true); foreach (ParticleSystem val in componentsInChildren) { if ((Object)(object)val != (Object)null && (Object)(object)((Component)val).gameObject != (Object)(object)template) { Object.DestroyImmediate((Object)(object)((Component)val).gameObject); TemplateEffectsRemoved++; } } Transform[] componentsInChildren2 = template.GetComponentsInChildren(true); foreach (Transform val2 in componentsInChildren2) { if (!((Object)(object)val2 == (Object)null) && !((Object)(object)((Component)val2).gameObject == (Object)(object)template)) { string text = ((Object)val2).name.ToLowerInvariant(); if (text.Contains("flare") || text.Contains("glow")) { Object.DestroyImmediate((Object)(object)((Component)val2).gameObject); TemplateEffectsRemoved++; } } } } public static void Spawn(Player player) { //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_0101: Unknown result type (might be due to invalid IL or missing references) //IL_0133: Unknown result type (might be due to invalid IL or missing references) //IL_0151: Unknown result type (might be due to invalid IL or missing references) //IL_0177: Unknown result type (might be due to invalid IL or missing references) GameObject val = (((Object)(object)ZNetScene.instance != (Object)null) ? ZNetScene.instance.GetPrefab("RevivalRevived_DownedMarker") : null); if ((Object)(object)val == (Object)null) { Plugin.Logger.LogError((object)"MarkerPrefab.Spawn: prefab 'RevivalRevived_DownedMarker' is not registered"); return; } GameObject val2 = Object.Instantiate(val, ((Character)player).GetCenterPoint(), ((Component)player).transform.rotation); ZNetView component = val2.GetComponent(); if ((Object)(object)component == (Object)null || !component.IsValid()) { Plugin.Logger.LogError((object)"MarkerPrefab.Spawn: marker has no valid ZNetView"); return; } DownedMarkerView downedMarkerView = new DownedMarkerView(component); downedMarkerView.IsMarker = true; downedMarkerView.LinkedPlayer = ((Character)player).m_nview.GetZDO().m_uid; downedMarkerView.OwnerPlayerId = player.GetPlayerID(); downedMarkerView.OwnerName = player.GetPlayerName(); downedMarkerView.DownedTime = (float)ZNet.instance.GetTimeSeconds(); DownedStateMachineView downedStateMachineView = new DownedStateMachineView(((Character)player).m_nview); downedStateMachineView.Marker = component.GetZDO().m_uid; Rigidbody component2 = val2.GetComponent(); if ((Object)(object)component2 != (Object)null) { component2.linearVelocity = new Vector3(0f, 5f, 0f); } LastPopVelY = (((Object)(object)component2 != (Object)null) ? component2.linearVelocity.y : 0f); Plugin.Logger.LogInfo((object)$"Spawned downed marker for {player.GetPlayerName()}, ZDOID {component.GetZDO().m_uid}"); } public static GameObject? FindFor(long playerId) { if (playerId == 0) { return null; } DownedMarker[] array = Object.FindObjectsOfType(); foreach (DownedMarker downedMarker in array) { ZNetView component = ((Component)downedMarker).GetComponent(); if (!((Object)(object)component == (Object)null) && component.IsValid()) { DownedMarkerView downedMarkerView = new DownedMarkerView(component); if (!downedMarkerView.ReplacedByGrave && downedMarkerView.OwnerPlayerId == playerId) { return ((Component)downedMarker).gameObject; } } } return null; } } public static class PlayerDownedExtensions { private static EffectList? s_cachedRemoveEffect; private static bool s_removeEffectSearched; public static int LastPoofCount { get; private set; } public static string LastPoofSourceName { get; private set; } = ""; public static DownedStateMachineView State(this Player player) { return new DownedStateMachineView(((Character)player).m_nview); } public static GraveReplaceView GraveReplace(this Player player) { return new GraveReplaceView(((Character)player).m_nview); } public static bool TryState(this Player? player, out DownedStateMachineView state) { ZNetView val = ((Character)(player?)).m_nview; if ((Object)(object)val == (Object)null || !val.IsValid()) { state = default(DownedStateMachineView); return false; } state = new DownedStateMachineView(val); return true; } public static bool IsDowned(this Player? player) { DownedStateMachineView state; return player.TryState(out state) && state.Downed; } public static bool IsReviveWindowExpired(this Player player) { return player.NonChanneledDownedElapsed() > Plugin.ReviveWindow; } public static float GetDownedRemainingTime(this Player? player) { if (!player.TryState(out var _)) { return 0f; } return Mathf.Max(0f, Plugin.ReviveWindow - player.NonChanneledDownedElapsed()); } private static float NonChanneledDownedElapsed(this Player? player) { if (!player.TryState(out var state)) { return 0f; } float num = (float)ZNet.instance.GetTimeSeconds() - state.DownedTime; return Mathf.Max(0f, num - player.OpenChannelSeconds()); } private static float OpenChannelSeconds(this Player? player) { GameObject val = player.FindDownedMarker(); ZNetView val2 = (((Object)(object)val != (Object)null) ? val.GetComponent() : null); if ((Object)(object)val2 == (Object)null || !val2.IsValid()) { return 0f; } ReviveChannelDecayingProgressView reviveChannelDecayingProgressView = new ReviveChannelDecayingProgressView(val2); if (!reviveChannelDecayingProgressView.Channeling) { return 0f; } return Mathf.Max(0f, (float)ZNet.instance.GetTimeSeconds() - reviveChannelDecayingProgressView.AnchorTime); } public static float GetReviveProgress(this Player? player) { GameObject val = player.FindDownedMarker(); ReviveChannelDecayingProgress reviveChannelDecayingProgress = (((Object)(object)val != (Object)null) ? val.GetComponent() : null); return ((Object)(object)reviveChannelDecayingProgress != (Object)null) ? reviveChannelDecayingProgress.Fraction : 0f; } public static GameObject? FindDownedMarker(this Player? player) { //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_002e: Unknown result type (might be due to invalid IL or missing references) if (!player.TryState(out var state) || state.Marker == ZDOID.None) { return null; } return ZNetScene.instance.FindInstance(state.Marker); } public static void EnterDownedState(this Player player) { GraveReplaceView graveReplaceView = player.GraveReplace(); graveReplaceView.Pending = false; DownedStateMachineView downedStateMachineView = player.State(); downedStateMachineView.Downed = true; downedStateMachineView.DownedTime = (float)ZNet.instance.GetTimeSeconds(); MarkerPrefab.Spawn(player); ((Character)player).Message((MessageType)2, Localization.instance.Localize("You are downed!\nHold [$KEY_Use] to give up"), 0, (Sprite)null, false); Plugin.Logger.LogInfo((object)(player.GetPlayerName() + " entered downed state (owner)")); } public static void ReviveFromDowned(this Player player, long reviverId = 0L) { if (!((Object)(object)player == (Object)null) && ((Character)player).m_nview.IsValid()) { if (!((Character)player).m_nview.IsOwner()) { Plugin.Logger.LogWarning((object)"ReviveFromDowned called on non-owner; ignoring"); return; } DownedStateMachineView downedStateMachineView = player.State(); downedStateMachineView.Downed = false; ((Character)player).SetHealth(Mathf.Max(((Character)player).GetMaxHealth() * 0.25f, 1f)); DownedMarker.CrumbleLinked(((Character)player).m_nview); ((Character)player).Message((MessageType)2, "You have been revived!", 0, (Sprite)null, false); Plugin.Logger.LogInfo((object)(player.GetPlayerName() + " was revived by " + ReviverName(reviverId))); } } public static void ExpireDownedState(this Player player) { //IL_005d: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)player == (Object)null) && ((Character)player).m_nview.IsValid()) { DownedStateMachineView downedStateMachineView = player.State(); downedStateMachineView.Downed = false; GameObject val = player.FindDownedMarker(); if ((Object)(object)val != (Object)null) { GraveReplaceView graveReplaceView = player.GraveReplace(); graveReplaceView.Pending = true; graveReplaceView.Pos = val.transform.position; } Plugin.Logger.LogInfo((object)(player.GetPlayerName() + " revive window expired, proceeding to death")); } } private static string ReviverName(long reviverId) { if (reviverId == 0) { return "someone"; } Player player = Player.GetPlayer(reviverId); return ((Object)(object)player != (Object)null) ? player.GetPlayerName() : reviverId.ToString(); } public static int PlayDownedPoof(this Player player) { //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) EffectList val = FindRagdollRemoveEffect(); if (val == null) { LastPoofCount = 0; return 0; } GameObject[] array = val.Create(((Character)player).GetCenterPoint(), Quaternion.identity, (Transform)null, 1f, -1, default(ZDOID)); LastPoofCount = ((array != null) ? array.Length : 0); return LastPoofCount; } private static EffectList? FindRagdollRemoveEffect() { if (s_removeEffectSearched) { return s_cachedRemoveEffect; } if ((Object)(object)ZNetScene.instance == (Object)null) { return null; } s_removeEffectSearched = true; string[] array = new string[2] { "Greyling_ragdoll", "Greydwarf_ragdoll" }; foreach (string text in array) { GameObject prefab = ZNetScene.instance.GetPrefab(text); Ragdoll val = (((Object)(object)prefab != (Object)null) ? prefab.GetComponent() : null); if ((Object)(object)val != (Object)null && val.m_removeEffect != null && val.m_removeEffect.m_effectPrefabs.Length != 0) { s_cachedRemoveEffect = val.m_removeEffect; LastPoofSourceName = ((Object)prefab).name; Plugin.Logger.LogInfo((object)("Downed poof: using remove-effect from '" + ((Object)prefab).name + "'")); return s_cachedRemoveEffect; } } foreach (GameObject prefab2 in ZNetScene.instance.m_prefabs) { if (!((Object)(object)prefab2 == (Object)null)) { Ragdoll component = prefab2.GetComponent(); if ((Object)(object)component != (Object)null && component.m_removeEffect != null && component.m_removeEffect.m_effectPrefabs.Length != 0) { s_cachedRemoveEffect = component.m_removeEffect; LastPoofSourceName = ((Object)prefab2).name; Plugin.Logger.LogInfo((object)("Downed poof: using remove-effect from '" + ((Object)prefab2).name + "' (fallback)")); break; } } } if (s_cachedRemoveEffect == null) { Plugin.Logger.LogWarning((object)"Downed poof: no ragdoll prefab with a remove-effect found"); } return s_cachedRemoveEffect; } } public class ProgressUI : MonoBehaviour { public static readonly Color GiveUpRed = new Color(0.9f, 0.15f, 0.15f); private const float IdleTimeout = 0.5f; private IDecayingProgress? m_source; private Color m_color; private bool m_isGiveUp; private float m_idle; private GameObject? m_root; private Image? m_fillImg; public static bool Visible { get; private set; } public static float Fill { get; private set; } public static bool GivingUp { get; private set; } public static ProgressUI Create(IDecayingProgress source, Color color, bool isGiveUp) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Expected O, but got Unknown //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject("ReviveAllies_ProgressUI"); Object.DontDestroyOnLoad((Object)(object)val); ProgressUI progressUI = val.AddComponent(); progressUI.m_source = source; progressUI.m_color = color; progressUI.m_isGiveUp = isGiveUp; source.Finished += progressUI.OnFinished; return progressUI; } private void OnFinished() { Close(); } public void Close() { if ((Object)(object)this != (Object)null) { Object.Destroy((Object)(object)((Component)this).gameObject); } } private void Awake() { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Expected O, but got Unknown //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject("Canvas"); val.transform.SetParent(((Component)this).transform, false); Canvas val2 = val.AddComponent(); val2.renderMode = (RenderMode)0; val2.sortingOrder = 5000; Sprite sprite = MakeDiscSprite(); MakeImage(val.transform, "bg", sprite, new Color(0f, 0f, 0f, 0.55f), 84f); m_fillImg = MakeImage(val.transform, "fill", sprite, Color.white, 72f); m_fillImg.type = (Type)3; m_fillImg.fillMethod = (FillMethod)4; m_fillImg.fillOrigin = 2; m_fillImg.fillClockwise = true; m_fillImg.fillAmount = 0f; m_root = val; m_root.SetActive(false); } private void Update() { //IL_00b2: Unknown result type (might be due to invalid IL or missing references) IDecayingProgress? source = m_source; Object val = (Object)((source is Object) ? source : null); if (val != null && val == (Object)null) { Close(); return; } float num = Mathf.Clamp01(m_source?.Fraction ?? 0f); bool flag = num > 0.01f; if ((Object)(object)m_root != (Object)null && m_root.activeSelf != flag) { m_root.SetActive(flag); } if (flag && (Object)(object)m_fillImg != (Object)null) { m_fillImg.fillAmount = num; ((Graphic)m_fillImg).color = m_color; } Visible = flag; Fill = (flag ? num : 0f); GivingUp = flag && m_isGiveUp; if (flag) { m_idle = 0f; return; } m_idle += Time.unscaledDeltaTime; if (m_idle >= 0.5f) { Close(); } } private void OnDestroy() { if (m_source != null) { m_source.Finished -= OnFinished; } Visible = false; Fill = 0f; GivingUp = false; } private static Image MakeImage(Transform parent, string name, Sprite sprite, Color color, float size) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Expected O, but got Unknown //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0060: 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) GameObject val = new GameObject(name); val.transform.SetParent(parent, false); Image val2 = val.AddComponent(); val2.sprite = sprite; ((Graphic)val2).color = color; RectTransform rectTransform = ((Graphic)val2).rectTransform; Vector2 val3 = default(Vector2); ((Vector2)(ref val3))..ctor(0.5f, 0.5f); rectTransform.anchorMax = val3; rectTransform.anchorMin = val3; rectTransform.anchoredPosition = new Vector2(0f, -110f); rectTransform.sizeDelta = new Vector2(size, size); return val2; } private static Sprite MakeDiscSprite() { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Expected O, but got Unknown //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: 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_0034: 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) Texture2D val = new Texture2D(64, 64, (TextureFormat)5, false); float num = 32f; for (int i = 0; i < 64; i++) { for (int j = 0; j < 64; j++) { float num2 = Vector2.Distance(new Vector2((float)j + 0.5f, (float)i + 0.5f), new Vector2(num, num)); float num3 = Mathf.Clamp01((num - 1f - num2) / 2f); val.SetPixel(j, i, new Color(1f, 1f, 1f, num3)); } } val.Apply(); return Sprite.Create(val, new Rect(0f, 0f, 64f, 64f), new Vector2(0.5f, 0.5f)); } } public class Revivable : MonoBehaviour, Hoverable, Interactable { private ZNetView? m_nview; private void Awake() { m_nview = ((Component)this).GetComponentInParent(); } private ZDOID LinkedPlayerZdoId() { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) return ((Object)(object)m_nview != (Object)null && m_nview.IsValid()) ? new DownedMarkerView(m_nview).LinkedPlayer : ZDOID.None; } private Player? FindDownedPlayer() { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) ZDOID val = LinkedPlayerZdoId(); if (val == ZDOID.None) { return null; } ZDO zDO = ZDOMan.instance.GetZDO(val); if (zDO == null) { return null; } ZNetView val2 = ZNetScene.instance.FindInstance(zDO); Player val3 = (((Object)(object)val2 != (Object)null) ? ((Component)val2).GetComponent() : null); return ((Object)(object)val3 != (Object)null && val3.IsDowned()) ? val3 : null; } private bool LinkedPlayerDisconnected() { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_0009: 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) ZDOID val = LinkedPlayerZdoId(); if (val == ZDOID.None) { return false; } return ZDOMan.instance.GetZDO(val) == null; } private string OwnerName() { if ((Object)(object)m_nview == (Object)null || !m_nview.IsValid()) { return "Viking"; } string ownerName = new DownedMarkerView(m_nview).OwnerName; return string.IsNullOrEmpty(ownerName) ? "Viking" : ownerName; } public string GetHoverText() { Player val = FindDownedPlayer(); if ((Object)(object)val == (Object)null) { if (LinkedPlayerDisconnected()) { return Localization.instance.Localize(OwnerName() + " (disconnected)"); } return ""; } string text = OwnerName(); string arg = (Plugin.RevivePressMode ? "" : "Hold "); string text2 = text + " (downed)\n"; text2 += $"[{arg}$KEY_Use] Revive ({val.GetDownedRemainingTime():F0}s)"; return Localization.instance.Localize(text2); } public string GetHoverName() { if ((Object)(object)m_nview == (Object)null || !m_nview.IsValid()) { return ""; } if ((Object)(object)FindDownedPlayer() != (Object)null || LinkedPlayerDisconnected()) { return OwnerName(); } return ""; } public bool Interact(Humanoid user, bool hold, bool alt) { if (!hold && !Plugin.RevivePressMode) { return false; } Player val = (Player)(object)((user is Player) ? user : null); if (val == null) { return false; } Player val2 = FindDownedPlayer(); if ((Object)(object)val2 == (Object)null || (Object)(object)val == (Object)(object)val2) { return false; } ((Component)val).GetComponent()?.Request(val2); return true; } public void SimulateHold() { Player val = FindDownedPlayer(); Player localPlayer = Player.m_localPlayer; if ((Object)(object)val != (Object)null && (Object)(object)localPlayer != (Object)null) { ((Component)localPlayer).GetComponent()?.Request(val); } } public bool UseItem(Humanoid user, ItemData item) { return false; } public float GetHoverOffset() { return 0f; } } public class ReviveChannelDecayingProgress : MonoBehaviour, IDecayingProgress { private const float DecayRate = 2f; private ZNetView m_nview = null; private long m_channeler; private float m_channelStart = -1f; private bool m_active; private bool Owner => (Object)(object)m_nview != (Object)null && m_nview.IsValid() && m_nview.IsOwner(); private float Now => (float)ZNet.instance.GetTimeSeconds(); private ReviveChannelDecayingProgressView View => new ReviveChannelDecayingProgressView(m_nview); public float Fraction => Mathf.Clamp01(Seconds() / Plugin.ReviveDuration); public bool Acked => (Object)(object)m_nview != (Object)null && m_nview.IsValid() && View.Channeling; public event Action? Finished; private void Awake() { m_nview = ((Component)this).GetComponent(); } private float Seconds() { if ((Object)(object)m_nview == (Object)null || !m_nview.IsValid()) { return 0f; } ReviveChannelDecayingProgressView view = View; float num = Now - view.AnchorTime; return view.Channeling ? (view.AnchorSeconds + num) : Mathf.Max(0f, view.AnchorSeconds - num * 2f); } public void Begin(long channeler) { if (Owner) { m_channeler = channeler; m_channelStart = Now; ReviveChannelDecayingProgressView view = View; view.AnchorSeconds = Seconds(); view.AnchorTime = Now; view.Channeling = true; } } public void End() { if (Owner) { ReviveChannelDecayingProgressView view = View; view.AnchorSeconds = Seconds(); view.AnchorTime = Now; view.Channeling = false; if (m_channelStart >= 0f) { PauseWindow(Now - m_channelStart); } m_channelStart = -1f; } } private void Update() { if (Owner) { ReviveChannelDecayingProgressView view = View; if (view.Channeling && (Plugin.RevivePressMode || Seconds() >= Plugin.ReviveDuration)) { view.Channeling = false; view.AnchorSeconds = 0f; view.AnchorTime = Now; LinkedPlayer()?.ReviveFromDowned(m_channeler); return; } } if (Fraction > 0.01f) { m_active = true; } else if (m_active) { m_active = false; this.Finished?.Invoke(); } } private void OnDestroy() { if (m_active) { m_active = false; this.Finished?.Invoke(); } } private void PauseWindow(float seconds) { if (!(seconds <= 0f)) { Player val = LinkedPlayer(); if (!((Object)(object)val == (Object)null) && ((Character)val).m_nview.IsValid() && ((Character)val).m_nview.IsOwner()) { DownedStateMachineView downedStateMachineView = new DownedStateMachineView(((Character)val).m_nview); downedStateMachineView.DownedTime += seconds; } } } private Player? LinkedPlayer() { //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_003f: 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) if ((Object)(object)m_nview == (Object)null || !m_nview.IsValid()) { return null; } ZDOID linkedPlayer = new DownedMarkerView(m_nview).LinkedPlayer; if (linkedPlayer == ZDOID.None) { return null; } ZDO zDO = ZDOMan.instance.GetZDO(linkedPlayer); ZNetView val = ((zDO != null) ? ZNetScene.instance.FindInstance(zDO) : null); return ((Object)(object)val != (Object)null) ? ((Component)val).GetComponent() : null; } } public struct ReviveChannelDecayingProgressView { private static readonly int kAnchorTime = StringExtensionMethods.GetStableHashCode("RevivalRevived_reviveAnchorTime"); private static readonly int kAnchorSeconds = StringExtensionMethods.GetStableHashCode("RevivalRevived_reviveAnchorSeconds"); private static readonly int kChanneling = StringExtensionMethods.GetStableHashCode("RevivalRevived_reviveChanneling"); private readonly ZDO _z; public float AnchorTime { get { return _z.GetFloat(kAnchorTime, 0f); } set { _z.Set(kAnchorTime, value); } } public float AnchorSeconds { get { return _z.GetFloat(kAnchorSeconds, 0f); } set { _z.Set(kAnchorSeconds, value); } } public bool Channeling { get { return _z.GetBool(kChanneling, false); } set { _z.Set(kChanneling, value); } } public ReviveChannelDecayingProgressView(ZNetView nview) : this(nview.GetZDO()) { } public ReviveChannelDecayingProgressView(ZDO zdo) { _z = zdo; } } public class ReviveRequest : MonoBehaviour { private const float RequestTimeout = 0.5f; private Player? m_target; private float m_lastRequest = -999f; private ProgressUI? m_ui; public bool WantsToChannel => Time.time - m_lastRequest < 0.5f; public Player? Target => ((Object)(object)m_target != (Object)null && m_target.IsDowned()) ? m_target : null; public void Request(Player downed) { m_target = downed; m_lastRequest = Time.time; } public void SendBegin() { SendChannelEdge(channeling: true); } public void SendEnd() { SendChannelEdge(channeling: false); } private void SendChannelEdge(bool channeling) { //IL_0061: Unknown result type (might be due to invalid IL or missing references) ZNetView component = ((Component)this).GetComponent(); if (!((Object)(object)m_target == (Object)null) && ((Character)m_target).m_nview.IsValid() && component.IsValid()) { ((Character)m_target).m_nview.InvokeRPC("RevivalRevived_Channel", new object[2] { channeling, component.GetZDO().m_uid }); } } public void ShowUI() { //IL_004a: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)m_ui != (Object)null)) { Player? target = Target; object obj; if (target == null) { obj = null; } else { GameObject? obj2 = target.FindDownedMarker(); obj = ((obj2 != null) ? obj2.GetComponent() : null); } ReviveChannelDecayingProgress reviveChannelDecayingProgress = (ReviveChannelDecayingProgress)obj; if ((Object)(object)reviveChannelDecayingProgress != (Object)null && reviveChannelDecayingProgress.Acked) { m_ui = ProgressUI.Create(reviveChannelDecayingProgress, DownedMarker.ReviveGreen, isGiveUp: false); } } } public void CloseUI() { m_ui?.Close(); m_ui = null; } } public class ReviverStateMachine : StateMachine { protected override IState CreateInitialState() { return new ReviverIdleState(((Component)this).GetComponent()); } private void Update() { Tick(); } } public interface IState { void Enter(); IState? Tick(); void Exit(); } public abstract class StateMachine : MonoBehaviour { public IState? Current { get; private set; } protected abstract IState CreateInitialState(); protected virtual void Awake() { Change(CreateInitialState()); } private void Change(IState? next) { if (Current != next) { Current?.Exit(); Current = next; Current?.Enter(); } } protected void Tick() { IState state = Current?.Tick(); if (state != null && state != Current) { Change(state); } } } } namespace ReviveAllies.Components.States { public sealed class AliveState : IState { private readonly Player _p; public AliveState(Player p) { _p = p; } public void Enter() { ((Component)_p).GetComponent().Reset(); } public IState? Tick() { return _p.IsDowned() ? new WaitingState(_p) : null; } public void Exit() { } } public sealed class GivingUpState : IState { private readonly Player _p; private readonly GiveUp _giveUp; public GivingUpState(Player p) { _p = p; _giveUp = ((Component)p).GetComponent(); } public void Enter() { _giveUp.ShowUI(); } public void Exit() { } public IState? Tick() { if (!_p.IsDowned()) { return new AliveState(_p); } if (!_giveUp.Held()) { return new WaitingState(_p); } if (_giveUp.Channel(Time.deltaTime)) { DownedStateMachineView downedStateMachineView = _p.State(); downedStateMachineView.DownedTime = (float)ZNet.instance.GetTimeSeconds() - Plugin.ReviveWindow - 1f; if (((Character)_p).GetHealth() > 0f) { ((Character)_p).SetHealth(0f); } _giveUp.Reset(); Plugin.Logger.LogInfo((object)(_p.GetPlayerName() + " gave up")); } return null; } } public sealed class RevivingState : IState { private readonly Player _p; private readonly GiveUp _giveUp; private readonly ChannelSignal _channel; public RevivingState(Player p) { _p = p; _giveUp = ((Component)p).GetComponent(); _channel = ((Component)p).GetComponent(); } public void Enter() { //IL_002b: Unknown result type (might be due to invalid IL or missing references) ReviveChannelDecayingProgress reviveChannelDecayingProgress = Timer(); reviveChannelDecayingProgress?.Begin(_channel.LastChanneler); if ((Object)(object)reviveChannelDecayingProgress != (Object)null) { ProgressUI.Create(reviveChannelDecayingProgress, DownedMarker.ReviveGreen, isGiveUp: false); } } public void Exit() { Timer()?.End(); } public IState? Tick() { if (!_p.IsDowned()) { return new AliveState(_p); } if (_giveUp.Held()) { return new GivingUpState(_p); } if (!_channel.IsChanneling) { return new WaitingState(_p); } return null; } private ReviveChannelDecayingProgress? Timer() { GameObject? obj = _p.FindDownedMarker(); return (obj != null) ? obj.GetComponent() : null; } } public sealed class WaitingState : IState { private readonly Player _p; private readonly GiveUp _giveUp; private readonly ChannelSignal _channel; public WaitingState(Player p) { _p = p; _giveUp = ((Component)p).GetComponent(); _channel = ((Component)p).GetComponent(); } public void Enter() { } public void Exit() { } public IState? Tick() { if (!_p.IsDowned()) { return new AliveState(_p); } if (_giveUp.Held()) { return new GivingUpState(_p); } if (_channel.IsChanneling) { return new RevivingState(_p); } if (_p.IsReviveWindowExpired() && ((Character)_p).GetHealth() > 0f) { ((Character)_p).SetHealth(0f); } _giveUp.Decay(Time.deltaTime); return null; } } public sealed class ReviverChannelingState : IState { private readonly Player _p; private readonly ReviveRequest _req; public ReviverChannelingState(Player p) { _p = p; _req = ((Component)p).GetComponent(); } public void Enter() { _req.SendBegin(); } public void Exit() { _req.SendEnd(); _req.CloseUI(); } public IState? Tick() { if (!_req.WantsToChannel || (Object)(object)_req.Target == (Object)null) { return new ReviverIdleState(_p); } _req.ShowUI(); return null; } } public sealed class ReviverIdleState : IState { private readonly Player _p; private readonly ReviveRequest _req; public ReviverIdleState(Player p) { _p = p; _req = ((Component)p).GetComponent(); } public void Enter() { } public void Exit() { } public IState? Tick() { return (_req.WantsToChannel && (Object)(object)_req.Target != (Object)null) ? new ReviverChannelingState(_p) : null; } } } namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] internal sealed class IgnoresAccessChecksToAttribute : Attribute { public IgnoresAccessChecksToAttribute(string assemblyName) { } } }