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 StartSpawnOnDeath v1.0.0
BepInEx/plugins/StartSpawnOnDeath/StartSpawnOnDeath.dll
Decompiled 2 months agousing System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using BepInEx; using BepInEx.Configuration; using Splatform; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("StartSpawnOnDeath")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("StartSpawnOnDeath")] [assembly: AssemblyTitle("StartSpawnOnDeath")] [assembly: AssemblyVersion("1.0.0.0")] namespace StartSpawnOnDeath; [BepInPlugin("startspawnondeath", "Start Spawn On Death", "1.0.0")] public sealed class StartSpawnOnDeathPlugin : BaseUnityPlugin { private sealed class PlayerState { public string StableKey; public string PlayerName; public bool SeenCharacter; public bool LastDead; public bool PendingTeleport; public float AliveSince = -1f; public float NextTeleportAttempt; public int TeleportAttempts; } private sealed class HudNotice { public string Message; public float Until; public float NextSend; } public const string PluginGuid = "startspawnondeath"; public const string PluginName = "Start Spawn On Death"; public const string PluginVersion = "1.0.0"; private readonly Dictionary<long, PlayerState> _states = new Dictionary<long, PlayerState>(); private readonly HashSet<string> _persistedPending = new HashSet<string>(StringComparer.Ordinal); private readonly List<HudNotice> _hudNotices = new List<HudNotice>(); private ConfigEntry<bool> _enabled; private ConfigEntry<float> _pollInterval; private ConfigEntry<float> _respawnSettleDelay; private ConfigEntry<float> _retryInterval; private ConfigEntry<float> _slowRetryInterval; private ConfigEntry<int> _fastRetryCount; private ConfigEntry<float> _verificationRadius; private ConfigEntry<float> _spawnYOffset; private ConfigEntry<bool> _persistPendingDeaths; private ConfigEntry<bool> _verboseLogging; private ConfigEntry<bool> _broadcastChatAnnouncement; private ConfigEntry<string> _chatAnnouncementText; private ConfigEntry<string> _serverChatName; private ConfigEntry<bool> _showExtendedHudAnnouncement; private ConfigEntry<string> _hudAnnouncementText; private ConfigEntry<float> _hudAnnouncementDuration; private ConfigEntry<float> _hudRefreshInterval; private float _nextPoll; private float _nextSpawnLookup; private bool _spawnResolved; private Vector3 _spawnPosition; private string _pendingFilePath; private void Awake() { _enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Enable server-side forced return to StartTemple after player death."); _pollInterval = ((BaseUnityPlugin)this).Config.Bind<float>("General", "PollIntervalSeconds", 0.25f, "How often the dedicated server checks player death/respawn ZDO state."); _respawnSettleDelay = ((BaseUnityPlugin)this).Config.Bind<float>("General", "RespawnSettleDelaySeconds", 0.5f, "Delay after the player is seen alive again before the first forced teleport."); _retryInterval = ((BaseUnityPlugin)this).Config.Bind<float>("General", "RetryIntervalSeconds", 1f, "Delay between initial teleport retries while waiting for the client position to update."); _slowRetryInterval = ((BaseUnityPlugin)this).Config.Bind<float>("General", "SlowRetryIntervalSeconds", 5f, "Retry interval after the fast retry count has been exceeded. The latch is never dropped until verified."); _fastRetryCount = ((BaseUnityPlugin)this).Config.Bind<int>("General", "FastRetryCount", 5, "Number of quick teleport attempts before switching to the slower retry interval."); _verificationRadius = ((BaseUnityPlugin)this).Config.Bind<float>("General", "VerificationRadiusMeters", 12f, "Horizontal distance from StartTemple considered a successful return to spawn."); _spawnYOffset = ((BaseUnityPlugin)this).Config.Bind<float>("General", "SpawnYOffsetMeters", 1.5f, "Vertical safety offset added to the StartTemple position."); _persistPendingDeaths = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "PersistPendingDeaths", true, "Remember an uncompleted death->spawn return across disconnects/server restarts."); _verboseLogging = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "VerboseLogging", false, "Write extra state-machine details to the BepInEx log."); _broadcastChatAnnouncement = ((BaseUnityPlugin)this).Config.Bind<bool>("Chat", "BroadcastDeathReturn", false, "Broadcast a vanilla chat message to all connected players after a death return to StartTemple is verified."); _chatAnnouncementText = ((BaseUnityPlugin)this).Config.Bind<string>("Chat", "AnnouncementText", "{player} died and was returned to the Sacrificial Stones.", "Chat announcement. Use {player} for the player's name."); _serverChatName = ((BaseUnityPlugin)this).Config.Bind<string>("Chat", "ServerName", "Server", "Name shown as the sender of server chat announcements."); _showExtendedHudAnnouncement = ((BaseUnityPlugin)this).Config.Bind<bool>("Display", "ShowExtendedHudAnnouncement", true, "Also show a center-screen death-return notice. This is refreshed by the server so vanilla clients see it for longer."); _hudAnnouncementText = ((BaseUnityPlugin)this).Config.Bind<string>("Display", "HudAnnouncementText", "{player} died and was returned to the Sacrificial Stones.", "Center-screen announcement. Use {player} for the player's name."); _hudAnnouncementDuration = ((BaseUnityPlugin)this).Config.Bind<float>("Display", "HudDurationSeconds", 5f, "Approximate number of seconds to keep the center-screen notice visible. Set to 0 to disable it."); _hudRefreshInterval = ((BaseUnityPlugin)this).Config.Bind<float>("Display", "HudRefreshIntervalSeconds", 0.75f, "How often the server refreshes the vanilla center-screen notice while it is active. Lower values look more continuous but send more RPCs."); _pendingFilePath = Path.Combine(Paths.ConfigPath, "startspawnondeath.pending.txt"); LoadPendingDeaths(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Start Spawn On Death 1.0.0 loaded. Clients do not need this plugin."); } private void Update() { //IL_00f5: Unknown result type (might be due to invalid IL or missing references) if (!_enabled.Value) { return; } float realtimeSinceStartup = Time.realtimeSinceStartup; UpdateHudAnnouncements(realtimeSinceStartup); if (realtimeSinceStartup < _nextPoll) { return; } _nextPoll = realtimeSinceStartup + Mathf.Max(0.1f, _pollInterval.Value); ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer()) { return; } if (!_spawnResolved && realtimeSinceStartup >= _nextSpawnLookup) { _nextSpawnLookup = realtimeSinceStartup + 5f; TryResolveStartTemple(); } List<ZNetPeer> peers = instance.GetPeers(); if (peers == null) { return; } HashSet<long> hashSet = new HashSet<long>(); List<ZNetPeer> list = new List<ZNetPeer>(peers); for (int i = 0; i < list.Count; i++) { ZNetPeer val = list[i]; if (val == null || val.m_uid == 0L) { continue; } hashSet.Add(val.m_uid); if (!val.IsReady()) { continue; } PlayerState orCreateState = GetOrCreateState(val); if (!((ZDOID)(ref val.m_characterID)).IsNone() && ZDOMan.instance != null) { ZDO zDO = ZDOMan.instance.GetZDO(val.m_characterID); if (zDO != null) { bool dead = zDO.GetBool("dead", false); UpdatePlayerState(val, zDO, orCreateState, dead, realtimeSinceStartup); } } } CleanupDisconnected(hashSet); } private PlayerState GetOrCreateState(ZNetPeer peer) { if (_states.TryGetValue(peer.m_uid, out var value)) { return value; } string text = BuildStablePlayerKey(peer); value = new PlayerState { StableKey = text, PlayerName = (string.IsNullOrEmpty(peer.m_playerName) ? "<unknown>" : peer.m_playerName), PendingTeleport = _persistedPending.Contains(text) }; _states[peer.m_uid] = value; if (value.PendingTeleport) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("Restored pending return-to-spawn latch for " + value.PlayerName + ".")); } return value; } private void UpdatePlayerState(ZNetPeer peer, ZDO zdo, PlayerState state, bool dead, float now) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) if (!state.SeenCharacter) { state.SeenCharacter = true; state.LastDead = dead; if (dead) { MarkDeathPending(state, zdo.GetPosition()); } else if (state.PendingTeleport) { state.AliveSince = now; } } else { if (dead) { if (!state.LastDead || !state.PendingTeleport) { MarkDeathPending(state, zdo.GetPosition()); } state.LastDead = true; state.AliveSince = -1f; return; } if (state.LastDead) { state.AliveSince = now; state.TeleportAttempts = 0; state.NextTeleportAttempt = 0f; if (_verboseLogging.Value) { ((BaseUnityPlugin)this).Logger.LogInfo((object)(state.PlayerName + " is alive again; waiting briefly before enforcing StartTemple.")); } } state.LastDead = false; } if (!dead && state.PendingTeleport) { if (state.AliveSince < 0f) { state.AliveSince = now; } EnforceReturnToSpawn(peer, zdo, state, now); } } private void MarkDeathPending(PlayerState state, Vector3 deathPosition) { //IL_005b: Unknown result type (might be due to invalid IL or missing references) state.PendingTeleport = true; state.AliveSince = -1f; state.NextTeleportAttempt = 0f; state.TeleportAttempts = 0; if (_persistedPending.Add(state.StableKey) && _persistPendingDeaths.Value) { SavePendingDeaths(); } ((BaseUnityPlugin)this).Logger.LogInfo((object)(state.PlayerName + " died at " + FormatVector(deathPosition) + "; return to StartTemple queued.")); } private void EnforceReturnToSpawn(ZNetPeer peer, ZDO zdo, PlayerState state, float now) { //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_0012: 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_0088: 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_013d: Unknown result type (might be due to invalid IL or missing references) if (!_spawnResolved) { return; } Vector3 position = zdo.GetPosition(); if (HorizontalDistance(position, _spawnPosition) <= Mathf.Max(1f, _verificationRadius.Value)) { CompletePendingReturn(state, position); } else if (!(now < state.AliveSince + Mathf.Max(0f, _respawnSettleDelay.Value)) && !(now < state.NextTeleportAttempt) && ZRoutedRpc.instance != null) { ZRoutedRpc.instance.InvokeRoutedRPC(peer.m_uid, "RPC_TeleportPlayer", new object[3] { _spawnPosition, Quaternion.identity, true }); state.TeleportAttempts++; float num = ((state.TeleportAttempts <= Math.Max(1, _fastRetryCount.Value)) ? Mathf.Max(0.25f, _retryInterval.Value) : Mathf.Max(1f, _slowRetryInterval.Value)); state.NextTeleportAttempt = now + num; if (state.TeleportAttempts == 1) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("Teleporting " + state.PlayerName + " to StartTemple at " + FormatVector(_spawnPosition) + ".")); } else if (_verboseLogging.Value) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("Retrying StartTemple teleport for " + state.PlayerName + " (attempt " + state.TeleportAttempts + ").")); } if (state.TeleportAttempts == Math.Max(1, _fastRetryCount.Value) + 1) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("StartTemple return for " + state.PlayerName + " has not verified yet; continuing with slow retries.")); } } } private void CompletePendingReturn(PlayerState state, Vector3 verifiedPosition) { //IL_005b: Unknown result type (might be due to invalid IL or missing references) state.PendingTeleport = false; state.AliveSince = -1f; state.NextTeleportAttempt = 0f; state.TeleportAttempts = 0; if (_persistedPending.Remove(state.StableKey) && _persistPendingDeaths.Value) { SavePendingDeaths(); } ((BaseUnityPlugin)this).Logger.LogInfo((object)(state.PlayerName + " return to StartTemple verified at " + FormatVector(verifiedPosition) + ".")); BroadcastReturnAnnouncement(state); QueueExtendedHudAnnouncement(state); } private void BroadcastReturnAnnouncement(PlayerState state) { //IL_007b: 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_0087: Unknown result type (might be due to invalid IL or missing references) //IL_0090: 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: Expected O, but got Unknown //IL_00b0: Unknown result type (might be due to invalid IL or missing references) if (!_broadcastChatAnnouncement.Value || ZRoutedRpc.instance == null) { return; } try { string text = (_chatAnnouncementText.Value ?? string.Empty).Replace("{player}", state.PlayerName ?? "<unknown>"); if (!string.IsNullOrWhiteSpace(text)) { string name = (string.IsNullOrWhiteSpace(_serverChatName.Value) ? "Server" : _serverChatName.Value.Trim()); UserInfo val = new UserInfo { Name = name, UserId = new PlatformUserID("Bot", 0uL, false) }; ZRoutedRpc.instance.InvokeRoutedRPC(0L, "ChatMessage", new object[4] { _spawnPosition, 2, val, text }); } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Failed to broadcast return-to-spawn chat announcement: " + ex.Message)); } } private void QueueExtendedHudAnnouncement(PlayerState state) { if (_showExtendedHudAnnouncement.Value && !(_hudAnnouncementDuration.Value <= 0f)) { string text = (_hudAnnouncementText.Value ?? string.Empty).Replace("{player}", state.PlayerName ?? "<unknown>"); if (!string.IsNullOrWhiteSpace(text)) { float realtimeSinceStartup = Time.realtimeSinceStartup; _hudNotices.Add(new HudNotice { Message = text, Until = realtimeSinceStartup + Mathf.Max(0.5f, _hudAnnouncementDuration.Value), NextSend = realtimeSinceStartup }); SendHudMessage(text); _hudNotices[_hudNotices.Count - 1].NextSend = realtimeSinceStartup + Mathf.Max(0.25f, _hudRefreshInterval.Value); } } } private void UpdateHudAnnouncements(float now) { if (_hudNotices.Count == 0 || ZRoutedRpc.instance == null) { return; } float num = Mathf.Max(0.25f, _hudRefreshInterval.Value); for (int num2 = _hudNotices.Count - 1; num2 >= 0; num2--) { HudNotice hudNotice = _hudNotices[num2]; if (now >= hudNotice.Until) { _hudNotices.RemoveAt(num2); } else if (now >= hudNotice.NextSend) { SendHudMessage(hudNotice.Message); hudNotice.NextSend = now + num; } } } private void SendHudMessage(string message) { try { ZRoutedRpc.instance.InvokeRoutedRPC(0L, "ShowMessage", new object[2] { 2, message }); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Failed to show extended return-to-spawn HUD announcement: " + ex.Message)); } } private bool TryResolveStartTemple() { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_004f: 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_006d: 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_0086: Unknown result type (might be due to invalid IL or missing references) ZoneSystem instance = ZoneSystem.instance; if ((Object)(object)instance == (Object)null) { return false; } try { LocationInstance val = default(LocationInstance); if (!instance.FindClosestLocation("StartTemple", Vector3.zero, ref val)) { if (_verboseLogging.Value) { ((BaseUnityPlugin)this).Logger.LogWarning((object)"StartTemple is not available yet; will retry."); } return false; } Vector3 position = val.m_position; position.y += _spawnYOffset.Value; _spawnPosition = position; _spawnResolved = true; ((BaseUnityPlugin)this).Logger.LogInfo((object)("Resolved StartTemple at " + FormatVector(_spawnPosition) + ".")); return true; } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Could not resolve StartTemple yet: " + ex.Message)); return false; } } private void CleanupDisconnected(HashSet<long> connected) { if (_states.Count == 0) { return; } List<long> list = new List<long>(); foreach (KeyValuePair<long, PlayerState> state in _states) { if (!connected.Contains(state.Key)) { list.Add(state.Key); } } for (int i = 0; i < list.Count; i++) { PlayerState playerState = _states[list[i]]; if (_verboseLogging.Value && playerState.PendingTeleport) { ((BaseUnityPlugin)this).Logger.LogInfo((object)(playerState.PlayerName + " disconnected with a pending StartTemple return; latch remains persisted.")); } _states.Remove(list[i]); } } private string BuildStablePlayerKey(ZNetPeer peer) { string text = string.Empty; try { if (peer.m_socket != null) { text = peer.m_socket.GetHostName() ?? string.Empty; } } catch { text = string.Empty; } string text2 = peer.m_playerName ?? string.Empty; string s = text + "\n" + text2; if (string.IsNullOrEmpty(text) && string.IsNullOrEmpty(text2)) { s = peer.m_uid.ToString(); } using SHA256 sHA = SHA256.Create(); byte[] array = sHA.ComputeHash(Encoding.UTF8.GetBytes(s)); StringBuilder stringBuilder = new StringBuilder(array.Length * 2); for (int i = 0; i < array.Length; i++) { stringBuilder.Append(array[i].ToString("x2")); } return stringBuilder.ToString(); } private void LoadPendingDeaths() { if (!_persistPendingDeaths.Value) { return; } try { if (!File.Exists(_pendingFilePath)) { return; } string[] array = File.ReadAllLines(_pendingFilePath); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length > 0) { _persistedPending.Add(text); } } if (_persistedPending.Count > 0) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("Loaded " + _persistedPending.Count + " persisted pending death latch(es).")); } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Failed to load pending-death persistence file: " + ex.Message)); } } private void SavePendingDeaths() { try { List<string> list = new List<string>(_persistedPending); list.Sort(StringComparer.Ordinal); File.WriteAllLines(_pendingFilePath, list.ToArray()); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Failed to save pending-death persistence file: " + ex.Message)); } } private static float HorizontalDistance(Vector3 a, Vector3 b) { //IL_0000: 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_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) float num = a.x - b.x; float num2 = a.z - b.z; return Mathf.Sqrt(num * num + num2 * num2); } private static string FormatVector(Vector3 p) { return "(" + p.x.ToString("0.0") + ", " + p.y.ToString("0.0") + ", " + p.z.ToString("0.0") + ")"; } }
StartSpawnOnDeath.dll
Decompiled 2 months agousing System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using BepInEx; using BepInEx.Configuration; using Splatform; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("StartSpawnOnDeath")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("StartSpawnOnDeath")] [assembly: AssemblyTitle("StartSpawnOnDeath")] [assembly: AssemblyVersion("1.0.0.0")] namespace StartSpawnOnDeath; [BepInPlugin("startspawnondeath", "Start Spawn On Death", "1.0.0")] public sealed class StartSpawnOnDeathPlugin : BaseUnityPlugin { private sealed class PlayerState { public string StableKey; public string PlayerName; public bool SeenCharacter; public bool LastDead; public bool PendingTeleport; public float AliveSince = -1f; public float NextTeleportAttempt; public int TeleportAttempts; } private sealed class HudNotice { public string Message; public float Until; public float NextSend; } public const string PluginGuid = "startspawnondeath"; public const string PluginName = "Start Spawn On Death"; public const string PluginVersion = "1.0.0"; private readonly Dictionary<long, PlayerState> _states = new Dictionary<long, PlayerState>(); private readonly HashSet<string> _persistedPending = new HashSet<string>(StringComparer.Ordinal); private readonly List<HudNotice> _hudNotices = new List<HudNotice>(); private ConfigEntry<bool> _enabled; private ConfigEntry<float> _pollInterval; private ConfigEntry<float> _respawnSettleDelay; private ConfigEntry<float> _retryInterval; private ConfigEntry<float> _slowRetryInterval; private ConfigEntry<int> _fastRetryCount; private ConfigEntry<float> _verificationRadius; private ConfigEntry<float> _spawnYOffset; private ConfigEntry<bool> _persistPendingDeaths; private ConfigEntry<bool> _verboseLogging; private ConfigEntry<bool> _broadcastChatAnnouncement; private ConfigEntry<string> _chatAnnouncementText; private ConfigEntry<string> _serverChatName; private ConfigEntry<bool> _showExtendedHudAnnouncement; private ConfigEntry<string> _hudAnnouncementText; private ConfigEntry<float> _hudAnnouncementDuration; private ConfigEntry<float> _hudRefreshInterval; private float _nextPoll; private float _nextSpawnLookup; private bool _spawnResolved; private Vector3 _spawnPosition; private string _pendingFilePath; private void Awake() { _enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Enable server-side forced return to StartTemple after player death."); _pollInterval = ((BaseUnityPlugin)this).Config.Bind<float>("General", "PollIntervalSeconds", 0.25f, "How often the dedicated server checks player death/respawn ZDO state."); _respawnSettleDelay = ((BaseUnityPlugin)this).Config.Bind<float>("General", "RespawnSettleDelaySeconds", 0.5f, "Delay after the player is seen alive again before the first forced teleport."); _retryInterval = ((BaseUnityPlugin)this).Config.Bind<float>("General", "RetryIntervalSeconds", 1f, "Delay between initial teleport retries while waiting for the client position to update."); _slowRetryInterval = ((BaseUnityPlugin)this).Config.Bind<float>("General", "SlowRetryIntervalSeconds", 5f, "Retry interval after the fast retry count has been exceeded. The latch is never dropped until verified."); _fastRetryCount = ((BaseUnityPlugin)this).Config.Bind<int>("General", "FastRetryCount", 5, "Number of quick teleport attempts before switching to the slower retry interval."); _verificationRadius = ((BaseUnityPlugin)this).Config.Bind<float>("General", "VerificationRadiusMeters", 12f, "Horizontal distance from StartTemple considered a successful return to spawn."); _spawnYOffset = ((BaseUnityPlugin)this).Config.Bind<float>("General", "SpawnYOffsetMeters", 1.5f, "Vertical safety offset added to the StartTemple position."); _persistPendingDeaths = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "PersistPendingDeaths", true, "Remember an uncompleted death->spawn return across disconnects/server restarts."); _verboseLogging = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "VerboseLogging", false, "Write extra state-machine details to the BepInEx log."); _broadcastChatAnnouncement = ((BaseUnityPlugin)this).Config.Bind<bool>("Chat", "BroadcastDeathReturn", false, "Broadcast a vanilla chat message to all connected players after a death return to StartTemple is verified."); _chatAnnouncementText = ((BaseUnityPlugin)this).Config.Bind<string>("Chat", "AnnouncementText", "{player} died and was returned to the Sacrificial Stones.", "Chat announcement. Use {player} for the player's name."); _serverChatName = ((BaseUnityPlugin)this).Config.Bind<string>("Chat", "ServerName", "Server", "Name shown as the sender of server chat announcements."); _showExtendedHudAnnouncement = ((BaseUnityPlugin)this).Config.Bind<bool>("Display", "ShowExtendedHudAnnouncement", true, "Also show a center-screen death-return notice. This is refreshed by the server so vanilla clients see it for longer."); _hudAnnouncementText = ((BaseUnityPlugin)this).Config.Bind<string>("Display", "HudAnnouncementText", "{player} died and was returned to the Sacrificial Stones.", "Center-screen announcement. Use {player} for the player's name."); _hudAnnouncementDuration = ((BaseUnityPlugin)this).Config.Bind<float>("Display", "HudDurationSeconds", 5f, "Approximate number of seconds to keep the center-screen notice visible. Set to 0 to disable it."); _hudRefreshInterval = ((BaseUnityPlugin)this).Config.Bind<float>("Display", "HudRefreshIntervalSeconds", 0.75f, "How often the server refreshes the vanilla center-screen notice while it is active. Lower values look more continuous but send more RPCs."); _pendingFilePath = Path.Combine(Paths.ConfigPath, "startspawnondeath.pending.txt"); LoadPendingDeaths(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Start Spawn On Death 1.0.0 loaded. Clients do not need this plugin."); } private void Update() { //IL_00f5: Unknown result type (might be due to invalid IL or missing references) if (!_enabled.Value) { return; } float realtimeSinceStartup = Time.realtimeSinceStartup; UpdateHudAnnouncements(realtimeSinceStartup); if (realtimeSinceStartup < _nextPoll) { return; } _nextPoll = realtimeSinceStartup + Mathf.Max(0.1f, _pollInterval.Value); ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer()) { return; } if (!_spawnResolved && realtimeSinceStartup >= _nextSpawnLookup) { _nextSpawnLookup = realtimeSinceStartup + 5f; TryResolveStartTemple(); } List<ZNetPeer> peers = instance.GetPeers(); if (peers == null) { return; } HashSet<long> hashSet = new HashSet<long>(); List<ZNetPeer> list = new List<ZNetPeer>(peers); for (int i = 0; i < list.Count; i++) { ZNetPeer val = list[i]; if (val == null || val.m_uid == 0L) { continue; } hashSet.Add(val.m_uid); if (!val.IsReady()) { continue; } PlayerState orCreateState = GetOrCreateState(val); if (!((ZDOID)(ref val.m_characterID)).IsNone() && ZDOMan.instance != null) { ZDO zDO = ZDOMan.instance.GetZDO(val.m_characterID); if (zDO != null) { bool dead = zDO.GetBool("dead", false); UpdatePlayerState(val, zDO, orCreateState, dead, realtimeSinceStartup); } } } CleanupDisconnected(hashSet); } private PlayerState GetOrCreateState(ZNetPeer peer) { if (_states.TryGetValue(peer.m_uid, out var value)) { return value; } string text = BuildStablePlayerKey(peer); value = new PlayerState { StableKey = text, PlayerName = (string.IsNullOrEmpty(peer.m_playerName) ? "<unknown>" : peer.m_playerName), PendingTeleport = _persistedPending.Contains(text) }; _states[peer.m_uid] = value; if (value.PendingTeleport) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("Restored pending return-to-spawn latch for " + value.PlayerName + ".")); } return value; } private void UpdatePlayerState(ZNetPeer peer, ZDO zdo, PlayerState state, bool dead, float now) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) if (!state.SeenCharacter) { state.SeenCharacter = true; state.LastDead = dead; if (dead) { MarkDeathPending(state, zdo.GetPosition()); } else if (state.PendingTeleport) { state.AliveSince = now; } } else { if (dead) { if (!state.LastDead || !state.PendingTeleport) { MarkDeathPending(state, zdo.GetPosition()); } state.LastDead = true; state.AliveSince = -1f; return; } if (state.LastDead) { state.AliveSince = now; state.TeleportAttempts = 0; state.NextTeleportAttempt = 0f; if (_verboseLogging.Value) { ((BaseUnityPlugin)this).Logger.LogInfo((object)(state.PlayerName + " is alive again; waiting briefly before enforcing StartTemple.")); } } state.LastDead = false; } if (!dead && state.PendingTeleport) { if (state.AliveSince < 0f) { state.AliveSince = now; } EnforceReturnToSpawn(peer, zdo, state, now); } } private void MarkDeathPending(PlayerState state, Vector3 deathPosition) { //IL_005b: Unknown result type (might be due to invalid IL or missing references) state.PendingTeleport = true; state.AliveSince = -1f; state.NextTeleportAttempt = 0f; state.TeleportAttempts = 0; if (_persistedPending.Add(state.StableKey) && _persistPendingDeaths.Value) { SavePendingDeaths(); } ((BaseUnityPlugin)this).Logger.LogInfo((object)(state.PlayerName + " died at " + FormatVector(deathPosition) + "; return to StartTemple queued.")); } private void EnforceReturnToSpawn(ZNetPeer peer, ZDO zdo, PlayerState state, float now) { //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_0012: 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_0088: 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_013d: Unknown result type (might be due to invalid IL or missing references) if (!_spawnResolved) { return; } Vector3 position = zdo.GetPosition(); if (HorizontalDistance(position, _spawnPosition) <= Mathf.Max(1f, _verificationRadius.Value)) { CompletePendingReturn(state, position); } else if (!(now < state.AliveSince + Mathf.Max(0f, _respawnSettleDelay.Value)) && !(now < state.NextTeleportAttempt) && ZRoutedRpc.instance != null) { ZRoutedRpc.instance.InvokeRoutedRPC(peer.m_uid, "RPC_TeleportPlayer", new object[3] { _spawnPosition, Quaternion.identity, true }); state.TeleportAttempts++; float num = ((state.TeleportAttempts <= Math.Max(1, _fastRetryCount.Value)) ? Mathf.Max(0.25f, _retryInterval.Value) : Mathf.Max(1f, _slowRetryInterval.Value)); state.NextTeleportAttempt = now + num; if (state.TeleportAttempts == 1) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("Teleporting " + state.PlayerName + " to StartTemple at " + FormatVector(_spawnPosition) + ".")); } else if (_verboseLogging.Value) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("Retrying StartTemple teleport for " + state.PlayerName + " (attempt " + state.TeleportAttempts + ").")); } if (state.TeleportAttempts == Math.Max(1, _fastRetryCount.Value) + 1) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("StartTemple return for " + state.PlayerName + " has not verified yet; continuing with slow retries.")); } } } private void CompletePendingReturn(PlayerState state, Vector3 verifiedPosition) { //IL_005b: Unknown result type (might be due to invalid IL or missing references) state.PendingTeleport = false; state.AliveSince = -1f; state.NextTeleportAttempt = 0f; state.TeleportAttempts = 0; if (_persistedPending.Remove(state.StableKey) && _persistPendingDeaths.Value) { SavePendingDeaths(); } ((BaseUnityPlugin)this).Logger.LogInfo((object)(state.PlayerName + " return to StartTemple verified at " + FormatVector(verifiedPosition) + ".")); BroadcastReturnAnnouncement(state); QueueExtendedHudAnnouncement(state); } private void BroadcastReturnAnnouncement(PlayerState state) { //IL_007b: 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_0087: Unknown result type (might be due to invalid IL or missing references) //IL_0090: 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: Expected O, but got Unknown //IL_00b0: Unknown result type (might be due to invalid IL or missing references) if (!_broadcastChatAnnouncement.Value || ZRoutedRpc.instance == null) { return; } try { string text = (_chatAnnouncementText.Value ?? string.Empty).Replace("{player}", state.PlayerName ?? "<unknown>"); if (!string.IsNullOrWhiteSpace(text)) { string name = (string.IsNullOrWhiteSpace(_serverChatName.Value) ? "Server" : _serverChatName.Value.Trim()); UserInfo val = new UserInfo { Name = name, UserId = new PlatformUserID("Bot", 0uL, false) }; ZRoutedRpc.instance.InvokeRoutedRPC(0L, "ChatMessage", new object[4] { _spawnPosition, 2, val, text }); } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Failed to broadcast return-to-spawn chat announcement: " + ex.Message)); } } private void QueueExtendedHudAnnouncement(PlayerState state) { if (_showExtendedHudAnnouncement.Value && !(_hudAnnouncementDuration.Value <= 0f)) { string text = (_hudAnnouncementText.Value ?? string.Empty).Replace("{player}", state.PlayerName ?? "<unknown>"); if (!string.IsNullOrWhiteSpace(text)) { float realtimeSinceStartup = Time.realtimeSinceStartup; _hudNotices.Add(new HudNotice { Message = text, Until = realtimeSinceStartup + Mathf.Max(0.5f, _hudAnnouncementDuration.Value), NextSend = realtimeSinceStartup }); SendHudMessage(text); _hudNotices[_hudNotices.Count - 1].NextSend = realtimeSinceStartup + Mathf.Max(0.25f, _hudRefreshInterval.Value); } } } private void UpdateHudAnnouncements(float now) { if (_hudNotices.Count == 0 || ZRoutedRpc.instance == null) { return; } float num = Mathf.Max(0.25f, _hudRefreshInterval.Value); for (int num2 = _hudNotices.Count - 1; num2 >= 0; num2--) { HudNotice hudNotice = _hudNotices[num2]; if (now >= hudNotice.Until) { _hudNotices.RemoveAt(num2); } else if (now >= hudNotice.NextSend) { SendHudMessage(hudNotice.Message); hudNotice.NextSend = now + num; } } } private void SendHudMessage(string message) { try { ZRoutedRpc.instance.InvokeRoutedRPC(0L, "ShowMessage", new object[2] { 2, message }); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Failed to show extended return-to-spawn HUD announcement: " + ex.Message)); } } private bool TryResolveStartTemple() { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_004f: 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_006d: 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_0086: Unknown result type (might be due to invalid IL or missing references) ZoneSystem instance = ZoneSystem.instance; if ((Object)(object)instance == (Object)null) { return false; } try { LocationInstance val = default(LocationInstance); if (!instance.FindClosestLocation("StartTemple", Vector3.zero, ref val)) { if (_verboseLogging.Value) { ((BaseUnityPlugin)this).Logger.LogWarning((object)"StartTemple is not available yet; will retry."); } return false; } Vector3 position = val.m_position; position.y += _spawnYOffset.Value; _spawnPosition = position; _spawnResolved = true; ((BaseUnityPlugin)this).Logger.LogInfo((object)("Resolved StartTemple at " + FormatVector(_spawnPosition) + ".")); return true; } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Could not resolve StartTemple yet: " + ex.Message)); return false; } } private void CleanupDisconnected(HashSet<long> connected) { if (_states.Count == 0) { return; } List<long> list = new List<long>(); foreach (KeyValuePair<long, PlayerState> state in _states) { if (!connected.Contains(state.Key)) { list.Add(state.Key); } } for (int i = 0; i < list.Count; i++) { PlayerState playerState = _states[list[i]]; if (_verboseLogging.Value && playerState.PendingTeleport) { ((BaseUnityPlugin)this).Logger.LogInfo((object)(playerState.PlayerName + " disconnected with a pending StartTemple return; latch remains persisted.")); } _states.Remove(list[i]); } } private string BuildStablePlayerKey(ZNetPeer peer) { string text = string.Empty; try { if (peer.m_socket != null) { text = peer.m_socket.GetHostName() ?? string.Empty; } } catch { text = string.Empty; } string text2 = peer.m_playerName ?? string.Empty; string s = text + "\n" + text2; if (string.IsNullOrEmpty(text) && string.IsNullOrEmpty(text2)) { s = peer.m_uid.ToString(); } using SHA256 sHA = SHA256.Create(); byte[] array = sHA.ComputeHash(Encoding.UTF8.GetBytes(s)); StringBuilder stringBuilder = new StringBuilder(array.Length * 2); for (int i = 0; i < array.Length; i++) { stringBuilder.Append(array[i].ToString("x2")); } return stringBuilder.ToString(); } private void LoadPendingDeaths() { if (!_persistPendingDeaths.Value) { return; } try { if (!File.Exists(_pendingFilePath)) { return; } string[] array = File.ReadAllLines(_pendingFilePath); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length > 0) { _persistedPending.Add(text); } } if (_persistedPending.Count > 0) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("Loaded " + _persistedPending.Count + " persisted pending death latch(es).")); } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Failed to load pending-death persistence file: " + ex.Message)); } } private void SavePendingDeaths() { try { List<string> list = new List<string>(_persistedPending); list.Sort(StringComparer.Ordinal); File.WriteAllLines(_pendingFilePath, list.ToArray()); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Failed to save pending-death persistence file: " + ex.Message)); } } private static float HorizontalDistance(Vector3 a, Vector3 b) { //IL_0000: 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_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) float num = a.x - b.x; float num2 = a.z - b.z; return Mathf.Sqrt(num * num + num2 * num2); } private static string FormatVector(Vector3 p) { return "(" + p.x.ToString("0.0") + ", " + p.y.ToString("0.0") + ", " + p.z.ToString("0.0") + ")"; } }