Decompiled source of GhostBuster v1.0.1

Mods/GhostBuster.dll

Decompiled 11 hours ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Text;
using BoneLib;
using BoneLib.BoneMenu;
using BoneLib.Notifications;
using GhostBuster;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using Il2CppSLZ.Marrow.Interaction;
using LabFusion.Entities;
using LabFusion.Network;
using LabFusion.Player;
using LabFusion.SDK.Metadata;
using MelonLoader;
using MelonLoader.Preferences;
using MelonLoader.Utils;
using UnityEngine;
using UnityEngine.Rendering;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: MelonInfo(typeof(GhostBusterMod), "GhostBuster", "1.4.0", "Claude", null)]
[assembly: MelonGame("Stress Level Zero", "BONELAB")]
[assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")]
[assembly: AssemblyCompany("GhostBuster")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("GhostBuster")]
[assembly: AssemblyTitle("GhostBuster")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace GhostBuster;

internal static class Alerts
{
	private static string _logPath;

	internal static void Init()
	{
		try
		{
			string text = Path.Combine(MelonEnvironment.UserDataDirectory, "GhostBuster");
			Directory.CreateDirectory(text);
			_logPath = Path.Combine(text, "detections.log");
			DebugLog.RotateIfBig(_logPath, "detections.old.log", 1048576L);
		}
		catch (Exception e)
		{
			_logPath = null;
			DebugLog.Error("Alerts.Init", e);
		}
		GhostState.Vanished += OnVanished;
		GhostState.Reappeared += OnReappeared;
		GhostState.KnownJoined += OnKnownJoined;
	}

	internal static void Toast(string title, string message, NotificationType type, float seconds)
	{
		//IL_0000: Unknown result type (might be due to invalid IL or missing references)
		//IL_0005: Unknown result type (might be due to invalid IL or missing references)
		//IL_0007: Unknown result type (might be due to invalid IL or missing references)
		//IL_000c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0011: Unknown result type (might be due to invalid IL or missing references)
		//IL_0013: Unknown result type (might be due to invalid IL or missing references)
		//IL_0018: Unknown result type (might be due to invalid IL or missing references)
		//IL_001d: Unknown result type (might be due to invalid IL or missing references)
		//IL_001e: Unknown result type (might be due to invalid IL or missing references)
		//IL_001f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0024: Unknown result type (might be due to invalid IL or missing references)
		//IL_002b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0037: Expected O, but got Unknown
		try
		{
			Notifier.Send(new Notification
			{
				Title = NotificationText.op_Implicit(title),
				Message = NotificationText.op_Implicit(message),
				Type = type,
				PopupLength = seconds,
				ShowTitleOnPopup = true
			});
		}
		catch (Exception)
		{
		}
	}

	internal static void WriteLog(string line)
	{
		if (!Settings.AlertLog.Value || _logPath == null)
		{
			return;
		}
		DebugLog.RotateIfBig(_logPath, "detections.old.log", 1048576L);
		try
		{
			File.AppendAllText(_logPath, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " " + line + Environment.NewLine);
		}
		catch (Exception)
		{
		}
	}

	private static void OnVanished(GhostRecord r)
	{
		string text = r.GhostId + " " + r.Name + " went invisible [" + r.MethodText() + "; likely " + r.Tool() + "]" + ((r.PlatformId != null) ? (" platform=" + r.PlatformId) : "") + (r.Known ? " (SAVED GHOST)" : "") + (r.Friend ? " (allowed)" : "");
		if (!r.IsTest)
		{
			WriteLog(text);
		}
		if (Settings.AutoSave.Value && !r.IsTest && r.PlatformId != null && !GhostBook.Has(r.PlatformId))
		{
			GhostBook.Save(r.PlatformId, r.Name);
			r.Known = true;
		}
		if (!r.Friend)
		{
			GhostBusterMod.Log.Warning(text);
			Sounds.Beep(known: false);
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			if (Settings.AlertToast.Value && realtimeSinceStartup - r.LastAlertAt > 4f)
			{
				r.LastAlertAt = realtimeSinceStartup;
				Toast("Ghost detected", r.Name + " [" + r.GhostId + "] went invisible (" + r.MethodText() + ")", (NotificationType)1, 3.5f);
			}
		}
	}

	private static void OnReappeared(GhostRecord r)
	{
		string line = r.GhostId + " " + r.Name + " is visible again";
		if (!r.IsTest)
		{
			WriteLog(line);
		}
		if (!r.Friend && Settings.AlertToast.Value)
		{
			Toast("Ghost gone", r.Name + " is visible again", (NotificationType)0, 2f);
		}
	}

	private static void OnKnownJoined(GhostRecord r, SavedGhost saved, bool renamed)
	{
		WriteLog("Saved ghost " + saved.Id + " joined as '" + r.Name + "'" + (renamed ? (" (previously: " + string.Join(", ", saved.Names) + ")") : ""));
		if (Settings.KnownAlert.Value)
		{
			Sounds.Beep(known: true);
			Toast("Known ghost joined", renamed ? (saved.Id + " is now '" + r.Name + "' (was: " + OldNames(saved, r.Name) + ")") : (saved.Id + " '" + r.Name + "' joined the lobby"), (NotificationType)1, 5f);
		}
	}

	private static string OldNames(SavedGhost g, string current)
	{
		string text = "";
		foreach (string name in g.Names)
		{
			if (name != current)
			{
				text = text + ((text.Length > 0) ? ", " : "") + name;
			}
		}
		if (text.Length != 0)
		{
			return text;
		}
		return "?";
	}

	internal static void Info(byte id)
	{
		GhostRecord ghostRecord = GhostState.Get(id);
		if (ghostRecord == null)
		{
			Toast("GhostBuster", "That player is gone.", (NotificationType)0, 2f);
			return;
		}
		Vector3 pos;
		string kind;
		string text = ((!GhostState.TryGetBestMarker(ghostRecord, out pos, out kind)) ? "no position known" : (kind + " at " + pos.x.ToString("0") + ", " + pos.y.ToString("0") + ", " + pos.z.ToString("0")));
		string text2 = ghostRecord.GhostId + " | " + ghostRecord.MethodText() + (ghostRecord.Hidden ? (" | invisible for " + GhostHistory.Duration(Time.realtimeSinceStartup - ghostRecord.HiddenSince)) : "") + " | " + text + ((ghostRecord.OwnedCount > 0) ? (" | " + ghostRecord.OwnedCount + " owned props") : "") + (ghostRecord.MetaVoice ? " | mic on" : "") + ((ghostRecord.PlatformId != null) ? (" | platform ID: " + ghostRecord.PlatformId) : " | no platform ID") + (ghostRecord.Known ? " | SAVED" : "");
		Toast(ghostRecord.Name, text2, (NotificationType)0, 8f);
		GhostBusterMod.Log.Msg(ghostRecord.Name + ": " + text2);
	}

	internal static void InfoHistory(string key)
	{
		if (!GhostHistory.TryGet(key, out var e))
		{
			Toast("GhostBuster", "Entry not found.", (NotificationType)0, 2f);
			return;
		}
		string text = e.GhostId + " | " + e.State.ToString() + " | invisible " + e.Times + "x, " + GhostHistory.Duration(GhostHistory.SecondsInvisible(e)) + " in total | " + e.Tool + " | names: " + string.Join(", ", e.Names) + " | last " + e.LastAt.ToString("HH:mm:ss") + ((e.PlatformId != null) ? (" | platform ID: " + e.PlatformId) : " | no platform ID") + (GhostBook.Has(e.PlatformId) ? " | SAVED" : "");
		Toast(e.Name, text, (NotificationType)0, 8f);
		GhostBusterMod.Log.Msg(e.Name + ": " + text);
	}

	internal static void InfoSaved(string platformId)
	{
		if (!GhostBook.TryGet(platformId, out var g))
		{
			Toast("GhostBuster", "Entry not found.", (NotificationType)0, 2f);
			return;
		}
		string text = g.Id + " | names: " + string.Join(", ", g.Names) + " | platform ID: " + g.PlatformId + " | saved " + g.SavedAt.ToString("yyyy-MM-dd") + " | last seen " + g.LastSeen.ToString("yyyy-MM-dd HH:mm");
		Toast(g.CurrentName, text, (NotificationType)0, 8f);
		GhostBusterMod.Log.Msg(g.CurrentName + ": " + text);
	}

	internal static void SaveGhost(string platformId, string name)
	{
		if (string.IsNullOrEmpty(platformId))
		{
			Toast("GhostBuster", "Can't save " + name + ": no platform ID available for this player.", (NotificationType)1, 3f);
			return;
		}
		GhostBook.Save(platformId, name);
		if (!GhostBook.TryGet(platformId, out var g))
		{
			return;
		}
		foreach (GhostRecord value in GhostState.Records.Values)
		{
			if (value.PlatformId == platformId)
			{
				value.Known = true;
			}
		}
		Toast("Ghost saved", name + " saved as " + g.Id + ". You'll be alerted if they join, even under another name.", (NotificationType)3, 4f);
		WriteLog("Saved ghost " + g.Id + " '" + name + "' platform=" + platformId);
		DebugLog.Info("SAVED ghost " + g.Id + " '" + name + "' platform " + platformId);
	}

	internal static void ForgetGhost(string platformId, string name)
	{
		if (!GhostBook.Remove(platformId))
		{
			return;
		}
		foreach (GhostRecord value in GhostState.Records.Values)
		{
			if (value.PlatformId == platformId)
			{
				value.Known = false;
				value.KnownAnnounced = false;
			}
		}
		Toast("Ghost removed", name + " removed from the saved list.", (NotificationType)0, 3f);
		DebugLog.Info("FORGOT ghost '" + name + "' platform " + platformId);
	}

	internal static void JoinSummary()
	{
		int scanned = GhostState.Scanned;
		if (scanned <= 0)
		{
			return;
		}
		int num = 0;
		int num2 = 0;
		int num3 = 0;
		foreach (GhostRecord value in GhostState.Records.Values)
		{
			if (!value.IsTest)
			{
				if (value.Hidden && !value.Friend)
				{
					num++;
				}
				if (value.Known)
				{
					num2++;
				}
				if (!value.Hidden && GhostHistory.WasInvisible(value))
				{
					num3++;
				}
			}
		}
		string text = scanned + ((scanned == 1) ? " other player here" : " other players here");
		if (num > 0)
		{
			text = text + ", " + num + " invisible now";
		}
		if (num2 > 0)
		{
			text = text + ", " + num2 + " saved ghost" + ((num2 == 1) ? "" : "s");
		}
		if (num3 > 0)
		{
			text = text + ", " + num3 + " went invisible before";
		}
		if (num == 0 && num2 == 0 && num3 == 0)
		{
			text += ". Nobody invisible.";
		}
		bool flag = num > 0 || num2 > 0 || num3 > 0;
		Toast("Lobby", text, (NotificationType)(flag ? 1 : 0), 6f);
		WriteLog("Joined lobby: " + text);
	}

	internal static void ExportHistory()
	{
		if (GhostHistory.Count == 0)
		{
			Toast("GhostBuster", "Nothing to export yet. Nobody has been invisible.", (NotificationType)0, 3f);
			return;
		}
		string text = GhostHistory.Export();
		if (text == null)
		{
			Toast("GhostBuster", "Could not write the file.", (NotificationType)2, 3f);
			return;
		}
		Toast("History saved", "UserData\\GhostBuster\\" + Path.GetFileName(text), (NotificationType)3, 5f);
		GhostBusterMod.Log.Msg("History exported to " + text);
	}

	internal static void RadarPing()
	{
		//IL_0092: Unknown result type (might be due to invalid IL or missing references)
		//IL_0095: Unknown result type (might be due to invalid IL or missing references)
		//IL_009a: Unknown result type (might be due to invalid IL or missing references)
		//IL_009f: Unknown result type (might be due to invalid IL or missing references)
		//IL_00a3: Unknown result type (might be due to invalid IL or missing references)
		//IL_00af: Unknown result type (might be due to invalid IL or missing references)
		//IL_00bc: Unknown result type (might be due to invalid IL or missing references)
		//IL_00c1: Unknown result type (might be due to invalid IL or missing references)
		//IL_00f2: Unknown result type (might be due to invalid IL or missing references)
		//IL_00f4: Unknown result type (might be due to invalid IL or missing references)
		//IL_00f6: Unknown result type (might be due to invalid IL or missing references)
		//IL_0166: Unknown result type (might be due to invalid IL or missing references)
		//IL_0184: Unknown result type (might be due to invalid IL or missing references)
		Transform val = null;
		try
		{
			val = Player.Head;
		}
		catch (Exception)
		{
		}
		if ((Object)(object)val == (Object)null)
		{
			Toast("Radar", "Player not ready.", (NotificationType)0, 2f);
			return;
		}
		string text = "";
		int num = 0;
		Vector3 val3 = default(Vector3);
		foreach (GhostRecord item in GhostState.HiddenList())
		{
			if (item.Friend)
			{
				continue;
			}
			if (!GhostState.TryGetBestMarker(item, out var pos, out var kind))
			{
				text = text + ((num > 0) ? " | " : "") + item.Name + ": no position";
				num++;
				continue;
			}
			Vector3 val2 = pos - val.position;
			((Vector3)(ref val3))..ctor(val2.x, 0f, val2.z);
			Vector3 forward = val.forward;
			forward.y = 0f;
			float num2 = ((((Vector3)(ref val3)).sqrMagnitude > 0.01f && ((Vector3)(ref forward)).sqrMagnitude > 0.01f) ? Vector3.SignedAngle(forward, val3, Vector3.up) : 0f);
			string text2 = ((Mathf.Abs(num2) < 20f) ? "ahead" : ((!(Mathf.Abs(num2) > 160f)) ? (Mathf.Abs(num2).ToString("0") + "° " + ((num2 > 0f) ? "right" : "left")) : "behind"));
			if (val2.y > 3f)
			{
				text2 += ", above";
			}
			else if (val2.y < -3f)
			{
				text2 += ", below";
			}
			text = text + ((num > 0) ? " | " : "") + item.Name + ": " + ((Vector3)(ref val2)).magnitude.ToString("0") + " m " + text2 + " (" + kind + ")";
			num++;
			if (num < 4)
			{
				continue;
			}
			break;
		}
		Toast("Ghost radar", (num == 0) ? "No invisible players detected." : text, (NotificationType)1, 5f);
	}
}
internal static class Beacons
{
	private sealed class Marker
	{
		public GameObject Root;

		public Transform Orb;
	}

	private static readonly Dictionary<int, Marker> _markers = new Dictionary<int, Marker>();

	private static readonly List<int> _stale = new List<int>();

	private static readonly Color Amber = new Color(1f, 0.65f, 0.1f, 1f);

	private static readonly Color Cyan = new Color(0.2f, 0.9f, 1f, 1f);

	private static readonly Color Red = new Color(1f, 0.2f, 0.2f, 1f);

	internal static void Update(float now)
	{
		//IL_009e: Unknown result type (might be due to invalid IL or missing references)
		//IL_00e4: Unknown result type (might be due to invalid IL or missing references)
		//IL_00e9: Unknown result type (might be due to invalid IL or missing references)
		//IL_00b2: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ab: Unknown result type (might be due to invalid IL or missing references)
		if (!Settings.Beacons.Value)
		{
			HideAll();
			return;
		}
		_stale.Clear();
		foreach (int key in _markers.Keys)
		{
			_stale.Add(key);
		}
		foreach (GhostRecord value in GhostState.Records.Values)
		{
			if (value.Hidden && !value.Friend)
			{
				if (value.HasLastSeen)
				{
					Show(value.Id * 2, value.LastSeen, value.LastSeenIsLeak ? Red : Amber, value.LastSeenIsLeak ? 0.45f : 0.3f, now);
				}
				if (value.HasEstimate)
				{
					Show(value.Id * 2 + 1, value.Estimate, Cyan, 0.5f, now);
				}
				_stale.Remove(value.Id * 2);
				_stale.Remove(value.Id * 2 + 1);
			}
		}
		for (int i = 0; i < _stale.Count; i++)
		{
			Destroy(_stale[i]);
		}
	}

	internal static void HideAll()
	{
		if (_markers.Count == 0)
		{
			return;
		}
		_stale.Clear();
		foreach (int key in _markers.Keys)
		{
			_stale.Add(key);
		}
		for (int i = 0; i < _stale.Count; i++)
		{
			Destroy(_stale[i]);
		}
	}

	private static void Show(int key, Vector3 pos, Color color, float orbRadius, float now)
	{
		//IL_001d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0040: Unknown result type (might be due to invalid IL or missing references)
		//IL_008f: Unknown result type (might be due to invalid IL or missing references)
		if (!_markers.TryGetValue(key, out var value) || (Object)(object)value.Root == (Object)null)
		{
			value = Create(color, orbRadius);
			if (value == null)
			{
				return;
			}
			_markers[key] = value;
		}
		value.Root.transform.position = pos;
		if ((Object)(object)value.Orb != (Object)null)
		{
			float num = 1f + 0.25f * Mathf.Sin(now * 4f);
			value.Orb.localScale = new Vector3(orbRadius * 2f * num, orbRadius * 2f * num, orbRadius * 2f * num);
		}
	}

	private static Marker Create(Color color, float orbRadius)
	{
		//IL_000c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0016: Expected O, but got Unknown
		//IL_0031: Unknown result type (might be due to invalid IL or missing references)
		//IL_0045: Unknown result type (might be due to invalid IL or missing references)
		//IL_004a: Unknown result type (might be due to invalid IL or missing references)
		//IL_0075: Unknown result type (might be due to invalid IL or missing references)
		//IL_008f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0094: Unknown result type (might be due to invalid IL or missing references)
		try
		{
			Marker marker = new Marker();
			marker.Root = new GameObject("GhostBuster_Beacon");
			((Object)Prim((PrimitiveType)2, marker.Root.transform, new Vector3(0f, 15f, 0f), new Vector3(0.06f, 15f, 0.06f), color)).name = "Pillar";
			GameObject val = Prim((PrimitiveType)0, marker.Root.transform, new Vector3(0f, 1f, 0f), new Vector3(orbRadius * 2f, orbRadius * 2f, orbRadius * 2f), color);
			((Object)val).name = "Orb";
			marker.Orb = val.transform;
			return marker;
		}
		catch (Exception)
		{
			return null;
		}
	}

	private static GameObject Prim(PrimitiveType type, Transform parent, Vector3 localPos, Vector3 localScale, Color color)
	{
		//IL_0000: Unknown result type (might be due to invalid IL or missing references)
		//IL_0030: Unknown result type (might be due to invalid IL or missing references)
		//IL_003c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0071: Unknown result type (might be due to invalid IL or missing references)
		//IL_007e: Unknown result type (might be due to invalid IL or missing references)
		GameObject val = GameObject.CreatePrimitive(type);
		Collider component = val.GetComponent<Collider>();
		if ((Object)(object)component != (Object)null)
		{
			Object.Destroy((Object)(object)component);
		}
		val.transform.SetParent(parent, false);
		val.transform.localPosition = localPos;
		val.transform.localScale = localScale;
		Renderer component2 = val.GetComponent<Renderer>();
		if ((Object)(object)component2 != (Object)null)
		{
			component2.shadowCastingMode = (ShadowCastingMode)0;
			component2.receiveShadows = false;
			Material material = component2.material;
			if ((Object)(object)material != (Object)null)
			{
				material.color = color;
				try
				{
					material.SetColor("_BaseColor", color);
				}
				catch (Exception)
				{
				}
			}
		}
		return val;
	}

	private static void Destroy(int key)
	{
		if (!_markers.TryGetValue(key, out var value))
		{
			return;
		}
		try
		{
			if ((Object)(object)value.Root != (Object)null)
			{
				Object.Destroy((Object)(object)value.Root);
			}
		}
		catch (Exception)
		{
		}
		_markers.Remove(key);
	}
}
internal static class DebugLog
{
	private const long MaxBytes = 2097152L;

	private static string _dir;

	private static string _path;

	private static int _writes;

	private static readonly Dictionary<string, float> _lastErr = new Dictionary<string, float>();

	private static readonly Dictionary<string, int> _repeats = new Dictionary<string, int>();

	internal static string DataDir => _dir;

	internal static string LogPath => _path;

	internal static void Init()
	{
		try
		{
			_dir = Path.Combine(MelonEnvironment.UserDataDirectory, "GhostBuster");
			Directory.CreateDirectory(_dir);
			_path = Path.Combine(_dir, "debug.log");
			if (File.Exists(_path))
			{
				string text = Path.Combine(_dir, "debug.old.log");
				try
				{
					if (File.Exists(text))
					{
						File.Delete(text);
					}
					File.Move(_path, text);
				}
				catch (Exception)
				{
				}
			}
			WriteReadme();
		}
		catch (Exception)
		{
			_path = null;
		}
		Raw("==== GhostBuster 1.4.0 started " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " ====");
		try
		{
			Raw("MelonLoader " + typeof(MelonMod).Assembly.GetName().Version?.ToString() + " | " + Application.productName + " " + Application.version + " | Unity " + Application.unityVersion);
		}
		catch (Exception)
		{
		}
		Raw("Data folder: " + _dir);
		GhostState.Vanished += OnVanished;
		GhostState.Reappeared += OnReappeared;
		GhostState.Left += OnLeft;
		GhostState.KnownJoined += OnKnownJoined;
	}

	internal static void Info(string message)
	{
		if (Settings.DebugLogging.Value)
		{
			Raw("INFO  " + message);
		}
	}

	internal static void Warn(string message)
	{
		if (Settings.DebugLogging.Value)
		{
			Raw("WARN  " + message);
		}
	}

	internal static void Error(string where, Exception e)
	{
		try
		{
			string key = where + "|" + e.GetType().Name + "|" + e.Message;
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			if (_lastErr.TryGetValue(key, out var value) && realtimeSinceStartup - value < 10f)
			{
				_repeats.TryGetValue(key, out var value2);
				_repeats[key] = value2 + 1;
				return;
			}
			if (_repeats.TryGetValue(key, out var value3) && value3 > 0)
			{
				Raw("ERROR " + where + ": (the same error happened " + value3 + " more times)");
				_repeats[key] = 0;
			}
			if (_lastErr.Count > 200)
			{
				_lastErr.Clear();
				_repeats.Clear();
			}
			_lastErr[key] = realtimeSinceStartup;
			Raw("ERROR " + where + ": " + e);
		}
		catch (Exception)
		{
		}
	}

	private static void Raw(string line)
	{
		if (_path == null)
		{
			return;
		}
		try
		{
			if (++_writes % 100 == 0)
			{
				FileInfo fileInfo = new FileInfo(_path);
				if (fileInfo.Exists && fileInfo.Length > 2097152)
				{
					string text = Path.Combine(_dir, "debug.old.log");
					if (File.Exists(text))
					{
						File.Delete(text);
					}
					File.Move(_path, text);
				}
			}
			File.AppendAllText(_path, DateTime.Now.ToString("HH:mm:ss.fff") + " " + line + Environment.NewLine);
		}
		catch (Exception)
		{
		}
	}

	internal static void RotateIfBig(string path, string oldName, long maxBytes)
	{
		try
		{
			if (path == null)
			{
				return;
			}
			FileInfo fileInfo = new FileInfo(path);
			if (fileInfo.Exists && fileInfo.Length > maxBytes)
			{
				string text = Path.Combine(Path.GetDirectoryName(path), oldName);
				if (File.Exists(text))
				{
					File.Delete(text);
				}
				File.Move(path, text);
			}
		}
		catch (Exception)
		{
		}
	}

	private static void WriteReadme()
	{
		string path = Path.Combine(_dir, "README.txt");
		if (!File.Exists(path))
		{
			File.WriteAllText(path, "GhostBuster data folder\r\n=======================\r\ndebug.log          What the mod did this session, plus every error with details. Send this when something is wrong.\r\ndebug.old.log      The log of the previous session.\r\ndetections.log     One line each time someone went invisible / became visible again.\r\nsaved_ghosts.tsv   The people you saved: ID, usernames, platform ID. Keep it private, it holds other players' account IDs.\r\nsaved_ghosts.tsv.bak  The previous version of the saved list (automatic backup).\r\nhistory_DATE.txt   Exports made with Tools > Export history to file.\r\nYour settings are in MelonPreferences.cfg (category GhostBuster) in the UserData folder.\r\n");
		}
	}

	private static string Pos(Vector3 v)
	{
		return "(" + v.x.ToString("0.0") + ", " + v.y.ToString("0.0") + ", " + v.z.ToString("0.0") + ")";
	}

	private static string Who(GhostRecord r)
	{
		return "'" + r.Name + "' id " + r.Id + " " + r.GhostId + (string.IsNullOrEmpty(r.PlatformId) ? " (no platform ID)" : (" platform " + r.PlatformId));
	}

	private static void OnVanished(GhostRecord r)
	{
		//IL_0051: Unknown result type (might be due to invalid IL or missing references)
		//IL_008c: Unknown result type (might be due to invalid IL or missing references)
		Info("VANISHED " + Who(r) + " | evidence: " + r.MethodText() + " | likely " + r.Tool() + (r.HasLastSeen ? (" | last seen " + Pos(r.LastSeen) + (r.LastSeenIsLeak ? " (leak)" : "")) : " | never seen visible") + (r.HasEstimate ? (" | prop estimate " + Pos(r.Estimate) + (r.EstimateLive ? " live" : " static")) : "") + (r.Friend ? " | allowed" : "") + (r.Known ? " | SAVED GHOST" : ""));
	}

	private static void OnReappeared(GhostRecord r)
	{
		Info("VISIBLE AGAIN " + Who(r) + " after " + GhostHistory.Duration(Time.realtimeSinceStartup - r.HiddenSince));
	}

	private static void OnLeft(GhostRecord r)
	{
		Info("LEFT " + Who(r) + (r.Hidden ? " (was invisible)" : ""));
	}

	private static void OnKnownJoined(GhostRecord r, SavedGhost saved, bool renamed)
	{
		Info("SAVED GHOST JOINED " + Who(r) + (renamed ? (" | renamed, old names: " + string.Join(", ", saved.Names)) : ""));
	}

	internal static void Snapshot(string fusionReport)
	{
		//IL_0277: Unknown result type (might be due to invalid IL or missing references)
		//IL_02c2: Unknown result type (might be due to invalid IL or missing references)
		StringBuilder stringBuilder = new StringBuilder();
		stringBuilder.AppendLine();
		stringBuilder.AppendLine("---- SNAPSHOT " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " ----");
		stringBuilder.AppendLine("LabFusion loaded: " + GhostState.FusionPresent + " | in a lobby: " + GhostState.InServer + " | lobby #" + GhostState.LobbyIndex + " | remote players scanned: " + GhostState.Scanned);
		stringBuilder.AppendLine("Invisible now: " + GhostState.HiddenCount() + " | history entries: " + GhostHistory.Count + " | saved ghosts: " + GhostBook.Count + " | rig outlines shown: " + Rigs.Count);
		stringBuilder.AppendLine("Settings: " + Settings.Describe());
		if (!string.IsNullOrEmpty(fusionReport))
		{
			stringBuilder.AppendLine("LabFusion self-check:");
			stringBuilder.Append(fusionReport);
		}
		float realtimeSinceStartup = Time.realtimeSinceStartup;
		stringBuilder.AppendLine("Players:");
		foreach (GhostRecord value in GhostState.Records.Values)
		{
			stringBuilder.AppendLine("  " + Who(value) + " | lobby " + value.Lobby + " | hidden=" + value.Hidden + (value.Hidden ? (" for " + GhostHistory.Duration(realtimeSinceStartup - value.HiddenSince)) : "") + " | evidence: " + value.MethodText() + " | ever invisible=" + value.EverHidden + " saved=" + value.Known + " allowed=" + value.Friend + (value.IsTest ? " TEST" : "") + " | last seen " + (value.HasLastSeen ? (Pos(value.LastSeen) + (value.LastSeenIsLeak ? " leak" : "")) : "none") + " | estimate " + (value.HasEstimate ? (Pos(value.Estimate) + (value.EstimateLive ? " live" : " static") + " age " + (realtimeSinceStartup - value.EstimateAt).ToString("0.0") + "s, " + value.OwnedCount + " props") : "none") + " | rig " + (value.HasRig ? "yes" : "no"));
		}
		if (GhostState.Records.Count == 0)
		{
			stringBuilder.AppendLine("  (nobody)");
		}
		stringBuilder.AppendLine("---- END SNAPSHOT ----");
		Raw(stringBuilder.ToString());
	}
}
internal sealed class SavedGhost
{
	public string Id;

	public string PlatformId;

	public List<string> Names = new List<string>();

	public DateTime SavedAt;

	public DateTime LastSeen;

	public string CurrentName
	{
		get
		{
			if (Names.Count <= 0)
			{
				return "?";
			}
			return Names[Names.Count - 1];
		}
	}
}
internal static class GhostBook
{
	private static string _path;

	private static readonly Dictionary<string, SavedGhost> _byPlatform = new Dictionary<string, SavedGhost>(StringComparer.Ordinal);

	internal static int Count => _byPlatform.Count;

	internal static void Init()
	{
		try
		{
			string text = Path.Combine(MelonEnvironment.UserDataDirectory, "GhostBuster");
			Directory.CreateDirectory(text);
			_path = Path.Combine(text, "saved_ghosts.tsv");
			if (!File.Exists(_path) && File.Exists(_path + ".bak"))
			{
				File.Copy(_path + ".bak", _path);
				DebugLog.Warn("saved_ghosts.tsv was missing - restored it from saved_ghosts.tsv.bak");
			}
			Load();
			DebugLog.Info("Saved ghosts loaded: " + _byPlatform.Count);
		}
		catch (Exception e)
		{
			_path = null;
			DebugLog.Error("GhostBook.Init", e);
		}
	}

	internal static string MakeId(string platformId)
	{
		try
		{
			using SHA256 sHA = SHA256.Create();
			byte[] array = sHA.ComputeHash(Encoding.UTF8.GetBytes(platformId));
			return "GB-" + array[0].ToString("X2") + array[1].ToString("X2") + array[2].ToString("X2");
		}
		catch (Exception)
		{
			return "GB-" + (platformId.GetHashCode() & 0xFFFFFF).ToString("X6");
		}
	}

	internal static bool TryGet(string platformId, out SavedGhost g)
	{
		g = null;
		if (!string.IsNullOrEmpty(platformId))
		{
			return _byPlatform.TryGetValue(platformId, out g);
		}
		return false;
	}

	internal static bool Has(string platformId)
	{
		if (!string.IsNullOrEmpty(platformId))
		{
			return _byPlatform.ContainsKey(platformId);
		}
		return false;
	}

	internal static List<SavedGhost> All()
	{
		List<SavedGhost> list = new List<SavedGhost>(_byPlatform.Values);
		list.Sort((SavedGhost a, SavedGhost b) => b.LastSeen.CompareTo(a.LastSeen));
		return list;
	}

	internal static bool Save(string platformId, string name)
	{
		if (string.IsNullOrEmpty(platformId))
		{
			return false;
		}
		if (!_byPlatform.TryGetValue(platformId, out var value))
		{
			value = new SavedGhost
			{
				Id = MakeId(platformId),
				PlatformId = platformId,
				SavedAt = DateTime.Now
			};
			_byPlatform[platformId] = value;
		}
		AddName(value, name);
		value.LastSeen = DateTime.Now;
		Write();
		GhostState.Dirty = true;
		return true;
	}

	internal static void Touch(string platformId, string name)
	{
		if (TryGet(platformId, out var g))
		{
			bool num = AddName(g, name);
			g.LastSeen = DateTime.Now;
			if (num)
			{
				Write();
				GhostState.Dirty = true;
			}
		}
	}

	internal static bool Remove(string platformId)
	{
		if (string.IsNullOrEmpty(platformId) || !_byPlatform.Remove(platformId))
		{
			return false;
		}
		Write();
		GhostState.Dirty = true;
		return true;
	}

	private static bool AddName(SavedGhost g, string name)
	{
		name = Clean(name);
		if (name.Length == 0)
		{
			return false;
		}
		if (g.Names.Count > 0 && g.Names[g.Names.Count - 1] == name)
		{
			return false;
		}
		g.Names.Remove(name);
		g.Names.Add(name);
		if (g.Names.Count > 12)
		{
			g.Names.RemoveAt(0);
		}
		return true;
	}

	private static string Clean(string s)
	{
		if (string.IsNullOrEmpty(s))
		{
			return "";
		}
		return s.Replace('\t', ' ').Replace('\r', ' ').Replace('\n', ' ')
			.Replace('|', '/')
			.Trim();
	}

	private static void Load()
	{
		_byPlatform.Clear();
		if (_path == null || !File.Exists(_path))
		{
			return;
		}
		string[] array = File.ReadAllLines(_path);
		foreach (string text in array)
		{
			if (string.IsNullOrWhiteSpace(text) || text[0] == '#')
			{
				continue;
			}
			string[] array2 = text.Split('\t');
			if (array2.Length < 5)
			{
				continue;
			}
			SavedGhost savedGhost = new SavedGhost
			{
				Id = array2[0],
				PlatformId = array2[1]
			};
			savedGhost.SavedAt = (DateTime.TryParse(array2[2], null, DateTimeStyles.RoundtripKind, out var result) ? result : DateTime.Now);
			savedGhost.LastSeen = (DateTime.TryParse(array2[3], null, DateTimeStyles.RoundtripKind, out result) ? result : savedGhost.SavedAt);
			string[] array3 = array2[4].Split('|');
			foreach (string text2 in array3)
			{
				if (text2.Length > 0)
				{
					savedGhost.Names.Add(text2);
				}
			}
			if (savedGhost.PlatformId.Length > 0)
			{
				_byPlatform[savedGhost.PlatformId] = savedGhost;
			}
		}
	}

	private static void Write()
	{
		if (_path == null)
		{
			return;
		}
		try
		{
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.AppendLine("# GhostBuster saved ghosts: id <TAB> platformId <TAB> saved <TAB> lastSeen <TAB> names(|separated, newest last)");
			foreach (SavedGhost value in _byPlatform.Values)
			{
				stringBuilder.Append(value.Id).Append('\t').Append(value.PlatformId)
					.Append('\t')
					.Append(value.SavedAt.ToString("o"))
					.Append('\t')
					.Append(value.LastSeen.ToString("o"))
					.Append('\t')
					.Append(string.Join("|", value.Names))
					.AppendLine();
			}
			string text = _path + ".tmp";
			File.WriteAllText(text, stringBuilder.ToString());
			if (File.Exists(_path))
			{
				try
				{
					File.Replace(text, _path, _path + ".bak");
					return;
				}
				catch (Exception)
				{
					File.Copy(_path, _path + ".bak", overwrite: true);
					File.Delete(_path);
					File.Move(text, _path);
					return;
				}
			}
			File.Move(text, _path);
		}
		catch (Exception e)
		{
			DebugLog.Error("GhostBook.Write", e);
		}
	}
}
public sealed class GhostBusterMod : MelonMod
{
	internal const string Version = "1.4.0";

	internal static GhostBusterMod Instance;

	private const float ScanInterval = 0.2f;

	private const float EstimateInterval = 0.5f;

	private float _nextScan;

	private float _nextEstimate;

	private float _nextRadar;

	private float _nextMenuTry;

	private float _nextFusionCheck;

	private float _nextTags;

	private float _summaryAt;

	private int _lastLobby;

	private bool _lastInServer;

	private int _menuTries;

	private bool _fusionBroken;

	private bool _scanRequested;

	internal static Instance Log => ((MelonBase)Instance).LoggerInstance;

	public override void OnInitializeMelon()
	{
		Instance = this;
		Settings.Init();
		DebugLog.Init();
		GhostBook.Init();
		GhostHistory.Init();
		Alerts.Init();
		((MelonBase)this).LoggerInstance.Msg("GhostBuster loaded. Watching for GhostMode / WristHub invisibility. Saved ghosts: " + GhostBook.Count);
		DebugLog.Info("Mod initialized. Saved ghosts: " + GhostBook.Count + " | settings: " + Settings.Describe());
	}

	public override void OnSceneWasInitialized(int buildIndex, string sceneName)
	{
		DebugLog.Info("Scene initialized: " + sceneName + " (" + buildIndex + ")");
		if (!GhostMenu.Built)
		{
			_nextMenuTry = 0f;
		}
	}

	public override void OnUpdate()
	{
		float realtimeSinceStartup = Time.realtimeSinceStartup;
		if (!GhostMenu.Built && _menuTries < 120 && realtimeSinceStartup >= _nextMenuTry)
		{
			_menuTries++;
			_nextMenuTry = realtimeSinceStartup + 2f;
			GhostMenu.TryBuild();
		}
		if (!GhostState.FusionPresent && realtimeSinceStartup >= _nextFusionCheck)
		{
			_nextFusionCheck = realtimeSinceStartup + 5f;
			GhostState.FusionPresent = IsFusionLoaded();
			if (GhostState.FusionPresent)
			{
				LogFusionReport("LabFusion found");
			}
		}
		if (Settings.Enabled.Value && GhostState.FusionPresent && !_fusionBroken)
		{
			if (_scanRequested || realtimeSinceStartup >= _nextScan)
			{
				_scanRequested = false;
				_nextScan = realtimeSinceStartup + 0.2f;
				SafeScan(realtimeSinceStartup);
			}
			if (Settings.EntityEstimate.Value && realtimeSinceStartup >= _nextEstimate)
			{
				_nextEstimate = realtimeSinceStartup + 0.5f;
				SafeEstimate(realtimeSinceStartup);
			}
			if (realtimeSinceStartup >= _nextTags)
			{
				_nextTags = realtimeSinceStartup + 0.5f;
				SafeNameTags();
			}
		}
		else if (!Settings.Enabled.Value)
		{
			Beacons.HideAll();
			if (GhostState.FusionPresent && !_fusionBroken && realtimeSinceStartup >= _nextTags)
			{
				_nextTags = realtimeSinceStartup + 0.5f;
				SafeNameTags();
			}
		}
		if (GhostState.InServer != _lastInServer)
		{
			_lastInServer = GhostState.InServer;
			DebugLog.Info(GhostState.InServer ? ("JOINED a Fusion lobby (#" + GhostState.LobbyIndex + ")") : "LEFT the Fusion lobby");
		}
		if (GhostState.LobbyIndex != _lastLobby)
		{
			_lastLobby = GhostState.LobbyIndex;
			_summaryAt = realtimeSinceStartup + 6f;
		}
		if (_summaryAt > 0f && realtimeSinceStartup >= _summaryAt)
		{
			_summaryAt = 0f;
			if (Settings.JoinSummary.Value && Settings.Enabled.Value && GhostState.InServer)
			{
				Alerts.JoinSummary();
			}
		}
		if (!GhostState.FusionPresent)
		{
			ExpireTest(realtimeSinceStartup);
		}
		if (Settings.RigEnabled.Value && Settings.Enabled.Value && GhostState.FusionPresent && !_fusionBroken)
		{
			SafeRig(realtimeSinceStartup);
		}
		AnimateTest(realtimeSinceStartup);
		Beacons.Update(realtimeSinceStartup);
		Rigs.Tick(realtimeSinceStartup);
		float value = Settings.RadarSeconds.Value;
		if (value >= 1f && realtimeSinceStartup >= _nextRadar)
		{
			_nextRadar = realtimeSinceStartup + value;
			if (GhostState.HiddenCount() > 0)
			{
				Alerts.RadarPing();
			}
		}
		GhostMenu.Tick(realtimeSinceStartup);
	}

	[MethodImpl(MethodImplOptions.NoInlining)]
	private void SafeScan(float now)
	{
		try
		{
			GhostDetector.Scan(now);
		}
		catch (Exception ex) when (ex is TypeLoadException || ex is MissingMethodException || ex is MissingFieldException || ex is FileNotFoundException || ex is TypeInitializationException)
		{
			_fusionBroken = true;
			((MelonBase)this).LoggerInstance.Error("LabFusion API mismatch - detection disabled: " + ex.GetType().Name + ": " + ex.Message);
			DebugLog.Error("Scan (LabFusion API mismatch - detection disabled)", ex);
		}
		catch (Exception ex2)
		{
			((MelonBase)this).LoggerInstance.Warning("Scan error: " + ex2.GetType().Name + ": " + ex2.Message);
			DebugLog.Error("Scan", ex2);
		}
	}

	[MethodImpl(MethodImplOptions.NoInlining)]
	private void SafeEstimate(float now)
	{
		try
		{
			foreach (GhostRecord value in GhostState.Records.Values)
			{
				if (value.Hidden && !value.Friend && !value.IsTest)
				{
					EntityEstimator.Update(value, now);
				}
			}
		}
		catch (Exception e)
		{
			DebugLog.Error("Estimate", e);
		}
	}

	[MethodImpl(MethodImplOptions.NoInlining)]
	private void SafeNameTags()
	{
		try
		{
			NameTags.Update();
		}
		catch (Exception e)
		{
			DebugLog.Error("NameTags", e);
		}
	}

	[MethodImpl(MethodImplOptions.NoInlining)]
	private void SafeRig(float now)
	{
		try
		{
			foreach (GhostRecord value in GhostState.Records.Values)
			{
				if (value.Hidden && !value.Friend && !value.IsTest)
				{
					RigReader.Update(value, now);
				}
			}
		}
		catch (Exception e)
		{
			DebugLog.Error("Rig", e);
		}
	}

	[MethodImpl(MethodImplOptions.NoInlining)]
	private static bool TeleportSafe(Vector3 p)
	{
		//IL_0000: Unknown result type (might be due to invalid IL or missing references)
		try
		{
			return FusionActions.TeleportTo(p);
		}
		catch (Exception e)
		{
			DebugLog.Error("Teleport", e);
			return false;
		}
	}

	[MethodImpl(MethodImplOptions.NoInlining)]
	private static string FusionReportSafe()
	{
		try
		{
			return FusionInfo.Report();
		}
		catch (Exception ex)
		{
			DebugLog.Error("FusionInfo", ex);
			return "  (self-check failed: " + ex.GetType().Name + ")\r\n";
		}
	}

	private static void LogFusionReport(string title)
	{
		DebugLog.Info(title + ". Self-check:\r\n" + FusionReportSafe().TrimEnd());
	}

	internal static void WriteSnapshot()
	{
		DebugLog.Snapshot(GhostState.FusionPresent ? FusionReportSafe() : "  LabFusion is not loaded.\r\n");
		Alerts.Toast("Debug snapshot", "Written to UserData\\GhostBuster\\debug.log. Send me that file if something is wrong.", (NotificationType)3, 5f);
	}

	internal static void ShowDataFolder()
	{
		string text = DebugLog.DataDir ?? "(could not be created)";
		Alerts.Toast("Data folder", text, (NotificationType)0, 8f);
		Log.Msg("Data folder: " + text);
	}

	private static bool IsFusionLoaded()
	{
		Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
		for (int i = 0; i < assemblies.Length; i++)
		{
			if (assemblies[i].GetName().Name == "LabFusion")
			{
				return true;
			}
		}
		return false;
	}

	internal static void RequestScan()
	{
		if (Instance != null)
		{
			Instance._scanRequested = true;
		}
		Alerts.Toast("GhostBuster", "Scanning...", (NotificationType)0, 1.5f);
	}

	internal static void GoToMarker(byte id)
	{
		//IL_003b: Unknown result type (might be due to invalid IL or missing references)
		GhostRecord ghostRecord = GhostState.Get(id);
		if (ghostRecord == null || !GhostState.TryGetBestMarker(ghostRecord, out var pos, out var kind))
		{
			Alerts.Toast("GhostBuster", "No marker position for that player yet.", (NotificationType)0, 2.5f);
		}
		else if (ghostRecord.IsTest || !GhostState.FusionPresent || !TeleportSafe(pos))
		{
			Alerts.Toast("GhostBuster", "Could not teleport (" + kind + " position).", (NotificationType)1, 2.5f);
		}
	}

	internal static void SpawnTestGhost()
	{
		//IL_0043: Unknown result type (might be due to invalid IL or missing references)
		//IL_0049: Unknown result type (might be due to invalid IL or missing references)
		//IL_0053: Unknown result type (might be due to invalid IL or missing references)
		//IL_0058: Unknown result type (might be due to invalid IL or missing references)
		//IL_003b: Unknown result type (might be due to invalid IL or missing references)
		//IL_005d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0071: Unknown result type (might be due to invalid IL or missing references)
		//IL_00d8: Unknown result type (might be due to invalid IL or missing references)
		//IL_00d9: Unknown result type (might be due to invalid IL or missing references)
		//IL_00f1: Unknown result type (might be due to invalid IL or missing references)
		//IL_0101: Unknown result type (might be due to invalid IL or missing references)
		//IL_0106: Unknown result type (might be due to invalid IL or missing references)
		//IL_010b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0126: Unknown result type (might be due to invalid IL or missing references)
		//IL_0127: Unknown result type (might be due to invalid IL or missing references)
		//IL_014a: Unknown result type (might be due to invalid IL or missing references)
		//IL_014f: 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_0169: Unknown result type (might be due to invalid IL or missing references)
		//IL_017e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0183: Unknown result type (might be due to invalid IL or missing references)
		//IL_0189: Unknown result type (might be due to invalid IL or missing references)
		//IL_018e: Unknown result type (might be due to invalid IL or missing references)
		if (GhostState.Get(250) != null)
		{
			GhostState.RemoveRecord(250);
		}
		Transform val = null;
		try
		{
			val = Player.Head;
		}
		catch (Exception)
		{
		}
		Vector3 val2 = (Vector3)(((Object)(object)val != (Object)null) ? (val.position + val.forward * 6f) : new Vector3(0f, 1f, 0f));
		val2.y = (((Object)(object)val != (Object)null) ? (val.position.y - 1f) : 0f);
		float realtimeSinceStartup = Time.realtimeSinceStartup;
		GhostRecord ghostRecord = new GhostRecord
		{
			Id = 250,
			Name = "TestGhost",
			IsTest = true,
			TestExpiresAt = realtimeSinceStartup + 600f,
			PoseSpoof = true,
			Hidden = true,
			HiddenSince = realtimeSinceStartup,
			HasLastSeen = true,
			LastSeen = val2,
			LastSeenAt = realtimeSinceStartup,
			GhostId = "GB-T0-250",
			TestCenter = val2 - new Vector3(2.5f, 0f, 0f),
			TestStart = realtimeSinceStartup,
			HasEstimate = true,
			EstimateLive = true,
			Estimate = val2,
			EstimateAt = realtimeSinceStartup,
			HasRig = true,
			RigHead = new Vector3(0f, 0.7f, 0f),
			RigLeft = new Vector3(-0.3f, 0.1f, 0.35f),
			RigRight = new Vector3(0.3f, 0.1f, 0.35f),
			RigHeadRot = Quaternion.identity,
			RigAt = realtimeSinceStartup
		};
		GhostState.Records[250] = ghostRecord;
		GhostState.RaiseVanished(ghostRecord);
	}

	internal static void StopTestGhost()
	{
		GhostRecord ghostRecord = GhostState.Get(250);
		if (ghostRecord != null && ghostRecord.IsTest)
		{
			GhostState.RemoveRecord(250);
			Alerts.Toast("GhostBuster", "Test ghost stopped.", (NotificationType)0, 2f);
		}
		else
		{
			Alerts.Toast("GhostBuster", "No test ghost is running.", (NotificationType)0, 2f);
		}
	}

	private static void ExpireTest(float now)
	{
		GhostRecord ghostRecord = GhostState.Get(250);
		if (ghostRecord != null && ghostRecord.IsTest && now >= ghostRecord.TestExpiresAt)
		{
			GhostState.RemoveRecord(250);
		}
	}

	private static void AnimateTest(float now)
	{
		//IL_0031: Unknown result type (might be due to invalid IL or missing references)
		//IL_0047: Unknown result type (might be due to invalid IL or missing references)
		//IL_0051: Unknown result type (might be due to invalid IL or missing references)
		//IL_0056: Unknown result type (might be due to invalid IL or missing references)
		//IL_005b: Unknown result type (might be due to invalid IL or missing references)
		//IL_00a6: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ab: 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)
		//IL_00cb: Unknown result type (might be due to invalid IL or missing references)
		//IL_00d0: Unknown result type (might be due to invalid IL or missing references)
		//IL_00d5: Unknown result type (might be due to invalid IL or missing references)
		GhostRecord ghostRecord = GhostState.Get(250);
		if (ghostRecord != null && ghostRecord.IsTest && !(now >= ghostRecord.TestExpiresAt))
		{
			float num = (now - ghostRecord.TestStart) * 0.7f;
			ghostRecord.Estimate = ghostRecord.TestCenter + new Vector3(Mathf.Cos(num), 0f, Mathf.Sin(num)) * 2.5f;
			ghostRecord.HasEstimate = true;
			ghostRecord.EstimateLive = true;
			ghostRecord.EstimateAt = now;
			ghostRecord.HasRig = true;
			ghostRecord.RigAt = now;
			ghostRecord.RigRight = new Vector3(0.3f, 0.25f + 0.35f * Mathf.Sin(now * 4f), 0.3f);
			Vector3 val = default(Vector3);
			((Vector3)(ref val))..ctor(0f - Mathf.Sin(num), 0f, Mathf.Cos(num));
			ghostRecord.RigHeadRot = Quaternion.LookRotation(val, Vector3.up);
		}
	}
}
internal static class GhostDetector
{
	private const string WristHubKey = "WristHub.Invisibility";

	private static readonly Vector3 ParkingSpot = new Vector3(0f, -4096f, 0f);

	private const float ReleaseGrace = 1f;

	private const float HeuristicHold = 2f;

	private static readonly List<NetworkPlayer> _players = new List<NetworkPlayer>(32);

	private static readonly HashSet<byte> _seen = new HashSet<byte>();

	private static readonly List<byte> _gone = new List<byte>();

	private static readonly Vector3[] _net = (Vector3[])(object)new Vector3[3];

	private static Vector3 _phys;

	private static bool _bound;

	private static PropertyInfo _hasReceivedPose;

	private static PropertyInfo _receivedPelvis;

	private static PropertyInfo _platformProp;

	private static FieldInfo _platformField;

	private static bool _wasInServer;

	private static bool _hasPhys;

	private static void Bind()
	{
		if (_bound)
		{
			return;
		}
		_bound = true;
		try
		{
			Type typeFromHandle = typeof(NetworkPlayer);
			BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public;
			_hasReceivedPose = typeFromHandle.GetProperty("HasReceivedPose", bindingFlags);
			if (_hasReceivedPose == null || _hasReceivedPose.PropertyType != typeof(bool))
			{
				PropertyInfo property = typeFromHandle.GetProperty("ReceivedPose", bindingFlags);
				_hasReceivedPose = ((property != null && property.PropertyType == typeof(bool)) ? property : null);
			}
			PropertyInfo property2 = typeFromHandle.GetProperty("ReceivedPelvisPose", bindingFlags);
			_receivedPelvis = ((property2 != null && property2.PropertyType == typeof(BodyPose)) ? property2 : null);
			_platformProp = typeof(PlayerID).GetProperty("PlatformID", bindingFlags | BindingFlags.NonPublic);
			if (_platformProp == null)
			{
				_platformField = typeof(PlayerID).GetField("<PlatformID>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic);
			}
		}
		catch (Exception e)
		{
			_hasReceivedPose = null;
			_receivedPelvis = null;
			_platformProp = null;
			_platformField = null;
			DebugLog.Error("GhostDetector.Bind", e);
		}
	}

	internal static string BindReport()
	{
		Bind();
		Type type = ((_platformProp != null) ? _platformProp.PropertyType : ((_platformField != null) ? _platformField.FieldType : null));
		string text = ((type == typeof(ulong)) ? " -> Steam/PCVR LabFusion" : ((type == typeof(string)) ? " -> Epic/EOS LabFusion" : ""));
		StringBuilder stringBuilder = new StringBuilder();
		stringBuilder.AppendLine("  PlayerID.PlatformID: " + ((type == null) ? "NOT FOUND (ghost IDs and saving will not work)" : (type.Name + text)));
		stringBuilder.AppendLine("  received-pose flag: " + ((_hasReceivedPose != null) ? _hasReceivedPose.Name : "not found (fine)"));
		stringBuilder.AppendLine("  ReceivedPelvisPose: " + ((_receivedPelvis != null) ? "found" : "not found (fine on the Steam build)"));
		return stringBuilder.ToString();
	}

	private static string ReadPlatformId(PlayerID pid)
	{
		try
		{
			object value;
			if (_platformProp != null)
			{
				value = _platformProp.GetValue(pid, null);
			}
			else
			{
				if (!(_platformField != null))
				{
					return null;
				}
				value = _platformField.GetValue(pid);
			}
			string text = value?.ToString();
			if (string.IsNullOrWhiteSpace(text) || text == "0")
			{
				return null;
			}
			return text.Trim();
		}
		catch (Exception e)
		{
			DebugLog.Error("ReadPlatformId", e);
			return null;
		}
	}

	internal static void Scan(float now)
	{
		//IL_03c1: Unknown result type (might be due to invalid IL or missing references)
		//IL_03c6: Unknown result type (might be due to invalid IL or missing references)
		//IL_03c8: Unknown result type (might be due to invalid IL or missing references)
		//IL_03cd: Unknown result type (might be due to invalid IL or missing references)
		//IL_0401: Unknown result type (might be due to invalid IL or missing references)
		//IL_03fa: Unknown result type (might be due to invalid IL or missing references)
		//IL_0406: Unknown result type (might be due to invalid IL or missing references)
		//IL_0408: Unknown result type (might be due to invalid IL or missing references)
		//IL_0411: Unknown result type (might be due to invalid IL or missing references)
		//IL_0413: Unknown result type (might be due to invalid IL or missing references)
		//IL_0522: Unknown result type (might be due to invalid IL or missing references)
		//IL_0524: Unknown result type (might be due to invalid IL or missing references)
		//IL_0428: Unknown result type (might be due to invalid IL or missing references)
		//IL_04df: Unknown result type (might be due to invalid IL or missing references)
		//IL_04db: Unknown result type (might be due to invalid IL or missing references)
		//IL_04e1: Unknown result type (might be due to invalid IL or missing references)
		//IL_044f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0451: Unknown result type (might be due to invalid IL or missing references)
		//IL_0442: Unknown result type (might be due to invalid IL or missing references)
		//IL_0444: Unknown result type (might be due to invalid IL or missing references)
		Bind();
		bool num = (GhostState.InServer = NetworkInfo.HasServer);
		if (num && !_wasInServer)
		{
			GhostState.LobbyIndex++;
		}
		_wasInServer = num;
		if (!num)
		{
			List<byte> list = new List<byte>();
			foreach (KeyValuePair<byte, GhostRecord> record in GhostState.Records)
			{
				if (!record.Value.IsTest || !(now < record.Value.TestExpiresAt))
				{
					list.Add(record.Key);
				}
			}
			foreach (byte item in list)
			{
				GhostState.RemoveRecord(item);
			}
			GhostState.Scanned = 0;
			return;
		}
		_players.Clear();
		foreach (NetworkPlayer player in NetworkPlayer.Players)
		{
			if (player != null)
			{
				_players.Add(player);
			}
		}
		_seen.Clear();
		int num2 = 0;
		float tol = Mathf.Max(0.05f, Settings.PoseTolerance.Value);
		bool value = Settings.UnderworldHeuristic.Value;
		float value2 = Settings.UnderworldY.Value;
		float num3 = Mathf.Max(0f, Settings.PoseHoldSeconds.Value);
		for (int i = 0; i < _players.Count; i++)
		{
			NetworkPlayer val = _players[i];
			PlayerID playerID = val.PlayerID;
			if (playerID == null || playerID.IsMe || !playerID.IsValid)
			{
				continue;
			}
			byte smallID = playerID.SmallID;
			_seen.Add(smallID);
			num2++;
			if (!GhostState.Records.TryGetValue(smallID, out var value3))
			{
				value3 = new GhostRecord
				{
					Id = smallID,
					Lobby = GhostState.LobbyIndex,
					FirstSeenAt = now
				};
				value3.GhostId = "GB-T" + value3.Lobby + "-" + smallID;
				GhostState.Records[smallID] = value3;
			}
			string text = SafeName(val, smallID);
			if (text != value3.Name)
			{
				value3.Name = text;
				value3.Friend = Settings.IsFriend(text);
				GhostState.Dirty = true;
				if (value3.Known)
				{
					GhostBook.Touch(value3.PlatformId, text);
				}
			}
			if (value3.PlatformId == null)
			{
				string text2 = ReadPlatformId(playerID);
				if (text2 != null)
				{
					value3.PlatformId = text2;
					value3.GhostId = GhostBook.MakeId(text2);
					DebugLog.Info("PLAYER '" + text + "' (id " + smallID + ") platform ID " + text2 + " -> " + value3.GhostId);
					if (GhostBook.TryGet(text2, out var g))
					{
						value3.Known = true;
						bool renamed = !g.Names.Contains(text);
						GhostBook.Touch(text2, text);
						if (!value3.KnownAnnounced)
						{
							value3.KnownAnnounced = true;
							GhostState.RaiseKnownJoined(value3, g, renamed);
						}
					}
					GhostHistory.OnSeen(value3);
					GhostState.Dirty = true;
				}
			}
			if (value3.PlatformId == null && !value3.PlatformWarned && now - value3.FirstSeenAt > 5f)
			{
				value3.PlatformWarned = true;
				DebugLog.Warn("Could not read the platform ID of '" + text + "' (id " + smallID + ") after 5 seconds - ghost IDs and saving will not work for this player.");
			}
			ReadWristHubTag(playerID, out var invisible, out var voice);
			bool flag = false;
			bool flag2 = false;
			bool flag3 = false;
			bool flag4 = false;
			Vector3 val2 = Vector3.zero;
			Vector3 val3 = Vector3.zero;
			int num4 = CollectPositions(val);
			for (int j = 0; j < num4; j++)
			{
				bool flag5 = j == num4 - 1 && _hasPhys;
				Vector3 val4 = (flag5 ? _phys : _net[j]);
				if (!IsFinite(val4))
				{
					continue;
				}
				if (Near(val4, ParkingSpot, tol))
				{
					flag = true;
				}
				else if (value && val4.y < value2)
				{
					flag2 = true;
				}
				else if (flag5)
				{
					if (!flag4)
					{
						flag4 = true;
						val3 = val4;
					}
				}
				else if (!flag3)
				{
					flag3 = true;
					val2 = val4;
				}
			}
			bool flag6 = flag || flag2 || invisible;
			if (flag6)
			{
				if (value3.SignalSince < 0f)
				{
					value3.SignalSince = now;
				}
				value3.LastSignalAt = now;
			}
			else
			{
				value3.SignalSince = -1f;
			}
			value3.PoseSpoof = flag;
			value3.Underworld = flag2 && !flag;
			value3.MetaInvisible = invisible;
			value3.MetaVoice = voice;
			if (!flag6)
			{
				if (flag3 || flag4)
				{
					value3.LastSeen = (flag3 ? val2 : val3);
					value3.LastSeenAt = now;
					value3.HasLastSeen = true;
					value3.LastSeenIsLeak = false;
					if (!value3.Hidden)
					{
						value3.HasEstimate = false;
						value3.OwnedCount = 0;
					}
				}
			}
			else if (invisible && flag3)
			{
				value3.LastSeen = val2;
				value3.LastSeenAt = now;
				value3.HasLastSeen = true;
				value3.LastSeenIsLeak = true;
			}
			float num5 = (invisible ? 0f : (flag ? num3 : 2f));
			if (flag6 && now - value3.SignalSince >= num5 && !value3.Hidden)
			{
				value3.Hidden = true;
				value3.HiddenSince = now;
				GhostState.RaiseVanished(value3);
			}
			else if (!flag6 && value3.Hidden && now - value3.LastSignalAt > 1f)
			{
				value3.Hidden = false;
				GhostState.RaiseReappeared(value3);
			}
		}
		_gone.Clear();
		foreach (KeyValuePair<byte, GhostRecord> record2 in GhostState.Records)
		{
			GhostRecord value4 = record2.Value;
			if (!_seen.Contains(record2.Key) && (!value4.IsTest || !(now < value4.TestExpiresAt)))
			{
				_gone.Add(record2.Key);
			}
		}
		for (int k = 0; k < _gone.Count; k++)
		{
			GhostState.RemoveRecord(_gone[k]);
		}
		GhostState.Scanned = num2;
	}

	private static int CollectPositions(NetworkPlayer np)
	{
		//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_00a7: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ac: Unknown result type (might be due to invalid IL or missing references)
		//IL_00cf: Unknown result type (might be due to invalid IL or missing references)
		//IL_00d4: Unknown result type (might be due to invalid IL or missing references)
		int num = 0;
		_hasPhys = false;
		try
		{
			if (!np.HasRig)
			{
				return 0;
			}
			if (_hasReceivedPose != null)
			{
				object value = _hasReceivedPose.GetValue(np, null);
				if (value is bool && !(bool)value)
				{
					return 0;
				}
			}
			if (_receivedPelvis != null)
			{
				object? value2 = _receivedPelvis.GetValue(np, null);
				BodyPose val = (BodyPose)((value2 is BodyPose) ? value2 : null);
				if (val != null)
				{
					_net[num++] = val.Position;
				}
			}
			RigPose rigPose = np.RigPose;
			if (rigPose != null && rigPose.PelvisPose != null)
			{
				_net[num++] = rigPose.PelvisPose.Position;
			}
			RigSkeleton rigSkeleton = np.RigSkeleton;
			if (rigSkeleton != null)
			{
				Rigidbody physicsPelvis = rigSkeleton.PhysicsPelvis;
				if ((Object)(object)physicsPelvis != (Object)null)
				{
					_phys = physicsPelvis.position;
					_hasPhys = true;
					num++;
				}
			}
		}
		catch (Exception e)
		{
			DebugLog.Error("CollectPositions", e);
		}
		return num;
	}

	private static void ReadWristHubTag(PlayerID pid, out bool invisible, out bool voice)
	{
		invisible = false;
		voice = false;
		try
		{
			PlayerMetadata metadata = pid.Metadata;
			if (metadata == null)
			{
				return;
			}
			NetworkMetadata metadata2 = metadata.Metadata;
			string text = default(string);
			if (metadata2 != null && metadata2.TryGetMetadata("WristHub.Invisibility", ref text) && !string.IsNullOrWhiteSpace(text) && text.Length >= 4 && (text.StartsWith("v1:", StringComparison.Ordinal) || text.StartsWith("v2:", StringComparison.Ordinal)))
			{
				string text2 = text.Substring(3);
				int num = text2.IndexOf(':');
				if (byte.TryParse((num >= 0) ? text2.Substring(0, num) : text2, out var result) && (result & 0xFC) == 0)
				{
					invisible = (result & 1) != 0;
					voice = (result & 2) != 0;
				}
			}
		}
		catch (Exception e)
		{
			DebugLog.Error("ReadWristHubTag", e);
		}
	}

	private static string SafeName(NetworkPlayer np, byte id)
	{
		string text = null;
		try
		{
			text = np.Username;
		}
		catch (Exception)
		{
		}
		if (string.IsNullOrWhiteSpace(text))
		{
			text = "Player#" + id;
		}
		return text.Replace("<", "").Replace(">", "").Trim();
	}

	private static bool Near(Vector3 a, Vector3 b, float tol)
	{
		//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_0015: Unknown result type (might be due to invalid IL or missing references)
		//IL_001b: Unknown result type (might be due to invalid IL or missing references)
		//IL_002a: Unknown result type (might be due to invalid IL or missing references)
		//IL_0030: Unknown result type (might be due to invalid IL or missing references)
		if (Mathf.Abs(a.x - b.x) <= tol && Mathf.Abs(a.y - b.y) <= tol)
		{
			return Mathf.Abs(a.z - b.z) <= tol;
		}
		return false;
	}

	private static bool IsFinite(Vector3 v)
	{
		//IL_0000: 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_001a: Unknown result type (might be due to invalid IL or missing references)
		//IL_0027: Unknown result type (might be due to invalid IL or missing references)
		//IL_0034: Unknown result type (might be due to invalid IL or missing references)
		//IL_0041: Unknown result type (might be due to invalid IL or missing references)
		if (!float.IsNaN(v.x) && !float.IsNaN(v.y) && !float.IsNaN(v.z) && !float.IsInfinity(v.x) && !float.IsInfinity(v.y))
		{
			return !float.IsInfinity(v.z);
		}
		return false;
	}
}
internal static class EntityEstimator
{
	private static MethodInfo _getExtender;

	private static bool _bound;

	private static readonly List<Vector3> _pts = new List<Vector3>(64);

	private static readonly List<Vector3> _mov = new List<Vector3>(32);

	private static readonly Dictionary<NetworkEntity, Vector3> _last = new Dictionary<NetworkEntity, Vector3>();

	private static void Bind()
	{
		if (_bound)
		{
			return;
		}
		_bound = true;
		try
		{
			MethodInfo methodInfo = typeof(NetworkEntity).GetMethods(BindingFlags.Instance | BindingFlags.Public).FirstOrDefault((MethodInfo m) => m.Name == "GetExtender" && m.IsGenericMethodDefinition && m.GetParameters().Length == 0);
			if (methodInfo != null)
			{
				_getExtender = methodInfo.MakeGenericMethod(typeof(IMarrowEntityExtender));
			}
		}
		catch (Exception e)
		{
			_getExtender = null;
			DebugLog.Error("EntityEstimator.Bind", e);
		}
	}

	internal static string BindReport()
	{
		Bind();
		return "  NetworkEntity.GetExtender<IMarrowEntityExtender>: " + ((_getExtender != null) ? "found" : "NOT FOUND (prop estimate is off)") + "\r\n";
	}

	internal static void Update(GhostRecord rec, float now)
	{
		//IL_02b1: Unknown result type (might be due to invalid IL or missing references)
		//IL_02b6: Unknown result type (might be due to invalid IL or missing references)
		//IL_025e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0266: Unknown result type (might be due to invalid IL or missing references)
		//IL_026b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0270: Unknown result type (might be due to invalid IL or missing references)
		//IL_02c1: Unknown result type (might be due to invalid IL or missing references)
		//IL_02c9: Unknown result type (might be due to invalid IL or missing references)
		//IL_02ce: Unknown result type (might be due to invalid IL or missing references)
		//IL_02d3: Unknown result type (might be due to invalid IL or missing references)
		//IL_0309: Unknown result type (might be due to invalid IL or missing references)
		//IL_0313: Unknown result type (might be due to invalid IL or missing references)
		//IL_0318: Unknown result type (might be due to invalid IL or missing references)
		//IL_02e3: Unknown result type (might be due to invalid IL or missing references)
		//IL_02e7: Unknown result type (might be due to invalid IL or missing references)
		//IL_02ec: 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_010b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0110: Unknown result type (might be due to invalid IL or missing references)
		//IL_0112: Unknown result type (might be due to invalid IL or missing references)
		//IL_0123: Unknown result type (might be due to invalid IL or missing references)
		//IL_0134: Unknown result type (might be due to invalid IL or missing references)
		//IL_0145: Unknown result type (might be due to invalid IL or missing references)
		//IL_0153: Unknown result type (might be due to invalid IL or missing references)
		//IL_01a8: Unknown result type (might be due to invalid IL or missing references)
		//IL_01b4: Unknown result type (might be due to invalid IL or missing references)
		//IL_017c: Unknown result type (might be due to invalid IL or missing references)
		//IL_017e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0180: Unknown result type (might be due to invalid IL or missing references)
		//IL_0185: Unknown result type (might be due to invalid IL or missing references)
		//IL_019a: Unknown result type (might be due to invalid IL or missing references)
		Bind();
		if (_getExtender == null)
		{
			rec.HasEstimate = false;
			return;
		}
		_pts.Clear();
		_mov.Clear();
		if (_last.Count > 4000)
		{
			_last.Clear();
		}
		Vector3 val2;
		try
		{
			EntityIDManager<NetworkEntity> iDManager = NetworkEntityManager.IDManager;
			if (iDManager == null)
			{
				return;
			}
			EntityIDList<NetworkEntity> registeredEntities = iDManager.RegisteredEntities;
			if (registeredEntities == null)
			{
				return;
			}
			Dictionary<ushort, NetworkEntity> iDEntityLookup = registeredEntities.IDEntityLookup;
			if (iDEntityLookup == null)
			{
				return;
			}
			foreach (KeyValuePair<ushort, NetworkEntity> item in iDEntityLookup)
			{
				NetworkEntity value = item.Value;
				if (value == null || !value.HasOwner)
				{
					continue;
				}
				PlayerID ownerID = value.OwnerID;
				if (ownerID == null || ownerID.SmallID != rec.Id)
				{
					continue;
				}
				object? obj = _getExtender.Invoke(value, null);
				IMarrowEntityExtender val = (IMarrowEntityExtender)((obj is IMarrowEntityExtender) ? obj : null);
				if (val == null)
				{
					continue;
				}
				MarrowEntity marrowEntity = val.MarrowEntity;
				if ((Object)(object)marrowEntity == (Object)null)
				{
					continue;
				}
				Vector3 position = ((Component)marrowEntity).transform.position;
				if (float.IsNaN(position.x) || float.IsNaN(position.y) || float.IsNaN(position.z) || position.y < -200f || Mathf.Abs(position.y + 4096f) < 50f)
				{
					continue;
				}
				if (_last.TryGetValue(value, out var value2))
				{
					val2 = position - value2;
					if (((Vector3)(ref val2)).sqrMagnitude > 0.0009f)
					{
						_mov.Add(position);
					}
				}
				_last[value] = position;
				_pts.Add(position);
				if (_pts.Count >= 200)
				{
					break;
				}
			}
		}
		catch (Exception e)
		{
			_pts.Clear();
			_mov.Clear();
			DebugLog.Error("EntityEstimator.Update", e);
		}
		if (_pts.Count == 0)
		{
			rec.HasEstimate = false;
			rec.EstimateLive = false;
			rec.OwnedCount = 0;
			return;
		}
		List<Vector3> list = ((_mov.Count > 0) ? _mov : _pts);
		int index = 0;
		int num = -1;
		for (int i = 0; i < list.Count; i++)
		{
			int num2 = 0;
			for (int j = 0; j < list.Count; j++)
			{
				val2 = list[i] - list[j];
				if (((Vector3)(ref val2)).sqrMagnitude <= 225f)
				{
					num2++;
				}
			}
			if (num2 > num)
			{
				num = num2;
				index = i;
			}
		}
		Vector3 val3 = Vector3.zero;
		int num3 = 0;
		for (int k = 0; k < list.Count; k++)
		{
			val2 = list[index] - list[k];
			if (((Vector3)(ref val2)).sqrMagnitude <= 225f)
			{
				val3 += list[k];
				num3++;
			}
		}
		rec.Estimate = val3 / (float)Mathf.Max(1, num3);
		rec.HasEstimate = true;
		rec.EstimateLive = _mov.Count > 0;
		rec.OwnedCount = _pts.Count;
		rec.EstimateAt = now;
	}
}
internal static class RigReader
{
	internal static void Update(GhostRecord rec, float now)
	{
		//IL_00bd: 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_00c8: Unknown result type (might be due to invalid IL or missing references)
		//IL_00cd: Unknown result type (might be due to invalid IL or missing references)
		//IL_00cf: Unknown result type (might be due to invalid IL or missing references)
		//IL_00d4: Unknown result type (might be due to invalid IL or missing references)
		//IL_00da: Unknown result type (might be due to invalid IL or missing references)
		//IL_00df: Unknown result type (might be due to invalid IL or missing references)
		//IL_00e1: Unknown result type (might be due to invalid IL or missing references)
		//IL_00e6: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ec: Unknown result type (might be due to invalid IL or missing references)
		//IL_00f1: Unknown result type (might be due to invalid IL or missing references)
		//IL_00f3: Unknown result type (might be due to invalid IL or missing references)
		//IL_00f8: Unknown result type (might be due to invalid IL or missing references)
		//IL_012e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0130: 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_0138: Unknown result type (might be due to invalid IL or missing references)
		//IL_013e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0140: Unknown result type (might be due to invalid IL or missing references)
		//IL_014a: Unknown result type (might be due to invalid IL or missing references)
		//IL_014f: Unknown result type (might be due to invalid IL or missing references)
		try
		{
			foreach (NetworkPlayer player in NetworkPlayer.Players)
			{
				if (player == null)
				{
					continue;
				}
				PlayerID playerID = player.PlayerID;
				if (playerID == null || playerID.SmallID != rec.Id)
				{
					continue;
				}
				if (!player.HasRig)
				{
					rec.HasRig = false;
					return;
				}
				RigSkeleton rigSkeleton = player.RigSkeleton;
				if (rigSkeleton == null)
				{
					rec.HasRig = false;
					return;
				}
				Rigidbody physicsPelvis = rigSkeleton.PhysicsPelvis;
				Transform[] trackedPoints = rigSkeleton.TrackedPoints;
				if ((Object)(object)physicsPelvis == (Object)null || trackedPoints == null || trackedPoints.Length < 3 || (Object)(object)trackedPoints[0] == (Object)null || (Object)(object)trackedPoints[1] == (Object)null || (Object)(object)trackedPoints[2] == (Object)null)
				{
					rec.HasRig = false;
					return;
				}
				Vector3 position = physicsPelvis.position;
				Vector3 rigHead = trackedPoints[0].position - position;
				Vector3 rigLeft = trackedPoints[1].position - position;
				Vector3 rigRight = trackedPoints[2].position - position;
				if (((Vector3)(ref rigHead)).magnitude > 4f || ((Vector3)(ref rigLeft)).magnitude > 5f || ((Vector3)(ref rigRight)).magnitude > 5f)
				{
					rec.HasRig = false;
					return;
				}
				rec.RigHead = rigHead;
				rec.RigLeft = rigLeft;
				rec.RigRight = rigRight;
				rec.RigHeadRot = trackedPoints[0].rotation;
				rec.RigAt = now;
				rec.HasRig = true;
				return;
			}
			rec.HasRig = false;
		}
		catch (Exception e)
		{
			rec.HasRig = false;
			DebugLog.Error("RigReader.Update", e);
		}
	}
}
internal static class NameTags
{
	private static readonly Color Red = new Color(1f, 0.15f, 0.15f, 1f);

	private static readonly Dictionary<byte, Color> _orig = new Dictionary<byte, Color>();

	private static readonly HashSet<byte> _seen = new HashSet<byte>();

	private static readonly List<byte> _drop = new List<byte>();

	private static FieldInfo _field;

	private static bool _bound;

	private static void Bind()
	{
		if (_bound)
		{
			return;
		}
		_bound = true;
		try
		{
			_field = typeof(NetworkPlayer).GetField("_nametag", BindingFlags.Instance | BindingFlags.NonPublic);
		}
		catch (Exception e)
		{
			_field = null;
			DebugLog.Error("NameTags.Bind", e);
		}
	}

	internal static string BindReport()
	{
		Bind();
		return "  NetworkPlayer._nametag: " + ((_field != null) ? "found" : "NOT FOUND (red name tags will not work)") + "\r\n";
	}

	internal static void Update()
	{
		//IL_010d: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ef: Unknown result type (might be due to invalid IL or missing references)
		//IL_00e3: Unknown result type (might be due to invalid IL or missing references)
		Bind();
		if (_field == null)
		{
			return;
		}
		bool flag = Settings.RedNameTags.Value && Settings.Enabled.Value;
		_seen.Clear();
		foreach (NetworkPlayer player in NetworkPlayer.Players)
		{
			if (player == null)
			{
				continue;
			}
			PlayerID playerID = player.PlayerID;
			if (playerID == null || playerID.IsMe || !playerID.IsValid)
			{
				continue;
			}
			byte smallID = playerID.SmallID;
			_seen.Add(smallID);
			GhostRecord ghostRecord = GhostState.Get(smallID);
			bool flag2 = flag && ghostRecord != null && !ghostRecord.Friend && GhostHistory.WasInvisible(ghostRecord);
			object? value = _field.GetValue(player);
			RigNameTag val = (RigNameTag)((value is RigNameTag) ? value : null);
			if (val == null)
			{
				continue;
			}
			Color value2;
			if (flag2)
			{
				if (!_orig.ContainsKey(smallID))
				{
					_orig[smallID] = val.Color;
				}
				val.Color = Red;
			}
			else if (_orig.TryGetValue(smallID, out value2))
			{
				val.Color = value2;
				_orig.Remove(smallID);
			}
		}
		_drop.Clear();
		foreach (KeyValuePair<byte, Color> item in _orig)
		{
			if (!_seen.Contains(item.Key))
			{
				_drop.Add(item.Key);
			}
		}
		for (int i = 0; i < _drop.Count; i++)
		{
			_orig.Remove(_drop[i]);
		}
	}
}
internal static class FusionInfo
{
	internal static string Report()
	{
		StringBuilder stringBuilder = new StringBuilder();
		try
		{
			AssemblyName name = typeof(NetworkPlayer).Assembly.GetName();
			stringBuilder.AppendLine("  LabFusion assembly: " + name.Name + " " + name.Version);
		}
		catch (Exception ex)
		{
			stringBuilder.AppendLine("  LabFusion assembly: error " + ex.GetType().Name);
		}
		stringBuilder.Append(GhostDetector.BindReport());
		stringBuilder.Append(EntityEstimator.BindReport());
		stringBuilder.Append(NameTags.BindReport());
		try
		{
			stringBuilder.AppendLine("  NetworkInfo.HasServer: " + NetworkInfo.HasServer);
		}
		catch (Exception ex2)
		{
			stringBuilder.AppendLine("  NetworkInfo.HasServer: error " + ex2.GetType().Name);
		}
		return stringBuilder.ToString();
	}
}
internal static class FusionActions
{
	internal static bool TeleportTo(Vector3 position)
	{
		//IL_0000: 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_0015: Unknown result type (might be due to invalid IL or missing references)
		try
		{
			LocalPlayer.TeleportToPosition(position + new Vector3(0f, 0.5f, 0f));
			return true;
		}
		catch (Exception)
		{
			return false;
		}
	}
}
internal enum HistoryState
{
	Invisible,
	Visible,
	Left
}
internal sealed class HistoryEntry
{
	public string Key;

	public string GhostId;

	public string PlatformId;

	public string Name;

	public List<string> Names = new List<string>();

	public string Tool;

	public int Times;

	public DateTime FirstAt;

	public DateTime LastAt;

	public int Lobby;

	public HistoryState State;

	public float Since;

	public float TotalSeconds;
}
internal static class GhostHistory
{
	private static readonly Dictionary<string, HistoryEntry> _entries = new Dictionary<string, HistoryEntry>(StringComparer.Ordinal);

	internal static int Count => _entries.Count;

	internal static void Init()
	{
		GhostState.Vanished += OnVanished;
		GhostState.Reappeared += OnReappeared;
		GhostState.Left += OnLeft;
	}

	internal static string KeyFor(GhostRecord r)
	{
		if (!string.IsNullOrEmpty(r.PlatformId))
		{
			return r.PlatformId;
		}
		return "s" + r.Lobby + ":" + r.Id;
	}

	internal static bool TryGet(string key, out HistoryEntry e)
	{
		return _entries.TryGetValue(key, out e);
	}

	internal static string Duration(float seconds)
	{
		if (seconds < 60f)
		{
			return "<1m";
		}
		int num = (int)(seconds / 60f);
		if (num < 60)
		{
			return num + "m";
		}
		return num / 60 + "h " + num % 60 + "m";
	}

	internal static float SecondsInvisible(HistoryEntry e)
	{
		float num = e.TotalSeconds;
		if (e.State == HistoryState.Invisible)
		{
			num += Mathf.Max(0f, Time.realtimeSinceStartup - e.Since);
		}
		return num;
	}

	internal static bool WasInvisible(GhostRecord r)
	{
		if (r.IsTest)
		{
			return false;
		}
		if (r.EverHidden || r.Known)
		{
			return true;
		}
		if (_entries.TryGetValue(KeyFor(r), out var value))
		{
			return value.Times > 0;
		}
		return false;
	}

	internal static string Export()
	{
		try
		{
			string text = Path.Combine(MelonEnvironment.UserDataDirectory, "GhostBuster");
			Directory.CreateDirectory(text);
			string text2 = Path.Combine(text, "history_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".txt");
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.AppendLine("GhostBuster invisible history - " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
			stringBuilder.AppendLine("(platform ID = the player's Steam / EOS account ID; it stays the same when they rename)");
			stringBuilder.AppendLine();
			foreach (HistoryEntry item in List())
			{
				stringBuilder.AppendLine(item.GhostId + " | " + item.State.ToString() + " | names: " + string.Join(", ", item.Names) + " | platform ID: " + (string.IsNullOrEmpty(item.PlatformId) ? "unknown" : item.PlatformId) + " | invisible " + item.Times + "x, " + Duration(SecondsInvisible(item)) + " in total | likely " + item.Tool + " | first " + item.FirstAt.ToString("HH:mm:ss") + ", last " + item.LastAt.ToString("HH:mm:ss"));
			}
			File.WriteAllText(text2, stringBuilder.ToString());
			return text2;
		}
		catch (Exception e)
		{
			DebugLog.Error("History export", e);
			return null;
		}
	}

	internal static List<HistoryEntry> List()
	{
		List<HistoryEntry> list = new List<HistoryEntry>(_entries.Values);
		list.Sort(delegate(HistoryEntry a, HistoryEntry b)
		{
			int num = a.State.CompareTo(b.State);
			return (num == 0) ? b.LastAt.CompareTo(a.LastAt) : num;
		});
		return list;
	}

	internal static void Clear()
	{
		_entries.Clear();
		GhostState.Dirty = true;
	}

	private static HistoryEntry GetOrCreate(GhostRecord r)
	{
		string key = KeyFor(r);
		if (!_entries.TryGetValue(key, out var value))
		{
			value = new HistoryEntry
			{
				Key = key,
				FirstAt = DateTime.Now
			};
			_entries[key] = value;
		}
		value.GhostId = r.GhostId;
		value.PlatformId = r.PlatformId;
		value.Lobby = r.Lobby;
		value.Name = r.Name;
		if (!value.Names.Contains(r.Name))
		{
			value.Names.Add(r.Name);
		}
		return value;
	}

	private static void OnVanished(GhostRecord r)
	{
		if (!r.IsTest)
		{
			HistoryEntry orCreate = GetOrCreate(r);
			orCreate.Times++;
			orCreate.State = HistoryState.Invisible;
			orCreate.Since = Time.realtimeSinceStartup;
			orCreate.Tool = r.Tool();
			orCreate.LastAt = DateTime.Now;
		}
	}

	private static void OnReappeared(GhostRecord r)
	{
		if (!r.IsTest && _entries.TryGetValue(KeyFor(r), out var value))
		{
			if (value.State == HistoryState.Invisible)
			{
				value.TotalSeconds += Mathf.Max(0f, Time.realtimeSinceStartup - value.Since);
			}
			value.State = HistoryState.Visible;
			value.LastAt = DateTime.Now;
			value.Name = r.Name;
		}
	}

	private static void OnLeft(GhostRecord r)
	{
		if (!r.IsTest && _entries.TryGetValue(KeyFor(r), out var value))
		{
			if (value.State == HistoryState.Invisible)
			{
				value.TotalSeconds += Mathf.Max(0f, Time.realtimeSinceStartup - value.Since);
			}
			value.State = HistoryState.Left;
			value.LastAt = DateTime.Now;
		}
	}

	internal static void OnSeen(GhostRecord r)
	{
		if (_entries.TryGetValue(KeyFor(r), out var value))
		{
			value.GhostId = r.GhostId;
			if (!value.Names.Contains(r.Name))
			{
				value.Names.Add(r.Name);
				value.Name = r.Name;
				GhostState.Dirty = true;
			}
			if (value.State == HistoryState.Left && !r.Hidden)
			{
				value.State = HistoryState.Visible;
				GhostState.Dirty = true;
			}
		}
	}
}
internal static class GhostMenu
{
	private const int MaxHistoryShown = 60;

	private const int MaxSavedShown = 80;

	private static Page _root;

	private static Page _now;

	private static Page _hist;

	private static Page _saved;

	internal static bool Built;

	private static float _lastRebuild;

	private static string _sig = "";

	private static readonly Color Teal = new Color(0.25f, 0.9f, 0.85f, 1f);

	private static readonly Color Green = new Color(0.35f, 1f, 0.55f, 1f);

	private static readonly Color Red = new Color(1f, 0.35f, 0.35f, 1f);

	private static readonly Color Amber = new Color(1f, 0.75f, 0.2f, 1f);

	private static readonly Color Grey = new Color(0.7f, 0.7f, 0.7f, 1f);

	private static readonly Color Blue = new Color(0.45f, 0.7f, 1f, 1f);

	private static readonly Color White = Color.white;

	internal static bool TryBuild()
	{
		//IL_0014: Unknown result type (might be due to invalid IL or missing references)
		//IL_002f: Unknown result type (might be due to invalid IL or missing references)
		//IL_006d: Unknown result type (might be due to invalid IL or missing references)
		//IL_008e: Unknown result type (might be due to invalid IL or missing references)
		//IL_00c2: Unknown result type (might be due to invalid IL or missing references)
		//IL_00e3: 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)
		//IL_0119: Unknown result type (might be due to invalid IL or missing references)
		if (Built)
		{
			return true;
		}
		try
		{
			_root = Page.Root.CreatePage("GhostBuster", Teal, 6, true);
			_root.CreateBool("Detection", Green, Settings.Enabled.Value, (Action<bool>)delegate(bool v)
			{
				Settings.Enabled.Value = v;
				Settings.Save();
			});
			_root.CreateFunction("Status", White, (Action)ShowStatus);
			_root.CreateFunction("Scan now", White, (Action)delegate
			{
				GhostBusterMod.RequestScan();
			});
			_root.CreateFunction("Radar ping", Amber, (Action)Alerts.RadarPing);
			_now = _root.CreatePage("Invisible now", Red, 6, true);
			_hist = _root.CreatePage("Invisible history", Amber, 6, true);
			_saved = _root.CreatePage("Saved ghosts", Green, 6, true);
			BuildRigPage();
			BuildAlertsPage();
			BuildTuningPage();
			BuildToolsPage();
			Built = true;
			Rebuild(force: true);
			GhostBusterMod.Log.Msg("BoneMenu page created.");
			DebugLog.Info("BoneMenu page created.");
			return true;
		}
		catch (Exception ex)
		{
			GhostBusterMod.Log.Warning("BoneMenu not ready yet: " + ex.GetType().Name + ": " + ex.Message);
			DebugLog.Error("Menu build", ex);
			return false;
		}
	}

	private static void BuildRigPage()
	{
		//IL_000a: Unknown result type (might be due to invalid IL or missing references)
		//IL_001c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0056: Unknown result type (might be due to invalid IL or missing references)
		//IL_008f: Unknown result type (might be due to invalid IL or missing references)
		Page obj = _root.CreatePage("Rig outline", Blue, 6, true);
		obj.CreateBool("Rig outline (stick figure)", Green, Settings.RigEnabled.Value, (Action<bool>)delegate(bool v)
		{
			Settings.RigEnabled.Value = v;
			Settings.Save();
			if (!v)
			{
				Rigs.RemoveAll();
			}
		});
		obj.CreateBool("Rig through walls", White, Settings.RigThroughWalls.Value, (Action<bool>)delegate(bool v)
		{
			Settings.RigThroughWalls.Value = v;
			Settings.Save();
			Rigs.ResetMaterial();
		});
		obj.CreateFunction("How many shown?", White, (Action)delegate
		{
			Alerts.Toast("Rig outline", Rigs.Count + " outline(s) shown right now.", (NotificationType)0, 3f);
		});
	}

	private static void BuildAlertsPage()
	{
		//IL_000a: Unknown result type (might be due to invalid IL or missing references)
		//IL_001c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0056: Unknown result type (might be due to invalid IL or missing references)
		//IL_0090: 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)
		//IL_0104: Unknown result type (might be due to invalid IL or missing references)
		//IL_013e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0187: Unknown result type (might be due to invalid IL or missing references)
		//IL_01b7: Unknown result type (might be due to invalid IL or missing references)
		//IL_01f1: Unknown result type (might be due to invalid IL or missing references)
		//IL_022b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0264: Unknown result type (might be due to invalid IL or missing references)
		Page obj = _root.CreatePage("Alerts & beacons", Amber, 6, true);
		obj.CreateBool("Pop-up alerts", White, Settings.AlertToast.Value, (Action<bool>)delegate(bool v)
		{
			Settings.AlertToast.Value = v;
			Settings.Save();
		});
		obj.CreateBool("Known-ghost join alert", White, Settings.KnownAlert.Value, (Action<bool>)delegate(bool v)
		{
			Settings.KnownAlert.Value = v;
			Settings.Save();
		});
		obj.CreateBool("Red name tags (went invisible before)", Red, Settings.RedNameTags.Value, (Action<bool>)delegate(bool v)
		{
			Settings.RedNameTags.Value = v;
			Settings.Save();
		});
		obj.CreateBool("Join summary", White, Settings.JoinSummary.Value, (Action<bool>)delegate(bool v)
		{
			Settings.JoinSummary.Value = v;
			Settings.Save();
		});
		obj.CreateBool("Beep sound", White, Settings.BeepSound.Value, (Action<bool>)delegate(bool v)
		{
			Settings.BeepSound.Value = v;
			Settings.Save();
		});
		obj.CreateFloat("Beep volume", White, 0.1f, Settings.BeepVolume.Value, 0f, 1f, (Action<float>)delegate(float v)
		{
			Settings.BeepVolume.Value = v;
			Settings.Save();
		});
		obj.CreateFunction("Play test beep", White, (Action)delegate
		{
			Sounds.Play(known: false);
		});
		obj.CreateBool("Log to file", White, Settings.AlertLog.Value, (Action<bool>)delegate(bool v)
		{
			Settings.AlertLog.Value = v;
			Settings.Save();
		});
		obj.CreateBool("World beacons", White, Settings.Beacons.Value, (Action<bool>)delegate(bool v)
		{
			Settings.Beacons.Value = v;
			Settings.Save();
		});
		obj.CreateBool("Estimate from owned props", White, Settings.EntityEstimate.Value, (Action<bool>)delegate(bool v)
		{
			Settings.EntityEstimate.Value = v;
			Settings.Save();
		});
		obj.CreateFloat("Auto radar (s, 0=off)", White, 5f, Settings.RadarSeconds.Value, 0f, 60f, (Action<float>)delegate(float v)
		{
			Settings.RadarSeconds.Value = v;
			Settings.Save();
		});
	}

	private static void BuildTuningPage()
	{
		//IL_000a: Unknown result type (might be due to invalid IL or missing references)
		//IL_001c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0065: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ae: Unknown result type (might be due to invalid IL or missing references)
		//IL_00e7: Unknown result type (might be due to invalid IL or missing references)
		Page obj = _root.CreatePage("Detection tuning", Grey, 6, true);
		obj.CreateFloat("Pose tolerance (m)", White, 0.25f, Settings.PoseTolerance.Value, 0.25f, 5f, (Action<float>)delegate(float v)
		{
			Settings.PoseTolerance.Value = v;
			Settings.Save();
		});
		obj.CreateFloat("Pose hold (s)", White, 0.05f, Settings.PoseHoldSeconds.Value, 0f, 2f, (Action<float>)delegate(float v)
		{
			Settings.PoseHoldSeconds.Value = v;
			Settings.Save();
		});
		obj.CreateBool("Generic under-world check", White, Settings.UnderworldHeuristic.Value, (Action<bool>)delegate(bool v)
		{
			Settings.UnderworldHeuristic.Value = v;
			Settings.Save();
		});
		obj.CreateFloat("Under-world Y", White, 100f, Settings.UnderworldY.Value, -4000f, -300f, (Action<float>)delegate(float v)
		{
			Settings.UnderworldY.Value = v;
			Settings.Save();
		});
	}

	private static void BuildToolsPage()
	{
		//IL_000a: Unknown result type (might be due to invalid IL or missing references)
		//IL_001c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0056: Unknown result type (might be due to invalid IL or missing references)
		//IL_0086: Unknown result type (might be due to invalid IL or missing references)
		//IL_00b6: Unknown result type (might be due to invalid IL or missing references)
		//IL_00f0: Unknown result type (might be due to invalid IL or missing references)
		//IL_0120: Unknown result type (might be due to invalid IL or missing references)
		//IL_0150: Unknown result type (might be due to invalid IL or missing references)
		//IL_016d: Unknown result type (might be due to invalid IL or missing references)
		//IL_019d: Unknown result type (might be due to invalid IL or missing references)
		//IL_01cc: Unknown result type (might be due to invalid IL or missing references)
		Page obj = _root.CreatePage("Tools", Grey, 6, true);
		obj.CreateBool("Auto-save every ghost", Green, Settings.AutoSave.Value, (Action<bool>)delegate(bool v)
		{
			Settings.AutoSave.Value = v;
			Settings.Save();
		});
		obj.CreateFunction("Start test ghost", White, (Action)delegate
		{
			GhostBusterMod.SpawnTestGhost();
		});
		obj.CreateFunction("STOP test ghost", Red, (Action)delegate
		{
			GhostBusterMod.StopTestGhost();
		});
		obj.CreateBool("Debug log", White, Settings.DebugLogging.Value, (Action<bool>)delegate(bool v)
		{
			Settings.DebugLogging.Value = v;
			Settings.Save();
		});
		obj.CreateFunction("Write debug snapshot", Teal, (Action)delegate
		{
			GhostBusterMod.WriteSnapshot();
		});
		obj.CreateFunction("Where are my files?", White, (Action)delegate
		{
			GhostBusterMod.ShowDataFolder();
		});
		obj.CreateFunction("Export history to file", Green, (Action)Alerts.ExportHistory);
		obj.CreateFunction("Clear history", White, (Action)delegate
		{
			GhostHistory.Clear();
			Alerts.Toast("GhostBuster", "History cleared (saved ghosts are kept).", (NotificationType)0, 2.5f);
		});
		obj.CreateFunction("Clear allowed list", White, (Action)delegate
		{
			Settings.ClearFriends();
			Alerts.Toast("GhostBuster", "Allowed list cleared.", (NotificationType)0, 2f);
		});
		obj.CreateFunction("Remove all beacons", White, (Action)Beacons.HideAll);
	}

	private static void ShowStatus()
	{
		string text = ((!GhostState.FusionPresent) ? "LabFusion not found." : (GhostState.InServer ? (GhostState.Scanned + " players scanned, " + GhostState.HiddenCount() + " invisible now. ") : "Not in a Fusion lobby."));
		text = text + GhostHistory.Count + " in history, " + GhostBook.Count + " saved.";
		Alerts.Toast("GhostBuster", text, (NotificationType)0, 4f);
	}

	private static string Signature()
	{
		float realtimeSinceStartup = Time.realtimeSinceStartup;
		StringBuilder stringBuilder = new StringBuilder();
		foreach (GhostRecord item in GhostState.HiddenList())
		{
			stringBuilder.Append(item.Id).Append('|').Append(item.GhostId)
				.Append('|')
				.Append(item.Name)
				.Append('|')
				.Append(item.Friend)
				.Append('|')
				.Append(item.Known)
				.Append('|')
				.Append((int)((realtimeSinceStartup - item.HiddenSince) / 60f))
				.Append(';');
		}
		stringBuilder.Append('#');
		int num = 0;
		foreach (HistoryEntry item2 in GhostHistory.List())
		{
			if (num++ >= 60)
			{
				break;
			}
			stringBuilder.Append(item2.Key).Append('|').Append((int)item2.State)
				.Append('|')
				.Append(item2.Name)
				.Append('|')
				.Append(item2.Times)
				.Append('|')
				.Append(GhostBook.Has(item2.PlatformId))
				.Append('|')
				.Append(Settings.IsFriend(item2.Name))
				.Append('|')
				.Append((item2.State == HistoryState.Invisible) ? ((int)(GhostHistory.SecondsInvisible(item2) / 60f)) : (-1))
				.Append(';');
		}
		stringBuilder.Append('#');
		foreach (SavedGhost item3 in GhostBook.All())
		{
			stringBuilder.Append(item3.PlatformId).Append('|').Append(item3.CurrentName)
				.Append(';');
		}
		return stringBuilder.ToString();
	}

	private static bool UserIsInsideLists()
	{
		try
		{
			Page val = Menu.CurrentPage;
			int num = 0;
			while (val != null && num < 8)
			{
				if (val == _now || val == _hist || val == _saved)
				{
					return true;
				}
				val = val.Parent;
				num++;
			}
		}
		catch (Exception)
		{
		}
		return false;
	}

	internal static void Rebuild(bool force)
	{
		if (!Built || _now == null || (!force && UserIsInsideLists()))
		{
			return;
		}
		string text = Signature();
		if (!force && text == _sig)
		{
			GhostState.Dirty = false;
			return;
		}
		_sig = text;
		_lastRebuild = Time.realtimeSinceStartup;
		GhostState.Dirty = false;
		try
		{
			BuildNow();
			BuildHistory();
			BuildSaved();
		}
		catch (Exception ex)
		{
			GhostBusterMod.Log.Warning("Menu rebuild failed: " + ex.GetType().Name + ": " + ex.Message);
			DebugLog.Error("Menu rebuild", ex);
		}
	}

	private static void BuildNow()
	{
		//IL_0014: Unknown result type (might be due to invalid IL or missing references)
		//IL_01fa: Unknown result type (might be due to invalid IL or missing references)
		//IL_011d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0116: Unknown result type (might be due to invalid IL or missing references)
		//IL_0132: 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)
		//IL_0192: Unknown result type (might be due to invalid IL or missing references)
		//IL_0172: Unknown result type (might be due to invalid IL or missing references)
		//IL_01b0: Unknown result type (might be due to invalid IL or missing references)
		_now.RemoveAll();
		_now.CreateFunction("Refresh list", Teal, (Action)delegate
		{
			Rebuild(force: true);
		});
		int num = 0;
		foreach (GhostRecord item in GhostState.HiddenList())
		{
			byte id = item.Id;
			string name = item.Name;
			string plat = item.PlatformId;
			bool flag = plat != null && GhostBook.Has(plat);
			string text = item.GhostId + " " + name + " - " + GhostHistory.Duration(Time.realtimeSinceStartup - item.HiddenSince) + (flag ? " [saved]" : "") + (item.Friend ? " (allowed)" : "");
			Page val = _now.CreatePage(text, item.Friend ? Grey : Red, 6, true);
			val.CreateFunction("Info", White, (Action)delegate
			{
				Alerts.Info(id);
			});
			val.CreateFunction("Go to marker", Amber, (Action)delegate
			{
				GhostBusterMod.GoToMarker(id);
			});
			if (flag)
			{
				val.CreateFunction("Forget this ghost", Red, (Action)delegate
				{
					Alerts.ForgetGhost(plat, name);
				});
			}
			else
			{
				val.CreateFunction("Save this ghost", Green, (Action)delegate
				{
					Alerts.SaveGhost(plat, name);
				});
			}
			val.CreateBool("Allow (never alert)", Green, item.Friend, (Action<bool>)delegate(bool v)
			{
				Settings.SetFriend(name, v);
			});
			num++;
		}
		if (num == 0)
		{
			_now.CreateFunction("Nobody is invisible right now", Grey, (Action)delegate
			{
			});
		}
	}

	private static void BuildHistory()
	{
		//IL_0014: Unknown result type (might be due to invalid IL or missing references)
		//IL_023a: Unknown result type (might be due to invalid IL or missing references)
		//IL_013b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0140: Unknown result type (might be due to invalid IL or missing references)
		//IL_0179: Unknown result type (might be due to invalid IL or missing references)
		//IL_018b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0134: Unknown result type (might be due to invalid IL or missing references)
		//IL_012d: Unknown result type (might be due to invalid IL or missing references)
		//IL_01cd: Unknown result type (might be due to invalid IL or missing references)
		//IL_01ad: Unknown result type (might be due to invalid IL or missing references)
		//IL_01eb: Unknown result type (might be due to invalid IL or missing references)
		_hist.RemoveAll();
		_hist.CreateFunction("Refresh list", Teal, (Action)delegate
		{
			Rebuild(force: true);
		});
		int num = 0;
		foreach (HistoryEntry item in GhostHistory.List())
		{
			if (num >= 60)
			{
				break;
			}
			string key = item.Key;
			string name = item.Name;
			string plat = item.PlatformId;
			bool flag = plat != null && GhostBook.Has(plat);
			float seconds = GhostHistory.SecondsInvisible(item);
			string text = ((item.State == HistoryState.Invisible) ? ("INVISIBLE " + GhostHistory.Duration(seconds)) : (((item.State == HistoryState.Visible) ? "visible" : "left") + " (" + item.Times + "x, " + GhostHistory.Duration(seconds) + ")"));
			Color val = ((item.State == HistoryState.Invisible) ? Red : ((item.State == HistoryState.Visible) ? Amber : Grey));
			Page val2 = _hist.CreatePage(item.GhostId + " " + name + " - " + text, val, 6, true);
			val2.CreateFunction("Info", White, (Action)delegate
			{
				Alerts.InfoHistory(key);
			});
			if (flag)
			{
				val2.CreateFunction("Forget this ghost", Red, (Action)delegate
				{
					Alerts.ForgetGhost(plat, name);
				});
			}
			else
			{
				val2.CreateFunction("Save this ghost", Green, (Action)delegate
				{
					Alerts.SaveGhost(plat, name);
				});
			}
			val2.CreateBool("Allow (never alert)", Green, Settings.IsFriend(name), (Action<bool>)delegate(bool v)
			{
				Settings.SetFriend(name, v);
			});
			num++;
		}
		if (num == 0)
		{
			_hist.CreateFunction("Nobody has been invisible yet", Grey, (Action)delegate
			{
			});
		}
	}

	private static void BuildSaved()
	{
		//IL_0014: 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_00ad: Unknown result type (might be due to invalid IL or missing references)
		//IL_00c9: Unknown result type (might be due to invalid IL or missing references)
		//IL_010d: Unknown result type (might be due to invalid IL or missing references)
		_saved.RemoveAll();
		_saved.CreateFunction("Refresh list", Teal, (Action)delegate
		{
			Rebuild(force: true);
		});
		int num = 0;
		foreach (SavedGhost item in GhostBook.All())
		{
			if (num >= 80)
			{
				break;
			}
			string plat = item.PlatformId;
			string name = item.CurrentName;
			Page obj = _saved.CreatePage(item.Id + " " + name, Green, 6, true);
			obj.CreateFunction("Info (names + platform ID)", White, (Action)delegate
			{
				Alerts.InfoSaved(plat);
			});
			obj.CreateFunction("Forget this ghost", Red, (Action)delegate
			{
				Alerts.ForgetGhost(plat, name);
			});
			num++;
		}
		if (num == 0)
		{
			_saved.CreateFunction("No saved ghosts yet", Grey, (Action)delegate
			{
			});
		}
	}

	internal static void Tick(float now)
	{
		if (Built && GhostState.Dirty && now - _lastRebuild > 1f)
		{
			Rebuild(force: false);
		}
	}
}
internal sealed class GhostRecord
{
	public byte Id;

	public string Name = "";

	public string PlatformId;

	public string GhostId = "";

	public int Lobby;

	public bool Known;

	public bool KnownAnnounced;

	public bool EverHidden;

	public float FirstSeenAt;

	public bool PlatformWarned;

	public bool Hidden;

	public bool IsTest;

	public float TestExpiresAt;

	public Vector3 TestCenter;

	public float TestStart;

	public bool PoseSpoof;

	public bool Underworld;

	public bool MetaInvisible;

	public bool MetaVoice;

	public float SignalSince = -1f;

	public float LastSignalAt = -100f;

	public float HiddenSince;

	public float LastAlertAt = -100f;

	public bool HasLastSeen;

	public Vector3 LastSeen;

	public float LastSeenAt;

	public bool LastSeenIsLeak;

	public bool HasEstimate;

	public Vector3 Estimate;

	public bool EstimateLive;

	public int OwnedCount;

	public float EstimateAt;

	public bool HasRig;

	public Vector3 RigHead;

	public Vector3 RigLeft;

	public Vector3 RigRight;

	public Quaternion RigHeadRot;

	public float RigAt;

	public bool Friend;

	public string MethodText()
	{
		string text = "";
		if (PoseSpoof)
		{
			text += "pose-spoof ";
		}
		if (MetaInvisible)
		{
			text += "WristHub-tag ";
		}
		if (Underworld)
		{
			text += "under-world ";
		}
		if (IsTest)
		{
			text += "test ";
		}
		if (text.Length != 0)
		{
			return text.Trim().Replace(" ", " + ");
		}
		return "none";
	}

	public string Tool()
	{
		if (MetaInvisible)
		{
			return "WristHub";
		}
		if (PoseSpoof)
		{
			return "WristHub or GhostMode";
		}
		if (Underworld)
		{
			return "unknown variant";
		}
		return "?";
	}
}
internal static class GhostState
{
	internal static readonly Dictionary<byte, GhostRecord> Records = new Dictionary<byte, GhostRecord>();

	internal static bool Dirty;

	internal static int Scanned;

	internal static bool FusionPresent;

	internal static bool InServer;

	internal static int LobbyIndex;

	internal static event Action<GhostRecord> Vanished;

	internal static event Action<GhostRecord> Reappeared;

	internal static event Action<GhostRecord> Left;

	internal static event Action<GhostRecord, SavedGhost, bool> KnownJoined;

	internal static void RaiseVanished(GhostRecord r)
	{
		r.EverHidden = true;
		GhostState.Vanished?.Invoke(r);
		Dirty = true;
	}

	internal static void RaiseReappeared(GhostRecord r)
	{
		GhostState.Reappeared?.Invoke(r);
		Dirty = true;
	}

	internal static void RaiseKnownJoined(GhostRecord r, SavedGhost s, bool renamed)
	{
		GhostState.KnownJoined?.Invoke(r, s, renamed);
		Dirty = true;
	}

	internal static void RemoveRecord(byte id)
	{
		if (Records.TryGetValue(id, out var value))
		{
			Records.Remove(id);
			GhostState.Left?.Invoke(value);
			Dirty = true;
		}
	}

	internal static int HiddenCount()
	{
		int num = 0;
		foreach (GhostRecord value in Records.Values)
		{
			if (value.Hidden)
			{
				num++;
			}
		}
		return num;
	}

	internal static List<GhostRecord> HiddenList()
	{
		List<GhostRecord> list = new List<GhostRecord>();
		foreach (GhostRecord value in Records.Values)
		{
			if (value.Hidden)
			{
				list.Add(value);
			}
		}
		list.Sort((GhostRecord a, GhostRecord b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase));
		return list;
	}

	internal static GhostRecord Get(byte id)
	{
		if (!Records.TryGetValue(id, out var value))
		{
			return null;
		}
		return value;
	}

	internal static void RefreshFriendFlags()
	{
		foreach (GhostRecord value in Records.Values)
		{
			value.Friend = Settings.IsFriend(value.Name);
		}
		Dirty = true;
	}

	internal static bool TryGetBestMarker(GhostRecord r, out Vector3 pos, out string kind)
	{
		//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_004a: Unknown result type (might be due to invalid IL or missing references)
		//IL_004f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0027: Unknown result type (might be due to invalid IL or missing references)
		//IL_002c: Unknown result type (might be due to invalid IL or missing references)
		if (r.HasEstimate)
		{
			pos = r.Estimate;
			kind = "estimated";
			return true;
		}
		if (r.HasLastSeen)
		{
			pos = r.LastSeen;
			kind = (r.LastSeenIsLeak ? "leaked" : "last seen");
			return true;
		}
		pos = Vector3.zero;
		kind = "none";
		return false;
	}
}
internal sealed class RigDraw
{
	public GameObject Root;

	public LineRenderer Body;

	public