Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of AttackRescue v1.1.0
BepInEx/plugins/AttackRescue/AttackRescue.dll
Decompiled 2 weeks agousing System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using UnityEngine; using UnityEngine.UI; [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: CompilationRelaxations(8)] [assembly: AssemblyVersion("0.0.0.0")] namespace AttackRescue { internal static class CaptainCompatibility { internal const string Id = "CaptainSkillTree.SkillTreeMod"; private static MethodInfo deathFallback; internal static void Install(Harmony harmony) { //IL_010f: Unknown result type (might be due to invalid IL or missing references) //IL_011d: Expected O, but got Unknown //IL_0131: Unknown result type (might be due to invalid IL or missing references) //IL_013f: Expected O, but got Unknown deathFallback = null; if (Chainloader.PluginInfos.ContainsKey("CaptainSkillTree.SkillTreeMod")) { Assembly assembly = ((object)Chainloader.PluginInfos["CaptainSkillTree.SkillTreeMod"].Instance).GetType().Assembly; Type type = assembly.GetType("CaptainSkillTree.SkillTree.BerserkerSkills"); Type type2 = assembly.GetType("CaptainSkillTree.SkillTree.BerserkerSkills+Berserker_Player_OnDeath_Invincibility_Patch"); MethodInfo methodInfo = ((type2 == null) ? null : AccessTools.Method(type2, "Prefix", new Type[1] { typeof(Player) }, (Type[])null)); MethodInfo methodInfo2 = ((type == null) ? null : AccessTools.Method(type, "CheckBerserkerPassiveSkill", new Type[2] { typeof(Player), typeof(bool) }, (Type[])null)); if (methodInfo == null || methodInfo.ReturnType != typeof(bool) || !methodInfo.IsStatic || methodInfo2 == null) { throw new InvalidOperationException("Unsupported CaptainSkillTree berserker API; compatibility could not be installed."); } harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(CaptainCompatibility), "DeathFallbackPrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)methodInfo2, new HarmonyMethod(typeof(CaptainCompatibility), "PassiveCheckPrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); deathFallback = methodInfo; Plugin.Log.LogInfo((object)"CaptainSkillTree berserker compatibility enabled."); } } private static bool Suppress(Player player) { Plugin instance = Plugin.Instance; if (!Object.op_Implicit((Object)(object)instance) || !instance.Active || (Object)(object)player != (Object)(object)Player.m_localPlayer) { return false; } RescueState rescueState = instance.Net.For(player); if (!instance.ForceDeath && !Plugin.Downed(player)) { if (rescueState != null) { return rescueState.Phase == RescuePhase.Dead; } return false; } return true; } private static bool DeathFallbackPrefix(Player __0, ref bool __result) { if (!Suppress(__0)) { return true; } __result = true; return false; } private static bool PassiveCheckPrefix(Player __0) { return !Suppress(__0); } internal static bool TryPreventDeath(Player player) { if (deathFallback == null || Suppress(player)) { return false; } try { return !(bool)deathFallback.Invoke(null, new object[1] { player }); } catch (Exception ex) { Plugin.Log.LogError((object)("CaptainSkillTree death fallback failed; continuing rescue handling: " + ex)); return false; } } } internal static class DownedInputGuard { [ThreadStatic] private static int rescueInputScope; internal static bool ReadSurrender() { int num = rescueInputScope; rescueInputScope++; try { return ZInput.GetButton("Use") || ZInput.GetButton("JoyUse"); } finally { rescueInputScope = num; } } internal static void Install(Harmony harmony) { //IL_0027: 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_0049: Expected O, but got Unknown //IL_0049: Expected O, but got Unknown harmony.Patch((MethodBase)AccessTools.Method(typeof(ZInput), "InternalUpdate", (Type[])null, (Type[])null), new HarmonyMethod(typeof(DownedInputGuard), "EnterRescueInput", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(DownedInputGuard), "LeaveRescueInput", (Type[])null), (HarmonyMethod)null); string[] array = new string[3] { "GetKey", "GetKeyDown", "GetKeyUp" }; foreach (string text in array) { harmony.Patch((MethodBase)AccessTools.Method(typeof(Input), text, new Type[1] { typeof(KeyCode) }, (Type[])null), Hook("KeyCodePrefix"), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)AccessTools.Method(typeof(Input), text, new Type[1] { typeof(string) }, (Type[])null), Hook("KeyNamePrefix"), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)AccessTools.Method(typeof(ZInput), text, new Type[2] { typeof(KeyCode), typeof(bool) }, (Type[])null), Hook("KeyCodePrefix"), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } string[] array2 = new string[3] { "GetButton", "GetButtonDown", "GetButtonUp" }; foreach (string text2 in array2) { harmony.Patch((MethodBase)AccessTools.Method(typeof(ZInput), text2, new Type[1] { typeof(string) }, (Type[])null), Hook("ButtonPrefix"), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } harmony.Patch((MethodBase)AccessTools.Method(typeof(Humanoid), "DropItem", (Type[])null, (Type[])null), Hook("DropPrefix"), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } private static HarmonyMethod Hook(string method) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Expected O, but got Unknown HarmonyMethod val = new HarmonyMethod(typeof(DownedInputGuard), method, (Type[])null); val.priority = 800; return val; } private static bool LocalLocked() { return Plugin.Locked(Player.m_localPlayer); } private static void EnterRescueInput(out int __state) { __state = rescueInputScope; rescueInputScope++; } private static Exception LeaveRescueInput(Exception __exception, int __state) { rescueInputScope = __state; return __exception; } private static bool KeyCodePrefix(KeyCode __0, ref bool __result) { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Invalid comparison between Unknown and I4 //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Invalid comparison between Unknown and I4 if (!LocalLocked() || rescueInputScope > 0 || (int)__0 == 27 || (int)__0 == 9) { return true; } __result = false; return false; } private static bool KeyNamePrefix(string __0, ref bool __result) { if (!LocalLocked() || rescueInputScope > 0 || InputPolicy.AllowedKeyName(__0)) { return true; } __result = false; return false; } private static bool ButtonPrefix(string __0, ref bool __result) { if (!LocalLocked() || rescueInputScope > 0 || InputPolicy.AllowedButton(__0)) { return true; } __result = false; return false; } private static bool DropPrefix(Humanoid __instance, ref bool __result) { Player val = (Player)(object)((__instance is Player) ? __instance : null); if (!Object.op_Implicit((Object)(object)val) || !Plugin.Locked(val)) { return true; } __result = false; return false; } } } internal static class InputPolicy { internal static bool AllowedKeyName(string key) { if (!string.Equals(key, "escape", StringComparison.OrdinalIgnoreCase)) { return string.Equals(key, "tab", StringComparison.OrdinalIgnoreCase); } return true; } internal static bool AllowedButton(string button) { switch (button) { default: return button == "JoyMenu"; case "Inventory": case "JoyInventory": case "Menu": return true; } } } namespace AttackRescue { internal sealed class HitContext { internal Player Attacker; internal string Token; internal bool Ranged; internal readonly HashSet<long> Targets = new HashSet<long>(); } internal static class DirectHit { [ThreadStatic] internal static HitContext Current; internal static readonly ConditionalWeakTable<Projectile, HitContext> Projectiles = new ConditionalWeakTable<Projectile, HitContext>(); internal static bool RescuePair(Character attacker, Character target) { if (Current != null && (Object)(object)attacker == (Object)(object)Current.Attacker && (Object)(object)attacker != (Object)(object)target && (Object)(object)attacker == (Object)(object)Player.m_localPlayer) { return Plugin.Downed((Player)(object)((target is Player) ? target : null)); } return false; } } [HarmonyPatch(typeof(ZNet), "OnNewConnection")] internal static class ConnectPatch { private static void Prefix(ZNetPeer peer) { Plugin.Instance.Net.NewConnection(peer); } private static void Postfix(ZNetPeer peer) { Plugin.Instance.Net.SendHello(peer); } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] internal static class VersionPatch { private static bool Prefix(ZRpc rpc, ZPackage pkg) { return Plugin.Instance.Net.GatePeerInfo(rpc, pkg); } } [HarmonyPatch(typeof(Character), "CheckDeath")] internal static class CheckDeathPatch { [HarmonyPriority(800)] private static bool Prefix(Character __instance) { Player val = (Player)(object)((__instance is Player) ? __instance : null); if (!Object.op_Implicit((Object)(object)val) || (Object)(object)val != (Object)(object)Player.m_localPlayer || __instance.GetHealth() > 0f || ((Character)val).IsDead()) { return true; } return Plugin.Instance.InterceptDeath(val); } } [HarmonyPatch(typeof(Player), "OnDeath")] internal static class DeathPatch { [HarmonyPriority(800)] private static bool Prefix(Player __instance) { return Plugin.Instance.InterceptDeath(__instance); } } [HarmonyPatch(typeof(Character), "ApplyDamage")] internal static class DotGuardPatch { [HarmonyPriority(800)] private static bool Prefix(Character __instance) { return !Plugin.Protected((Player)(object)((__instance is Player) ? __instance : null)); } } [HarmonyPatch(typeof(Character), "RPC_Damage")] internal static class DamageGuardPatch { [HarmonyPriority(800)] private static bool Prefix(Character __instance) { return !Plugin.Protected((Player)(object)((__instance is Player) ? __instance : null)); } } [HarmonyPatch(typeof(Character), "Damage")] internal static class RescueHitPatch { [HarmonyPriority(800)] private static bool Prefix(Character __instance, HitData hit) { //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Invalid comparison between Unknown and I4 //IL_007d: Unknown result type (might be due to invalid IL or missing references) Player val = (Player)(object)((__instance is Player) ? __instance : null); if (!Plugin.Downed(val)) { return true; } HitContext current = DirectHit.Current; if (current != null && Object.op_Implicit((Object)(object)current.Attacker) && (Object)(object)current.Attacker == (Object)(object)Player.m_localPlayer && (Object)(object)current.Attacker != (Object)(object)val && !Plugin.Locked(current.Attacker) && (Object)(object)hit.GetAttacker() == (Object)(object)current.Attacker && ((int)hit.m_hitType == 2 || (int)hit.m_hitType == 0)) { RescueState rescueState = Plugin.Instance.Net.For(val); if (rescueState != null && current.Targets.Add(val.GetPlayerID())) { Plugin.Instance.Net.Send(Request.Hit, val.GetPlayerID(), rescueState.Episode, current.Token, hit.GetTotalDamage(), current.Ranged); } } return false; } } [HarmonyPatch] internal static class MeleeScopePatch { private static IEnumerable<MethodBase> TargetMethods() { yield return AccessTools.Method(typeof(Attack), "DoMeleeAttack", (Type[])null, (Type[])null); yield return AccessTools.Method(typeof(Attack), "DoAreaAttack", (Type[])null, (Type[])null); } private static void Prefix(Humanoid ___m_character, out HitContext __state) { __state = DirectHit.Current; HitContext hitContext = new HitContext(); hitContext.Attacker = (Player)(object)((___m_character is Player) ? ___m_character : null); hitContext.Token = Guid.NewGuid().ToString("N"); DirectHit.Current = hitContext; } private static Exception Finalizer(Exception __exception, HitContext __state) { DirectHit.Current = __state; return __exception; } } [HarmonyPatch(typeof(Projectile), "OnHit")] internal static class ProjectileScopePatch { private static void Prefix(Projectile __instance, Character ___m_owner, out HitContext __state) { __state = DirectHit.Current; HitContext value = DirectHit.Projectiles.GetValue(__instance, (Projectile p) => new HitContext { Token = Guid.NewGuid().ToString("N"), Ranged = true }); value.Attacker = (Player)(object)((___m_owner is Player) ? ___m_owner : null); DirectHit.Current = value; } private static Exception Finalizer(Exception __exception, HitContext __state) { DirectHit.Current = __state; return __exception; } } [HarmonyPatch(typeof(BaseAI), "IsEnemy", new Type[] { typeof(Character), typeof(Character) })] internal static class FriendlyTargetPatch { private static void Postfix(Character a, Character b, ref bool __result) { if (DirectHit.RescuePair(a, b)) { __result = true; } else if (!(a is Player) && Plugin.Downed((Player)(object)((b is Player) ? b : null))) { __result = false; } else if (!(b is Player) && Plugin.Downed((Player)(object)((a is Player) ? a : null))) { __result = false; } } } [HarmonyPatch(typeof(MonsterAI), "UpdateTarget")] internal static class DropDownedTargetPatch { private static void Prefix(MonsterAI __instance, ref Character ___m_targetCreature, ref float ___m_updateTargetTimer, ref bool ___m_beenAtLastPos) { Character obj = ___m_targetCreature; if (Plugin.Downed((Player)(object)((obj is Player) ? obj : null))) { ___m_targetCreature = null; ___m_updateTargetTimer = 0f; ___m_beenAtLastPos = true; AccessTools.Method(typeof(BaseAI), "SetAlerted", (Type[])null, (Type[])null).Invoke(__instance, new object[1] { false }); } } } [HarmonyPatch(typeof(Player), "SetControls")] internal static class ControlsPatch { private static void Prefix(Player __instance, ref Vector3 movedir, ref bool attack, ref bool attackHold, ref bool secondaryAttack, ref bool secondaryAttackHold, ref bool block, ref bool blockHold, ref bool jump, ref bool crouch, ref bool run, ref bool autoRun, ref bool dodge) { //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) if (Plugin.Locked(__instance)) { movedir = Vector3.zero; attack = (attackHold = (secondaryAttack = (secondaryAttackHold = (block = (blockHold = (jump = (crouch = (run = (autoRun = (dodge = false)))))))))); } } } [HarmonyPatch(typeof(Character), "CustomFixedUpdate")] internal static class MovementPatch { private static void Postfix(Character __instance) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) if (Plugin.Locked((Player)(object)((__instance is Player) ? __instance : null))) { __instance.SetMoveDir(Vector3.zero); __instance.StopMovement(); } } } [HarmonyPatch] internal static class BlockActionsPatch { private static IEnumerable<MethodBase> TargetMethods() { yield return AccessTools.Method(typeof(Humanoid), "UseItem", (Type[])null, (Type[])null); yield return AccessTools.Method(typeof(Player), "Interact", (Type[])null, (Type[])null); yield return AccessTools.Method(typeof(Player), "PlacePiece", (Type[])null, (Type[])null); yield return AccessTools.Method(typeof(Player), "Dodge", (Type[])null, (Type[])null); } private static bool Prefix(Humanoid __instance) { return !Plugin.Locked((Player)(object)((__instance is Player) ? __instance : null)); } } [HarmonyPatch] internal static class BlockBooleanActionsPatch { private static IEnumerable<MethodBase> TargetMethods() { yield return AccessTools.Method(typeof(Humanoid), "StartAttack", (Type[])null, (Type[])null); yield return AccessTools.Method(typeof(Player), "ConsumeItem", (Type[])null, (Type[])null); yield return AccessTools.Method(typeof(Player), "TeleportTo", (Type[])null, (Type[])null); yield return AccessTools.Method(typeof(Player), "RemovePiece", (Type[])null, (Type[])null); yield return AccessTools.Method(typeof(Player), "TryPlacePiece", (Type[])null, (Type[])null); } private static bool Prefix(Humanoid __instance, ref bool __result) { if (!Plugin.Locked((Player)(object)((__instance is Player) ? __instance : null))) { return true; } __result = false; return false; } } [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInPlugin("local.valheim.attackrescue", "Attack Rescue", "1.0.5")] [BepInDependency(/*Could not decode attribute arguments.*/)] public sealed class Plugin : BaseUnityPlugin { public const string Id = "local.valheim.attackrescue"; public const string Version = "1.0.5"; internal static Plugin Instance; internal static ManualLogSource Log; internal RescueNetwork Net; internal bool Active; internal bool PendingDown; internal bool ForceDeath; internal string Notice; internal ConfigEntry<string> Language; internal ConfigEntry<float> UIScale; private ConfigEntry<float> resetSeconds; private ConfigEntry<float> deathSeconds; private ConfigEntry<float> refillDelaySeconds; private ConfigEntry<float> refillDurationSeconds; private ConfigEntry<int> hitsPerBar; private Harmony harmony; private RescueHud hud; private bool downedPose; private bool deathAcknowledged; private bool mustRelease; private float holdTime; private static readonly FieldInfo DeadField = AccessTools.Field(typeof(Character), "m_isDead"); private static readonly FieldInfo AttackField = AccessTools.Field(typeof(Humanoid), "m_currentAttack"); internal float HoldProgress => holdTime / 3f; private void Awake() { //IL_011b: Unknown result type (might be due to invalid IL or missing references) //IL_0125: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; resetSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Server", "ResetAfterReviveSeconds", 600f, "Seconds alive after rescue before the next fall returns to one segment. Server authoritative; positive."); deathSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Server", "DownedDeathSeconds", 120f, "Seconds until a fallen player dies, including time offline. Server authoritative; positive."); hitsPerBar = ((BaseUnityPlugin)this).Config.Bind<int>("Server", "ReviveHitsPerBar", 3, "Effective direct player hits required per rescue segment. Positive integer; server authoritative."); refillDelaySeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Server", "ReviveRefillDelaySeconds", 5f, "Seconds without an effective hit before ALL rescue segments refill. Positive; does not reset the death deadline."); refillDurationSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Server", "ReviveRefillDurationSeconds", 3f, "Time to refill an empty rescue gauge after the delay. Positive; server authoritative."); Language = ((BaseUnityPlugin)this).Config.Bind<string>("Client", "Language", "Auto", "Auto, Chinese, English, Russian, Portuguese_Brazilian, Spanish."); UIScale = ((BaseUnityPlugin)this).Config.Bind<float>("Client", "UIScale", 1f, new ConfigDescription("Rescue UI scale, multiplied by game GUI scale.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 2f), new object[0])); resetSeconds.SettingChanged += ValidateConfig; deathSeconds.SettingChanged += ValidateConfig; hitsPerBar.SettingChanged += ValidateConfig; refillDelaySeconds.SettingChanged += ValidateConfig; refillDurationSeconds.SettingChanged += ValidateConfig; ValidateConfig(null, EventArgs.Empty); Net = new RescueNetwork(); } private void Start() { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Expected O, but got Unknown if (Chainloader.PluginInfos.ContainsKey("balrond.astafaraios.BalrondSecondChance")) { Notice = "conflict"; ((BaseUnityPlugin)this).Logger.LogError((object)"Attack Rescue disabled: remove BalrondSecondChance from this profile, then restart."); return; } try { harmony = new Harmony("local.valheim.attackrescue"); harmony.PatchAll(typeof(Plugin).Assembly); DownedInputGuard.Install(harmony); CaptainCompatibility.Install(harmony); Active = true; hud = ((Component)this).gameObject.AddComponent<RescueHud>(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Attack Rescue 1.0.5 ready. Server and all clients must install the same version."); } catch (Exception ex) { if (harmony != null) { harmony.UnpatchSelf(); } Active = false; Notice = "error"; ((BaseUnityPlugin)this).Logger.LogError((object)("Patch installation failed: " + ex)); } } private void ValidateConfig(object sender, EventArgs args) { Validate(resetSeconds, 600f); Validate(deathSeconds, 120f); Validate(refillDelaySeconds, 5f); Validate(refillDurationSeconds, 3f); if (hitsPerBar.Value <= 0) { ((BaseUnityPlugin)this).Logger.LogWarning((object)"ReviveHitsPerBar must be a positive integer; using 3"); hitsPerBar.Value = 3; } } private void Validate(ConfigEntry<float> entry, float fallback) { if (!(RescueRules.Positive(entry.Value, -1.0) > 0.0)) { ((BaseUnityPlugin)this).Logger.LogWarning((object)(((ConfigEntryBase)entry).Definition.Key + " must be finite and positive; using " + fallback)); entry.Value = fallback; } } internal RescueRules Rules() { RescueRules rescueRules = new RescueRules(); rescueRules.ResetSeconds = resetSeconds.Value; rescueRules.DeathSeconds = deathSeconds.Value; rescueRules.HitsPerBar = hitsPerBar.Value; rescueRules.RefillDelaySeconds = refillDelaySeconds.Value; rescueRules.RefillDurationSeconds = refillDurationSeconds.Value; return rescueRules; } internal string T(string key, params object[] args) { string text = Language.Value; if (string.Equals(text, "Auto", StringComparison.OrdinalIgnoreCase)) { text = ((Localization.instance == null) ? "English" : Localization.instance.GetSelectedLanguage()); } return Texts.Get(text, key, args); } internal void ResetLocal() { PendingDown = false; downedPose = false; deathAcknowledged = false; holdTime = 0f; mustRelease = true; } internal static bool Downed(Player p) { if (!Object.op_Implicit((Object)(object)Instance) || !Instance.Active || !Object.op_Implicit((Object)(object)p)) { return false; } RescueState rescueState = Instance.Net.For(p); if (!((Object)(object)p == (Object)(object)Player.m_localPlayer) || !Instance.PendingDown) { if (rescueState != null) { return rescueState.Phase == RescuePhase.Downed; } return false; } return true; } internal static bool Locked(Player p) { if (Object.op_Implicit((Object)(object)p) && Object.op_Implicit((Object)(object)Instance) && Instance.Active && (Object)(object)p == (Object)(object)Player.m_localPlayer && !((Character)p).IsDead()) { if (Instance.Net.Bound && !Instance.Net.Failed && !Instance.PendingDown) { if (Instance.Net.For(p) != null) { return Instance.Net.For(p).Phase != RescuePhase.Alive; } return false; } return true; } return false; } internal static bool Protected(Player p) { if (!Object.op_Implicit((Object)(object)p) || !Object.op_Implicit((Object)(object)Instance) || !Instance.Active || Instance.ForceDeath) { return false; } RescueState rescueState = Instance.Net.For(p); if (!Locked(p)) { if (rescueState != null) { return rescueState.ImmuneUntil > Instance.Net.Now; } return false; } return true; } internal bool InterceptDeath(Player p) { if (!Active || ForceDeath || (Object)(object)p != (Object)(object)Player.m_localPlayer) { return true; } RescueState rescueState = Net.For(p); if (Net.Bound && !Net.Failed && !PendingDown && rescueState != null && rescueState.Phase == RescuePhase.Alive && ((Character)p).GetHealth() <= 0f && CaptainCompatibility.TryPreventDeath(p)) { ((Character)p).SetHealth(Mathf.Max(1f, ((Character)p).GetHealth())); DeadField.SetValue(p, false); return false; } ((Character)p).SetHealth(1f); DeadField.SetValue(p, false); if (!Net.Bound || Net.Failed) { return false; } RescueState rescueState2 = Net.For(p); if (!PendingDown && rescueState2 != null && rescueState2.Phase == RescuePhase.Alive && rescueState2.ImmuneUntil <= Net.Now) { PendingDown = true; Net.Send(Request.Down); } return false; } internal void StateChanged(RescueState before, RescueState after) { PendingDown = false; Player localPlayer = Player.m_localPlayer; if (Object.op_Implicit((Object)(object)localPlayer) && !((Character)localPlayer).IsDead()) { if (after.Phase == RescuePhase.Alive && before != null && before.Phase == RescuePhase.Downed) { ((Character)localPlayer).SetHealth(Mathf.Max(1f, ((Character)localPlayer).GetMaxHealth() * 0.3f)); ((Character)localPlayer).Message((MessageType)2, T("revived"), 0, (Sprite)null, false); StopDownedPose(localPlayer); } if (before != null && before.Bars > 0 && after.Bars == 0 && after.Phase == RescuePhase.Alive) { ((Character)localPlayer).Message((MessageType)1, T("reset"), 0, (Sprite)null, false); } } } private void Update() { //IL_0195: Unknown result type (might be due to invalid IL or missing references) //IL_0160: 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_00ab: Expected O, but got Unknown if (!Active) { return; } Net.Tick(); Player localPlayer = Player.m_localPlayer; if (!Object.op_Implicit((Object)(object)localPlayer) || !Net.Bound || Net.Failed) { return; } RescueState rescueState = Net.For(localPlayer); if (rescueState == null) { return; } if (rescueState.Phase == RescuePhase.Dead) { if (!((Character)localPlayer).IsDead()) { StopDownedPose(localPlayer); ForceDeath = true; try { if (AccessTools.Field(typeof(Character), "m_lastHit").GetValue(localPlayer) == null) { AccessTools.Field(typeof(Character), "m_lastHit").SetValue(localPlayer, (object?)new HitData()); } ((Character)localPlayer).SetHealth(0f); DeadField.SetValue(localPlayer, true); ((Character)localPlayer).OnDeath(); } finally { ForceDeath = false; } } if (((Character)localPlayer).IsDead() && !deathAcknowledged) { deathAcknowledged = true; Net.Send(Request.DeathAck); } } else if (Downed(localPlayer)) { if (!downedPose) { ((Character)localPlayer).AttachStop(); localPlayer.StopDoodadControl(); object? value = AttackField.GetValue(localPlayer); Attack val = (Attack)((value is Attack) ? value : null); if (val != null) { AccessTools.Method(typeof(Attack), "Stop", (Type[])null, (Type[])null).Invoke(val, null); AttackField.SetValue(localPlayer, null); } StopDownedPose(localPlayer); ((Character)localPlayer).SetMoveDir(Vector3.zero); ((Character)localPlayer).StopMovement(); downedPose = localPlayer.StartEmote("rest", true); mustRelease = true; } ((Character)localPlayer).SetHealth(1f); ((Character)localPlayer).SetMoveDir(Vector3.zero); ((Character)localPlayer).StopMovement(); if (!DownedInputGuard.ReadSurrender()) { mustRelease = false; holdTime = 0f; } else if (!mustRelease && !Menu.IsVisible() && !InventoryGui.IsVisible() && (!Object.op_Implicit((Object)(object)Chat.instance) || !Chat.instance.HasFocus())) { holdTime += Time.unscaledDeltaTime; if (holdTime >= 3f) { Net.Send(Request.GiveUp); mustRelease = true; holdTime = 0f; } } else { holdTime = 0f; } } else if (downedPose) { StopDownedPose(localPlayer); } } private void StopDownedPose(Player p) { //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Expected O, but got Unknown //IL_00d8: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Invalid comparison between Unknown and I4 //IL_00ee: Unknown result type (might be due to invalid IL or missing references) //IL_00f5: Invalid comparison between Unknown and I4 ZNetView component = ((Component)p).GetComponent<ZNetView>(); if (Object.op_Implicit((Object)(object)component) && component.IsValid()) { ZDO zDO = component.GetZDO(); zDO.Set(ZDOVars.s_emote, ""); zDO.Set(ZDOVars.s_emoteOneshot, false); zDO.Set(ZDOVars.s_emoteID, zDO.GetInt(ZDOVars.s_emoteID, 0) + 1, false); AccessTools.Method(typeof(Player), "UpdateEmote", (Type[])null, (Type[])null).Invoke(p, null); Animator val = (Animator)AccessTools.Field(typeof(Character), "m_animator").GetValue(p); if (Object.op_Implicit((Object)(object)val)) { AnimatorControllerParameter[] parameters = val.parameters; foreach (AnimatorControllerParameter val2 in parameters) { if (!(val2.name != "emote_sit") || !(val2.name != "emote_rest")) { if ((int)val2.type == 4) { val.SetBool(val2.nameHash, false); } if ((int)val2.type == 9) { val.ResetTrigger(val2.nameHash); } } } val.SetTrigger("emote_stop"); } } downedPose = false; holdTime = 0f; } private void OnDestroy() { Active = false; if (harmony != null) { harmony.UnpatchSelf(); } if (Object.op_Implicit((Object)(object)hud)) { Object.Destroy((Object)(object)hud); } if ((Object)(object)Instance == (Object)(object)this) { Instance = null; } } private void OnGUI() { //IL_002b: Unknown result type (might be due to invalid IL or missing references) if (Notice != null) { GUI.Box(new Rect(20f, 20f, (float)Mathf.Min(Screen.width - 40, 850), 65f), T(Notice)); } } } internal sealed class RescueHud : MonoBehaviour { private sealed class Gauge { internal RectTransform Root; internal Image[] Fill = (Image[])(object)new Image[3]; internal Image[] Back = (Image[])(object)new Image[3]; internal Image Timer; internal Text Name; } private Canvas canvas; private RectTransform root; private Font font; private Sprite segment; private Sprite ring; private readonly List<Object> assets = new List<Object>(); private readonly Dictionary<long, Gauge> gauges = new Dictionary<long, Gauge>(); private Gauge self; private Text status; private Text help; private Text timer; private Text giveup; private Image hold; private GameObject panel; private float nextCleanup; private void Awake() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Invalid comparison between Unknown and I4 //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Expected O, but got Unknown //IL_00f1: Unknown result type (might be due to invalid IL or missing references) //IL_0157: Unknown result type (might be due to invalid IL or missing references) //IL_015c: Unknown result type (might be due to invalid IL or missing references) //IL_015d: Unknown result type (might be due to invalid IL or missing references) //IL_0164: Unknown result type (might be due to invalid IL or missing references) //IL_019f: Unknown result type (might be due to invalid IL or missing references) //IL_01a9: Expected O, but got Unknown //IL_01d8: Unknown result type (might be due to invalid IL or missing references) //IL_01dd: Unknown result type (might be due to invalid IL or missing references) //IL_01de: Unknown result type (might be due to invalid IL or missing references) //IL_01e5: Unknown result type (might be due to invalid IL or missing references) //IL_01f7: Unknown result type (might be due to invalid IL or missing references) //IL_0210: Unknown result type (might be due to invalid IL or missing references) //IL_021f: Unknown result type (might be due to invalid IL or missing references) //IL_023d: Unknown result type (might be due to invalid IL or missing references) //IL_024c: Unknown result type (might be due to invalid IL or missing references) //IL_026a: Unknown result type (might be due to invalid IL or missing references) //IL_0279: Unknown result type (might be due to invalid IL or missing references) //IL_0297: Unknown result type (might be due to invalid IL or missing references) //IL_02a6: Unknown result type (might be due to invalid IL or missing references) //IL_02cd: Unknown result type (might be due to invalid IL or missing references) //IL_02f1: Unknown result type (might be due to invalid IL or missing references) //IL_0310: Unknown result type (might be due to invalid IL or missing references) if ((int)SystemInfo.graphicsDeviceType == 4) { ((Behaviour)this).enabled = false; return; } font = Font.CreateDynamicFontFromOSFont(new string[4] { "Microsoft YaHei", "Noto Sans CJK SC", "Arial", "Liberation Sans" }, 22); assets.Add((Object)(object)font); GameObject val = new GameObject("AttackRescueHUD", new Type[3] { typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler) }); val.transform.SetParent(((Component)this).transform, false); canvas = val.GetComponent<Canvas>(); canvas.renderMode = (RenderMode)0; canvas.sortingOrder = 100; root = val.GetComponent<RectTransform>(); CanvasScaler component = val.GetComponent<CanvasScaler>(); component.uiScaleMode = (ScaleMode)1; component.referenceResolution = new Vector2(1920f, 1080f); component.matchWidthOrHeight = 0.5f; segment = MakeSprite(outer: false); ring = MakeSprite(outer: true); self = CreateGauge((Transform)(object)root, 142f); RectTransform obj = self.Root; Vector2 anchorMin = (self.Root.anchorMax = new Vector2(0.5f, 0.28f)); obj.anchorMin = anchorMin; ((Component)self.Name).gameObject.SetActive(false); panel = new GameObject("RescueText", new Type[1] { typeof(RectTransform) }); panel.transform.SetParent((Transform)(object)root, false); RectTransform component2 = panel.GetComponent<RectTransform>(); Vector2 anchorMin2 = (component2.anchorMax = new Vector2(0.5f, 0.28f)); component2.anchorMin = anchorMin2; component2.sizeDelta = new Vector2(900f, 300f); status = Label((Transform)(object)component2, 24, new Vector2(0f, 98f), new Vector2(880f, 36f)); help = Label((Transform)(object)component2, 21, new Vector2(0f, -100f), new Vector2(880f, 35f)); timer = Label((Transform)(object)component2, 20, new Vector2(0f, -136f), new Vector2(880f, 32f)); giveup = Label((Transform)(object)component2, 19, new Vector2(0f, -176f), new Vector2(880f, 42f)); hold = Picture((Transform)(object)component2, "GiveUpHold", null, new Color(0.75f, 0.66f, 0.91f)); ((Graphic)hold).rectTransform.anchoredPosition = new Vector2(0f, -205f); ((Graphic)hold).rectTransform.sizeDelta = new Vector2(0f, 3f); } private Sprite MakeSprite(bool outer) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Expected O, but got Unknown //IL_01a9: Unknown result type (might be due to invalid IL or missing references) //IL_01b8: Unknown result type (might be due to invalid IL or missing references) //IL_015d: Unknown result type (might be due to invalid IL or missing references) //IL_0162: Unknown result type (might be due to invalid IL or missing references) Texture2D val = new Texture2D(256, 256, (TextureFormat)4, false); ((Object)val).name = (outer ? "AR_Original_TimerRing" : "AR_Original_RescueSegment"); ((Texture)val).wrapMode = (TextureWrapMode)1; ((Texture)val).filterMode = (FilterMode)1; Color[] array = (Color[])(object)new Color[65536]; for (int i = 0; i < 256; i++) { for (int j = 0; j < 256; j++) { float num = ((float)j + 0.5f - 128f) / 128f; float num2 = ((float)i + 0.5f - 128f) / 128f; float num3 = Mathf.Sqrt(num * num + num2 * num2); float num4 = Mathf.Atan2(num, num2) * 57.29578f; if (num4 < 0f) { num4 += 360f; } float num5 = (outer ? 0.91f : 0.6f); float num6 = (outer ? 0.935f : 0.82f); float num7 = Mathf.Min(num3 - num5, num6 - num3); float num8 = Mathf.Clamp01(num7 * 128f + 0.5f); if (!outer) { num8 *= Mathf.Clamp01(Mathf.Min(num4 - 6f, 114f - num4) * 1.5f); } float num9 = (outer ? 1f : Mathf.Lerp(0.68f, 1f, Mathf.InverseLerp(num5, num6, num3))); ref Color reference = ref array[i * 256 + j]; reference = new Color(num9, num9, num9, num8); } } val.SetPixels(array); val.Apply(false, true); Sprite val2 = Sprite.Create(val, new Rect(0f, 0f, 256f, 256f), new Vector2(0.5f, 0.5f), 100f); assets.Add((Object)(object)val); assets.Add((Object)(object)val2); return val2; } private Image Picture(Transform parent, string name, Sprite sprite, Color color) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Expected O, but got Unknown //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_006f: 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) GameObject val = new GameObject(name, new Type[2] { typeof(RectTransform), typeof(Image) }); val.transform.SetParent(parent, false); Image component = val.GetComponent<Image>(); component.sprite = sprite; ((Graphic)component).color = color; ((Graphic)component).raycastTarget = false; RectTransform rectTransform = ((Graphic)component).rectTransform; Vector2 anchorMin = (((Graphic)component).rectTransform.anchorMax = new Vector2(0.5f, 0.5f)); rectTransform.anchorMin = anchorMin; return component; } private Text Label(Transform parent, int size, Vector2 position, Vector2 bounds) { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Expected O, but got Unknown //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00ca: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject("Label", new Type[3] { typeof(RectTransform), typeof(Text), typeof(Shadow) }); val.transform.SetParent(parent, false); Text component = val.GetComponent<Text>(); component.font = font; component.fontSize = size; component.alignment = (TextAnchor)4; ((Graphic)component).color = new Color(0.94f, 0.92f, 1f); ((Graphic)component).raycastTarget = false; component.supportRichText = false; component.horizontalOverflow = (HorizontalWrapMode)0; ((Graphic)component).rectTransform.anchoredPosition = position; ((Graphic)component).rectTransform.sizeDelta = bounds; val.GetComponent<Shadow>().effectColor = new Color(0f, 0f, 0f, 0.9f); return component; } private Gauge CreateGauge(Transform parent, float size) { //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Expected O, but got Unknown //IL_004f: 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_0096: Unknown result type (might be due to invalid IL or missing references) //IL_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_00e2: Unknown result type (might be due to invalid IL or missing references) //IL_0136: Unknown result type (might be due to invalid IL or missing references) //IL_0174: Unknown result type (might be due to invalid IL or missing references) //IL_0228: Unknown result type (might be due to invalid IL or missing references) //IL_0249: Unknown result type (might be due to invalid IL or missing references) //IL_0269: Unknown result type (might be due to invalid IL or missing references) //IL_0289: Unknown result type (might be due to invalid IL or missing references) //IL_0298: Unknown result type (might be due to invalid IL or missing references) //IL_01bc: Unknown result type (might be due to invalid IL or missing references) //IL_01dc: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject("RescueGauge", new Type[1] { typeof(RectTransform) }); val.transform.SetParent(parent, false); Gauge gauge = new Gauge(); gauge.Root = val.GetComponent<RectTransform>(); Gauge gauge2 = gauge; gauge2.Root.sizeDelta = new Vector2(size, size); Image val2 = Picture(val.transform, "TimerTrack", ring, new Color(0.16f, 0.14f, 0.22f, 0.95f)); ((Graphic)val2).rectTransform.sizeDelta = gauge2.Root.sizeDelta; gauge2.Timer = Picture(val.transform, "TimeRemaining", ring, new Color(0.85f, 0.77f, 1f)); ((Graphic)gauge2.Timer).rectTransform.sizeDelta = gauge2.Root.sizeDelta; Radial(gauge2.Timer); for (int i = 0; i < 3; i++) { gauge2.Back[i] = Picture(val.transform, "SegmentTrack" + i, segment, new Color(0.18f, 0.13f, 0.25f, 0.95f)); gauge2.Fill[i] = Picture(val.transform, "SegmentRemaining" + i, segment, new Color(0.66f, 0.35f, 0.96f)); Image[] array = (Image[])(object)new Image[2] { gauge2.Back[i], gauge2.Fill[i] }; foreach (Image val3 in array) { ((Graphic)val3).rectTransform.sizeDelta = gauge2.Root.sizeDelta; ((Transform)((Graphic)val3).rectTransform).localRotation = Quaternion.Euler(0f, 0f, (float)(-120 * i)); } Radial(gauge2.Fill[i]); } Image val4 = Picture(val.transform, "RescueRune", null, new Color(0.86f, 0.79f, 1f)); ((Graphic)val4).rectTransform.sizeDelta = new Vector2(size * 0.08f, size * 0.08f); ((Transform)((Graphic)val4).rectTransform).localRotation = Quaternion.Euler(0f, 0f, 45f); gauge2.Name = Label(val.transform, 17, new Vector2(0f, size * 0.62f), new Vector2(350f, 32f)); return gauge2; } private static void Radial(Image image) { image.type = (Type)3; image.fillMethod = (FillMethod)4; image.fillOrigin = 2; image.fillClockwise = true; } private void UpdateGauge(Gauge g, RescueState s) { //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) double num = Math.Max(0.0, s.Deadline - Plugin.Instance.Net.Now); g.Timer.fillAmount = (float)(num / s.Rules.DeathSeconds); ((Graphic)g.Timer).color = ((num <= 15.0) ? new Color(1f, 0.47f, 0.52f) : new Color(0.85f, 0.77f, 1f)); for (int i = 0; i < 3; i++) { ((Behaviour)g.Back[i]).enabled = i < s.Bars; ((Behaviour)g.Fill[i]).enabled = i < s.Bars; float num2 = 1f - Mathf.Clamp01((float)s.ProgressAt(Plugin.Instance.Net.Now) / (float)s.Rules.HitsPerBar - (float)(s.Bars - 1 - i)); g.Fill[i].fillAmount = ((num2 <= 0f) ? 0f : ((6f + 108f * num2) / 360f)); } } private void LateUpdate() { //IL_007a: 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_0095: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_01ec: Unknown result type (might be due to invalid IL or missing references) //IL_0286: Unknown result type (might be due to invalid IL or missing references) //IL_0330: Unknown result type (might be due to invalid IL or missing references) //IL_033c: Unknown result type (might be due to invalid IL or missing references) //IL_0359: Unknown result type (might be due to invalid IL or missing references) //IL_035e: Unknown result type (might be due to invalid IL or missing references) //IL_0368: Unknown result type (might be due to invalid IL or missing references) //IL_036d: Unknown result type (might be due to invalid IL or missing references) //IL_0372: Unknown result type (might be due to invalid IL or missing references) //IL_0377: Unknown result type (might be due to invalid IL or missing references) //IL_0412: Unknown result type (might be due to invalid IL or missing references) //IL_0414: Unknown result type (might be due to invalid IL or missing references) //IL_0429: Unknown result type (might be due to invalid IL or missing references) //IL_0437: Unknown result type (might be due to invalid IL or missing references) //IL_043d: Unknown result type (might be due to invalid IL or missing references) if (!Object.op_Implicit((Object)(object)canvas)) { return; } Plugin instance = Plugin.Instance; Player localPlayer = Player.m_localPlayer; ((Behaviour)canvas).enabled = Object.op_Implicit((Object)(object)localPlayer) && instance.Active; if (!((Behaviour)canvas).enabled) { return; } float num = instance.UIScale.Value * Mathf.Clamp(PlayerPrefs.GetFloat("GuiScale", 1f), 0.5f, 2f); ((Transform)self.Root).localScale = Vector3.one * num; panel.transform.localScale = Vector3.one * num; RescueState rescueState = instance.Net.For(localPlayer); bool flag = rescueState != null && rescueState.Phase == RescuePhase.Downed && !((Character)localPlayer).IsDead(); ((Component)self.Root).gameObject.SetActive(flag); panel.SetActive(flag || !instance.Net.Bound); if (flag) { UpdateGauge(self, rescueState); status.text = instance.T("down"); help.text = instance.T("help"); timer.text = instance.T("time", Math.Ceiling(Math.Max(0.0, rescueState.Deadline - instance.Net.Now))); giveup.text = instance.T("giveup", Localization.instance.Localize("$KEY_Use")); ((Graphic)hold).rectTransform.sizeDelta = new Vector2(260f * instance.HoldProgress, 3f); } else { status.text = instance.T(instance.Net.SyncDelayed ? "sync_delayed" : "sync"); help.text = (instance.Net.SyncDelayed ? instance.T("version") : ""); Text obj = timer; string text = (giveup.text = ""); obj.text = text; ((Graphic)hold).rectTransform.sizeDelta = Vector2.zero; } Camera mainCamera = Utils.GetMainCamera(); foreach (Gauge value2 in gauges.Values) { ((Component)value2.Root).gameObject.SetActive(false); } if (Object.op_Implicit((Object)(object)mainCamera)) { Vector2 anchoredPosition = default(Vector2); foreach (Player allPlayer in Player.GetAllPlayers()) { if (!Object.op_Implicit((Object)(object)allPlayer) || (Object)(object)allPlayer == (Object)(object)localPlayer || !Plugin.Downed(allPlayer) || Vector3.Distance(((Component)localPlayer).transform.position, ((Component)allPlayer).transform.position) > 100f) { continue; } Vector3 val = mainCamera.WorldToScreenPoint(((Component)allPlayer).transform.position + Vector3.up * 2.1f); if (!(val.z <= 0f) && !(val.x < 0f) && !(val.x > (float)Screen.width) && !(val.y < 0f) && !(val.y > (float)Screen.height)) { long playerID = allPlayer.GetPlayerID(); if (!gauges.TryGetValue(playerID, out var value)) { value = CreateGauge((Transform)(object)root, 72f); gauges.Add(playerID, value); } RectTransformUtility.ScreenPointToLocalPointInRectangle(root, Vector2.op_Implicit(val), (Camera)null, ref anchoredPosition); value.Root.anchoredPosition = anchoredPosition; ((Transform)value.Root).localScale = Vector3.one * num; ((Component)value.Root).gameObject.SetActive(true); value.Name.text = allPlayer.GetPlayerName(); UpdateGauge(value, instance.Net.For(allPlayer)); } } } if (!(Time.unscaledTime > nextCleanup)) { return; } nextCleanup = Time.unscaledTime + 10f; long[] array = gauges.Keys.ToArray(); foreach (long key in array) { if (!((Component)gauges[key].Root).gameObject.activeSelf) { Object.Destroy((Object)(object)((Component)gauges[key].Root).gameObject); gauges.Remove(key); } } } private void OnDestroy() { if (Object.op_Implicit((Object)(object)canvas)) { Object.Destroy((Object)(object)((Component)canvas).gameObject); } foreach (Object asset in assets) { if (Object.op_Implicit(asset)) { Object.Destroy(asset); } } } } internal enum Request { Bind, Down, Hit, GiveUp, DeathAck, ExternalDeath } internal sealed class Connection { internal ZNetPeer Peer; internal bool Verified; internal float Created = Time.realtimeSinceStartup; internal ZPackage PendingPeerInfo; internal long BoundId; internal ZDOID Character; internal long LastSequence; internal float NextBindDiagnostic; } internal sealed class RescueNetwork { private const string Hello = "AR_Hello_v1"; private const string Command = "AR_Command_v1"; private const string Snapshot = "AR_Snapshot_v1"; private const string Config = "AR_Config_v1"; internal readonly Dictionary<long, RescueState> States = new Dictionary<long, RescueState>(); private readonly Dictionary<ZRpc, Connection> connections = new Dictionary<ZRpc, Connection>(); private readonly Dictionary<long, RescueState> authoritative = new Dictionary<long, RescueState>(); private StateStore store; private ZNet currentNet; private Player boundPlayer; private long boundPlayerId; private long sequence; private double clockOffset; private float nextBind; private float nextTick; private float nextConfig; private float bindStarted; private float nextSyncDiagnostic; internal bool Bound; internal bool Failed; internal RescueRules EffectiveRules = new RescueRules(); internal bool SyncDelayed { get { if (Object.op_Implicit((Object)(object)boundPlayer) && !Bound) { return Time.realtimeSinceStartup - bindStarted >= 15f; } return false; } } internal static double UtcNow => (DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds; internal double Now => UtcNow + clockOffset; internal bool IsServer { get { if (Object.op_Implicit((Object)(object)ZNet.instance)) { return ZNet.instance.IsServer(); } return false; } } private void EnsureSession() { if (!((Object)(object)currentNet == (Object)(object)ZNet.instance)) { currentNet = ZNet.instance; connections.Clear(); authoritative.Clear(); States.Clear(); store = null; Bound = false; Failed = false; boundPlayer = null; boundPlayerId = 0L; clockOffset = 0.0; sequence = 0L; } } internal void NewConnection(ZNetPeer peer) { EnsureSession(); connections[peer.m_rpc] = new Connection { Peer = peer }; peer.m_rpc.Register<string>("AR_Hello_v1", (Action<ZRpc, string>)ReceiveHello); peer.m_rpc.Register<ZPackage>("AR_Command_v1", (Action<ZRpc, ZPackage>)ReceiveCommand); peer.m_rpc.Register<ZPackage>("AR_Snapshot_v1", (Action<ZRpc, ZPackage>)ReceiveSnapshot); peer.m_rpc.Register<ZPackage>("AR_Config_v1", (Action<ZRpc, ZPackage>)ReceiveConfig); } internal void SendHello(ZNetPeer peer) { peer.m_rpc.Invoke("AR_Hello_v1", new object[1] { "1.0.5" }); } private void ReceiveHello(ZRpc rpc, string version) { if (!connections.TryGetValue(rpc, out var value)) { return; } if (version != "1.0.5") { Reject(value, "Version mismatch: " + version); return; } value.Verified = true; Plugin.Log.LogInfo((object)("Rescue version handshake accepted: " + version)); if (value.PendingPeerInfo != null) { ZPackage pendingPeerInfo = value.PendingPeerInfo; value.PendingPeerInfo = null; AccessTools.Method(typeof(ZNet), "RPC_PeerInfo", (Type[])null, (Type[])null).Invoke(ZNet.instance, new object[2] { rpc, pendingPeerInfo }); } } internal bool GatePeerInfo(ZRpc rpc, ZPackage pkg) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Expected O, but got Unknown if (!connections.TryGetValue(rpc, out var value)) { return false; } if (value.Verified) { return true; } value.PendingPeerInfo = new ZPackage(pkg.GetArray()); return false; } private void Reject(Connection c, string reason) { Plugin.Log.LogError((object)("Attack Rescue connection rejected: " + reason)); if (!IsServer) { Failed = true; Plugin.Instance.Notice = "version"; } c.Peer.m_rpc.Invoke("Error", new object[1] { 3 }); AccessTools.Method(typeof(ZNet), "Disconnect", (Type[])null, (Type[])null).Invoke(ZNet.instance, new object[1] { c.Peer }); connections.Remove(c.Peer.m_rpc); } private RescueState GetAuthoritative(long id) { if (authoritative.TryGetValue(id, out var value)) { return value; } if (store == null) { store = new StateStore(Path.Combine(Paths.ConfigPath, "AttackRescue", ZNet.instance.GetWorldUID().ToString())); } value = store.Load(id, Plugin.Instance.Rules()); authoritative.Add(id, value); return value; } private void Commit(RescueState state) { store.Save(state); ApplySnapshot(state.Encode(), UtcNow, bind: false); Connection[] array = connections.Values.ToArray(); foreach (Connection connection in array) { if (connection.Verified && connection.Peer.IsReady()) { SendState(connection.Peer.m_rpc, state, bind: false); } } } private static void SendState(ZRpc rpc, RescueState state, bool bind) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(UtcNow); val.Write(bind); val.Write(state.Encode()); rpc.Invoke("AR_Snapshot_v1", new object[1] { val }); } private bool IsOurServer(ZRpc rpc) { if (!IsServer && Object.op_Implicit((Object)(object)ZNet.instance) && ZNet.instance.GetServerPeer() != null) { return ZNet.instance.GetServerPeer().m_rpc == rpc; } return false; } private void ReceiveSnapshot(ZRpc rpc, ZPackage p) { if (!IsOurServer(rpc)) { return; } try { double now = p.ReadDouble(); bool bind = p.ReadBool(); ApplySnapshot(p.ReadByteArray(), now, bind); } catch (Exception ex) { Fail(ex); } } private void ApplySnapshot(byte[] data, double now, bool bind) { RescueState rescueState = RescueState.Decode(data); clockOffset = (IsServer ? 0.0 : (now - UtcNow)); if (States.TryGetValue(rescueState.PlayerId, out var value) && value.Revision > rescueState.Revision) { return; } States[rescueState.PlayerId] = rescueState; if (bind && Object.op_Implicit((Object)(object)boundPlayer) && boundPlayer.GetPlayerID() == rescueState.PlayerId) { if (!Bound) { Plugin.Log.LogInfo((object)"Rescue state synchronized; player controls released."); } Bound = true; } if (Object.op_Implicit((Object)(object)Player.m_localPlayer) && Player.m_localPlayer.GetPlayerID() == rescueState.PlayerId) { Plugin.Instance.StateChanged(value, rescueState); } } private void SendConfig(Connection c) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown ZPackage val = new ZPackage(); RescueRules rescueRules = Plugin.Instance.Rules(); val.Write(rescueRules.ResetSeconds); val.Write(rescueRules.DeathSeconds); val.Write(rescueRules.HitsPerBar); val.Write(rescueRules.RefillDelaySeconds); val.Write(rescueRules.RefillDurationSeconds); c.Peer.m_rpc.Invoke("AR_Config_v1", new object[1] { val }); } private void ReceiveConfig(ZRpc rpc, ZPackage p) { if (!IsOurServer(rpc)) { return; } try { EffectiveRules = new RescueRules { ResetSeconds = p.ReadDouble(), DeathSeconds = p.ReadDouble(), HitsPerBar = p.ReadInt(), RefillDelaySeconds = p.ReadDouble(), RefillDurationSeconds = p.ReadDouble() }; } catch (Exception ex) { Fail(ex); } } internal RescueState For(Player player) { if (!Object.op_Implicit((Object)(object)player) || !States.TryGetValue(player.GetPlayerID(), out var value)) { return null; } return value; } internal void Send(Request op, long target, long episode, string token, float damage, bool ranged) { //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Expected O, but got Unknown if (!Object.op_Implicit((Object)(object)ZNet.instance) || !Object.op_Implicit((Object)(object)Player.m_localPlayer) || Failed) { return; } if (IsServer) { try { Handle(null, Player.m_localPlayer.GetPlayerID(), op, target, episode, token, damage, ranged); return; } catch (Exception ex) { Fail(ex); return; } } ZNetPeer serverPeer = ZNet.instance.GetServerPeer(); if (serverPeer != null && connections.TryGetValue(serverPeer.m_rpc, out var value) && value.Verified && serverPeer.IsReady()) { ZPackage val = new ZPackage(); val.Write(++sequence); val.Write((int)op); val.Write(Player.m_localPlayer.GetPlayerID()); val.Write(target); val.Write(episode); val.Write(token ?? ""); val.Write(damage); val.Write(ranged); serverPeer.m_rpc.Invoke("AR_Command_v1", new object[1] { val }); } } internal void Send(Request op) { Send(op, 0L, For(Player.m_localPlayer)?.Episode ?? 0, "", 0f, ranged: false); } private void ReceiveCommand(ZRpc rpc, ZPackage p) { //IL_00f3: Unknown result type (might be due to invalid IL or missing references) //IL_00fe: Unknown result type (might be due to invalid IL or missing references) if (!IsServer || !connections.TryGetValue(rpc, out var value) || !value.Verified || !value.Peer.IsReady()) { return; } try { long num = p.ReadLong(); Request request = (Request)p.ReadInt(); long num2 = p.ReadLong(); long target = p.ReadLong(); long episode = p.ReadLong(); string token = p.ReadString(); float damage = p.ReadSingle(); bool ranged = p.ReadBool(); if (num <= value.LastSequence || num2 == 0 || !Enum.IsDefined(typeof(Request), request)) { return; } value.LastSequence = num; if (!ValidateCharacterIdentity(value.Peer, num2, out var reason)) { if (request == Request.Bind && Time.realtimeSinceStartup >= value.NextBindDiagnostic) { value.NextBindDiagnostic = Time.realtimeSinceStartup + 5f; Plugin.Log.LogWarning((object)("Rescue binding waiting: " + reason)); } } else if (request == Request.Bind || (value.BoundId == num2 && !(value.Character != value.Peer.m_characterID))) { Handle(value, num2, request, target, episode, token, damage, ranged); } } catch (Exception ex) { Plugin.Log.LogError((object)ex); Reject(value, "Invalid command or persistent state error"); } } internal static bool ValidateCharacterIdentity(ZNetPeer peer, long claimedId, out string reason) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) if (peer.m_characterID == ZDOID.None) { reason = "CharacterID has not arrived"; return false; } ZDO val = ((ZDOMan.instance == null) ? null : ZDOMan.instance.GetZDO(peer.m_characterID)); if (val == null) { reason = "character ZDO has not arrived"; return false; } if (val.GetOwner() != peer.m_uid) { reason = "character ownership does not match connection"; return false; } long num = val.GetLong(ZDOVars.s_playerID, 0L); if (num == 0) { reason = "character playerID has not arrived"; return false; } if (num != claimedId) { reason = "claimed playerID does not match character ZDO"; return false; } if (peer.m_playerID != 0 && peer.m_playerID != num) { reason = "optional PlayerID conflicts with character ZDO"; return false; } reason = null; return true; } private void Handle(Connection c, long id, Request op, long target, long episode, string token, float damage, bool ranged) { //IL_0030: 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_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_01b6: Unknown result type (might be due to invalid IL or missing references) //IL_01b8: Unknown result type (might be due to invalid IL or missing references) RescueState rescueState = GetAuthoritative(id); double utcNow = UtcNow; if (rescueState.Tick(utcNow)) { Commit(rescueState); } if (op == Request.Bind) { if (c != null) { if (c.Peer.m_characterID == ZDOID.None) { return; } c.BoundId = id; c.Character = c.Peer.m_characterID; Plugin.Log.LogInfo((object)"Rescue state bound using connection-owned character ZDO."); SendConfig(c); { foreach (RescueState value2 in authoritative.Values) { SendState(c.Peer.m_rpc, value2, value2.PlayerId == id); } return; } } EffectiveRules = Plugin.Instance.Rules(); ApplySnapshot(rescueState.Encode(), utcNow, bind: true); return; } bool flag = false; switch (op) { case Request.Down: if (episode == rescueState.Episode) { flag = rescueState.Down(utcNow, Plugin.Instance.Rules()); } break; case Request.GiveUp: if (rescueState.Phase == RescuePhase.Downed && rescueState.Episode == episode) { flag = rescueState.Die(); } break; case Request.DeathAck: flag = rescueState.AcknowledgeDeath(); break; case Request.ExternalDeath: flag = rescueState.Die(); break; case Request.Hit: { if (rescueState.Phase == RescuePhase.Alive && id != target && token != null && token.Length <= 96 && authoritative.TryGetValue(target, out var value) && value.Phase == RescuePhase.Downed && Position(id, out var position) && Position(target, out var position2) && !(Vector3.Distance(position, position2) > (ranged ? 250f : 12f)) && value.Hit(utcNow, episode, id, token, damage)) { Commit(value); } return; } } if (flag) { Commit(rescueState); } else if (c != null) { SendState(c.Peer.m_rpc, rescueState, bind: false); } else { ApplySnapshot(rescueState.Encode(), utcNow, bind: false); } } private bool Position(long id, out Vector3 position) { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: 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) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) if (Object.op_Implicit((Object)(object)Player.m_localPlayer) && Player.m_localPlayer.GetPlayerID() == id) { position = ((Component)Player.m_localPlayer).transform.position; return true; } foreach (Connection value in connections.Values) { if (value.BoundId == id && value.Peer.IsReady()) { ZDO zDO = ZDOMan.instance.GetZDO(value.Peer.m_characterID); if (zDO != null) { position = zDO.GetPosition(); return true; } } } position = Vector3.zero; return false; } internal void Tick() { //IL_014b: Unknown result type (might be due to invalid IL or missing references) //IL_0150: Unknown result type (might be due to invalid IL or missing references) EnsureSession(); if (!Object.op_Implicit((Object)(object)ZNet.instance) || Failed) { return; } float realtimeSinceStartup = Time.realtimeSinceStartup; Connection[] array = connections.Values.ToArray(); foreach (Connection connection in array) { if (!ZNet.instance.GetPeers().Contains(connection.Peer)) { connections.Remove(connection.Peer.m_rpc); } else if (!connection.Verified && realtimeSinceStartup - connection.Created > 20f) { Reject(connection, "Required mod handshake missing"); } } if ((Object)(object)boundPlayer != (Object)(object)Player.m_localPlayer || (Object.op_Implicit((Object)(object)boundPlayer) && boundPlayer.GetPlayerID() != boundPlayerId)) { boundPlayer = Player.m_localPlayer; boundPlayerId = (Object.op_Implicit((Object)(object)boundPlayer) ? boundPlayer.GetPlayerID() : 0); Bound = false; nextBind = 0f; bindStarted = realtimeSinceStartup; nextSyncDiagnostic = realtimeSinceStartup + 15f; Plugin.Instance.ResetLocal(); } if (Object.op_Implicit((Object)(object)boundPlayer) && boundPlayerId != 0 && ((Character)boundPlayer).GetZDOID() != ZDOID.None && !Bound && realtimeSinceStartup >= nextBind) { if (SyncDelayed && realtimeSinceStartup >= nextSyncDiagnostic) { nextSyncDiagnostic = realtimeSinceStartup + 10f; Plugin.Log.LogWarning((object)"Rescue synchronization still pending. Check the server binding diagnostics and install the same plugin version on both ends."); } nextBind = realtimeSinceStartup + 1f; Send(Request.Bind); } if (!IsServer) { return; } if (realtimeSinceStartup >= nextTick) { nextTick = realtimeSinceStartup + 0.1f; try { RescueState[] array2 = authoritative.Values.ToArray(); foreach (RescueState rescueState in array2) { if (rescueState.Tick(UtcNow)) { Commit(rescueState); } } } catch (Exception ex) { Fail(ex); } } if (!(realtimeSinceStartup >= nextConfig)) { return; } nextConfig = realtimeSinceStartup + 5f; foreach (Connection value in connections.Values) { if (value.Verified && value.Peer.IsReady()) { SendConfig(value); } } EffectiveRules = Plugin.Instance.Rules(); } private void Fail(Exception ex) { Failed = true; Plugin.Instance.Notice = "error"; Plugin.Log.LogError((object)("Rescue stopped to preserve state: " + ex)); } } public enum RescuePhase { Alive, Downed, Dead } public sealed class RescueRules { public double ResetSeconds = 600.0; public double DeathSeconds = 120.0; public int HitsPerBar = 3; public double RefillDelaySeconds = 5.0; public double RefillDurationSeconds = 3.0; public RescueRules Copy() { return (RescueRules)MemberwiseClone(); } public static double Positive(double value, double fallback) { if (!double.IsNaN(value) && !double.IsInfinity(value) && !(value <= 0.0)) { return value; } return fallback; } } public sealed class RescueState { public long PlayerId; public long Revision; public long Episode; public RescuePhase Phase; public int Bars; public double Deadline; public double ResetAt; public double ImmuneUntil; public double Progress; public double LastHitAt; public RescueRules Rules = new RescueRules(); private readonly HashSet<string> hits = new HashSet<string>(); public double ProgressAt(double now) { if (Phase != RescuePhase.Downed) { return Progress; } double num = Math.Max(0.0, now - LastHitAt - Rules.RefillDelaySeconds); return Math.Max(0.0, Progress - num * (double)Bars * (double)Rules.HitsPerBar / Rules.RefillDurationSeconds); } public bool Tick(double now) { if (Phase == RescuePhase.Downed && now >= Deadline) { return Die(); } if (Phase == RescuePhase.Downed && Progress > 0.0 && ProgressAt(now) <= 0.0) { Progress = 0.0; LastHitAt = 0.0; Revision++; return true; } if (Phase == RescuePhase.Alive && ResetAt > 0.0 && now >= ResetAt) { Bars = 0; Progress = 0.0; LastHitAt = 0.0; ResetAt = 0.0; Revision++; return true; } return false; } public bool Down(double now, RescueRules rules) { Tick(now); if (Phase != RescuePhase.Alive || now < ImmuneUntil) { return false; } Rules = rules.Copy(); Phase = RescuePhase.Downed; Bars = Math.Min(3, Bars + 1); Progress = 0.0; LastHitAt = 0.0; ResetAt = 0.0; Deadline = now + Rules.DeathSeconds; Episode++; Revision++; hits.Clear(); return true; } public bool Hit(double now, long episode, long attacker, string token, float damage) { bool result = Tick(now); if (Phase != RescuePhase.Downed || Episode != episode || attacker == PlayerId || attacker == 0 || string.IsNullOrEmpty(token) || token.Length > 96 || float.IsNaN(damage) || float.IsInfinity(damage) || damage <= 0f) { return result; } if (!hits.Add(attacker.ToString(CultureInfo.InvariantCulture) + ":" + token)) { return result; } Progress = Math.Min((double)Bars * (double)Rules.HitsPerBar, ProgressAt(now) + 1.0); LastHitAt = now; if (Progress >= (double)((long)Bars * (long)Rules.HitsPerBar)) { Phase = RescuePhase.Alive; ResetAt = now + Rules.ResetSeconds; ImmuneUntil = now + 3.0; } Revision++; return true; } public bool Die() { if (Phase == RescuePhase.Dead) { return false; } Phase = RescuePhase.Dead; Bars = 0; Progress = 0.0; LastHitAt = 0.0; ResetAt = 0.0; ImmuneUntil = 0.0; Revision++; return true; } public bool AcknowledgeDeath() { if (Phase != RescuePhase.Dead) { return false; } Phase = RescuePhase.Alive; Revision++; return true; } public void Write(BinaryWriter w) { w.Write(PlayerId); w.Write(Revision); w.Write(Episode); w.Write((int)Phase); w.Write(Bars); w.Write(Deadline); w.Write(ResetAt); w.Write(ImmuneUntil); w.Write(Progress); w.Write(Rules.ResetSeconds); w.Write(Rules.DeathSeconds); w.Write(Rules.HitsPerBar); w.Write(Rules.RefillDelaySeconds); w.Write(LastHitAt); w.Write(Rules.RefillDurationSeconds); w.Write(hits.Count); foreach (string hit in hits) { w.Write(hit); } } public static RescueState Read(BinaryReader r, int format = 3, RescueRules migrationRules = null) { if (format != 1 && format != 2 && format != 3) { throw new InvalidDataException("Unsupported rescue save version"); } RescueState rescueState = new RescueState(); rescueState.PlayerId = r.ReadInt64(); rescueState.Revision = r.ReadInt64(); rescueState.Episode = r.ReadInt64(); rescueState.Phase = (RescuePhase)r.ReadInt32(); rescueState.Bars = r.ReadInt32(); rescueState.Deadline = r.ReadDouble(); rescueState.ResetAt = r.ReadDouble(); rescueState.ImmuneUntil = r.ReadDouble(); if (format == 1) { double num = r.ReadDouble(); rescueState.Rules.ResetSeconds = r.ReadDouble(); rescueState.Rules.DeathSeconds = r.ReadDouble(); float num2 = r.ReadSingle(); if (double.IsNaN(num) || double.IsInfinity(num) || num < 0.0 || RescueRules.Positive(num2, -1.0) < 0.0) { throw new InvalidDataException("Invalid legacy damage progress"); } rescueState.Rules.HitsPerBar = migrationRules?.HitsPerBar ?? 3; rescueState.Rules.RefillDelaySeconds = migrationRules?.RefillDelaySeconds ?? 5.0; rescueState.Progress = 0.0; } else { rescueState.Progress = ((format == 2) ? ((double)r.ReadInt64()) : r.ReadDouble()); rescueState.Rules.ResetSeconds = r.ReadDouble(); rescueState.Rules.DeathSeconds = r.ReadDouble(); rescueState.Rules.HitsPerBar = r.ReadInt32(); rescueState.Rules.RefillDelaySeconds = r.ReadDouble(); rescueState.LastHitAt = r.ReadDouble(); } rescueState.Rules.RefillDurationSeconds = ((format >= 3) ? r.ReadDouble() : (migrationRules?.RefillDurationSeconds ?? 3.0)); int num3 = r.ReadInt32(); if (num3 < 0 || num3 > 100000 || rescueState.Bars < 0 || rescueState.Bars > 3 || !Enum.IsDefined(typeof(RescuePhase), rescueState.Phase) || RescueRules.Positive(rescueState.Rules.ResetSeconds, -1.0) < 0.0 || RescueRules.Positive(rescueState.Rules.DeathSeconds, -1.0) < 0.0 || double.IsNaN(rescueState.Progress) || double.IsInfinity(rescueState.Progress) || RescueRules.Positive(rescueState.Rules.RefillDurationSeconds, -1.0) < 0.0 || rescueState.Rules.HitsPerBar <= 0 || rescueState.Progress < 0.0 || rescueState.Progress > (double)((long)rescueState.Bars * (long)rescueState.Rules.HitsPerBar) || RescueRules.Positive(rescueState.Rules.RefillDelaySeconds, -1.0) < 0.0 || double.IsNaN(rescueState.LastHitAt) || double.IsInfinity(rescueState.LastHitAt) || double.IsNaN(rescueState.Deadline) || double.IsInfinity(rescueState.Deadline) || double.IsNaN(rescueState.ResetAt) || double.IsInfinity(rescueState.ResetAt) || double.IsNaN(rescueState.ImmuneUntil) || double.IsInfinity(rescueState.ImmuneUntil)) { throw new InvalidDataException("Invalid rescue state"); } for (int i = 0; i < num3; i++) { rescueState.hits.Add(r.ReadString()); } return rescueState; } public byte[] Encode() { using MemoryStream memoryStream = new MemoryStream(); using BinaryWriter binaryWriter = new BinaryWriter(memoryStream); binaryWriter.Write(3); Write(binaryWriter); return memoryStream.ToArray(); } public static RescueState Decode(byte[] data) { using MemoryStream input = new MemoryStream(data); using BinaryReader binaryReader = new BinaryReader(input); return Read(binaryReader, binaryReader.ReadInt32()); } } public sealed class StateStore { private readonly string directory; public StateStore(string directory) { this.directory = directory; Directory.CreateDirectory(directory); } public RescueState Load(long id, RescueRules migrationRules = null) { string path = Path.Combine(directory, id + ".bin"); if (!File.Exists(path)) { RescueState rescueState = new RescueState(); rescueState.PlayerId = id; return rescueState; } using FileStream input = File.OpenRead(path); using BinaryReader binaryReader = new BinaryReader(input); int format = binaryReader.ReadInt32(); RescueState rescueState2 = RescueState.Read(binaryReader, format, migrationRules); if (rescueState2.PlayerId != id) { throw new InvalidDataException("Rescue player ID mismatch"); } return rescueState2; } public void Save(RescueState state) { string text = Path.Combine(directory, state.PlayerId + ".bin"); string text2 = text + ".tmp"; using (FileStream fileStream = new FileStream(text2, FileMode.Create, FileAccess.Write, FileShare.None)) { using BinaryWriter binaryWriter = new BinaryWriter(fileStream); binaryWriter.Write(3); state.Write(binaryWriter); binaryWriter.Flush(); fileStream.Flush(flushToDisk: true); } if (File.Exists(text)) { File.Replace(text2, text, text + ".bak"); } else { File.Move(text2, text); } } } public static class Texts { public static readonly string[] Keys = new string[12] { "down", "help", "time", "giveup", "revived", "reset", "sync", "conflict", "version", "progress", "error", "sync_delayed" }; public static readonly Dictionary<string, string[]> Languages = new Dictionary<string, string[]> { { "English", new string[12] { "FALLEN", "Allies: attack to revive", "Death in {0}s", "Hold {0} for 3s to give up", "Revived", "Next fall: 1 segment", "Synchronizing rescue state…", "Attack Rescue disabled: another revival mod is active.", "Attack Rescue: install the same version on every client and server.", "Rescue {0}%", "Rescue data error. See BepInEx log.", "Waiting for server character data. Check server logs." } }, { "Chinese", new string[12] { "已倒地", "队友攻击你即可推进救援", "距离死亡 {0} 秒", "长按 {0} 3 秒放弃救援", "已被救起", "倒地等级已重置,下次为 1 条", "正在同步救援状态…", "攻击救援已停用:检测到其他复活模组。", "攻击救援:所有玩家和服务器必须安装相同版本。", "救援进度 {0}%", "救援数据异常,请查看 BepInEx 日志。", "等待服务器确认角色数据,请检查服务器日志。" } }, { "Russian", new string[12] { "ПРИ СМЕРТИ", "Союзники: атакуйте, чтобы оживить", "До смерти: {0} с", "Удерживайте {0} 3 с, чтобы сдаться", "Вы оживлены", "Следующее падение: 1 сегмент", "Синхронизация состояния…", "Attack Rescue отключён: активен другой мод воскрешения.", "Attack Rescue: установите одинаковую версию на всех клиентах и сервере.", "Спасение: {0}%", "Ошибка данных спасения. См. журнал BepInEx.", "Ожидание данных персонажа. Проверьте журнал сервера." } }, { "Portuguese_Brazilian", new string[12] { "CAÍDO", "Aliados: ataquem para reanimar", "Morte em {0}s", "Segure {0} por 3s para desistir", "Você foi reanimado", "Próxima queda: 1 segmento", "Sincronizando estado de resgate…", "Attack Rescue desativado: outro mod de reanimação está ativo.", "Attack Rescue: instale a mesma versão em todos os clientes e no servidor.", "Resgate: {0}%", "Erro nos dados de resgate. Veja o log do BepInEx.", "Aguardando dados do personagem. Verifique o log do servidor." } }, { "Spanish", new string[12] { "ABATIDO", "Aliados: atacad para reanimar", "Muerte en {0}s", "Mantén {0} durante 3s para rendirte", "Te han reanimado", "Próxima caída: 1 segmento", "Sincronizando el estado de rescate…", "Attack Rescue desactivado: hay otro mod de reanimación activo.", "Attack Rescue: instala la misma versión en todos los clientes y el servidor.", "Rescate: {0}%", "Error de datos de rescate. Consulta el registro de BepInEx.", "Esperando datos del personaje. Revisa el registro del servidor." } } }; public static string Normalize(string language) { switch ((language ?? "").ToLowerInvariant().Replace("-", "_").Replace(" ", "")) { case "zh": case "zh_cn": case "chinese": case "简体中文": return "Chinese"; case "ru": case "russian": return "Russian"; case "pt_br": case "portuguese_brazilian": case "brazilianportuguese": return "Portuguese_Brazilian"; case "es": case "spanish": return "Spanish"; default: return "English"; } } public static string Get(string language, string key, params object[] args) { int num = Array.IndexOf(Keys, key); if (num < 0) { return key; } string text = Languages[Normalize(language)][num]; if (args.Length != 0) { return string.Format(text, args); } return text; } } }