Decompiled source of StartSpawnOnDeath v1.0.0

BepInEx/plugins/StartSpawnOnDeath/StartSpawnOnDeath.dll

Decompiled 2 months ago
using 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 ago
using 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") + ")";
	}
}