Decompiled source of FiresDiscordIntegration v1.0.34

plugins/FiresDiscordIntegration/FiresDiscordIntegration.dll

Decompiled 2 weeks ago
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using FiresCore.Async;
using FiresCore.Bridge;
using FiresCore.ClientLogRelay;
using FiresCore.ClientLogRelay.Interactions;
using FiresCore.ClientLogRelay.Transport;
using FiresCore.Config;
using FiresCore.Help;
using FiresCore.Lifecycle;
using FiresCore.Logging;
using FiresCore.Sync;
using FiresDiscordIntegration.ChatRelay;
using FiresDiscordIntegration.ClientLogRelay;
using FiresDiscordIntegration.ClientLogRelay.Consumers;
using FiresDiscordIntegration.ClientLogRelay.Interactions;
using FiresDiscordIntegration.ClientLogRelay.Webhook;
using FiresDiscordIntegration.Discord;
using FiresDiscordIntegration.Utilities;
using FiresLogAnalysis;
using FiresLogAnalysis.Problems;
using GUIFramework;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Splatform;
using TMPro;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyTitle("FiresDiscordIntegration")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("FiresDiscordIntegration")]
[assembly: AssemblyCopyright("Copyright ©  2025")]
[assembly: AssemblyTrademark("")]
[assembly: ComVisible(false)]
[assembly: Guid("c1f16343-d521-42e6-a7a7-1b3aa4d63f4c")]
[assembly: AssemblyFileVersion("1.0.4.0")]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.0.4.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 FiresDiscordIntegration
{
	[BepInPlugin("com.Fire.FiresDiscordIntegration", "FiresDiscordIntegration", "1.0.34")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	public class FiresDiscordIntegrationPlugin : BaseUnityPlugin
	{
		public const string PluginGUID = "com.Fire.FiresDiscordIntegration";

		public const string PluginName = "FiresDiscordIntegration";

		public const string PluginVersion = "1.0.34";

		public static FiresDiscordIntegrationPlugin Instance;

		public static readonly Harmony Harmony = new Harmony("com.Fire.FiresDiscordIntegration");

		internal static ManualLogSource Log;

		public ConfigSync configSync;

		private void Awake()
		{
			//IL_0036: 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_0046: 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_0061: Expected O, but got Unknown
			Log = ((BaseUnityPlugin)this).Logger;
			try
			{
				FiresDiscordBanner.PrintBig();
			}
			catch (Exception ex)
			{
				Debug.LogWarning((object)("[FiresDiscordIntegration] Big banner failed: " + ex.Message));
			}
			Instance = this;
			configSync = new ConfigSync("com.Fire.FiresDiscordIntegration")
			{
				DisplayName = "FiresDiscordIntegration",
				CurrentVersion = "1.0.34",
				MinimumRequiredVersion = "1.0.34"
			};
			ConfigManager.Instance.Initialize(((BaseUnityPlugin)this).Config);
			DiscordIntegrationConfig.Initialize(((BaseUnityPlugin)this).Config);
			DiscordIntegrationConfig.BindToSync(configSync);
			Harmony.PatchAll();
			DiscordSink.Register((IDiscordSink)(object)new DiscordSinkAdapter());
			try
			{
				DiscordBotIdentity.ResolveUsername("FiresDiscordIntegration");
			}
			catch (Exception ex2)
			{
				Debug.LogWarning((object)("[FiresDiscordIntegration] BotIdentity pre-warm failed: " + ex2.Message));
			}
			try
			{
				DiscordMentionResolver.FetchRoles();
			}
			catch (Exception ex3)
			{
				Debug.LogWarning((object)("[FiresDiscordIntegration] MentionResolver pre-warm failed: " + ex3.Message));
			}
			WrapperBatExtractor.ExtractIfMissing();
			try
			{
				FdiHelpContent.Register();
			}
			catch (Exception ex4)
			{
				Log.LogWarning((object)("Help registration failed: " + ex4.Message));
			}
			Log.LogInfo((object)"v1.0.34 loaded.");
			((MonoBehaviour)this).StartCoroutine(EmitCompactBannerWhenZNetReady());
		}

		private IEnumerator EmitCompactBannerWhenZNetReady()
		{
			while ((Object)(object)ZNetScene.instance == (Object)null || ZNetScene.instance.m_prefabs == null || ZNetScene.instance.m_prefabs.Count == 0)
			{
				yield return null;
			}
			yield return null;
			EmitDiscordLoadSummary();
			try
			{
				FiresDiscordBanner.Print();
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogInfo((object)("FiresDiscordIntegration v1.0.34 loaded. (banner failed: " + ex.Message + ")"));
			}
		}

		private static void EmitDiscordLoadSummary()
		{
			//IL_000b: 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)
			try
			{
				bool heartbeatArmed = ServerHeartbeat.HeartbeatArmed;
				TaggedEmitter val = LoadSummary.For("FiresDiscordIntegration");
				((TaggedEmitter)(ref val)).EmitMiniBox("📡 DISCORD LINK", new string[5]
				{
					"bot listener: " + (DiscordBotListener.Running ? "on" : "off"),
					"crash detector: " + (ServerHeartbeat.CrashNotifierArmed ? "on" : "off"),
					heartbeatArmed ? $"heartbeat: {ServerHeartbeat.IntervalSeconds:0}s" : "heartbeat: off",
					ClientLogRelayIntegration.RelayActive ? "client log relay: on" : "client log relay: off",
					ChatRelaySummary(out var detail)
				});
				Debug.Log((object)("[Discord] " + detail));
			}
			catch (Exception ex)
			{
				FiresLogger.LogWarning("discord summary failed: " + ex.Message);
			}
		}

		private static string ChatRelaySummary(out string detail)
		{
			string text = "shout hooks: " + Hooked(typeof(Chat), "SendText", "client relay") + ", " + Hooked(typeof(ZRoutedRpc), "RouteRPC", "server route") + ", " + Hooked(typeof(ZRoutedRpc), "HandleRoutedRPC", "server handle");
			if (!DiscordIntegrationCore.IsActive())
			{
				detail = "chat relay: off (Discord disabled); " + text;
				return "chat relay: off";
			}
			string text2 = ((!string.IsNullOrEmpty(DiscordIntegrationConfig.ChatWebhookUrl?.Value)) ? "ChatWebhookUrl" : ((!string.IsNullOrEmpty(DiscordIntegrationConfig.GeneralWebhookUrl?.Value)) ? "GeneralWebhookUrl" : null));
			if (text2 == null)
			{
				detail = "chat relay: OFF - set ChatWebhookUrl or GeneralWebhookUrl; " + text;
				return "chat relay: OFF (no webhook)";
			}
			List<string> list = new List<string>();
			ConfigEntry<bool> notifyPlayerShout = DiscordIntegrationConfig.NotifyPlayerShout;
			if (notifyPlayerShout != null && notifyPlayerShout.Value)
			{
				list.Add("shout");
			}
			ConfigEntry<bool> notifyPlayerSay = DiscordIntegrationConfig.NotifyPlayerSay;
			if (notifyPlayerSay != null && notifyPlayerSay.Value)
			{
				list.Add("say");
			}
			ConfigEntry<bool> notifyPlayerWhisper = DiscordIntegrationConfig.NotifyPlayerWhisper;
			if (notifyPlayerWhisper != null && notifyPlayerWhisper.Value)
			{
				list.Add("whisper");
			}
			detail = "chat relay: on via " + text2 + " (" + ((list.Count > 0) ? string.Join(", ", list) : "no chat types on") + "); " + text;
			return "chat relay: on";
		}

		private static string Hooked(Type type, string method, string label)
		{
			MethodInfo methodInfo = AccessTools.Method(type, method, (Type[])null, (Type[])null);
			Patches val = ((methodInfo != null) ? Harmony.GetPatchInfo((MethodBase)methodInfo) : null);
			if (val == null || (!val.Prefixes.Any((Patch p) => p.owner == "com.Fire.FiresDiscordIntegration") && !val.Postfixes.Any((Patch p) => p.owner == "com.Fire.FiresDiscordIntegration")))
			{
				return "NO " + label;
			}
			return label;
		}

		private void OnDestroy()
		{
			ClientLogRelayWorker.Drain();
			if (configSync != null)
			{
				configSync = null;
			}
			Instance = null;
		}
	}
	public sealed class ConfigManager
	{
		private static ConfigManager _instance;

		public ConfigEntry<bool> configVerboseLogging;

		public static ConfigManager Instance => _instance ?? (_instance = new ConfigManager());

		public ConfigFile Config { get; private set; }

		public void Initialize(ConfigFile config)
		{
			Config = config;
			configVerboseLogging = config.Bind<bool>("General", "VerboseLogging", false, "Emit additional debug logs for the Discord/log-relay subsystems. Off by default — most operators only want this on for troubleshooting.");
			ConfigHotReload.Watch(config);
		}
	}
	internal static class FiresDiscordBanner
	{
		private static bool s_compactPrinted;

		private static readonly Segment[][] s_compactLines = new Segment[13][]
		{
			(Segment[])(object)new Segment[1]
			{
				new Segment("         ╭──────────╮", ConsoleColor.Blue)
			},
			(Segment[])(object)new Segment[3]
			{
				new Segment("         │    ", ConsoleColor.Blue),
				new Segment("_/", ConsoleColor.Cyan),
				new Segment("    │", ConsoleColor.Blue)
			},
			(Segment[])(object)new Segment[3]
			{
				new Segment("         │   ", ConsoleColor.Blue),
				new Segment("/_", ConsoleColor.Cyan),
				new Segment("     │", ConsoleColor.Blue)
			},
			(Segment[])(object)new Segment[3]
			{
				new Segment("         │    ", ConsoleColor.Blue),
				new Segment("/", ConsoleColor.Cyan),
				new Segment("     │", ConsoleColor.Blue)
			},
			(Segment[])(object)new Segment[1]
			{
				new Segment("         ╰──╮  ╭────╯", ConsoleColor.Blue)
			},
			(Segment[])(object)new Segment[1]
			{
				new Segment("             ╰╯", ConsoleColor.DarkBlue)
			},
			(Segment[])(object)new Segment[1]
			{
				new Segment("   ╔══════════════════════════╗", ConsoleColor.DarkGray)
			},
			(Segment[])(object)new Segment[5]
			{
				new Segment("   ║   ", ConsoleColor.DarkGray),
				new Segment("[", ConsoleColor.Gray),
				new Segment("██████████████████", ConsoleColor.Green),
				new Segment("]", ConsoleColor.Gray),
				new Segment("   ║", ConsoleColor.DarkGray)
			},
			(Segment[])(object)new Segment[3]
			{
				new Segment("   ║     ", ConsoleColor.DarkGray),
				new Segment("\ud83d\udce1 RELAY LINKED", ConsoleColor.Cyan),
				new Segment("       ║", ConsoleColor.DarkGray)
			},
			(Segment[])(object)new Segment[1]
			{
				new Segment("   ╚══════════════════════════╝", ConsoleColor.DarkGray)
			},
			(Segment[])(object)new Segment[1]
			{
				new Segment("    FIRES DISCORD INTEGRATION", ConsoleColor.Cyan)
			},
			(Segment[])(object)new Segment[0],
			(Segment[])(object)new Segment[1]
			{
				new Segment("   Fires Discord Integration Loaded", ConsoleColor.Magenta)
			}
		};

		private static readonly (string text, ConsoleColor color)[] s_bigLines = new(string, ConsoleColor)[70]
		{
			("", ConsoleColor.Gray),
			("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Blue),
			("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%", ConsoleColor.DarkBlue),
			("%%%%%%%%%%@%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%@%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Blue),
			("%%%%%%%%%%%%%%%%%%%%%%%%%%%@%@%%%%%%%%%%%=----::::::::----=%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%@@%", ConsoleColor.DarkBlue),
			("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%=--::::::::::::::::::::--=%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Blue),
			("%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%=-::::::::::::::::::::::::::--+%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkBlue),
			("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%-::::::::::::::::::::::::::::::::-=%%%%%%%@%%@%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Blue),
			("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%=-::::::::::::::::::-::::::::::::::::-%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%", ConsoleColor.DarkBlue),
			("%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%-:::::::::::::::::::=-:::::::::::::::::-=%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Blue),
			("%%%%%%%@%%%%%%%%%%%%%%%%@%%%%-:::::::::::::::::::-%-:::::::::::::::::::=%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkBlue),
			("%%%%%%%%%%%%%%%%%%%%%%%%%%%%-:::::::::::::::::::-%@:::::::::::::::::::::=%%%%%%%@%%%%%%%@%%%%%%%%%%%", ConsoleColor.Blue),
			("%%%%%%%%%%%%%%%%%%%%%%%%%%%-:::::::::::::::::::-%%=:::::::::::::::::::::-%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkBlue),
			("%%%%%%@%%@%%%%%%%%%%%%%%%%+::::::::::::::::::::=%%-::::::::::::::::::::::=%%%%%%%%%%%%%%%%%%%%%%%%%@", ConsoleColor.Blue),
			("%%%%%%%%%@%%%%%%%%%%%%%%%%-:::::::::::::::::::=%%%-::::::::::::::::::::::-%%%%%%%%%%%%@%%%%@%%%%%%%%", ConsoleColor.DarkBlue),
			("%%%%%%%%%%%%%%%%%%%%%%%%%%-::::::::::::::::::-%%%%*****-::::::::::::::::::%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Blue),
			("%%%%%%%%%%%%%%%@%%%%%%%%%%::::::::::::::::::-%%%%%%%%%-:::::::::::::::::::*%%%%%%%%%%%%%%%%%%@%%%%%%", ConsoleColor.DarkBlue),
			("%%%%%%%%%%%%%%%%%%%%%%%%%%:::::::::::::::::-@@%%%%%%%-::::::::::::::::::::*%%%%%%%%%%%%%%%%%@%%%%%%%", ConsoleColor.Blue),
			("%%%%%%%%%%%@%%%%%@%%%%%%%%-::::::::::::::::::::::#%%-:::::::::::::::::::::%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkBlue),
			("%%%%%%%%%%%%%%@%%%%%%%%%%@-:::::::::::::::::::::-%%-:::::::::::::::::::::-%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Blue),
			("%%%%%%%%%%%%%%%%%%%%%%%%%%=:::::::::::::::::::::%%-::::::::::::::::::::::-%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkBlue),
			("%%%%%%%%%%%%%%%%%%%%%%%%%%%-:::::::::::::::::::=%-::::::::::::::::::::::-%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Blue),
			("%%%%%%%%%%%%%%%%%%%%%%%%%%%%-:::::::::::::::::-%-:::::::::::::::::::::::=%%@%%%%%%%%%%%@%%%%%%%%%%%%", ConsoleColor.DarkBlue),
			("%@%%%%%%%%%%%%%%%%%%%%%%%@%%+-::::::::::::::::=-:::::::::::::::::::::::=%%%%%%%%%%%%@%%%%%%%%%%%%%%%", ConsoleColor.Blue),
			("%%%%%%%%%%%@%%%%%%@%%%%%%@%%%#-::::::::::::::--::::::::::::::::::::::-=%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkBlue),
			("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%-:::::::::::::-::::::::::::::::::::::-%%%%%@%%%%%%%%%@%%%%%%%%%%%%%%%", ConsoleColor.Blue),
			("%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%+-::::::::::::::::::::::::::::::::-=%%%@%%%%%%%%%%%%%%%%%%%%%%%%@%%%", ConsoleColor.DarkBlue),
			("%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%=-::::::::::::::::::::::::::::-=%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%@%%%", ConsoleColor.Blue),
			("%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%=-::::::::::::::::::::::-=%%%%%%%%@%%%%%%%%%%%%%%%%@%%%%%%%%%%%", ConsoleColor.DarkBlue),
			("%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%-::::::::::::::::---=%%%@%%%%%%%%%%%%+++#%%%@%%%%%%%%%%%%%%%%", ConsoleColor.Blue),
			("%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%@%%=::::::::::-=*%%%%%%%%%%%%%%%%%%%%%%%%+=+#%%%%%%@%%%%%%%%%%%%%", ConsoleColor.DarkBlue),
			("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%-:::::::::=%%%%%%%%%%%%%%%%%%%%%=-=#%%%#%%%%%%%@%%%%%%%%%%%%%%", ConsoleColor.Blue),
			("%%%%%%%%%%%%%%%@%%%%%%%%%%%%%@%%%%%%%@::::::::-%%%%%%%%%%%%%%%%%%%%%%%=--*%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkCyan),
			("%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%-::::::-=%%%%@%%%%%%%%%%%%%-::#%%%*%%%%%@%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Cyan),
			("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%:::::-=%%%%%%%%%%%%%%%%%%%%:::*%%%%%%%%%%%%%%%%%%%@%%%%@%%%%%%%", ConsoleColor.DarkCyan),
			("%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%=::::-%%%%%%%%%%@%%%%%%%%%%%%*%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%", ConsoleColor.Cyan),
			("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%-::-=%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@", ConsoleColor.DarkCyan),
			("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%=:-=%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Cyan),
			("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%--%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@", ConsoleColor.DarkCyan),
			("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%@%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%", ConsoleColor.Cyan),
			("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkCyan),
			("%%%%%%%%%%%%%%%@@%%%%%%%%%%%%%%%%@%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%@%%", ConsoleColor.Cyan),
			("%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@@%%%%%%%%%%%@%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%", ConsoleColor.White),
			("%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Blue),
			("%%%%%%%%%%%%-::::::::-%%-::::%%-:::.:::-%%%-::.:::-%%%@-::.:::-%%-::::::::-%%-::::::::-%%%%%%%%%%%%%", ConsoleColor.White),
			("%%%%%%%%%%%%:.........-%:....%-....:...:%%:...::...:%%:...:....:%-.........-%:.........-%%%%%%%%%%%%", ConsoleColor.Blue),
			("%%%%%%%%%%%%:....%:...:%:....%:...:@....%+....--....%+....-:...:%-....#:...:%:...:%....:%%%%%%%%%%%%", ConsoleColor.White),
			("%%%@%%%%%%%%:....%:...:%:....%-....::-%%%+....--....%-....=:...:%-....:....-%:...:%....:%%%%@%%%%%%%", ConsoleColor.Blue),
			("%%%%%%%%%%%%:....%:...:%:....%%-:.....:-%+....-%%%%@%-....=:....%-........:%%:...:%....:%%%%@%%%%%%%", ConsoleColor.White),
			("%@%%%%%%%%%%:....%:...:%:....%----::....-+....--::::%-....=:...:%-....#:...-%:...:%....:%%%%%%%%%%%%", ConsoleColor.Blue),
			("%%%%%%%%%%%%:....%:...:%:....%:...:%....-*....--....%=....-:...:%-....#:...:%:...:%....:%@%%%%%%%%%%", ConsoleColor.White),
			("%%%%%%%%%%%%:.........:%:....%-....:....-%:...::...:%%:...:....:%-....#:...:%:.........-%%%%%%%%%%%%", ConsoleColor.Blue),
			("%%%%%%%%%%%%-::::::::-@%-::::%%-::...::-%%=:::..::-%%%*-::..::-%%-::::#::::-%-::::::::-%%%%%%%%%%%%%", ConsoleColor.White),
			("%%@%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Blue),
			("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%", ConsoleColor.White),
			("%%%%%%%%%%%%%%%%%%%%@%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%", ConsoleColor.Blue),
			("%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.White),
			("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Blue),
			("%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.White),
			("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%", ConsoleColor.Blue),
			("%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.White),
			("%%%%%%%%%%%%%%%%%%@%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%@%%%%%%@%%%%%%%%%%%%", ConsoleColor.Blue),
			("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%@%%%%%%%%%%%%%%%%@%%%%%%%%%@%%%%%%%%%%%%%@%%%%%%%%%%%", ConsoleColor.White),
			("@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%", ConsoleColor.Blue),
			("%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkGray),
			("%%%%%%%%%%%%@%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkGray),
			("%%%%%%%%%%%@%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkGray),
			("%%%%@%%%%%%%@%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%@%%%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%", ConsoleColor.DarkGray),
			("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%%%%%%%", ConsoleColor.DarkGray),
			("", ConsoleColor.Gray)
		};

		public static void PrintBig()
		{
			try
			{
				EmitHeader("Fires Discord Integration Loading...");
				Banner.Print(ToSegments(s_bigLines));
			}
			catch (Exception ex)
			{
				Debug.LogWarning((object)("[FiresDiscordIntegration] Big banner failed: " + ex.Message));
			}
		}

		public static void Print()
		{
			if (s_compactPrinted)
			{
				return;
			}
			s_compactPrinted = true;
			try
			{
				EmitHeader("Fires Discord Integration Loaded.");
				Banner.Print(s_compactLines);
			}
			catch (Exception ex)
			{
				Debug.LogWarning((object)("[FiresDiscordIntegration] Compact banner failed: " + ex.Message));
			}
		}

		private static void EmitHeader(string text)
		{
			if (FiresDiscordIntegrationPlugin.Log != null)
			{
				FiresDiscordIntegrationPlugin.Log.LogInfo((object)text);
			}
			else
			{
				Debug.Log((object)("[FiresDiscordIntegration] " + text));
			}
		}

		private static Segment[][] ToSegments((string text, ConsoleColor color)[] lines)
		{
			//IL_002f: 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)
			Segment[][] array = new Segment[lines.Length][];
			for (int i = 0; i < lines.Length; i++)
			{
				array[i] = (Segment[])(object)new Segment[1]
				{
					new Segment(lines[i].text, lines[i].color)
				};
			}
			return array;
		}
	}
	public static class FiresLogger
	{
		private static readonly FiresLog Log = new FiresLog("FiresDiscordIntegration", (Func<bool>)(() => ConfigManager.Instance?.configVerboseLogging?.Value == true));

		public static bool VerboseEnabled => Log.VerboseEnabled;

		public static void LogInfo(string message)
		{
			Log.Info(message);
		}

		public static void LogVerbose(string message)
		{
			Log.Verbose(message);
		}

		public static void LogDebug(string message)
		{
			Log.Debug(message);
		}

		public static void LogWarning(string message)
		{
			Log.Warning(message);
		}

		public static void LogError(string message)
		{
			Log.Error(message);
		}
	}
}
namespace FiresDiscordIntegration.ClientLogRelay
{
	[HarmonyPatch]
	public static class ClientLogRelayIntegration
	{
		private const string MOD_ID = "FiresDiscordIntegration";

		private const string PLUGIN_BRAND = "FiresDiscordIntegration";

		private const string FallbackWebhookDisplayName = "Server Relay";

		private const string RPC_SUBMIT = "VAG_SubmitClientLog";

		private const string RPC_UPDATE = "VAG_UpdateClientLog";

		private const float CLIENT_PUSH_DELAY_SECONDS = 15f;

		private const float CLIENT_REPUSH_INTERVAL_SECONDS = 900f;

		private const float GLOBAL_MIN_POST_INTERVAL_SECONDS = 10f;

		private const int CHUNK_BYTES = 65536;

		private static readonly HashSet<long> _postedPeers = new HashSet<long>();

		private static readonly object _debounceLock = new object();

		private static DateTime _lastPostUtc = DateTime.MinValue;

		private static bool _clientPushedThisSession;

		private static bool _initialized;

		private static bool _isServerSide;

		private static readonly Dictionary<long, string> _peerToPlatformId = new Dictionary<long, string>();

		private static readonly Dictionary<long, string> _peerToPlayerName = new Dictionary<long, string>();

		private static bool MasterEnabled => DiscordIntegrationConfig.NotifyClientLoginArtifacts?.Value ?? true;

		private static string PrimaryWebhookUrl => DiscordIntegrationConfig.ResolveWebhookUrl(WebhookCategory.ClientLog) ?? string.Empty;

		private static string SecondaryWebhookUrl => string.Empty;

		private static bool AutoSendFullLog => DiscordIntegrationConfig.AttachFullClientLogOnLogin?.Value ?? false;

		private static bool AttachModListOnLogin => DiscordIntegrationConfig.AttachModListOnLogin?.Value ?? true;

		private static bool AttachErrorsWarningsOnLogin => DiscordIntegrationConfig.AttachErrorsWarningsOnLogin?.Value ?? true;

		private static bool OnlyNotifyLoginIfErrorsOrWarnings => DiscordIntegrationConfig.OnlyNotifyLoginIfErrorsOrWarnings?.Value ?? false;

		private static bool LogRequestReactionEnabled => DiscordIntegrationConfig.EnableLogRequestReaction?.Value ?? true;

		private static string BotTokenValue => BotTokenFile.Token;

		private static string ServerManagerWebhookUrl => DiscordIntegrationConfig.ServerManagerWebhookUrl?.Value ?? string.Empty;

		private static string ServerManagerChannelIdValue
		{
			get
			{
				string text = DiscordIntegrationConfig.ServerManagerChannelId?.Value;
				if (!string.IsNullOrEmpty(text))
				{
					return text;
				}
				return DiscordIntegrationConfig.StatusChannelId?.Value ?? string.Empty;
			}
		}

		internal static bool RelayActive
		{
			get
			{
				if (_initialized)
				{
					if (_isServerSide)
					{
						return MasterEnabled;
					}
					return true;
				}
				return false;
			}
		}

		public static string ClientLogsRoot => ClientLogRelayPaths.GetDefaultClientLogsDir("FiresDiscordIntegration");

		private static string ResolveWebhookName()
		{
			string text = DiscordIntegrationConfig.WebhookDisplayName?.Value;
			return DiscordBotIdentity.ResolveUsername((!string.IsNullOrEmpty(text) && text != "Valheim Server") ? text : "Server Relay");
		}

		private static string ResolveAvatarUrl()
		{
			string text = DiscordIntegrationConfig.WebhookAvatarUrl?.Value;
			if (!string.IsNullOrEmpty(text))
			{
				return text;
			}
			return DiscordBotIdentity.ResolveAvatarUrl(null);
		}

		public static void InitServer()
		{
			if (!_initialized)
			{
				_initialized = true;
				_isServerSide = true;
				ClientLogRelayWorker.Prepare();
				if (!MasterEnabled)
				{
					FiresLogger.LogVerbose("[ClientLogRelay] Disabled by config (Discord.ClientLogs.NotifyClientLoginArtifacts=false) - skipping initialisation.");
					return;
				}
				TryRun("RegisterDiskConsumer", RegisterDiskConsumer);
				TryRun("RegisterDiscordConsumer", RegisterDiscordConsumer);
				TryRun("StartReactionPoller", StartReactionPoller);
				TryRun("StartServerStatusPoster", StartServerStatusPoster);
				FiresLogger.LogVerbose("[ClientLogRelay] Server-side integration initialised.");
			}
		}

		public static void InitClient()
		{
			if (!_initialized)
			{
				_initialized = true;
				_isServerSide = false;
				FiresLogger.LogVerbose("[ClientLogRelay] Client-side integration initialised (will push log on connect).");
			}
		}

		private static void TryRun(string label, Action action)
		{
			try
			{
				action();
			}
			catch (Exception ex)
			{
				Debug.LogWarning((object)("[ClientLogRelay] " + label + " failed: " + ex.GetType().Name + ": " + ex.Message));
			}
		}

		private static void RegisterDiskConsumer()
		{
			ClientLogRelay.RegisterConsumer((IClientLogConsumer)(object)new DiskConsumer("FiresDiscordIntegration.ClientLogRelay.Disk", () => ClientLogRelayPaths.GetDefaultClientLogsDir("FiresDiscordIntegration"), () => true));
			FiresLogger.LogVerbose("[ClientLogRelay] Disk consumer will persist artifacts under: " + ClientLogsRoot);
		}

		private static void RegisterDiscordConsumer()
		{
			ClientLogRelay.RegisterConsumer((IClientLogConsumer)(object)new DiscordWebhookConsumer("FiresDiscordIntegration.ClientLogRelay.Discord.Primary")
			{
				EnabledGate = () => MasterEnabled && !string.IsNullOrEmpty(PrimaryWebhookUrl),
				WebhookUrl = () => PrimaryWebhookUrl,
				WebhookName = ResolveWebhookName,
				AvatarUrl = ResolveAvatarUrl,
				BrandLabel = () => "FiresDiscordIntegration",
				AttachFullLog = () => AutoSendFullLog,
				AttachModList = () => AttachModListOnLogin,
				AttachErrorsWarnings = () => AttachErrorsWarningsOnLogin,
				OnlyIfErrorsOrWarnings = () => OnlyNotifyLoginIfErrorsOrWarnings,
				EnableLogRequestReaction = () => LogRequestReactionEnabled,
				BotToken = () => BotTokenValue,
				ServerName = () => (!((Object)(object)ZNet.instance != (Object)null)) ? "Unknown" : ZNet.instance.GetWorldName()
			});
			ClientLogRelay.RegisterConsumer((IClientLogConsumer)(object)new DiscordWebhookConsumer("FiresDiscordIntegration.ClientLogRelay.Discord.Secondary")
			{
				EnabledGate = () => MasterEnabled && !string.IsNullOrEmpty(SecondaryWebhookUrl),
				WebhookUrl = () => SecondaryWebhookUrl,
				WebhookName = ResolveWebhookName,
				AvatarUrl = ResolveAvatarUrl,
				BrandLabel = () => "FiresDiscordIntegration",
				AttachFullLog = () => AutoSendFullLog,
				AttachModList = () => AttachModListOnLogin,
				AttachErrorsWarnings = () => AttachErrorsWarningsOnLogin,
				OnlyIfErrorsOrWarnings = () => OnlyNotifyLoginIfErrorsOrWarnings,
				EnableLogRequestReaction = () => false,
				BotToken = () => (string)null,
				ServerName = () => (!((Object)(object)ZNet.instance != (Object)null)) ? "Unknown" : ZNet.instance.GetWorldName()
			});
		}

		private static void StartReactionPoller()
		{
			if (string.IsNullOrEmpty(BotTokenValue))
			{
				FiresLogger.LogVerbose("[ClientLogRelay] No bot token configured — reaction poller disabled.");
				return;
			}
			if (string.IsNullOrEmpty(PrimaryWebhookUrl))
			{
				FiresLogger.LogVerbose("[ClientLogRelay] No primary webhook URL configured — reaction poller disabled.");
				return;
			}
			ReactionPoller.Create(() => BotTokenValue, () => PrimaryWebhookUrl, () => ClientLogsRoot, ResolveWebhookName);
			FiresLogger.LogVerbose("[ClientLogRelay] Reaction poller started — polling every 60s for log requests (90s warmup per fresh snapshot to dodge Discord's reactions rate-limit bucket).");
		}

		private static void StartServerStatusPoster()
		{
			string botTokenValue = BotTokenValue;
			string serverManagerChannelIdValue = ServerManagerChannelIdValue;
			if (string.IsNullOrEmpty(botTokenValue))
			{
				FiresLogger.LogVerbose("[ClientLogRelay] No bot token configured (Discord.BotListener.BotToken) - Server Manager panel disabled.");
			}
			else if (string.IsNullOrEmpty(serverManagerChannelIdValue))
			{
				FiresLogger.LogVerbose("[ClientLogRelay] No channel ID configured (Discord.ServerManager.ServerManagerChannelId or Discord.BotListener.StatusChannelId) - Server Manager panel disabled.");
			}
			else
			{
				FiresLogger.LogVerbose("[ClientLogRelay] Server Manager panel configured - will start when ZNet is ready.");
			}
		}

		[HarmonyPatch(typeof(ZNet), "Awake")]
		[HarmonyPostfix]
		public static void ZNet_Awake_Postfix(ZNet __instance)
		{
			try
			{
				if (!_initialized)
				{
					if ((Object)(object)__instance != (Object)null && __instance.IsServer())
					{
						InitServer();
					}
					else
					{
						InitClient();
					}
				}
				if (ZRoutedRpc.instance == null)
				{
					Debug.LogWarning((object)"[ClientLogRelay] ZNet.Awake postfix fired but ZRoutedRpc.instance is null - RPC registration skipped.");
				}
				else if (_isServerSide)
				{
					ZRoutedRpc.instance.Register<ZPackage>("VAG_SubmitClientLog", (Action<long, ZPackage>)OnServerReceiveLog);
					ZRoutedRpc.instance.Register<ZPackage>("VAG_UpdateClientLog", (Action<long, ZPackage>)OnServerReceiveLogUpdate);
					FiresLogger.LogVerbose("[ClientLogRelay] Registered server-side handlers for 'VAG_SubmitClientLog' + 'VAG_UpdateClientLog'.");
					((MonoBehaviour)ZNet.instance).StartCoroutine(PeriodicTransferCleanup());
					string botTokenValue = BotTokenValue;
					string serverManagerWebhookUrl = ServerManagerWebhookUrl;
					string serverManagerChannelIdValue = ServerManagerChannelIdValue;
					if (!string.IsNullOrEmpty(botTokenValue) && !string.IsNullOrEmpty(serverManagerChannelIdValue))
					{
						ServerHeartbeat.OnServerStart(botTokenValue, serverManagerWebhookUrl, serverManagerChannelIdValue, "FiresDiscordIntegration Relay", "FiresDiscordIntegration");
						FiresLogger.LogVerbose("[ClientLogRelay] Server heartbeat + control panel started.");
					}
				}
				else
				{
					_clientPushedThisSession = false;
					FiresLogger.LogVerbose("[ClientLogRelay] Client-side ZNet awake — push-on-connect armed.");
				}
			}
			catch (Exception ex)
			{
				Debug.LogWarning((object)("[ClientLogRelay] ZNet.Awake postfix threw: " + ex.GetType().Name + ": " + ex.Message));
			}
		}

		[HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")]
		[HarmonyPostfix]
		public static void ZNet_RPC_PeerInfo_Postfix(ZNet __instance, ZRpc rpc)
		{
			try
			{
				if (!_isServerSide && !((Object)(object)__instance == (Object)null) && rpc != null)
				{
					if (_clientPushedThisSession)
					{
						FiresLogger.LogVerbose("[ClientLogRelay] PeerInfo postfix fired but client already pushed this session — skipping.");
						return;
					}
					_clientPushedThisSession = true;
					FiresLogger.LogVerbose($"[ClientLogRelay] PeerInfo handshake complete — scheduling log push in {15f:F0}s.");
					((MonoBehaviour)__instance).StartCoroutine(DelayedClientPush());
					((MonoBehaviour)__instance).StartCoroutine(PeriodicClientRepush());
				}
			}
			catch (Exception ex)
			{
				Debug.LogWarning((object)("[ClientLogRelay] ZNet.RPC_PeerInfo postfix threw: " + ex.GetType().Name + ": " + ex.Message));
			}
		}

		private static bool ServerKeepsClientLogs()
		{
			if (MasterEnabled)
			{
				return true;
			}
			FiresLogger.LogVerbose("[ClientLogRelay] The server's client-log relay is off (Discord.ClientLogs.NotifyClientLoginArtifacts); not sending this client's log.");
			return false;
		}

		private static IEnumerator DelayedClientPush()
		{
			FiresLogger.LogVerbose("[ClientLogRelay][DIAG] DelayedClientPush ENTER (waiting for PlayerSpawnGate to open)");
			bool playerReady = false;
			int attempt = 0;
			while (!playerReady && attempt < 20)
			{
				attempt++;
				bool thisAttemptReady = false;
				PlayerSpawnGate.RunWhenLocalReady(30f, (Action<Player>)delegate
				{
					thisAttemptReady = true;
				});
				float attemptDeadlineAt = Time.realtimeSinceStartup + 30f + 5f;
				while (!thisAttemptReady && Time.realtimeSinceStartup < attemptDeadlineAt)
				{
					yield return null;
				}
				if (thisAttemptReady)
				{
					playerReady = true;
					FiresLogger.LogVerbose($"[ClientLogRelay] Player ready on attempt {attempt}; proceeding with log push.");
					break;
				}
				FiresLogger.LogVerbose($"[ClientLogRelay] Player still not ready after {30f:F0}s " + $"(attempt {attempt}/{20}) — retrying.");
			}
			if (!playerReady)
			{
				Debug.LogWarning((object)($"[ClientLogRelay] Player never became ready after {20} attempts " + $"({10f:F0} min total) — " + "giving up on log push for this session."));
				yield break;
			}
			FiresLogger.LogVerbose("[ClientLogRelay][DIAG] DelayedClientPush playerReady=true, entering 2s post-ready grace");
			yield return (object)new WaitForSeconds(2f);
			FiresLogger.LogVerbose("[ClientLogRelay][DIAG] DelayedClientPush 2s grace elapsed, about to call PushLogToServerCoroutine");
			if ((Object)(object)ZNet.instance == (Object)null)
			{
				FiresLogger.LogVerbose("[ClientLogRelay] ZNet.instance gone before push — cancelling.");
				yield break;
			}
			if (ZRoutedRpc.instance == null)
			{
				Debug.LogWarning((object)"[ClientLogRelay] ZRoutedRpc.instance null at push time — cancelling.");
				yield break;
			}
			long serverPeerID = ZRoutedRpc.instance.GetServerPeerID();
			if (serverPeerID == 0L)
			{
				Debug.LogWarning((object)"[ClientLogRelay] ServerPeerID is 0 at push time — connection no longer valid; cancelling.");
			}
			else
			{
				yield return ((MonoBehaviour)ZNet.instance).StartCoroutine(PushLogToServerCoroutine(serverPeerID));
			}
		}

		private static IEnumerator PushLogToServerCoroutine(long serverId)
		{
			if (!ServerKeepsClientLogs())
			{
				yield break;
			}
			FiresLogger.LogVerbose($"[ClientLogRelay] PushLogToServer entered (serverId={serverId}) - reading local LogOutput.log...");
			byte[] logBytes = null;
			Dictionary<string, string> modList = null;
			try
			{
				ClientLogCollector.TryCollect(ref logBytes, ref modList);
				if (modList == null)
				{
					modList = new Dictionary<string, string>(0, StringComparer.OrdinalIgnoreCase);
				}
				byte[] array = logBytes;
				FiresLogger.LogVerbose($"[ClientLogRelay] PushLogToServer read complete - logBytes={((array != null) ? array.Length : 0)}B, mods={modList.Count}.");
			}
			catch (Exception ex)
			{
				Debug.LogWarning((object)("[ClientLogRelay] TryCollect failed: " + ex.Message));
				yield break;
			}
			yield return null;
			string text = string.Empty;
			try
			{
				text = GetOwnSteamId() ?? string.Empty;
			}
			catch (Exception ex2)
			{
				FiresLogger.LogVerbose("[ClientLogRelay] SteamID unavailable (" + ex2.GetType().Name + ": " + ex2.Message + "); pushing without it.");
			}
			logBytes = logBytes ?? new byte[0];
			int logSize = logBytes.Length;
			int totalChunks = ((logSize == 0) ? 1 : ((logSize + 65536 - 1) / 65536));
			FiresLogger.LogVerbose($"[ClientLogRelay] Pushing log to server: {logSize}B in {totalChunks} chunk(s) + metadata, mods={modList.Count}");
			ZPackage val = new ZPackage();
			val.Write(-1);
			val.Write(totalChunks);
			val.Write(modList.Count);
			foreach (KeyValuePair<string, string> item in modList)
			{
				val.Write(item.Key ?? string.Empty);
				val.Write(item.Value ?? string.Empty);
			}
			val.Write(text ?? string.Empty);
			byte[] array2 = val.GetArray();
			if (array2 != null && array2.Length > 420000)
			{
				Debug.LogWarning((object)$"[ClientLogRelay] Metadata ZPackage is {array2.Length}B - very large mod list!");
			}
			if (ZRoutedRpc.instance == null)
			{
				Debug.LogWarning((object)"[ClientLogRelay] ZRoutedRpc gone before metadata send - aborting push.");
				yield break;
			}
			ZRoutedRpc.instance.InvokeRoutedRPC(serverId, "VAG_SubmitClientLog", new object[1] { val });
			FiresLogger.LogVerbose($"[ClientLogRelay]  -> Sent metadata ({((array2 != null) ? array2.Length : 0)}B total, {modList.Count} mods)");
			yield return null;
			for (int chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++)
			{
				if (ZRoutedRpc.instance == null)
				{
					Debug.LogWarning((object)$"[ClientLogRelay] ZRoutedRpc gone at chunk {chunkIndex + 1}/{totalChunks} - aborting push.");
					yield break;
				}
				ZPackage val2 = new ZPackage();
				val2.Write(chunkIndex);
				val2.Write(totalChunks);
				int num = chunkIndex * 65536;
				int num2 = Math.Min(65536, logSize - num);
				byte[] array3 = new byte[num2];
				if (num2 > 0)
				{
					Array.Copy(logBytes, num, array3, 0, num2);
				}
				val2.Write(array3);
				byte[] array4 = val2.GetArray();
				if (array4 != null && array4.Length > 420000)
				{
					Debug.LogWarning((object)$"[ClientLogRelay] CRITICAL: Chunk {chunkIndex + 1} ZPackage is {array4.Length}B!");
				}
				ZRoutedRpc.instance.InvokeRoutedRPC(serverId, "VAG_SubmitClientLog", new object[1] { val2 });
				if (totalChunks > 1)
				{
					FiresLogger.LogVerbose($"[ClientLogRelay]  -> Sent chunk {chunkIndex + 1}/{totalChunks} ({num2}B data, {((array4 != null) ? array4.Length : 0)}B total)");
				}
				yield return null;
			}
			Debug.Log((object)$"[ClientLogRelay] Push complete: {logSize}B in {totalChunks} chunk(s)");
		}

		private static IEnumerator PeriodicClientRepush()
		{
			while (true)
			{
				yield return (object)new WaitForSeconds(900f);
				if ((Object)(object)ZNet.instance == (Object)null || ZRoutedRpc.instance == null)
				{
					FiresLogger.LogVerbose("[ClientLogRelay] Repush skipped: ZNet or ZRoutedRpc is null (will retry next interval)");
				}
				else if (ServerKeepsClientLogs())
				{
					long serverPeerID = ZRoutedRpc.instance.GetServerPeerID();
					if (serverPeerID == 0L)
					{
						FiresLogger.LogVerbose("[ClientLogRelay] Repush skipped: not connected to server (will retry next interval)");
					}
					else
					{
						yield return ((MonoBehaviour)ZNet.instance).StartCoroutine(RepushLogToServerCoroutine(serverPeerID));
					}
				}
			}
		}

		private static IEnumerator PeriodicTransferCleanup()
		{
			while (true)
			{
				yield return (object)new WaitForSeconds(30f);
				ClientLogChunkedTransfer.CleanupTimedOutTransfers();
			}
		}

		private static IEnumerator RepushLogToServerCoroutine(long serverId)
		{
			byte[] logBytes = null;
			try
			{
				Dictionary<string, string> dictionary = default(Dictionary<string, string>);
				ClientLogCollector.TryCollect(ref logBytes, ref dictionary);
			}
			catch (Exception ex)
			{
				Debug.LogWarning((object)("[ClientLogRelay] Repush TryCollect failed: " + ex.Message));
				yield break;
			}
			yield return null;
			string steamId = string.Empty;
			try
			{
				steamId = GetOwnSteamId() ?? string.Empty;
			}
			catch
			{
			}
			logBytes = logBytes ?? new byte[0];
			int logSize = logBytes.Length;
			if (logSize <= 65536)
			{
				if (ZRoutedRpc.instance != null)
				{
					ZPackage val = new ZPackage();
					val.Write(logBytes);
					val.Write(steamId);
					ZRoutedRpc.instance.InvokeRoutedRPC(serverId, "VAG_UpdateClientLog", new object[1] { val });
					FiresLogger.LogVerbose(string.Format("[ClientLogRelay] Re-pushed '{0}' to server: {1}B (single message)", "VAG_UpdateClientLog", logSize));
				}
				yield break;
			}
			int totalChunks = (logSize + 65536 - 1) / 65536;
			FiresLogger.LogVerbose($"[ClientLogRelay] Re-pushing large log to server: {logSize}B in {totalChunks} chunk(s)");
			for (int chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++)
			{
				if (ZRoutedRpc.instance == null)
				{
					yield break;
				}
				ZPackage val2 = new ZPackage();
				val2.Write(chunkIndex);
				val2.Write(totalChunks);
				int num = chunkIndex * 65536;
				int num2 = Math.Min(65536, logSize - num);
				byte[] array = new byte[num2];
				if (num2 > 0)
				{
					Array.Copy(logBytes, num, array, 0, num2);
				}
				val2.Write(array);
				val2.Write(steamId);
				ZRoutedRpc.instance.InvokeRoutedRPC(serverId, "VAG_UpdateClientLog", new object[1] { val2 });
				if (totalChunks > 1)
				{
					FiresLogger.LogVerbose($"[ClientLogRelay]  -> Re-pushed chunk {chunkIndex + 1}/{totalChunks} ({num2}B)");
				}
				yield return null;
			}
			FiresLogger.LogVerbose($"[ClientLogRelay] Re-push complete: {logSize}B in {totalChunks} chunk(s)");
		}

		public static void ResetDebounce()
		{
			lock (_debounceLock)
			{
				int count = _postedPeers.Count;
				_postedPeers.Clear();
				_lastPostUtc = DateTime.MinValue;
				Debug.Log((object)$"[ClientLogRelay] Debounce reset (cleared {count} posted peers).");
			}
		}

		private static void OnServerReceiveLog(long sender, ZPackage pkg)
		{
			//IL_0306: Unknown result type (might be due to invalid IL or missing references)
			//IL_0310: Expected O, but got Unknown
			try
			{
				if (!MasterEnabled)
				{
					return;
				}
				if (pkg == null)
				{
					Debug.LogWarning((object)$"[ClientLogRelay] Submit from peer {sender} had a null payload.");
					return;
				}
				int num = pkg.ReadInt();
				int num2 = pkg.ReadInt();
				if (num == -1)
				{
					int num3 = pkg.ReadInt();
					if (num3 < 0)
					{
						num3 = 0;
					}
					Dictionary<string, string> dictionary = new Dictionary<string, string>(num3, StringComparer.OrdinalIgnoreCase);
					for (int i = 0; i < num3; i++)
					{
						string text = pkg.ReadString();
						string text2 = pkg.ReadString();
						if (!string.IsNullOrEmpty(text))
						{
							dictionary[text] = text2 ?? string.Empty;
						}
					}
					string text3 = null;
					try
					{
						text3 = pkg.ReadString();
					}
					catch
					{
					}
					ClientLogChunkedTransfer.StoreMetadata(sender, num2, dictionary, text3);
					FiresLogger.LogVerbose($"[ClientLogRelay] ← Received metadata from peer {sender}: {dictionary.Count} mods, expecting {num2} chunk(s)");
					return;
				}
				byte[] array = pkg.ReadByteArray();
				FiresLogger.LogVerbose($"[ClientLogRelay] ← Received chunk {num + 1}/{num2} from peer {sender} ({array.Length}B)");
				TransferResult val = ClientLogChunkedTransfer.ReceiveChunk(sender, num, num2, array, (Dictionary<string, string>)null, (string)null);
				if (val == null)
				{
					return;
				}
				byte[] fullLog = val.LogBytes;
				Dictionary<string, string> dictionary2 = val.ModList;
				string steamId = val.SteamId;
				if (dictionary2 == null)
				{
					dictionary2 = new Dictionary<string, string>(0, StringComparer.OrdinalIgnoreCase);
				}
				FiresLogger.LogVerbose($"[ClientLogRelay] ← Complete log received from peer {sender}: {fullLog.Length}B, {dictionary2.Count} mods");
				string playerName = GetPeerPlayerName(sender);
				string platformId = ((!string.IsNullOrEmpty(steamId)) ? steamId : sender.ToString());
				lock (_debounceLock)
				{
					if (!_postedPeers.Add(sender))
					{
						FiresLogger.LogVerbose("[ClientLogRelay] Dropping duplicate submission from '" + playerName + "' (" + platformId + ") — already posted this session.");
						return;
					}
					TimeSpan timeSpan = DateTime.UtcNow - _lastPostUtc;
					if (timeSpan.TotalSeconds < 10.0)
					{
						_postedPeers.Remove(sender);
						FiresLogger.LogVerbose($"[ClientLogRelay] Rate-limiting post from '{playerName}' ({platformId}); {timeSpan.TotalSeconds:F1}s since last post (floor {10f:F0}s).");
						return;
					}
					_lastPostUtc = DateTime.UtcNow;
				}
				Dictionary<string, string> dictionary3 = null;
				try
				{
					dictionary3 = ClientLogCollector.BuildLocalModList();
				}
				catch (Exception ex)
				{
					FiresLogger.LogVerbose("[ClientLogRelay] Could not enumerate server-side mod list: " + ex.GetType().Name + ": " + ex.Message);
				}
				ClientLogArtifacts artifacts = new ClientLogArtifacts(platformId, playerName, fullLog, (IReadOnlyDictionary<string, string>)dictionary2, (IReadOnlyDictionary<string, string>)dictionary3, "FiresDiscordIntegration");
				lock (_debounceLock)
				{
					_peerToPlatformId[sender] = platformId;
					_peerToPlayerName[sender] = playerName;
				}
				int clientModCount = dictionary2.Count;
				int serverModCount = dictionary3?.Count ?? 0;
				ClientLogRelayWorker.Enqueue(delegate
				{
					ClientLogRelay.ReportArtifacts(artifacts);
					FiresLogger.LogInfo($"[ClientLogRelay] Processed log from '{playerName}' ({platformId}): {fullLog.Length} bytes, {clientModCount} client mods, {serverModCount} server mods.");
				});
			}
			catch (Exception ex2)
			{
				Debug.LogWarning((object)("[ClientLogRelay] Failed to process submitted log: " + ex2.Message));
			}
		}

		private static void OnServerReceiveLogUpdate(long sender, ZPackage pkg)
		{
			try
			{
				if (pkg == null || !MasterEnabled)
				{
					return;
				}
				pkg.SetPos(0);
				int num = pkg.ReadInt();
				pkg.SetPos(0);
				string text = null;
				byte[] log;
				if (num >= 0 && num < 1000)
				{
					int num2 = pkg.ReadInt();
					int num3 = pkg.ReadInt();
					byte[] array = pkg.ReadByteArray();
					try
					{
						text = pkg.ReadString();
					}
					catch
					{
					}
					FiresLogger.LogVerbose($"[ClientLogRelay] ← Received update chunk {num2 + 1}/{num3} from peer {sender} ({array.Length}B)");
					TransferResult val = ClientLogChunkedTransfer.ReceiveUpdateChunk(sender, num2, num3, array, text);
					if (val == null)
					{
						return;
					}
					log = val.LogBytes;
					text = val.SteamId;
					FiresLogger.LogVerbose($"[ClientLogRelay] ← Complete update received from peer {sender}: {log.Length}B");
				}
				else
				{
					log = pkg.ReadByteArray();
					try
					{
						text = pkg.ReadString();
					}
					catch
					{
					}
				}
				string playerName = GetPeerPlayerName(sender);
				string platformId = ((!string.IsNullOrEmpty(text)) ? text : sender.ToString());
				if (log == null || log.Length == 0)
				{
					FiresLogger.LogVerbose("[ClientLogRelay] Update from '" + playerName + "' (" + platformId + ") had empty log — skipping.");
					return;
				}
				string logsRoot = ClientLogsRoot;
				if (!string.IsNullOrEmpty(logsRoot))
				{
					Dictionary<string, string> serverMods = null;
					try
					{
						serverMods = ClientLogCollector.BuildLocalModList();
					}
					catch
					{
					}
					ClientLogRelayWorker.Enqueue(delegate
					{
						RewriteCachedArtifacts(logsRoot, platformId, playerName, log, serverMods);
					});
				}
				lock (_debounceLock)
				{
					_peerToPlatformId[sender] = platformId;
					_peerToPlayerName[sender] = playerName;
				}
			}
			catch (Exception ex)
			{
				Debug.LogWarning((object)("[ClientLogRelay] Failed to process log update: " + ex.Message));
			}
		}

		private static void RewriteCachedArtifacts(string logsRoot, string platformId, string playerName, byte[] log, Dictionary<string, string> serverMods)
		{
			//IL_00f9: Unknown result type (might be due to invalid IL or missing references)
			//IL_0100: Expected O, but got Unknown
			Dictionary<string, string> dictionary = new Dictionary<string, string>(0, StringComparer.OrdinalIgnoreCase);
			string text = playerName ?? "unknown";
			char[] invalidFileNameChars = Path.GetInvalidFileNameChars();
			text = string.Concat(text.Split(invalidFileNameChars));
			string text2 = (platformId ?? "unknown").Replace(":", "_").Replace("/", "_").Replace("\\", "_");
			string path = Path.Combine(logsRoot, text + "_" + text2, "modlist.txt");
			if (File.Exists(path))
			{
				try
				{
					string[] array = File.ReadAllLines(path);
					foreach (string text3 in array)
					{
						if (!string.IsNullOrEmpty(text3) && !text3.StartsWith("#"))
						{
							int num = text3.IndexOf('=');
							if (num > 0)
							{
								dictionary[text3.Substring(0, num)] = text3.Substring(num + 1);
							}
						}
					}
				}
				catch
				{
				}
			}
			ClientLogArtifacts val = new ClientLogArtifacts(platformId, playerName, log, (IReadOnlyDictionary<string, string>)dictionary, (IReadOnlyDictionary<string, string>)serverMods, "FiresDiscordIntegration");
			ClientLogRelay.AnalyzeLog(val);
			if (dictionary.Count > 0)
			{
				ClientLogRelay.ComputeModDiff(val);
			}
			ClientLogArtifactWriter.Write(logsRoot, val);
			FiresLogger.LogVerbose($"[ClientLogRelay] ← Updated all cached artifacts for '{playerName}' ({platformId}): {log.Length} bytes.");
		}

		[HarmonyPatch(typeof(ZNet), "Disconnect")]
		[HarmonyPrefix]
		public static void ZNet_Disconnect_Prefix(ZNetPeer peer)
		{
			try
			{
				if (!_isServerSide || peer == null)
				{
					return;
				}
				ClientLogChunkedTransfer.CancelTransfer(peer.m_uid);
				string value;
				string value2;
				lock (_debounceLock)
				{
					_peerToPlatformId.TryGetValue(peer.m_uid, out value);
					_peerToPlayerName.TryGetValue(peer.m_uid, out value2);
					_peerToPlatformId.Remove(peer.m_uid);
					_peerToPlayerName.Remove(peer.m_uid);
					_postedPeers.Remove(peer.m_uid);
				}
				if (string.IsNullOrEmpty(value))
				{
					value = peer.m_uid.ToString();
				}
				if (string.IsNullOrEmpty(value2))
				{
					value2 = (string.IsNullOrEmpty(peer.m_playerName) ? "unknown" : peer.m_playerName);
				}
				Entry val = DisconnectLogRegistry.TakeIfRegistered(value);
				if (val != null)
				{
					Debug.Log((object)("[ClientLogRelay] Player '" + value2 + "' (" + value + ") disconnected — posting session log (requested by " + val.RequestedByName + ")."));
					if ((Object)(object)ZNet.instance != (Object)null)
					{
						((MonoBehaviour)ZNet.instance).StartCoroutine(PostDisconnectLogAsync(value, value2, val.RequestedByName));
					}
				}
			}
			catch (Exception ex)
			{
				Debug.LogWarning((object)("[ClientLogRelay] Disconnect hook threw: " + ex.Message));
			}
		}

		private static IEnumerator PostDisconnectLogAsync(string platformId, string playerName, string requestedByName)
		{
			yield return (object)new WaitForSeconds(1f);
			ClientLogRelayWorker.Enqueue(delegate
			{
				byte[] logBytes = ReadCachedSessionLog(platformId, playerName);
				if (logBytes != null)
				{
					MainThreadDispatcher.Enqueue((Action)delegate
					{
						PostDisconnectLog(platformId, playerName, requestedByName, logBytes);
					});
				}
			});
		}

		private static byte[] ReadCachedSessionLog(string platformId, string playerName)
		{
			try
			{
				string clientLogsRoot = ClientLogsRoot;
				if (string.IsNullOrEmpty(clientLogsRoot))
				{
					return null;
				}
				string text = playerName ?? "unknown";
				char[] invalidFileNameChars = Path.GetInvalidFileNameChars();
				text = string.Concat(text.Split(invalidFileNameChars));
				string text2 = (platformId ?? "unknown").Replace(":", "_").Replace("/", "_").Replace("\\", "_");
				string text3 = Path.Combine(clientLogsRoot, text + "_" + text2, "LogOutput.log");
				if (!File.Exists(text3))
				{
					FiresLogger.LogWarning("[ClientLogRelay] No cached log at '" + text3 + "' for disconnect post.");
					return null;
				}
				byte[] array = File.ReadAllBytes(text3);
				return (array.Length == 0) ? null : array;
			}
			catch (Exception ex)
			{
				FiresLogger.LogWarning("[ClientLogRelay] Failed to read the cached log for the disconnect post: " + ex.Message);
				return null;
			}
		}

		private static void PostDisconnectLog(string platformId, string playerName, string requestedByName, byte[] logBytes)
		{
			try
			{
				string text = (platformId ?? "unknown").Replace(":", "_").Replace("/", "_").Replace("\\", "_");
				List<string> list = new List<string>(2);
				string[] array = new string[2] { PrimaryWebhookUrl, SecondaryWebhookUrl };
				foreach (string text2 in array)
				{
					if (!string.IsNullOrEmpty(text2) && MinimalWebhookPoster.IsValidWebhookUrl(text2))
					{
						list.Add(text2);
					}
				}
				if (list.Count == 0)
				{
					return;
				}
				string text3 = DateTime.UtcNow.ToString("yyyyMMdd_HHmmss");
				MinimalWebhookPoster.Embed embed = new MinimalWebhookPoster.Embed().SetTitle("♻\ufe0f Session Log — Player Disconnected").SetColor(3066993).AddField("\ud83d\udc64 Player", playerName, inline: true)
					.AddField("\ud83d\udd94 Steam ID", platformId ?? "?", inline: true)
					.AddField("\ud83d\udcc4 Log Size", FormatBytes(logBytes.Length), inline: true)
					.SetFooter("Requested by " + requestedByName);
				if (logBytes.Length <= 8323072)
				{
					List<MinimalWebhookPoster.Attachment> list2 = new List<MinimalWebhookPoster.Attachment>();
					list2.Add(new MinimalWebhookPoster.Attachment("session_log_" + text + "_" + text3 + ".log", logBytes));
					List<MinimalWebhookPoster.Attachment> files = list2;
					foreach (string item in list)
					{
						MinimalWebhookPoster.Post(item, embed, files, ResolveWebhookName());
					}
				}
				else
				{
					int num = 0;
					int num2 = 0;
					while (num2 < logBytes.Length)
					{
						num++;
						int num3 = Math.Min(8323072, logBytes.Length - num2);
						byte[] array2 = new byte[num3];
						Buffer.BlockCopy(logBytes, num2, array2, 0, num3);
						num2 += num3;
						int num4 = (logBytes.Length + 8323072 - 1) / 8323072;
						MinimalWebhookPoster.Embed embed2 = new MinimalWebhookPoster.Embed().SetTitle($"♻\ufe0f Session Log — Part {num}/{num4}").SetColor(3066993).AddField("\ud83d\udc64 Player", playerName, inline: true)
							.AddField("\ud83d\udcc4 Size", FormatBytes(array2.Length), inline: true)
							.SetFooter("Requested by " + requestedByName);
						List<MinimalWebhookPoster.Attachment> files2 = new List<MinimalWebhookPoster.Attachment>
						{
							new MinimalWebhookPoster.Attachment($"session_log_{text}_{text3}_part{num}.log", array2)
						};
						foreach (string item2 in list)
						{
							MinimalWebhookPoster.Post(item2, embed2, files2, ResolveWebhookName());
						}
					}
				}
				Debug.Log((object)$"[ClientLogRelay] Posted disconnect session log for '{playerName}' ({platformId}), {logBytes.Length} bytes.");
			}
			catch (Exception ex)
			{
				Debug.LogWarning((object)("[ClientLogRelay] Failed to post disconnect log: " + ex.Message));
			}
		}

		private static string FormatBytes(long bytes)
		{
			if (bytes <= 0)
			{
				return "0 B";
			}
			string[] array = new string[4] { "B", "KB", "MB", "GB" };
			double num = bytes;
			int num2 = 0;
			while (num >= 1024.0 && num2 < array.Length - 1)
			{
				num /= 1024.0;
				num2++;
			}
			return $"{num:0.##} {array[num2]}";
		}

		private static string GetPeerPlayerName(long uid)
		{
			if ((Object)(object)ZNet.instance == (Object)null)
			{
				return "unknown";
			}
			foreach (ZNetPeer peer in ZNet.instance.GetPeers())
			{
				if (peer != null && peer.m_uid == uid)
				{
					return string.IsNullOrEmpty(peer.m_playerName) ? "unknown" : peer.m_playerName;
				}
			}
			return "unknown";
		}

		private static string GetOwnSteamId()
		{
			try
			{
				Type type = Type.GetType("Steamworks.SteamAPI, Steamworks.NET") ?? AccessTools.TypeByName("Steamworks.SteamAPI");
				Type type2 = Type.GetType("Steamworks.SteamUser, Steamworks.NET") ?? AccessTools.TypeByName("Steamworks.SteamUser");
				if (type == null || type2 == null)
				{
					return string.Empty;
				}
				MethodInfo method = type.GetMethod("IsSteamRunning", BindingFlags.Static | BindingFlags.Public);
				if (method == null)
				{
					return string.Empty;
				}
				object obj = method.Invoke(null, null);
				if (!(obj is bool) || !(bool)obj)
				{
					return string.Empty;
				}
				MethodInfo method2 = type2.GetMethod("GetSteamID", BindingFlags.Static | BindingFlags.Public);
				if (method2 == null)
				{
					return string.Empty;
				}
				object obj2 = method2.Invoke(null, null);
				if (obj2 == null)
				{
					return string.Empty;
				}
				FieldInfo field = obj2.GetType().GetField("m_SteamID", BindingFlags.Instance | BindingFlags.Public);
				if (field != null)
				{
					object value = field.GetValue(obj2);
					if (value != null)
					{
						return value.ToString();
					}
				}
				return obj2.ToString();
			}
			catch
			{
			}
			return string.Empty;
		}
	}
	internal static class ClientLogRelayWorker
	{
		private const int DrainTimeoutMs = 10000;

		private const string ThreadName = "FiresDiscordIntegration.ClientLogRelay";

		private static readonly BlockingCollection<Action> _jobs = new BlockingCollection<Action>();

		private static readonly object _startLock = new object();

		private static Thread _thread;

		private static int _queued;

		private static bool _prepared;

		internal static bool OnWorkerThread
		{
			get
			{
				if (_thread != null)
				{
					return Thread.CurrentThread == _thread;
				}
				return false;
			}
		}

		internal static void Prepare()
		{
			if (!_prepared)
			{
				_prepared = true;
				MainThreadDispatcher.Enqueue((Action)delegate
				{
				});
			}
		}

		internal static void Enqueue(Action job)
		{
			if (job == null)
			{
				return;
			}
			EnsureThread();
			Interlocked.Increment(ref _queued);
			try
			{
				_jobs.Add(job);
			}
			catch (InvalidOperationException)
			{
				Run(job);
			}
		}

		internal static void Drain()
		{
			if (_thread == null)
			{
				return;
			}
			Stopwatch stopwatch = Stopwatch.StartNew();
			ManualResetEventSlim done = new ManualResetEventSlim(initialState: false);
			try
			{
				_jobs.Add(delegate
				{
					done.Set();
				});
			}
			catch (InvalidOperationException)
			{
				return;
			}
			if (done.Wait(10000))
			{
				Debug.Log((object)$"[ClientLogRelay] shutdown: relayed-log queue drained in {stopwatch.ElapsedMilliseconds} ms ({_queued} job(s) this session).");
			}
			else
			{
				Debug.LogWarning((object)$"[ClientLogRelay] relayed logs still writing after {10} s at shutdown.");
			}
		}

		private static void EnsureThread()
		{
			lock (_startLock)
			{
				if (_thread == null)
				{
					_thread = new Thread(Loop)
					{
						IsBackground = true,
						Name = "FiresDiscordIntegration.ClientLogRelay"
					};
					_thread.Start();
				}
			}
		}

		private static void Loop()
		{
			foreach (Action item in _jobs.GetConsumingEnumerable())
			{
				Run(item);
			}
		}

		private static void Run(Action job)
		{
			try
			{
				job();
			}
			catch (Exception ex)
			{
				FiresLogger.LogWarning("[ClientLogRelay] background job failed: " + ex.Message);
			}
		}
	}
}
namespace FiresDiscordIntegration.ClientLogRelay.Webhook
{
	public static class MinimalWebhookPoster
	{
		public sealed class Embed
		{
			public string Title;

			public string Description;

			public int Color = 3447003;

			public string Footer;

			public string ImageUrl;

			public readonly List<(string name, string value, bool inline)> Fields = new List<(string, string, bool)>();

			public Embed SetTitle(string title)
			{
				Title = title;
				return this;
			}

			public Embed SetDescription(string desc)
			{
				Description = desc;
				return this;
			}

			public Embed SetColor(int rgbInt)
			{
				Color = rgbInt;
				return this;
			}

			public Embed SetFooter(string footer)
			{
				Footer = footer;
				return this;
			}

			public Embed SetImage(string url)
			{
				ImageUrl = url;
				return this;
			}

			public Embed AddField(string name, string value, bool inline = false)
			{
				Fields.Add((name, value, inline));
				return this;
			}
		}

		public sealed class Attachment
		{
			public string FileName;

			public byte[] Content;

			public string ContentType = "text/plain";

			public Attachment(string fileName, byte[] bytes, string contentType = "text/plain")
			{
				FileName = fileName;
				Content = bytes ?? Array.Empty<byte>();
				ContentType = contentType;
			}

			public Attachment(string fileName, string text, string contentType = "text/plain")
				: this(fileName, Encoding.UTF8.GetBytes(text ?? string.Empty), contentType)
			{
			}
		}

		public sealed class CoroutineHost : MonoBehaviour
		{
		}

		private const string BoundaryPrefix = "------ClientLogRelayBoundary";

		private const float ReactionPutCooldownSeconds = 0.5f;

		private const float MessageDeleteCooldownSeconds = 0.5f;

		private const float PostMessageCleanupDelaySeconds = 2f;

		private static CoroutineHost _host;

		private static bool s_noUrlNoted;

		private static readonly HashSet<string> s_badUrlsWarned = new HashSet<string>(StringComparer.Ordinal);

		private const int UrlHintLength = 24;

		private static void NoteSkippedPost(string url)
		{
			if (string.IsNullOrWhiteSpace(url))
			{
				if (!s_noUrlNoted)
				{
					s_noUrlNoted = true;
					FiresLogger.LogDebug("[ClientLogRelay] No webhook URL set: relay posts are off.");
				}
			}
			else if (s_badUrlsWarned.Add(url))
			{
				string arg = ((url.Length > 24) ? (url.Substring(0, 24) + "...") : url);
				FiresLogger.LogWarning($"[ClientLogRelay] Webhook URL is not a Discord webhook (\"{arg}\", {url.Length} characters); " + "posts to it are skipped. Expected https://discord.com/api/webhooks/... or a discord-bot-channel:<id> target.");
			}
		}

		public static bool IsValidWebhookUrl(string url)
		{
			if (string.IsNullOrEmpty(url) || !url.StartsWith("https://", StringComparison.Ordinal))
			{
				return false;
			}
			if (url.IndexOf("discord.com/api/webhooks/", StringComparison.Ordinal) < 0)
			{
				return url.IndexOf("discordapp.com/api/webhooks/", StringComparison.Ordinal) >= 0;
			}
			return true;
		}

		public static void Post(string webhookUrl, Embed embed, List<Attachment> files, string username = null, string avatarUrl = null)
		{
			Post(webhookUrl, embed, files, username, avatarUrl, null);
		}

		public static void Post(string webhookUrl, Embed embed, List<Attachment> files, string username, string avatarUrl, Action<string, string> onPosted)
		{
			string botChannel = null;
			if (BotChannelTarget.TryParse(webhookUrl, out botChannel))
			{
				username = null;
				avatarUrl = null;
			}
			else if (!IsValidWebhookUrl(webhookUrl))
			{
				NoteSkippedPost(webhookUrl);
				onPosted?.Invoke(null, null);
				return;
			}
			ClientLogRelayWorker.Prepare();
			ClientLogRelayWorker.Enqueue(delegate
			{
				string boundary = "------ClientLogRelayBoundary" + Guid.NewGuid().ToString("N");
				byte[] body = null;
				try
				{
					body = BuildMultipart(boundary, embed, files, username, avatarUrl);
				}
				catch (Exception ex)
				{
					FiresLogger.LogWarning("[ClientLogRelay] Webhook body build failed: " + ex.Message);
				}
				MainThreadDispatcher.Enqueue((Action)delegate
				{
					if (body == null)
					{
						onPosted?.Invoke(null, null);
					}
					else
					{
						EnsureHost();
						((MonoBehaviour)_host).StartCoroutine(PostCoroutine(webhookUrl, boundary, body, onPosted, botChannel));
					}
				});
			});
		}

		private static IEnumerator PostCoroutine(string webhookUrl, string boundary, byte[] body, Action<string, string> onPosted, string botChannel = null)
		{
			string text = ((botChannel != null) ? BotChannelTarget.MessagesUrl(botChannel) : ((webhookUrl.IndexOf('?') >= 0) ? (webhookUrl + "&wait=true") : (webhookUrl + "?wait=true")));
			UnityWebRequest req = new UnityWebRequest(text, "POST");
			try
			{
				req.uploadHandler = (UploadHandler)new UploadHandlerRaw(body);
				req.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer();
				req.SetRequestHeader("Content-Type", "multipart/form-data; boundary=" + boundary);
				if (botChannel != null)
				{
					req.SetRequestHeader("Authorization", BotChannelTarget.AuthHeader());
				}
				req.timeout = 30;
				yield return req.SendWebRequest();
				if ((int)req.result == 2 || (int)req.result == 3)
				{
					string text2 = null;
					try
					{
						DownloadHandler downloadHandler = req.downloadHandler;
						text2 = ((downloadHandler != null) ? downloadHandler.text : null);
					}
					catch
					{
					}
					string text3 = (string.IsNullOrEmpty(text2) ? "<empty>" : ((text2.Length > 500) ? (text2.Substring(0, 500) + "…") : text2));
					Debug.LogWarning((object)((botChannel != null) ? $"[Discord] test shot to channel {BotChannelTarget.Short(botChannel)} (bot) FAILED ({req.responseCode}): {req.error} | body: {text3}" : $"[ClientLogRelay] Webhook POST failed ({req.responseCode}): {req.error} | body: {text3}"));
					onPosted?.Invoke(null, null);
					yield break;
				}
				if (botChannel != null)
				{
					Debug.Log((object)("[Discord] test shot posted to channel " + BotChannelTarget.Short(botChannel) + " (bot)"));
				}
				string arg = null;
				string arg2 = null;
				try
				{
					DownloadHandler downloadHandler2 = req.downloadHandler;
					string text4 = ((downloadHandler2 != null) ? downloadHandler2.text : null);
					if (!string.IsNullOrEmpty(text4))
					{
						Dictionary<string, object> dictionary = JsonConvert.DeserializeObject<Dictionary<string, object>>(text4);
						if (dictionary != null)
						{
							if (dictionary.TryGetValue("id", out var value) && value != null)
							{
								arg = value.ToString();
							}
							if (dictionary.TryGetValue("channel_id", out var value2) && value2 != null)
							{
								arg2 = value2.ToString();
							}
						}
					}
				}
				catch (Exception ex)
				{
					Debug.LogWarning((object)("[ClientLogRelay] Webhook response parse failed: " + ex.Message));
				}
				try
				{
					onPosted?.Invoke(arg, arg2);
				}
				catch (Exception ex2)
				{
					Debug.LogWarning((object)("[ClientLogRelay] onPosted callback threw: " + ex2.Message));
				}
			}
			finally
			{
				((IDisposable)req)?.Dispose();
			}
		}

		private static byte[] BuildMultipart(string boundary, Embed embed, List<Attachment> files, string username, string avatarUrl)
		{
			Dictionary<string, object> dictionary = new Dictionary<string, object>();
			if (!string.IsNullOrEmpty(username))
			{
				dictionary["username"] = username;
			}
			if (!string.IsNullOrEmpty(avatarUrl))
			{
				dictionary["avatar_url"] = avatarUrl;
			}
			if (embed != null)
			{
				Dictionary<string, object> dictionary2 = new Dictionary<string, object>();
				if (!string.IsNullOrEmpty(embed.Title))
				{
					dictionary2["title"] = embed.Title;
				}
				if (!string.IsNullOrEmpty(embed.Description))
				{
					dictionary2["description"] = embed.Description;
				}
				dictionary2["color"] = embed.Color;
				if (!string.IsNullOrEmpty(embed.Footer))
				{
					dictionary2["footer"] = new Dictionary<string, object> { ["text"] = embed.Footer };
				}
				if (!string.IsNullOrEmpty(embed.ImageUrl))
				{
					dictionary2["image"] = new Dictionary<string, object> { ["url"] = embed.ImageUrl };
				}
				if (embed.Fields.Count > 0)
				{
					List<object> list = new List<object>(embed.Fields.Count);
					foreach (var field in embed.Fields)
					{
						list.Add(new Dictionary<string, object>
						{
							["name"] = field.name ?? string.Empty,
							["value"] = (string.IsNullOrEmpty(field.value) ? "-" : field.value),
							["inline"] = field.inline
						});
					}
					dictionary2["fields"] = list;
				}
				dictionary["embeds"] = new Dictionary<string, object>[1] { dictionary2 };
			}
			string s = JsonConvert.SerializeObject((object)dictionary);
			byte[] bytes = Encoding.UTF8.GetBytes("--" + boundary + "\r\n");
			byte[] bytes2 = Encoding.UTF8.GetBytes("\r\n");
			byte[] bytes3 = Encoding.UTF8.GetBytes("--" + boundary + "--\r\n");
			List<byte[]> list2 = new List<byte[]>(6 + (files?.Count ?? 0) * 4);
			list2.Add(bytes);
			list2.Add(Encoding.UTF8.GetBytes("Content-Disposition: form-data; name=\"payload_json\"\r\nContent-Type: application/json\r\n\r\n"));
			list2.Add(Encoding.UTF8.GetBytes(s));
			list2.Add(bytes2);
			if (files != null)
			{
				for (int i = 0; i < files.Count; i++)
				{
					Attachment attachment = files[i];
					list2.Add(bytes);
					string s2 = $"Content-Disposition: form-data; name=\"files[{i}]\"; filename=\"{attachment.FileName}\"\r\n" + "Content-Type: " + attachment.ContentType + "\r\n\r\n";
					list2.Add(Encoding.UTF8.GetBytes(s2));
					list2.Add(attachment.Content);
					list2.Add(bytes2);
				}
			}
			list2.Add(bytes3);
			int num = 0;
			foreach (byte[] item in list2)
			{
				num += item.Length;
			}
			byte[] array = new byte[num];
			int num2 = 0;
			foreach (byte[] item2 in list2)
			{
				Buffer.BlockCopy(item2, 0, array, num2, item2.Length);
				num2 += item2.Length;
			}
			return array;
		}

		public static void AddReaction(string botToken, string channelId, string messageId, string emoji)
		{
			if (!string.IsNullOrEmpty(botToken) && !string.IsNullOrEmpty(channelId) && !string.IsNullOrEmpty(messageId) && !string.IsNullOrEmpty(emoji))
			{
				EnsureHost();
				((MonoBehaviour)_host).StartCoroutine(AddReactionCoroutine(botToken, channelId, messageId, emoji));
			}
		}

		public static void AddReactions(string botToken, string channelId, string messageId, params string[] emojis)
		{
			if (!string.IsNullOrEmpty(botToken) && !string.IsNullOrEmpty(channelId) && !string.IsNullOrEmpty(messageId) && emojis != null && emojis.Length != 0)
			{
				EnsureHost();
				((MonoBehaviour)_host).StartCoroutine(AddReactionsCoroutine(botToken, channelId, messageId, emojis));
			}
		}

		private static IEnumerator AddReactionsCoroutine(string botToken, string channelId, string messageId, string[] emojis)
		{
			for (int i = 0; i < emojis.Length; i++)
			{
				if (!string.IsNullOrEmpty(emojis[i]))
				{
					yield return AddReactionCoroutine(botToken, channelId, messageId, emojis[i]);
				}
			}
		}

		private static IEnumerator AddReactionCoroutine(string botToken, string channelId, string messageId, string emoji)
		{
			string text = Uri.EscapeDataString(emoji);
			string text2 = "https://discord.com/api/v10/channels/" + channelId + "/messages/" + messageId + "/reactions/" + text + "/@me";
			UnityWebRequest req = new UnityWebRequest(text2, "PUT");
			try
			{
				req.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer();
				req.SetRequestHeader("Authorization", "Bot " + botToken);
				req.timeout = 15;
				yield return req.SendWebRequest();
				if ((int)req.result == 2 || (int)req.result == 3)
				{
					Debug.LogWarning((object)$"[ClientLogRelay] AddReaction failed ({req.responseCode}): {req.error}");
				}
				else
				{
					Debug.Log((object)("[ClientLogRelay] Bot pre-reacted with " + emoji + " on message " + messageId));
				}
			}
			finally
			{
				((IDisposable)req)?.Dispose();
			}
			yield return (object)new WaitForSeconds(0.5f);
		}

		public static void StartCleanupOldClientLogs(string botToken, string channelId, int keepCount = 10)
		{
			if (!string.IsNullOrEmpty(botToken) && !string.IsNullOrEmpty(channelId))
			{
				EnsureHost();
				((MonoBehaviour)_host).StartCoroutine(CleanupOldClientLogsCoroutine(botToken, channelId, keepCount));
			}
		}

		private static IEnumerator CleanupOldClientLogsCoroutine(string botToken, string channelId, int keepCount)
		{
			yield return (object)new WaitForSeconds(2f);
			string text = "https://discord.com/api/v10/channels/" + channelId + "/messages?limit=50";
			UnityWebRequest req = UnityWebRequest.Get(text);
			try
			{
				req.SetRequestHeader("Authorization", "Bot " + botToken);
				req.timeout = 15;
				yield return req.SendWebRequest();
				if ((int)req.result == 2 || (int)req.result == 3)
				{
					if (req.responseCode != 403 && req.responseCode != 404)
					{
						Debug.LogWarning((object)$"[ClientLogRelay] Failed to fetch messages for cleanup ({req.responseCode})");
					}
					yield break;
				}
				JArray val;
				try
				{
					val = JArray.Parse(req.downloadHandler.text);
				}
				catch (Exception ex)
				{
					Debug.LogWarning((object)("[ClientLogRelay] Failed to parse messages for cleanup: " + ex.Message));
					yield break;
				}
				if (val == null || ((JContainer)val).Count <= keepCount)
				{
					yield break;
				}
				List<string> clientLogMessages = new List<string>();
				foreach (JToken item in val)
				{
					JObject val2 = (JObject)(object)((item is JObject) ? item : null);
					if (val2 == null)
					{
						continue;
					}
					JToken obj = val2["author"];
					JObject val3 = (JObject)(object)((obj is JObject) ? obj : null);
					if (val3 == null)
					{
						continue;
					}
					JToken val4 = val3["bot"];
					if (val4 == null || (int)val4.Type != 9 || !(bool)val4)
					{
						continue;
					}
					JToken obj2 = val2["embeds"];
					JArray val5 = (JArray)(object)((obj2 is JArray) ? obj2 : null);
					if (val5 == null || ((JContainer)val5).Count == 0)
					{
						continue;
					}
					bool flag = false;
					foreach (JToken item2 in val5)
					{
						JToken obj3 = ((item2 is JObject) ? item2 : null);
						string text2 = ((obj3 == null) ? null : ((object)((JObject)obj3)["title"])?.ToString()) ?? "";
						string text3 = ((obj3 == null) ? null : ((object)((JObject)obj3)["description"])?.ToString()) ?? "";
						if (text2.Contains("Client") || text2.Contains("Login") || text2.Contains("Snapshot") || text3.Contains("Platform ID") || text3.Contains("Captured"))
						{
							flag = true;
							break;
						}
					}
					if (flag)
					{
						string text4 = ((object)val2["id"])?.ToString();
						if (!string.IsNullOrEmpty(text4))
						{
							clientLogMessages.Add(text4);
						}
					}
				}
				int num = clientLogMessages.Count - keepCount;
				if (num <= 0)
				{
					yield break;
				}
				Debug.Log((object)$"[ClientLogRelay] Found {clientLogMessages.Count} client log messages, deleting oldest {num} to keep {keepCount}");
				for (int i = keepCount; i < clientLogMessages.Count; i++)
				{
					string messageId = clientLogMessages[i];
					string text5 = "https://discord.com/api/v10/channels/" + channelId + "/messages/" + messageId;
					UnityWebRequest delReq = UnityWebRequest.Delete(text5);
					try
					{
						delReq.SetRequestHeader("Authorization", "Bot " + botToken);
						delReq.timeout = 10;
						yield return delReq.SendWebRequest();
						if (((int)delReq.result == 2 || (int)delReq.result == 3) && delReq.responseCode != 404 && delReq.responseCode != 403)
						{
							Debug.LogWarning((object)$"[ClientLogRelay] Failed to delete message {messageId} ({delReq.responseCode})");
						}
					}
					finally
					{
						((IDisposable)delReq)?.Dispose();
					}
					yield return (object)new WaitForSeconds(0.5f);
				}
				Debug.Log((object)$"[ClientLogRelay] Client log cleanup complete - kept {keepCount} most recent messages");
			}
			finally
			{
				((IDisposable)req)?.Dispose();
			}
		}

		private static void EnsureHost()
		{
			//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_001e: Expected O, but got Unknown
			if (!((Object)(object)_host != (Object)null))
			{
				GameObject val = new GameObject("ClientLogRelay_WebhookHost");
				Object.DontDestroyOnLoad((Object)val);
				_host = val.AddComponent<CoroutineHost>();
			}
		}
	}
}
namespace FiresDiscordIntegration.ClientLogRelay.Interactions
{
	public sealed class ReactionPoller : MonoBehaviour
	{
		public const string ServerListConsumerId = "vangarde_serverlists";

		private const float POLL_INTERVAL_SECONDS = 60f;

		private const float RATE_LIMIT_BACKOFF_SECONDS = 300f;

		private static readonly TimeSpan PostRegistrationWarmupSeconds = TimeSpan.FromSeconds(90.0);

		private const float InitialPollStartupDelaySeconds = 30f;

		private const float InterEmojiPollGapSeconds = 0.75f;

		private const float InterMessagePollGapSeconds = 3f;

		private const float PostPublishCooldownSeconds = 1.5f;

		private static int _consecutiveRateLimits = 0;

		private static float _lastRateLimitTime = 0f;

		private const int DiscordFreeWebhookFileLimitBytes = 8388608;

		private const int MultipartEnvelopeMarginBytes = 65536;

		private const int MAX_ATTACHMENT_BYTES = 8323072;

		private Func<string> _botTokenResolver;

		private Func<string> _webhookUrlResolver;

		private Func<string> _clientLogsRootResolver;

		private Func<string> _webhookNameResolver;

		private readonly HashSet<string> _fulfilled = new HashSet<string>(StringComparer.Ordinal);

		private string _botUserId;

		private bool _botUserIdResolved;

		private static readonly string[] _emojiRaw = new string[4] { "\ud83d\udce9", "⛔", "\ud83e\udde9", "♻\ufe0f" };

		private static readonly string[] _emojiEncoded = _emojiRaw.Select((string e) => Uri.EscapeDataString(e)).ToArray();

		private static string EmojiLabel(string emoji)
		{
			return emoji switch
			{
				"\ud83d\udce9" => "LOG", 
				"⛔" => "ERRORS", 
				"\ud83e\udde9" => "MODS", 
				"♻\ufe0f" => "DISCONNECT", 
				"\ud83d\udd04" => "RESTART", 
				_ => emoji, 
			};
		}

		public static ReactionPoller Create(Func<string> botTokenResolver, Func<string> webhookUrlResolver, Func<string> clientLogsRootResolver, Func<string> webhookNameResolver = null)
		{
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_000a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0010: Expected O, but got Unknown
			GameObject val = new GameObject("ClientLogRelay_ReactionPoller");
			Object.DontDestroyOnLoad((Object)val);
			ReactionPoller reactionPoller = val.AddComponent<ReactionPoller>();
			reactionPoller._botTokenResolver = botTokenResolver;
			reactionPoller._webhookUrlResolver = webhookUrlResolver;
			reactionPoller._clientLogsRootResolver = clientLogsRootResolver;
			reactionPoller._webhookNameResolver = webhookNameResolver;
			return reactionPoller;
		}

		private void Start()
		{
			((MonoBehaviour)this).StartCoroutine(PollLoop());
		}

		private IEnumerator PollLoop()
		{
			yield return (object)new WaitForSeconds(30f);
			while (true)
			{
				if (_consecutiveRateLimits > 0)
				{
					float num = Time.realtimeSinceStartup - _lastRateLimitTime;
					if (num < 300f)
					{
						float num2 = 300f - num;
						Debug.Log((object)$"[ClientLogRelay] ReactionPoller: Rate limited, backing off for {num2:F0}s (attempt {_consecutiveRateLimits})");
						yield return (object)new WaitForSeconds(num2);
					}
				}
				yield return (object)new WaitForSeconds(60f);
				string botToken = _botTokenResolver?.Invoke();
				if (string.IsNullOrEmpty(botToken))
				{
					continue;
				}
				if (!_botUserIdResolved)
				{
					yield return ResolveBotUserId(botToken);
					_botUserIdResolved = true;
				}
				LogRequestContext[] array = LogRequestRegistry.Snapshot();
				if (array == null || array.Length == 0)
				{
					continue;
				}
				int polledCount = 0;
				int skippedWarmup = 0;
				LogRequestContext[] array2 = array;
				foreach (LogRequestContext ctx in array2)
				{
					if (string.IsNullOrEmpty(ctx.ChannelId) || string.IsNullOrEmpty(ctx.MessageId))
					{
						continue;
					}
					if (LogRequestRegistry.IsWithinRegistrationWindow(ctx.MessageId, PostRegistrationWarmupSeconds))
					{
						skippedWarmup++;
						continue;
					}
					bool hitRateLimit = false;
					for (int j = 0; j < _emojiRaw.Length; j++)
					{
						bool wasRateLimited = false;
						yield return PollReaction(botToken, ctx, _emojiRaw[j], _emojiEncoded[j], delegate(bool limited)
						{
							wasRateLimited = limited;
						});
						if (wasRateLimited)
						{
							hitRateLimit = true;
							Debug.Log((object)$"[ClientLogRelay] ReactionPoller: Hit rate limit after {polledCount} messages, pausing for this cycle");
							break;
						}
						if (j < _emojiRaw.Length - 1)
						{
							yield return (object)new WaitForSeconds(0.75f);
						}
					}
					if (hitRateLimit)
					{
						break;
					}
					polledCount++;
					yield return (object)new WaitForSeconds(3f);
				}
				if (polledCount > 0 || skippedWarmup > 0)
				{
					string arg = ((skippedWarmup > 0) ? $" (skipped {skippedWarmup} warming-up)" : "");
					FiresLogger.LogVerbose($"[ClientLogRelay] ReactionPoller: Polled {polledCount} message(s) this cycle{arg}");
				}
			}
		}

		private IEnumerator ResolveBotUserId(string botToken)
		{
			UnityWebRequest req = UnityWebRequest.Get("https://discord.com/api/v10/users/@me");
			try
			{
				req.SetRequestHeader("Authorization", "Bot " + botToken);
				req.timeout = 15;
				yield return req.SendWebRequest();
				if ((int)req.result != 2 && (int)req.result != 3 && req.downloadHandler != null)
				{
					try
					{
						Dictionary<string, object> dictionary = JsonConvert.DeserializeObject<Dictionary<string, object>>(req.downloadHandler.text);
						if (dictionary != null && dictionary.TryGetValue("id", out var value) && value != null)
						{
							_botUserId = value.ToString();
							Debug.Log((object)("[ClientLogRelay] ReactionPoller resolved bot user id: " + _botUserId));
						}
					}
					catch (Exception ex)
					{
						Debug.LogWarning((object)("[ClientLogRelay] ReactionPoller /users/@me parse failed: " + ex.Message));
					}
				}
				else
				{
					Debug.LogWarning((object)$"[ClientLogRelay] ReactionPoller /users/@me failed ({req.responseCode}): {req.error}");
				}
			}
			finally
			{
				((IDisposable)req)?.Dispose();
			}
		}

		private IEnumerator PollReaction(string botToken, LogRequestContext ctx, string emojiRaw, string emojiEncoded, Action<bool> rateLimitCallback = null)
		{
			string text = $"https://discord.com/api/v10/channels/{ctx.ChannelId}/messages/{ctx.MessageId}/reactions/{emojiEncoded}?limit={100}";
			UnityWebRequest req = UnityWebRequest.Get(text);
			try
			{
				req.SetRequestHeader("Authorization", "Bot " + botToken);
				req.timeout = 15;
				yield return req.SendWebRequest();
				if ((int)req.result == 2 || (int)req.result == 3)
				{
					if (req.responseCode == 429)
					{
						_consecutiveRateLimits++;
						_lastRateLimitTime = Time.realtimeSinceStartup;
						rateLimitCallback?.Invoke(obj: true);
						Debug.LogWarning((object)$"[ClientLogRelay] ReactionPoller rate limited (429) - backing off (consecutive: {_consecutiveRateLimits})");
					}
					else if (req.responseCode != 404)
					{
						Debug.LogWarning((object)$"[ClientLogRelay] ReactionPoller GET reactions failed ({req.responseCode}): {req.error}");
					}
					yield break;
				}
				if (_consecutiveRateLimits > 0)
				{
					Debug.Log((object)$"[ClientLogRelay] ReactionPoller: Rate limit cleared after {_consecutiveRateLimits} consecutive 429s");
					_consecutiveRateLimits = 0;
				}
				rateLimitCallback?.Invoke(obj: false);
				JArray val;
				try
				{
					val = JArray.Parse(req.downloadHandler.text);
				}
				catch (Exception ex)
				{
					Debug.LogWarning((object)("[ClientLogRelay] ReactionPoller parse failed: " + ex.Message));
					yield break;
				}
				foreach (JToken item2 in val)
				{
					string text2 = ((object)item2[(object)"id"])?.ToString();
					if (string.IsNullOrEmpty(text2) || (!string.IsNullOrEmpty(_botUserId) && text2 == _botUserId))
					{
						continue;
					}
					string item = ctx.MessageId + ":" + text2 + ":" + emojiRaw;
					if (!_fulfilled.Contains(item))
					{
						_fulfilled.Add(item);
						string text3 = ((object)item2[(object)"username"])?.ToString() ?? text2;
						string text4 = EmojiLabel(emojiRaw);
						if (!IsRequestAllowed(ctx, text2))
						{
							DiscordReactionAllowlist.LogIgnoredReaction("[ClientLogRelay] ReactionPoller: " + text4 + " from '" + text3 + "' (" + text2 + ") on " + ctx.PlayerName + " (" + ctx.PlatformId + ")");
						}
						else
						{
							Debug.Log((object)("[ClientLogRelay] ReactionPoller: '" + text3 + "' reacted " + text4 + " on " + ctx.PlayerName + " (" + ctx.PlatformId + ")"));
							yield return Fulfill(ctx, emojiRaw, text3, text2);
						}
					}
				}
			}
			finally
			{
				((IDisposable)req)?.Dispose();
			}
		}

		private static bool IsRequestAllowed(LogRequestContext ctx, string userId)
		{
			if (!(ctx.ConsumerId == "vangarde_serverlists"))
			{
				return DiscordReactionAllowlist.Allows(userId);
			}
			return true;
		}

		private IEnumerator Fulfill(LogRequestContext ctx, string emoji, string reqByName, string reqById)
		{
			if (ctx.ConsumerId == "vangarde_serverlists")
			{
				if (emoji == "\ud83e\udde9")
				{
					yield return FulfillServerLists(ctx, reqByName);
				}
				yield break;
			}
			string text = _clientLogsRootResolver?.Invoke();
			if (!string.IsNullOrEmpty(text))
			{
				string folder = BuildPlayerFolder(text, ctx);
				switch (emoji)
				{
				case "\ud83d\udce9":
					yield return FulfillFullLog(ctx, folder, reqByName);
					break;
				case "⛔":
					yield return FulfillErrorsWarnings(ctx, folder, reqByName);
					break;
				case "\ud83e\udde9":
					yield return FulfillMods(ctx, folder, reqByName);
					break;
				case "♻\ufe0f":
					FulfillDisconnect(ctx, reqByName);
					break;
				}
			}
		}

		private IEnumerator FulfillFullLog(LogRequestContext ctx, string folder, string reqByName)
		{
			string text = Path.Combine(folder, "LogOutput.log");
			byte[] logBytes = SafeReadFile(text);
			if (logBytes == null || logBytes.Length == 0)
			{
				Debug.LogWarning((object)("[ClientLogRelay] ReactionPoller: no cached log for " + ctx.PlatformId + " at '" + text + "'"));
				yield break;
			}
			string stamp = ctx.CapturedUtc.ToString("yyyyMMdd_HHmmss");
			string safeId = SafePlatformId(ctx);
			List<byte[]> chunks = ChunkBytes(logBytes, 8323072);
			for (int i = 0; i < chunks.Count; i++)
			{
				string text2 = ((chunks.Count == 1) ? "" : $"_part{i + 1}of{chunks.Count}");
				string fileName = "client_log_" + safeId + "_" + stamp + text2 + ".log";
				string title = ((chunks.Count == 1) ? "\ud83d\udce9 Full Log — Requested" : $"\ud83d\udce9 Full Log — Part {i + 1}/{chunks.Count}");
				MinimalWebhookPoster.Embed embed = new MinimalWebhookPoster.Embed().SetTitle(title).SetColor(3447003).AddField("\ud83d\udc64 Player", ctx.PlayerName, inline: true)
					.AddField("\ud83d\udd94 Steam ID", ctx.PlatformId ?? "?", inline: true)
					.AddField("\ud83d\udcc4 Size", FormatBytes(chunks[i].Length), inline: true)
					.SetFooter("Requested by " + reqByName);
				List<MinimalWebhookPoster.Attachment> files = new List<MinimalWebhookPoster.Attachment>
				{
					new MinimalWebhookPoster.Attachment(fileName, chunks[i])
				};
				yield return PostAndWait(embed, files);
			}
			Debug.Log((object)("[ClientLogRelay] ReactionPoller: posted full log for " + ctx.PlatformId + " " + $"({logBytes.Length} bytes, {chunks.Count} part(s)), requested by {reqByName}"));
		}

		private IEnumerator FulfillErrorsWarnings(LogRequestContext ctx, string folder, string reqByName)
		{
			byte[] array = SafeReadFile(Path.Combine(folder, "errors_warnings.txt"));
			if (array == null || array.Length == 0)
			{
				Debug.LogWarning((object)("[ClientLogRelay] ReactionPoller: no errors_warnings.txt for " + ctx.PlatformId));
				yield break;
			}
			string text = ctx.CapturedUtc.ToString("yyyyMMdd_HHmmss");
			string text2 = SafePlatformId(ctx);
			MinimalWebhookPoster.Embed embed = new MinimalWebhookPoster.Embed().SetTitle("⛔ Errors + Warnings — Requested").SetColor(15548997).AddField("\ud83d\udc64 Player", ctx.PlayerName, inline: true)
				.AddField("\ud83d\udd94 Steam ID", ctx.PlatformId ?? "?", inline: true)
				.AddField("\ud83d\udcc4 Size", FormatBytes(array.Length), inline: true)
				.SetFooter("Requested by " + reqByName);
			List<MinimalWebhookPoster.Attachment> list = new List<MinimalWebhookPoster.Attachment>();
			list.Add(new MinimalWebhookPoster.Attachment("errors_warnings_" + text2 + "_" + text + ".txt", array));
			List<MinimalWebhookPoster.Attachment> files = list;
			yield return PostAndWait(embed, files);
			Debug.Log((object)("[ClientLogRelay] ReactionPoller: posted errors/warnings for " + ctx.PlatformId + ", requested by " + reqByName));
		}

		private IEnumerator FulfillMods(LogRequestContext ctx, string folder, string reqByName)
		{
			string text = ctx.CapturedUtc.ToString("yyyyMMdd_HHmmss");
			string text2 = SafePlatformId(ctx);
			byte[] array = SafeReadFile(Path.Combine(folder, "modlist.txt"));
			byte[] array2 = SafeReadFile(Path.Combine(folder, "mod_diff.txt"));
			bool flag = array != null && array.Length != 0;
			bool flag2 = array2 != null && array2.Length != 0;
			if (!flag && !flag2)
			{
				Debug.LogWarning((object)("[ClientLogRelay] ReactionPoller: no mod artifacts for " + ctx.PlatformId));
				yield break;
			}
			MinimalWebhookPoster.Embed embed = new MinimalWebhookPoster.Embed().SetTitle("\ud83e\udde9 Mod Lists — Requested").SetColor(5763719).AddField("\ud83d\udc64 Player", ctx.PlayerName, inline: true)
				.AddField("\ud83d\udd94 Steam ID", ctx.PlatformId ?? "?", inline: true)
				.SetFooter("Requested by " + reqByName);
			List<MinimalWebhookPoster.Attachment> list = new List<MinimalWebhookPoster.Attachment>();
			if (flag)
			{
				list.Add(new MinimalWebhookPoster.Attachment("modlist_" + text2 + "_" + text + ".txt", array));
			}
			if (flag2)
			{
				list.Add(new MinimalWebhookPoster.Attachment("mod_diff_" + text2 + "_" + text + ".txt", array2));
			}
			yield return PostAndWait(embed, list);
			Debug.Log((object)("[ClientLogRelay] ReactionPoller: posted mod artifacts for " + ctx.PlatformId + ", requested by " + reqByName));
		}

		private void FulfillDisconnect(LogRequestContext ctx, string reqByName)
		{
			DisconnectLogRegistry.Register(ctx.PlatformId, ctx.PlayerName, reqByName);
		}

		private IEnumerator FulfillServerLists(LogRequestContext ctx, string reqByName)
		{
			string serverListSnapshot = DiscordSink.GetServerListSnapshot();
			if (string.IsNullOrEmpty(serverListSnapshot))
			{
				Debug.LogWarning((object)"[ClientLogRelay] ReactionPoller: no server-list snapshot provider available");
				yield break;
			}
			MinimalWebhookPoster.Embed embed = new MinimalWebhookPoster.Embed().SetTitle("\ud83e\udde9 Server Mod Lists — Requested").SetColor(5763719).SetDescription("Current enforced / whitelist / server-only / blacklist state, live from the server.")
				.SetFooter("Requested by " + reqByName);
			List<MinimalWebhookPoster.Attachment> files = new List<MinimalWebhookPoster.Attachment>
			{
				new MinimalWebhookPoster.Attachment("vangarde_server_lists.txt", serverListSnapshot)
			};
			string text = DiscordIntegrationConfig.ResolveWebhookUrl(WebhookCategory.AntiCheat);
			if (string.IsNullOrEmpty(text) || !MinimalWebhookPoster.IsValidWebhookUrl(text))
			{
				text = _webhookUrlResolver?.Invoke();
				if (string.IsNullOrEmpty(text) || !MinimalWebhookPoster.IsValidWebhookUrl(text))
				{
					yield break;
				}
			}
			MinimalWebhookPoster.Post(text, embed, files, _webhookNameResolver?.Invoke());
			yield return (object)new WaitForSeconds(1.5f);
			Debug.Log((object)("[ClientLogRelay] ReactionPoller: posted server mod lists, requested by " + reqByName));
		}

		private IEnumerator PostAndWait(MinimalWebhookPoster.Embed embed, List<MinimalWebhookPoster.Attachment> files)
		{
			string text = _webhookUrlResolver?.Invoke();
			if (!string.IsNullOrEmpty(text) && MinimalWebhookPoster.IsValidWebhookUrl(text))
			{
				MinimalWebhookPoster.Post(text, embed, files, _webhookNameResolver?.Invoke());
				yield return (object)new WaitForSeconds(1.5f);
			}
		}

		private static string BuildPlayerFolder(string logsRoot, LogRequestContext ctx)
		{
			string text = (ctx.PlatformId ?? "unknown").Replace(":", "_").Replace("/", "_").Replace("\\", "_");
			string text2 = ctx.PlayerName ?? "unknown";
			if (!string.IsNullOrEmpty(text2))
			{
				char[] invalidFileNameChars = Path.GetInvalidFileNameChars();
				text2 = string.Concat(text2.Split(invalidFileNameChars));
			}
			return Path.Combine(logsRoot, text2 + "_" + text);
		}

		private static string SafePlatformId(LogRequestContext ctx)
		{
			return (ctx.PlatformId ?? "unknown").Replace(":", "_").Replace("/", "_").Replace("\\", "_");
		}

		private static byte[] SafeReadFile(string path)
		{
			try
			{
				if (!string.IsNullOrEmpty(path) && File.Exists(path))
				{
					return File.ReadAllBytes(path);
				}
			}
			catch (Exception ex)
			{
				Debug.LogWarning((object)("[ClientLogRelay] ReactionPoller: read failed '" + path + "': " + ex.Message));
			}
			return null;
		}

		private static List<byte[]> ChunkBytes(byte[] data, int maxBytes)
		{
			if (data == null || data.Length == 0)
			{
				return new List<byte[]>(0);
			}
			if (data.Length <= maxBytes)
			{
				return new List<byte[]>(1) { data };
			}
			List<byte[]> list = new List<byte[]>();
			int num;
			for (int i = 0; i < data.Length; i += num)
			{
				num = Math.Min(maxBytes, data.Length - i);
				byte[] array = new byte[num];
				Buffer.BlockCopy(data, i, array, 0, num);
				list.Add(array);
			}
			return list;
		}

		private static string FormatBytes(long bytes)
		{
			if (bytes <= 0)
			{
				return "0 B";
			}
			string[] array = new string[4] { "B", "KB", "MB", "GB" };
			double num = bytes;
			int num2 = 0;
			while (num >= 1024.0 && num2 < array.Length - 1)
			{
				num /= 1024.0;
				num2++;
			}
			return $"{num:0.##} {array[num2]}";
		}
	}
	public static class ServerHeartbeat
	{
		private class HeartbeatHost : MonoBehaviour
		{
		}

		private const string DiscordApiBase = "https://discord.com/api/v10";

		private const float HeartbeatIntervalSeconds = 60f;

		private const string SentinelFileName = "server_heartbeat.json";

		private const string SentinelDirName = "FiresDiscordIntegration";

		private const string EMOJI_RESTART = "\ud83d\udd04";

		private const string EMOJI_STOP = "⛔";

		private static string _sentinelPath;

		private static string _statusMessageId;

		private static string _botToken;

		private static string _channelId;

		private static string _statusWebhookUrl;

		private static string _webhookName;

		private static string _brandLabel;

		private static string _botUserId;

		private static Coroutine _heartbeatCoroutine;

		private static MonoBehaviour _host;

		private static DateTime _serverStartUtc;

		private static bool _cleanShutdown;

		private static readonly HashSet<string> _fulfilledReactions = new HashSet<string>(StringComparer.Ordinal);

		private const int HeartbeatEditAttempts = 3;

		private const float HeartbeatRetryBaseSeconds = 2f;

		private const float HeartbeatRetryMaxSeconds = 20f;

		private const int HeartbeatWarnAfterFailedTicks = 3;

		private const float HeartbeatWarnRepeatSeconds = 3600f;

		private static int _heartbeatFailedTicks;

		private static string _heartbeatLastError;

		private static float _heartbeatLastWarnAt = float.NegativeInfinity;

		private static float _heartbeatHoldUntil;

		private const int MaxReactionAttempts = 3;

		private const int HttpStatusRateLimited = 429;

		private const int HttpStatusSuccessRangeStart = 200;

		private const int HttpStatusSuccessRangeEndExcl = 300;

		private const int AddReactionTimeoutSeconds = 10;

		private const int ResponseBodyLogMaxChars = 300;

		private const float DefaultRateLimitWaitSeconds = 1f;

		private const float RateLimitWaitBufferSeconds = 0.15f;

		internal static bool HeartbeatArmed { get; private set; }

		internal static bool CrashNotifierArmed { get; private set; }

		internal static float IntervalSeconds => 60f;

		public static void OnServerStart(string botToken, string statusWebhookUrl, string channelIdOverride, string webhookName, string brandLabel)
		{
			_botToken = botToken;
			_statusWebhookUrl = statusWebhookUrl;
			_webhookName = webhookName;
			_brandLabel = brandLabel;
			_channelId = channelIdOverride;
			if (string.IsNullOrEmpty(_botToken) || string.IsNullOrEmpty(_channelId))
			{
				FiresLogger.LogVerbose("[ServerHeartbeat] BotToken or StatusChannelId not set - heartbeat disabled.");
				InitSentinel();
				SpawnCrashNotifier();
				return;
			}
			HeartbeatArmed = true;
			_serverStartUtc = DateTime.UtcNow;
			_cleanShutdown = false;
			_statusMessageId = null;
			InitSentinel();
			EnsureHost();
			if (!((Object)(object)_host == (Object)null))
			{
				FiresLogger.LogVerbose($"[ServerHeartbeat] Starting - server start time: {_serverStartUtc:yyyy-MM-dd HH:mm:ss} UTC");
				_host.StartCoroutine(CleanupAndPostNewMessage());
				SpawnCrashNotifier();
				_host.StartCoroutine(ReactionPollLoop());
			}
		}

		public static void OnServerStop()
		{
			_cleanShutdown = true;
			try
			{
				WriteSentinel("stopped");
			}
			catch
			{
			}
			if (_heartbeatCoroutine != null && (Object)(object)_host != (Object)null)
			{
				_host.StopCoroutine(_heartbeatCoroutine);
				_heartbeatCoroutine = null;
			}
			if (!string.IsNullOrEmpty(_statusMessageId) && (!string.IsNullOrEmpty(_statusWebhookUrl) || (!string.IsNullOrEmpty(_botToken) && !string.IsNullOrEmpty(_channelId))))
			{
				try
				{
					EditStatusMessageSync(_statusMessageId, BuildOfflineEmbed());
					return;
				}
				catch (Exception ex)
				{
					Debug.LogWarning((object)("[ServerHeartbeat] Failed to edit status on shutdown: " + ex.Message));
					return;
				}
			}
			FiresLogger.LogVerbose("[ServerHeartbeat] OnServerStop: no status message id or auth available — skipping offline edit.");
		}

		private static IEnumerator CleanupAndPostNewMessage()
		{
			FiresLogger.LogVerbose("[ServerHeartbeat] Cleaning up old status messages...");
			string text = "https://discord.com/api/v10/channels/" + _channelId + "/messages?limit=50";
			UnityWebRequest req = UnityWebRequest.Get(text);
			try
			{
				req.SetRequestHeader("Authorization", "Bot " + _botToken);
				req.timeout = 15;
				yield return req.SendWebRequest();
				if (req.responseCode == 200)
				{
					JArray val;
					try
					{
						val = JArray.Parse(req.downloadHandler.text);
					}
					catch (Exception ex)
					{
						Debug.LogWarning((object)("[ServerHeartbeat] Failed to parse messages: " + ex.Message));
						val = null;
					}
					if (val != null && ((JContainer)val).Count > 0)
					{
						int deletedCount = 0;
						foreach (JToken item in val)
						{
							JObject val2 = (JObject)(object)((item is JObject) ? item : null);
							if (val2 == null)
							{
								continue;
							}
							JToken obj = val2["author"];
							JObject val3 = (JObject)(object)((obj is JObject) ? obj : null);
							if (val3 == null)
							{
								continue;
							}
							JToken obj2 = val3["bot"];
							if (obj2 == null || !Extensions.Value<bool>((IEnumerable<JToken>)obj2))
							{
								continue;
							}
							JToken obj3 = val2["embeds"];
							JArray val4 = (JArray)(object)((obj3 is JArray) ? obj3 : null);
							if (val4 == null || ((JContainer)val4).Count == 0)
							{
								continue;
							}
							bool flag = false;
							foreach (JToken item2 in val4)
							{
								JToken obj4 = ((item2 is JObject) ? item2 : null);
								string text2 = ((obj4 == null) ? null : ((object)((JObject)obj4)["title"])?.ToString()) ?? "";
								if (text2.Contains("Server Online") || text2.Contains("Server Offline") || text2.Contains("Server Restarting") || text2.Contains("Server Stopped") || text2.Contains("Server Crash"))
								{
									flag = true;
									break;
								}
							}
							if (!flag)
							{
								continue;
							}
							string messageId = ((object)val2["id"])?.ToString();
							if (string.IsNullOrEmpty(messageId))
							{
								continue;
							}
							string text3 = "https://discord.com/api/v10/channels/" + _channelId + "/messages/" + messageId;
							UnityWebRequest deleteReq = UnityWebRequest.Delete(text3);
							try
							{
								deleteReq.SetRequestHeader("Authorization", "Bot " + _botToken);
								deleteReq.timeout = 10;
								yield return deleteReq.SendWebRequest();
								if (deleteReq.responseCode == 204)
								{
									deletedCount++;
									FiresLogger.LogVerbose("[ServerHeartbeat] Deleted old status message: " + messageId);
								}
							}
							finally
							{
								((IDisposable)deleteReq)?.Dispose();
							}
							if (deletedCount > 0 && deletedCount % 3 == 0)
							{
								yield return (object)new WaitForSeconds(0.5f);
							}
						}
						if (deletedCount > 0)
						{
							FiresLogger.LogVerbose($"[ServerHeartbeat] Cleaned up {deletedCount} old status message(s)");