Decompiled source of FusionPresence v1.0.1

Mods/FusionPresence.dll

Decompiled 2 days ago
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using BoneLib.BoneMenu;
using FusionPresence;
using FusionPresence.Config;
using FusionPresence.Menu;
using FusionPresence.Net;
using FusionPresence.Runtime;
using LabFusion.Data;
using LabFusion.Network;
using LabFusion.Player;
using LabFusion.Representation;
using LabFusion.SDK.Metadata;
using LabFusion.Scene;
using LabFusion.Utilities;
using MelonLoader;
using MelonLoader.Preferences;
using Microsoft.CodeAnalysis;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: MelonInfo(typeof(Main), "FusionPresence", "1.0.1", "Quartz", null)]
[assembly: MelonGame("Stress Level Zero", "BONELAB")]
[assembly: MelonAdditionalDependencies(new string[] { "BoneLib" })]
[assembly: MelonOptionalDependencies(new string[] { "LabFusion" })]
[assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")]
[assembly: AssemblyCompany("Quartz")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.1.0")]
[assembly: AssemblyInformationalVersion("1.0.1")]
[assembly: AssemblyProduct("FusionPresence")]
[assembly: AssemblyTitle("FusionPresence")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.0.1.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace FusionPresence
{
	internal static class Log
	{
		private static Instance _logger;

		public static bool Verbose { get; set; }

		public static void Bind(Instance logger)
		{
			_logger = logger;
		}

		public static void Msg(string message)
		{
			Instance logger = _logger;
			if (logger != null)
			{
				logger.Msg(message);
			}
		}

		public static void Warn(string message)
		{
			Instance logger = _logger;
			if (logger != null)
			{
				logger.Warning(message);
			}
		}

		public static void Error(string message)
		{
			Instance logger = _logger;
			if (logger != null)
			{
				logger.Error(message);
			}
		}

		public static void Debug(string message)
		{
			if (Verbose)
			{
				Instance logger = _logger;
				if (logger != null)
				{
					logger.Msg("[debug] " + message);
				}
			}
		}
	}
	public class Main : MelonMod
	{
		private bool _hooked;

		public override void OnInitializeMelon()
		{
			Log.Bind(((MelonBase)this).LoggerInstance);
			Settings.Register();
			WebhookSender.Start();
			PresenceMenu.Create();
			if (FusionBridge.IsFusionPresent())
			{
				PresenceTracker.Start();
				_hooked = true;
				Log.Msg("Ready. Configure the webhook under BoneMenu → Fusion Presence.");
			}
			else
			{
				Log.Warn("LabFusion not found. Presence reporting is idle until it loads.");
			}
		}

		public override void OnSceneWasLoaded(int buildIndex, string sceneName)
		{
			if (!_hooked && FusionBridge.IsFusionPresent())
			{
				PresenceTracker.Start();
				_hooked = true;
				Log.Msg("LabFusion detected on scene load, presence reporting is now active.");
			}
		}

		public override void OnUpdate()
		{
			if (_hooked)
			{
				PresenceTracker.Tick(Time.unscaledDeltaTime);
			}
		}

		public override void OnApplicationQuit()
		{
			Shutdown();
		}

		public override void OnDeinitializeMelon()
		{
			Shutdown();
		}

		private void Shutdown()
		{
			if (_hooked)
			{
				PresenceTracker.Stop();
				_hooked = false;
			}
			WebhookSender.Stop();
			PresenceMenu.Destroy();
		}
	}
}
namespace FusionPresence.Runtime
{
	internal static class FusionBridge
	{
		private static ServerEvent _onJoined;

		private static ServerEvent _onStarted;

		private static ServerEvent _onDisconnected;

		private static PlayerUpdate _onPlayerJoined;

		private static PlayerUpdate _onPlayerLeft;

		private static UpdateEvent _onSceneInit;

		public static bool Subscribed { get; private set; }

		[MethodImpl(MethodImplOptions.NoInlining)]
		public static bool IsFusionPresent()
		{
			try
			{
				_ = NetworkInfo.HasServer;
				return true;
			}
			catch (Exception ex)
			{
				Log.Debug("Fusion not present: " + ex.GetType().Name);
				return false;
			}
		}

		[MethodImpl(MethodImplOptions.NoInlining)]
		public static void Subscribe(Action onJoined, Action onStarted, Action onDisconnected, Action<string> onPlayerJoined, Action<string> onPlayerLeft, Action onSceneInit)
		{
			//IL_0042: Unknown result type (might be due to invalid IL or missing references)
			//IL_004c: Expected O, but got Unknown
			//IL_0053: Unknown result type (might be due to invalid IL or missing references)
			//IL_005d: Expected O, but got Unknown
			//IL_0064: Unknown result type (might be due to invalid IL or missing references)
			//IL_006e: Expected O, but got Unknown
			//IL_0075: Unknown result type (might be due to invalid IL or missing references)
			//IL_007f: Expected O, but got Unknown
			//IL_0086: Unknown result type (might be due to invalid IL or missing references)
			//IL_0090: Expected O, but got Unknown
			//IL_0097: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a1: Expected O, but got Unknown
			if (Subscribed)
			{
				return;
			}
			try
			{
				_onJoined = (ServerEvent)delegate
				{
					onJoined();
				};
				_onStarted = (ServerEvent)delegate
				{
					onStarted();
				};
				_onDisconnected = (ServerEvent)delegate
				{
					onDisconnected();
				};
				_onPlayerJoined = (PlayerUpdate)delegate(PlayerID id)
				{
					onPlayerJoined(SafeName(id));
				};
				_onPlayerLeft = (PlayerUpdate)delegate(PlayerID id)
				{
					onPlayerLeft(SafeName(id));
				};
				_onSceneInit = (UpdateEvent)delegate
				{
					onSceneInit();
				};
				MultiplayerHooking.OnJoinedServer += _onJoined;
				MultiplayerHooking.OnStartedServer += _onStarted;
				MultiplayerHooking.OnDisconnected += _onDisconnected;
				MultiplayerHooking.OnPlayerJoined += _onPlayerJoined;
				MultiplayerHooking.OnPlayerLeft += _onPlayerLeft;
				MultiplayerHooking.OnMainSceneInitialized += _onSceneInit;
				Subscribed = true;
				Log.Msg("Hooked into Fusion multiplayer events.");
			}
			catch (Exception value)
			{
				Log.Error($"Failed to hook Fusion events: {value}");
			}
		}

		[MethodImpl(MethodImplOptions.NoInlining)]
		public static void Unsubscribe()
		{
			if (!Subscribed)
			{
				return;
			}
			try
			{
				MultiplayerHooking.OnJoinedServer -= _onJoined;
				MultiplayerHooking.OnStartedServer -= _onStarted;
				MultiplayerHooking.OnDisconnected -= _onDisconnected;
				MultiplayerHooking.OnPlayerJoined -= _onPlayerJoined;
				MultiplayerHooking.OnPlayerLeft -= _onPlayerLeft;
				MultiplayerHooking.OnMainSceneInitialized -= _onSceneInit;
			}
			catch (Exception value)
			{
				Log.Error($"Failed to unhook Fusion events: {value}");
			}
			finally
			{
				_onJoined = null;
				_onStarted = null;
				_onDisconnected = null;
				_onPlayerJoined = null;
				_onPlayerLeft = null;
				_onSceneInit = null;
				Subscribed = false;
			}
		}

		[MethodImpl(MethodImplOptions.NoInlining)]
		public static bool HasServer()
		{
			try
			{
				return NetworkInfo.HasServer;
			}
			catch
			{
				return false;
			}
		}

		[MethodImpl(MethodImplOptions.NoInlining)]
		public static bool LobbyInfoReady()
		{
			try
			{
				LobbyInfo lobbyInfo = LobbyInfoManager.LobbyInfo;
				return lobbyInfo != null && lobbyInfo.LobbyID != 0;
			}
			catch
			{
				return false;
			}
		}

		[MethodImpl(MethodImplOptions.NoInlining)]
		private static string SafeName(PlayerID id)
		{
			try
			{
				object obj;
				if (id == null)
				{
					obj = null;
				}
				else
				{
					PlayerMetadata metadata = id.Metadata;
					if (metadata == null)
					{
						obj = null;
					}
					else
					{
						MetadataVariable username = metadata.Username;
						obj = ((username != null) ? username.GetValue() : null);
					}
				}
				string text = (string)obj;
				if (!string.IsNullOrEmpty(text))
				{
					return text;
				}
				object obj2;
				if (id == null)
				{
					obj2 = null;
				}
				else
				{
					PlayerMetadata metadata2 = id.Metadata;
					if (metadata2 == null)
					{
						obj2 = null;
					}
					else
					{
						MetadataVariable nickname = metadata2.Nickname;
						obj2 = ((nickname != null) ? nickname.GetValue() : null);
					}
				}
				string text2 = (string)obj2;
				if (!string.IsNullOrEmpty(text2))
				{
					return text2;
				}
				return (id != null) ? id.PlatformID.ToString() : "unknown";
			}
			catch
			{
				return "unknown";
			}
		}

		[MethodImpl(MethodImplOptions.NoInlining)]
		public static int GetPlayerCount()
		{
			try
			{
				return PlayerIDManager.PlayerCount;
			}
			catch
			{
				return 0;
			}
		}

		[MethodImpl(MethodImplOptions.NoInlining)]
		public unsafe static SessionSnapshot BuildSnapshot(PresenceEvent kind, string changeSummary = "")
		{
			//IL_00a4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a9: Unknown result type (might be due to invalid IL or missing references)
			SessionSnapshot sessionSnapshot = new SessionSnapshot
			{
				Event = kind,
				ChangeSummary = (changeSummary ?? "")
			};
			try
			{
				if (Settings.IncludeOwnName.Value)
				{
					sessionSnapshot.LocalName = LocalPlayer.Username ?? "";
				}
				sessionSnapshot.IsHost = NetworkInfo.IsHost;
			}
			catch (Exception ex)
			{
				Log.Debug("local player read failed: " + ex.Message);
			}
			try
			{
				LobbyInfo lobbyInfo = LobbyInfoManager.LobbyInfo;
				if (lobbyInfo != null)
				{
					sessionSnapshot.LobbyName = lobbyInfo.LobbyName ?? "";
					sessionSnapshot.LobbyHostName = lobbyInfo.LobbyHostName ?? "";
					sessionSnapshot.MaxPlayers = lobbyInfo.MaxPlayers;
					sessionSnapshot.Privacy = ((object)lobbyInfo.Privacy/*cast due to .constrained prefix*/).ToString();
					sessionSnapshot.LevelTitle = lobbyInfo.LevelTitle ?? "";
					sessionSnapshot.LevelBarcode = lobbyInfo.LevelBarcode ?? "";
				}
			}
			catch (Exception ex2)
			{
				Log.Debug("lobby info read failed: " + ex2.Message);
			}
			try
			{
				string title = FusionSceneManager.Title;
				if (!string.IsNullOrEmpty(title))
				{
					sessionSnapshot.LevelTitle = title;
					sessionSnapshot.LevelBarcode = FusionSceneManager.Barcode ?? sessionSnapshot.LevelBarcode;
				}
			}
			catch (Exception ex3)
			{
				Log.Debug("scene read failed: " + ex3.Message);
			}
			if (Settings.IncludeServerCode.Value)
			{
				try
				{
					sessionSnapshot.LobbyCode = NetworkHelper.GetServerCode() ?? "";
				}
				catch (Exception ex4)
				{
					Log.Debug("server code read failed: " + ex4.Message);
				}
			}
			try
			{
				sessionSnapshot.PlayerCount = PlayerIDManager.PlayerCount;
				if (Settings.IncludePlayerList.Value)
				{
					PermissionLevel val = default(PermissionLevel);
					foreach (PlayerID playerID in PlayerIDManager.PlayerIDs)
					{
						if (playerID != null && playerID.IsValid)
						{
							SessionSnapshot.PlayerEntry playerEntry = new SessionSnapshot.PlayerEntry();
							PlayerMetadata metadata = playerID.Metadata;
							object obj;
							if (metadata == null)
							{
								obj = null;
							}
							else
							{
								MetadataVariable username = metadata.Username;
								obj = ((username != null) ? username.GetValue() : null);
							}
							if (obj == null)
							{
								obj = "";
							}
							playerEntry.Username = (string)obj;
							PlayerMetadata metadata2 = playerID.Metadata;
							object obj2;
							if (metadata2 == null)
							{
								obj2 = null;
							}
							else
							{
								MetadataVariable nickname = metadata2.Nickname;
								obj2 = ((nickname != null) ? nickname.GetValue() : null);
							}
							if (obj2 == null)
							{
								obj2 = "";
							}
							playerEntry.Nickname = (string)obj2;
							playerEntry.IsHost = playerID.IsHost;
							playerEntry.IsMe = playerID.IsMe;
							SessionSnapshot.PlayerEntry playerEntry2 = playerEntry;
							if (Settings.IncludePermissions.Value && MetadataHelper.TryGetPermissionLevel(playerID, ref val))
							{
								playerEntry2.Permission = ((object)(*(PermissionLevel*)(&val))/*cast due to .constrained prefix*/).ToString();
							}
							sessionSnapshot.Players.Add(playerEntry2);
						}
					}
				}
			}
			catch (Exception ex5)
			{
				Log.Debug("roster read failed: " + ex5.Message);
			}
			return sessionSnapshot;
		}
	}
	internal static class PresenceTracker
	{
		private static bool _inSession;

		private static float _joinTimer = -1f;

		private static float _joinElapsed;

		private static PresenceEvent _pendingJoinKind;

		private static float _rosterTimer = -1f;

		private static readonly List<string> _joinedSince = new List<string>();

		private static readonly List<string> _leftSince = new List<string>();

		private const float JoinHardTimeout = 15f;

		public static void Start()
		{
			FusionBridge.Subscribe(delegate
			{
				BeginSession(PresenceEvent.Joined);
			}, delegate
			{
				BeginSession(PresenceEvent.Hosting);
			}, OnDisconnected, OnPlayerJoined, OnPlayerLeft, OnSceneInitialized);
		}

		public static void Stop()
		{
			FusionBridge.Unsubscribe();
			Reset();
		}

		public static void Tick(float deltaTime)
		{
			if (_joinTimer >= 0f)
			{
				_joinTimer -= deltaTime;
				_joinElapsed += deltaTime;
				bool num = FusionBridge.LobbyInfoReady() && _joinTimer <= 0f;
				bool flag = _joinElapsed >= 15f;
				if (num || flag)
				{
					_joinTimer = -1f;
					if (flag && !FusionBridge.LobbyInfoReady())
					{
						Log.Warn("Lobby info never arrived; posting with whatever we could read.");
					}
					Dispatch(_pendingJoinKind);
				}
				else if (_joinTimer <= 0f)
				{
					_joinTimer = 0f;
				}
			}
			if (_rosterTimer >= 0f)
			{
				_rosterTimer -= deltaTime;
				if (_rosterTimer <= 0f)
				{
					_rosterTimer = -1f;
					FlushRoster();
				}
			}
		}

		private static void BeginSession(PresenceEvent kind)
		{
			_inSession = true;
			_rosterTimer = -1f;
			_joinedSince.Clear();
			_leftSince.Clear();
			if (!((kind == PresenceEvent.Hosting) ? Settings.ReportHost.Value : Settings.ReportJoin.Value))
			{
				Log.Debug($"{kind} reporting is disabled.");
				return;
			}
			_pendingJoinKind = kind;
			_joinElapsed = 0f;
			_joinTimer = Math.Max(0f, Settings.JoinDelay.Value);
			Log.Debug($"{kind} detected, posting in up to {_joinTimer:0.#}s.");
		}

		private static void OnDisconnected()
		{
			_joinTimer = -1f;
			_rosterTimer = -1f;
			bool inSession = _inSession;
			_inSession = false;
			if (inSession && Settings.ReportLeave.Value)
			{
				Dispatch(PresenceEvent.Left);
			}
			_joinedSince.Clear();
			_leftSince.Clear();
		}

		private static void OnPlayerJoined(string name)
		{
			if (_inSession && Settings.ReportRoster.Value && !(_joinTimer >= 0f))
			{
				_leftSince.Remove(name);
				if (!_joinedSince.Contains(name))
				{
					_joinedSince.Add(name);
				}
				_rosterTimer = Math.Max(0.5f, Settings.RosterDebounce.Value);
			}
		}

		private static void OnPlayerLeft(string name)
		{
			if (_inSession && Settings.ReportRoster.Value && !(_joinTimer >= 0f))
			{
				_joinedSince.Remove(name);
				if (!_leftSince.Contains(name))
				{
					_leftSince.Add(name);
				}
				_rosterTimer = Math.Max(0.5f, Settings.RosterDebounce.Value);
			}
		}

		private static void OnSceneInitialized()
		{
			if (_inSession && Settings.ReportLevel.Value && !(_joinTimer >= 0f))
			{
				Dispatch(PresenceEvent.LevelChanged);
			}
		}

		private static void FlushRoster()
		{
			if (_joinedSince.Count != 0 || _leftSince.Count != 0)
			{
				List<string> list = new List<string>(2);
				if (_joinedSince.Count > 0)
				{
					list.Add("joined: " + string.Join(", ", _joinedSince));
				}
				if (_leftSince.Count > 0)
				{
					list.Add("left: " + string.Join(", ", _leftSince));
				}
				string summary = string.Join(" • ", list);
				_joinedSince.Clear();
				_leftSince.Clear();
				Dispatch(PresenceEvent.RosterChanged, summary);
			}
		}

		private static void Dispatch(PresenceEvent kind, string summary = "")
		{
			try
			{
				WebhookSender.Enqueue(PayloadBuilder.Build(FusionBridge.BuildSnapshot(kind, summary)));
			}
			catch (Exception value)
			{
				Log.Error($"Failed to build the {kind} payload: {value}");
			}
		}

		public static void SendTest()
		{
			try
			{
				SessionSnapshot snap = ((!FusionBridge.IsFusionPresent() || !FusionBridge.HasServer()) ? new SessionSnapshot
				{
					Event = PresenceEvent.Test,
					LocalName = "Local player",
					LevelTitle = "Not in a Fusion session",
					PlayerCount = 0
				} : FusionBridge.BuildSnapshot(PresenceEvent.Test));
				WebhookSender.Enqueue(PayloadBuilder.Build(snap));
			}
			catch (Exception value)
			{
				Log.Error($"Test post failed: {value}");
			}
		}

		private static void Reset()
		{
			_inSession = false;
			_joinTimer = -1f;
			_rosterTimer = -1f;
			_joinedSince.Clear();
			_leftSince.Clear();
		}
	}
	internal enum PresenceEvent
	{
		Joined,
		Hosting,
		Left,
		RosterChanged,
		LevelChanged,
		Test
	}
	internal sealed class SessionSnapshot
	{
		public sealed class PlayerEntry
		{
			public string Username = "";

			public string Nickname = "";

			public string Permission = "";

			public bool IsHost;

			public bool IsMe;
		}

		public PresenceEvent Event;

		public DateTime TimestampUtc = DateTime.UtcNow;

		public string LocalName = "";

		public bool IsHost;

		public string LobbyName = "";

		public string LobbyHostName = "";

		public string LobbyCode = "";

		public string Privacy = "";

		public string LevelTitle = "";

		public string LevelBarcode = "";

		public int PlayerCount;

		public int MaxPlayers;

		public List<PlayerEntry> Players = new List<PlayerEntry>();

		public string ChangeSummary = "";
	}
}
namespace FusionPresence.Net
{
	internal static class Json
	{
		public static string Quote(string value)
		{
			StringBuilder stringBuilder = new StringBuilder((value?.Length ?? 0) + 16);
			stringBuilder.Append('"');
			Escape(stringBuilder, value);
			stringBuilder.Append('"');
			return stringBuilder.ToString();
		}

		public static void Escape(StringBuilder sb, string value)
		{
			if (string.IsNullOrEmpty(value))
			{
				return;
			}
			foreach (char c in value)
			{
				switch (c)
				{
				case '"':
					sb.Append("\\\"");
					continue;
				case '\\':
					sb.Append("\\\\");
					continue;
				case '\b':
					sb.Append("\\b");
					continue;
				case '\f':
					sb.Append("\\f");
					continue;
				case '\n':
					sb.Append("\\n");
					continue;
				case '\r':
					sb.Append("\\r");
					continue;
				case '\t':
					sb.Append("\\t");
					continue;
				}
				if (c < ' ' || c == '\u007f')
				{
					StringBuilder stringBuilder = sb.Append("\\u");
					int num = c;
					stringBuilder.Append(num.ToString("x4"));
				}
				else
				{
					sb.Append(c);
				}
			}
		}
	}
	internal static class PayloadBuilder
	{
		private const int FieldValueLimit = 1024;

		private const int TitleLimit = 256;

		private static readonly Regex RichText = new Regex("<[^>]*>", RegexOptions.Compiled);

		public static string Build(SessionSnapshot snap)
		{
			if (!Settings.DiscordEmbed.Value)
			{
				return BuildFlat(snap);
			}
			return BuildDiscord(snap);
		}

		private static string BuildDiscord(SessionSnapshot snap)
		{
			StringBuilder stringBuilder = new StringBuilder(768);
			stringBuilder.Append('{');
			stringBuilder.Append("\"username\":").Append(Json.Quote(Trim(Settings.BotName.Value, 80)));
			stringBuilder.Append(",\"embeds\":[{");
			stringBuilder.Append("\"title\":").Append(Json.Quote(Trim(Title(snap), 256)));
			stringBuilder.Append(",\"color\":").Append(Color(snap.Event));
			stringBuilder.Append(",\"timestamp\":").Append(Json.Quote(snap.TimestampUtc.ToString("yyyy-MM-ddTHH:mm:ssZ")));
			List<(string, string, bool)> list = new List<(string, string, bool)>();
			if (snap.Event != PresenceEvent.Left)
			{
				string item = (string.IsNullOrEmpty(snap.LevelTitle) ? "Unknown" : Clean(snap.LevelTitle));
				list.Add(("Map", item, true));
				list.Add(("Players", PlayerCountText(snap), true));
				if (!string.IsNullOrEmpty(snap.Privacy))
				{
					list.Add(("Privacy", Clean(snap.Privacy), true));
				}
				if (!string.IsNullOrEmpty(snap.LobbyHostName))
				{
					list.Add(("Host", Clean(snap.LobbyHostName), true));
				}
				if (Settings.IncludeLevelBarcode.Value && !string.IsNullOrEmpty(snap.LevelBarcode))
				{
					list.Add(("Barcode", "`" + Clean(snap.LevelBarcode) + "`", false));
				}
				if (Settings.IncludeServerCode.Value && !string.IsNullOrEmpty(snap.LobbyCode))
				{
					list.Add(("Join Code", "`" + Clean(snap.LobbyCode) + "`", true));
				}
				if (!string.IsNullOrEmpty(snap.ChangeSummary))
				{
					list.Add(("Change", Clean(snap.ChangeSummary), false));
				}
				if (Settings.IncludePlayerList.Value && snap.Players.Count > 0)
				{
					list.Add(("In Lobby", PlayerListText(snap), false));
				}
			}
			else if (!string.IsNullOrEmpty(snap.LevelTitle))
			{
				list.Add(("Last Map", Clean(snap.LevelTitle), true));
			}
			if (list.Count > 0)
			{
				stringBuilder.Append(",\"fields\":[");
				for (int i = 0; i < list.Count; i++)
				{
					if (i > 0)
					{
						stringBuilder.Append(',');
					}
					stringBuilder.Append('{');
					stringBuilder.Append("\"name\":").Append(Json.Quote(list[i].Item1));
					stringBuilder.Append(",\"value\":").Append(Json.Quote(Trim(list[i].Item2, 1024)));
					stringBuilder.Append(",\"inline\":").Append(list[i].Item3 ? "true" : "false");
					stringBuilder.Append('}');
				}
				stringBuilder.Append(']');
			}
			string value = (string.IsNullOrEmpty(snap.LobbyName) ? "Fusion" : Clean(snap.LobbyName));
			stringBuilder.Append(",\"footer\":{\"text\":").Append(Json.Quote(Trim(value, 2048))).Append('}');
			stringBuilder.Append("}]}");
			return stringBuilder.ToString();
		}

		private static string Title(SessionSnapshot snap)
		{
			string text = (string.IsNullOrEmpty(snap.LocalName) ? "Someone" : Clean(snap.LocalName));
			return snap.Event switch
			{
				PresenceEvent.Joined => text + " joined a Fusion lobby", 
				PresenceEvent.Hosting => text + " is hosting a Fusion lobby", 
				PresenceEvent.Left => text + " left the lobby", 
				PresenceEvent.RosterChanged => "Lobby roster changed", 
				PresenceEvent.LevelChanged => "Lobby moved to a new map", 
				PresenceEvent.Test => "Fusion Presence test message", 
				_ => "Fusion Presence", 
			};
		}

		private static int Color(PresenceEvent kind)
		{
			return kind switch
			{
				PresenceEvent.Joined => 3908957, 
				PresenceEvent.Hosting => 5793266, 
				PresenceEvent.Left => 10070709, 
				PresenceEvent.RosterChanged => 16426522, 
				PresenceEvent.LevelChanged => 45300, 
				_ => 3092790, 
			};
		}

		private static string BuildFlat(SessionSnapshot snap)
		{
			StringBuilder stringBuilder = new StringBuilder(512);
			stringBuilder.Append('{');
			stringBuilder.Append("\"event\":").Append(Json.Quote(snap.Event.ToString().ToLowerInvariant()));
			stringBuilder.Append(",\"timestamp\":").Append(Json.Quote(snap.TimestampUtc.ToString("o")));
			stringBuilder.Append(",\"localPlayer\":").Append(Json.Quote(Clean(snap.LocalName)));
			stringBuilder.Append(",\"isHost\":").Append(snap.IsHost ? "true" : "false");
			stringBuilder.Append(",\"lobbyName\":").Append(Json.Quote(Clean(snap.LobbyName)));
			stringBuilder.Append(",\"lobbyHost\":").Append(Json.Quote(Clean(snap.LobbyHostName)));
			stringBuilder.Append(",\"privacy\":").Append(Json.Quote(snap.Privacy));
			stringBuilder.Append(",\"levelTitle\":").Append(Json.Quote(Clean(snap.LevelTitle)));
			stringBuilder.Append(",\"levelBarcode\":").Append(Json.Quote(snap.LevelBarcode));
			stringBuilder.Append(",\"playerCount\":").Append(snap.PlayerCount);
			stringBuilder.Append(",\"maxPlayers\":").Append(snap.MaxPlayers);
			if (Settings.IncludeServerCode.Value)
			{
				stringBuilder.Append(",\"lobbyCode\":").Append(Json.Quote(snap.LobbyCode));
			}
			if (!string.IsNullOrEmpty(snap.ChangeSummary))
			{
				stringBuilder.Append(",\"change\":").Append(Json.Quote(Clean(snap.ChangeSummary)));
			}
			if (Settings.IncludePlayerList.Value)
			{
				stringBuilder.Append(",\"players\":[");
				for (int i = 0; i < snap.Players.Count; i++)
				{
					SessionSnapshot.PlayerEntry playerEntry = snap.Players[i];
					if (i > 0)
					{
						stringBuilder.Append(',');
					}
					stringBuilder.Append('{');
					stringBuilder.Append("\"username\":").Append(Json.Quote(Clean(playerEntry.Username)));
					if (Settings.IncludeNicknames.Value)
					{
						stringBuilder.Append(",\"nickname\":").Append(Json.Quote(Clean(playerEntry.Nickname)));
					}
					if (Settings.IncludePermissions.Value)
					{
						stringBuilder.Append(",\"permission\":").Append(Json.Quote(playerEntry.Permission));
					}
					stringBuilder.Append(",\"isHost\":").Append(playerEntry.IsHost ? "true" : "false");
					stringBuilder.Append(",\"isYou\":").Append(playerEntry.IsMe ? "true" : "false");
					stringBuilder.Append('}');
				}
				stringBuilder.Append(']');
			}
			stringBuilder.Append('}');
			return stringBuilder.ToString();
		}

		private static string PlayerListText(SessionSnapshot snap)
		{
			StringBuilder stringBuilder = new StringBuilder(256);
			foreach (SessionSnapshot.PlayerEntry player in snap.Players)
			{
				string value = Clean(player.Username);
				if (string.IsNullOrEmpty(value))
				{
					value = "(unnamed)";
				}
				stringBuilder.Append("• ").Append(value);
				if (Settings.IncludeNicknames.Value)
				{
					string text = Clean(player.Nickname);
					if (!string.IsNullOrEmpty(text) && !text.Equals(value, StringComparison.OrdinalIgnoreCase))
					{
						stringBuilder.Append(" *(").Append(text).Append(")*");
					}
				}
				if (player.IsHost)
				{
					stringBuilder.Append(" — host");
				}
				if (player.IsMe)
				{
					stringBuilder.Append(" — you");
				}
				if (Settings.IncludePermissions.Value && !string.IsNullOrEmpty(player.Permission))
				{
					stringBuilder.Append(" `").Append(player.Permission).Append('`');
				}
				stringBuilder.Append('\n');
				if (stringBuilder.Length > 960)
				{
					stringBuilder.Append("… list truncated");
					break;
				}
			}
			return stringBuilder.ToString().TrimEnd('\n');
		}

		private static string PlayerCountText(SessionSnapshot snap)
		{
			if (snap.MaxPlayers <= 0)
			{
				return snap.PlayerCount.ToString();
			}
			return $"{snap.PlayerCount} / {snap.MaxPlayers}";
		}

		private static string Clean(string value)
		{
			if (string.IsNullOrEmpty(value))
			{
				return "";
			}
			string text = RichText.Replace(value, "");
			StringBuilder stringBuilder = new StringBuilder(text.Length + 8);
			string text2 = text;
			foreach (char c in text2)
			{
				if (c >= ' ')
				{
					bool flag;
					switch (c)
					{
					case '\u007f':
						break;
					case '#':
					case '*':
					case '>':
					case '@':
					case '\\':
					case '_':
					case '`':
					case '|':
					case '~':
						flag = true;
						goto IL_00c0;
					default:
						{
							flag = false;
							goto IL_00c0;
						}
						IL_00c0:
						if (flag)
						{
							stringBuilder.Append('\\');
						}
						stringBuilder.Append(c);
						continue;
					}
				}
				if (stringBuilder.Length > 0 && stringBuilder[stringBuilder.Length - 1] != ' ')
				{
					stringBuilder.Append(' ');
				}
			}
			return stringBuilder.ToString().Trim();
		}

		private static string Trim(string value, int max)
		{
			if (string.IsNullOrEmpty(value))
			{
				return "";
			}
			if (value.Length > max)
			{
				return value.Substring(0, max - 1) + "…";
			}
			return value;
		}
	}
	internal static class WebhookSender
	{
		private const int QueueCapacity = 24;

		private static readonly HttpClient Client = new HttpClient
		{
			Timeout = TimeSpan.FromSeconds(10.0)
		};

		private static BlockingCollection<string> _queue;

		private static Thread _worker;

		private static volatile bool _running;

		public static string LastStatus { get; private set; } = "Nothing sent yet";

		public static void Start()
		{
			if (!_running)
			{
				_queue = new BlockingCollection<string>(24);
				_running = true;
				_worker = new Thread(WorkerLoop)
				{
					Name = "FusionPresence.Webhook",
					IsBackground = true
				};
				_worker.Start();
			}
		}

		public static void Enqueue(string json)
		{
			if (_running && _queue != null)
			{
				if (!Settings.Enabled.Value)
				{
					LastStatus = "Skipped — mod disabled";
				}
				else if (!Settings.HasWebhook)
				{
					LastStatus = "Skipped — no webhook URL set";
					Log.Warn("A presence event fired but no webhook URL is configured.");
				}
				else if (!_queue.TryAdd(json))
				{
					LastStatus = "Dropped — send queue full";
					Log.Warn("Webhook queue is full, dropping this event.");
				}
			}
		}

		public static void Stop(int flushMilliseconds = 2000)
		{
			if (_running)
			{
				_running = false;
				try
				{
					_queue?.CompleteAdding();
				}
				catch
				{
				}
				try
				{
					_worker?.Join(flushMilliseconds);
				}
				catch
				{
				}
				_worker = null;
				_queue = null;
			}
		}

		private static void WorkerLoop()
		{
			string empty = string.Empty;
			try
			{
				foreach (string item in _queue.GetConsumingEnumerable())
				{
					empty = Settings.WebhookUrl.Value;
					if (!string.IsNullOrWhiteSpace(empty))
					{
						Send(empty, item);
						Thread.Sleep((int)(Math.Max(0.2f, Settings.MinSendSpacing.Value) * 1000f));
					}
				}
			}
			catch (InvalidOperationException)
			{
			}
			catch (Exception ex2)
			{
				Log.Error($"Webhook worker died: {ex2}");
				LastStatus = "Worker error — " + ex2.GetType().Name;
			}
		}

		private static void Send(string url, string payload, int attempt = 0)
		{
			try
			{
				using StringContent content = new StringContent(payload, Encoding.UTF8, "application/json");
				using HttpResponseMessage httpResponseMessage = Client.PostAsync(url, content).GetAwaiter().GetResult();
				int statusCode = (int)httpResponseMessage.StatusCode;
				if (statusCode == 429 && attempt < 3)
				{
					double num = 2.0;
					if (httpResponseMessage.Headers.TryGetValues("Retry-After", out IEnumerable<string> values))
					{
						foreach (string item in values)
						{
							if (double.TryParse(item, out var result))
							{
								num = result;
								break;
							}
						}
					}
					LastStatus = $"Rate limited, retrying in {num:0.#}s";
					Log.Warn(LastStatus);
					Thread.Sleep((int)(Math.Min(num, 30.0) * 1000.0));
					Send(url, payload, attempt + 1);
				}
				else if (httpResponseMessage.IsSuccessStatusCode)
				{
					LastStatus = $"Sent OK ({statusCode}) at {DateTime.Now:HH:mm:ss}";
					Log.Debug(LastStatus);
				}
				else
				{
					string value = SafeRead(httpResponseMessage);
					LastStatus = $"Failed — HTTP {statusCode}";
					Log.Error($"Webhook rejected the post: HTTP {statusCode}. {value}");
				}
			}
			catch (Exception ex)
			{
				LastStatus = "Failed — " + ex.GetType().Name;
				Log.Error("Webhook send failed: " + ex.Message);
			}
		}

		private static string SafeRead(HttpResponseMessage response)
		{
			try
			{
				string result = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
				return (result.Length > 400) ? result.Substring(0, 400) : result;
			}
			catch
			{
				return "(no body)";
			}
		}
	}
}
namespace FusionPresence.Menu
{
	internal static class PresenceMenu
	{
		private static Page _root;

		private static FunctionElement _statusElement;

		private static FunctionElement _urlElement;

		private static StringElement _urlInput;

		public static void Create()
		{
			//IL_000a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0025: Unknown result type (might be due to invalid IL or missing references)
			//IL_0072: Unknown result type (might be due to invalid IL or missing references)
			//IL_0084: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b8: 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_0152: Unknown result type (might be due to invalid IL or missing references)
			//IL_0182: Unknown result type (might be due to invalid IL or missing references)
			//IL_01bc: Unknown result type (might be due to invalid IL or missing references)
			//IL_01eb: Unknown result type (might be due to invalid IL or missing references)
			//IL_022e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0240: Unknown result type (might be due to invalid IL or missing references)
			//IL_027a: Unknown result type (might be due to invalid IL or missing references)
			//IL_02b4: Unknown result type (might be due to invalid IL or missing references)
			//IL_02ee: Unknown result type (might be due to invalid IL or missing references)
			//IL_0327: Unknown result type (might be due to invalid IL or missing references)
			//IL_0374: Unknown result type (might be due to invalid IL or missing references)
			//IL_0386: Unknown result type (might be due to invalid IL or missing references)
			//IL_03c0: 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_0434: Unknown result type (might be due to invalid IL or missing references)
			//IL_046e: Unknown result type (might be due to invalid IL or missing references)
			//IL_04b6: Unknown result type (might be due to invalid IL or missing references)
			//IL_0503: Unknown result type (might be due to invalid IL or missing references)
			//IL_0515: Unknown result type (might be due to invalid IL or missing references)
			//IL_055e: Unknown result type (might be due to invalid IL or missing references)
			//IL_05a6: Unknown result type (might be due to invalid IL or missing references)
			//IL_0602: Unknown result type (might be due to invalid IL or missing references)
			//IL_0614: Unknown result type (might be due to invalid IL or missing references)
			//IL_0649: Unknown result type (might be due to invalid IL or missing references)
			//IL_0679: Unknown result type (might be due to invalid IL or missing references)
			//IL_06a8: Unknown result type (might be due to invalid IL or missing references)
			_root = Page.Root.CreatePage("Fusion Presence", Color.cyan, 0, true);
			_root.CreateBool("Enabled", Color.white, Settings.Enabled.Value, (Action<bool>)delegate(bool v)
			{
				Set(Settings.Enabled, v);
			});
			Page obj = _root.CreatePage("Webhook", new Color(0.4f, 0.8f, 1f), 0, true);
			_urlElement = obj.CreateFunction(UrlLabel(), Color.grey, (Action)delegate
			{
			});
			_urlInput = obj.CreateString("Set URL", Color.white, string.Empty, (Action<string>)delegate
			{
				CommitUrl();
			});
			StringElement urlInput = _urlInput;
			((Element)urlInput).OnElementChanged = (Action)Delegate.Combine(((Element)urlInput).OnElementChanged, new Action(CommitUrl));
			obj.CreateFunction("Save URL", Color.green, (Action)CommitUrl);
			obj.CreateFunction("Clear URL", Color.red, (Action)delegate
			{
				Settings.WebhookUrl.Value = string.Empty;
				Settings.Cat.SaveToFile(false);
				if (_urlInput != null)
				{
					_urlInput.Value = string.Empty;
				}
				if (_urlElement != null)
				{
					((Element)_urlElement).ElementName = UrlLabel();
				}
				RefreshStatus();
			});
			obj.CreateBool("Discord embed format", Color.white, Settings.DiscordEmbed.Value, (Action<bool>)delegate(bool v)
			{
				Set(Settings.DiscordEmbed, v);
			});
			obj.CreateFunction("Send test message", Color.green, (Action)delegate
			{
				PresenceTracker.SendTest();
				RefreshStatus();
			});
			_statusElement = obj.CreateFunction(StatusLabel(), Color.grey, (Action)RefreshStatus);
			Page obj2 = _root.CreatePage("Events", new Color(0.6f, 1f, 0.6f), 0, true);
			obj2.CreateBool("On joining a lobby", Color.white, Settings.ReportJoin.Value, (Action<bool>)delegate(bool v)
			{
				Set(Settings.ReportJoin, v);
			});
			obj2.CreateBool("On hosting", Color.white, Settings.ReportHost.Value, (Action<bool>)delegate(bool v)
			{
				Set(Settings.ReportHost, v);
			});
			obj2.CreateBool("On leaving", Color.white, Settings.ReportLeave.Value, (Action<bool>)delegate(bool v)
			{
				Set(Settings.ReportLeave, v);
			});
			obj2.CreateBool("On player join / leave", Color.white, Settings.ReportRoster.Value, (Action<bool>)delegate(bool v)
			{
				Set(Settings.ReportRoster, v);
			});
			obj2.CreateBool("On map change", Color.white, Settings.ReportLevel.Value, (Action<bool>)delegate(bool v)
			{
				Set(Settings.ReportLevel, v);
			});
			Page obj3 = _root.CreatePage("Message Contents", new Color(1f, 0.85f, 0.4f), 0, true);
			obj3.CreateBool("Include your own name", Color.white, Settings.IncludeOwnName.Value, (Action<bool>)delegate(bool v)
			{
				Set(Settings.IncludeOwnName, v);
			});
			obj3.CreateBool("Player list", Color.white, Settings.IncludePlayerList.Value, (Action<bool>)delegate(bool v)
			{
				Set(Settings.IncludePlayerList, v);
			});
			obj3.CreateBool("Nicknames", Color.white, Settings.IncludeNicknames.Value, (Action<bool>)delegate(bool v)
			{
				Set(Settings.IncludeNicknames, v);
			});
			obj3.CreateBool("Permission levels", Color.white, Settings.IncludePermissions.Value, (Action<bool>)delegate(bool v)
			{
				Set(Settings.IncludePermissions, v);
			});
			obj3.CreateBool("Level barcode", Color.white, Settings.IncludeLevelBarcode.Value, (Action<bool>)delegate(bool v)
			{
				Set(Settings.IncludeLevelBarcode, v);
			});
			obj3.CreateBool("[!] Lobby join code", new Color(1f, 0.5f, 0.5f), Settings.IncludeServerCode.Value, (Action<bool>)delegate(bool v)
			{
				Set(Settings.IncludeServerCode, v);
				if (v)
				{
					Log.Warn("Join code will now be posted. Anyone who can read that webhook target can join your lobby.");
				}
			});
			Page obj4 = _root.CreatePage("Timing", new Color(0.8f, 0.8f, 0.8f), 0, true);
			obj4.CreateFloat("Join delay (s)", Color.white, Settings.JoinDelay.Value, 0.5f, 0f, 15f, (Action<float>)delegate(float v)
			{
				Set(Settings.JoinDelay, v);
			});
			obj4.CreateFloat("Roster debounce (s)", Color.white, Settings.RosterDebounce.Value, 0.5f, 0.5f, 30f, (Action<float>)delegate(float v)
			{
				Set(Settings.RosterDebounce, v);
			});
			obj4.CreateFloat("Min send gap (s)", Color.white, Settings.MinSendSpacing.Value, 0.1f, 0.2f, 10f, (Action<float>)delegate(float v)
			{
				Set(Settings.MinSendSpacing, v);
			});
			Page obj5 = _root.CreatePage("Diagnostics", new Color(1f, 0.6f, 0.2f), 0, true);
			obj5.CreateBool("Verbose logging", Color.white, Log.Verbose, (Action<bool>)delegate(bool v)
			{
				Log.Verbose = v;
			});
			obj5.CreateFunction("Check Fusion link", Color.white, (Action)delegate
			{
				bool flag = FusionBridge.IsFusionPresent();
				bool subscribed = FusionBridge.Subscribed;
				bool value = flag && FusionBridge.HasServer();
				bool value2 = flag && FusionBridge.LobbyInfoReady();
				Log.Msg($"Fusion loaded: {flag} | hooks attached: {subscribed} | in a session: {value} | lobby info received: {value2}");
			});
			obj5.CreateFunction("Save settings now", Color.white, (Action)delegate
			{
				Settings.Cat.SaveToFile(false);
			});
			obj5.CreateFunction("Dump settings to log", Color.white, (Action)delegate
			{
				Log.Msg("--- FusionPresence live settings ---");
				foreach (MelonPreferences_Entry entry in Settings.Cat.Entries)
				{
					if (!(entry.Identifier == ((MelonPreferences_Entry)Settings.WebhookUrl).Identifier))
					{
						Log.Msg("  " + entry.Identifier + " = " + entry.GetValueAsString());
					}
				}
				Log.Msg($"  (webhook set: {Settings.HasWebhook})");
				Log.Msg("------------------------------------");
			});
		}

		public static void Destroy()
		{
			if (_root != null)
			{
				try
				{
					Page.Root.RemovePage(_root);
				}
				catch
				{
				}
				_root = null;
				_statusElement = null;
				_urlElement = null;
				_urlInput = null;
			}
		}

		private static void CommitUrl()
		{
			if (_urlInput == null)
			{
				return;
			}
			string text = (_urlInput.Value ?? string.Empty).Trim();
			if (!(text == Settings.WebhookUrl.Value))
			{
				Settings.WebhookUrl.Value = text;
				Settings.Cat.SaveToFile(false);
				if (_urlElement != null)
				{
					((Element)_urlElement).ElementName = UrlLabel();
				}
				RefreshStatus();
				Log.Msg(string.IsNullOrEmpty(text) ? "Webhook URL cleared." : "Webhook URL saved.");
			}
		}

		private static void Set<T>(MelonPreferences_Entry<T> entry, T value)
		{
			entry.Value = value;
			Settings.Cat.SaveToFile(false);
			Log.Debug($"{((MelonPreferences_Entry)entry).Identifier} = {value}");
		}

		private static void RefreshStatus()
		{
			if (_statusElement != null)
			{
				((Element)_statusElement).ElementName = StatusLabel();
			}
		}

		private static string StatusLabel()
		{
			return "Status: " + WebhookSender.LastStatus;
		}

		private static string UrlLabel()
		{
			string value = Settings.WebhookUrl.Value;
			if (string.IsNullOrWhiteSpace(value))
			{
				return "URL: (not set)";
			}
			string text = ((value.Length > 6) ? value.Substring(value.Length - 6) : value);
			return "URL: set (…" + text + ")";
		}
	}
}
namespace FusionPresence.Config
{
	internal static class Settings
	{
		private const string Category = "FusionPresence";

		public static MelonPreferences_Category Cat { get; private set; }

		public static MelonPreferences_Entry<bool> Enabled { get; private set; }

		public static MelonPreferences_Entry<string> WebhookUrl { get; private set; }

		public static MelonPreferences_Entry<bool> DiscordEmbed { get; private set; }

		public static MelonPreferences_Entry<string> BotName { get; private set; }

		public static MelonPreferences_Entry<bool> ReportJoin { get; private set; }

		public static MelonPreferences_Entry<bool> ReportHost { get; private set; }

		public static MelonPreferences_Entry<bool> ReportLeave { get; private set; }

		public static MelonPreferences_Entry<bool> ReportRoster { get; private set; }

		public static MelonPreferences_Entry<bool> ReportLevel { get; private set; }

		public static MelonPreferences_Entry<bool> IncludeOwnName { get; private set; }

		public static MelonPreferences_Entry<bool> IncludePlayerList { get; private set; }

		public static MelonPreferences_Entry<bool> IncludeNicknames { get; private set; }

		public static MelonPreferences_Entry<bool> IncludePermissions { get; private set; }

		public static MelonPreferences_Entry<bool> IncludeLevelBarcode { get; private set; }

		public static MelonPreferences_Entry<bool> IncludeServerCode { get; private set; }

		public static MelonPreferences_Entry<float> JoinDelay { get; private set; }

		public static MelonPreferences_Entry<float> RosterDebounce { get; private set; }

		public static MelonPreferences_Entry<float> MinSendSpacing { get; private set; }

		public static bool HasWebhook
		{
			get
			{
				if (!string.IsNullOrWhiteSpace(WebhookUrl.Value))
				{
					return WebhookUrl.Value.StartsWith("http", StringComparison.OrdinalIgnoreCase);
				}
				return false;
			}
		}

		public static void Register()
		{
			Cat = MelonPreferences.CreateCategory("FusionPresence", "Fusion Presence");
			Enabled = Cat.CreateEntry<bool>("Enabled", true, (string)null, "Master switch. When false nothing is ever sent.", false, false, (ValueValidator)null, (string)null);
			WebhookUrl = Cat.CreateEntry<string>("WebhookUrl", string.Empty, (string)null, "Full webhook URL. Discord webhooks look like https://discord.com/api/webhooks/<id>/<token>", false, false, (ValueValidator)null, (string)null);
			DiscordEmbed = Cat.CreateEntry<bool>("DiscordEmbed", true, (string)null, "true = Discord embed payload. false = flat JSON, for your own endpoint.", false, false, (ValueValidator)null, (string)null);
			BotName = Cat.CreateEntry<string>("BotName", "Fusion Presence", (string)null, "Display name on the Discord post. Ignored in flat JSON mode.", false, false, (ValueValidator)null, (string)null);
			ReportJoin = Cat.CreateEntry<bool>("ReportJoin", true, (string)null, "Post when you join someone else's lobby.", false, false, (ValueValidator)null, (string)null);
			ReportHost = Cat.CreateEntry<bool>("ReportHost", true, (string)null, "Post when you start hosting.", false, false, (ValueValidator)null, (string)null);
			ReportLeave = Cat.CreateEntry<bool>("ReportLeave", true, (string)null, "Post when you disconnect.", false, false, (ValueValidator)null, (string)null);
			ReportRoster = Cat.CreateEntry<bool>("ReportRoster", false, (string)null, "Post when players join or leave while you're in a lobby.", false, false, (ValueValidator)null, (string)null);
			ReportLevel = Cat.CreateEntry<bool>("ReportLevel", true, (string)null, "Post when the lobby changes level.", false, false, (ValueValidator)null, (string)null);
			IncludeOwnName = Cat.CreateEntry<bool>("IncludeOwnName", true, (string)null, "Include your own username. Turn off to report anonymously.", false, false, (ValueValidator)null, (string)null);
			IncludePlayerList = Cat.CreateEntry<bool>("IncludePlayerList", true, (string)null, "List everyone in the lobby by name.", false, false, (ValueValidator)null, (string)null);
			IncludeNicknames = Cat.CreateEntry<bool>("IncludeNicknames", true, (string)null, "Show nicknames alongside usernames when they differ.", false, false, (ValueValidator)null, (string)null);
			IncludePermissions = Cat.CreateEntry<bool>("IncludePermissions", false, (string)null, "Tag each player with their permission level.", false, false, (ValueValidator)null, (string)null);
			IncludeLevelBarcode = Cat.CreateEntry<bool>("IncludeLevelBarcode", false, (string)null, "Include the raw level barcode, not just the title.", false, false, (ValueValidator)null, (string)null);
			IncludeServerCode = Cat.CreateEntry<bool>("IncludeServerCode", false, (string)null, "Include the lobby join code. OFF by default - anyone who can read the webhook target could join your lobby.", false, false, (ValueValidator)null, (string)null);
			JoinDelay = Cat.CreateEntry<float>("JoinDelaySeconds", 4f, (string)null, "Wait this long after joining before posting, so the host's lobby info has arrived.", false, false, (ValueValidator)null, (string)null);
			RosterDebounce = Cat.CreateEntry<float>("RosterDebounceSeconds", 6f, (string)null, "Coalesce a burst of joins/leaves into one post.", false, false, (ValueValidator)null, (string)null);
			MinSendSpacing = Cat.CreateEntry<float>("MinSendSpacingSeconds", 1.5f, (string)null, "Minimum gap between outgoing requests. Keeps you under Discord's webhook rate limit.", false, false, (ValueValidator)null, (string)null);
		}
	}
}