Decompiled source of StreamChats v1.0.0

StreamChats.dll

Decompiled 5 hours ago
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Authentication;
using System.Security.Permissions;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using ChzzkChat.Configuration;
using ChzzkChat.Utils;
using ChzzkChat.chzzk;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using TMPro;
using UnityEngine;
using WebSocketSharp;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: IgnoresAccessChecksTo("0Harmony")]
[assembly: IgnoresAccessChecksTo("Assembly-CSharp")]
[assembly: IgnoresAccessChecksTo("BepInEx")]
[assembly: IgnoresAccessChecksTo("Unity.Netcode.Runtime")]
[assembly: IgnoresAccessChecksTo("Unity.TextMeshPro")]
[assembly: IgnoresAccessChecksTo("UnityEngine.CoreModule")]
[assembly: IgnoresAccessChecksTo("UnityEngine")]
[assembly: IgnoresAccessChecksTo("UnityEngine.IMGUIModule")]
[assembly: IgnoresAccessChecksTo("UnityEngine.InputLegacyModule")]
[assembly: IgnoresAccessChecksTo("UnityEngine.TextRenderingModule")]
[assembly: IgnoresAccessChecksTo("UnityEngine.UI")]
[assembly: IgnoresAccessChecksTo("UnityEngine.UIModule")]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("StreamChats")]
[assembly: AssemblyConfiguration("Debug")]
[assembly: AssemblyDescription("Bring CHZZK, Twitch, and YouTube chat into Lethal Company chat.")]
[assembly: AssemblyFileVersion("1.1.5.0")]
[assembly: AssemblyInformationalVersion("1.1.5+8edf1725d589ddff9379892fe8e0cbef462e98a8")]
[assembly: AssemblyProduct("StreamChats")]
[assembly: AssemblyTitle("StreamChats")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.1.5.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.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 StreamChats
{
	public static class MyPluginInfo
	{
		public const string PLUGIN_GUID = "StreamChats";

		public const string PLUGIN_NAME = "StreamChats";

		public const string PLUGIN_VERSION = "1.1.5";
	}
}
namespace ChzzkChat
{
	[BepInPlugin("asta.lethalcompany.streamchats", "StreamChats", "1.1.5")]
	public class Plugin : BaseUnityPlugin
	{
		private readonly struct PendingChatMessage
		{
			public string Name { get; }

			public string Message { get; }

			public int Amount { get; }

			public bool IsDonation { get; }

			private PendingChatMessage(string name, string message, int amount, bool isDonation)
			{
				Name = name;
				Message = message;
				Amount = amount;
				IsDonation = isDonation;
			}

			public static PendingChatMessage Chat(string name, string message)
			{
				return new PendingChatMessage(name, message, 0, isDonation: false);
			}

			public static PendingChatMessage Donation(string name, string message, int amount)
			{
				return new PendingChatMessage(name, message, amount, isDonation: true);
			}
		}

		private const string PluginGuid = "asta.lethalcompany.streamchats";

		private const int MaxMainThreadActionsPerFrame = 100;

		private const int MaxPendingMessages = 30;

		private const int HudWaitTimeoutSeconds = 120;

		private static readonly string[] Colors = new string[14]
		{
			"#f4dbd6", "#f0c6c6", "#f5bde6", "#c6a0f6", "#ed8796", "#ee99a0", "#f5a97f", "#eed49f", "#a6da95", "#8bd5ca",
			"#91d7e3", "#7dc4e4", "#8aadf4", "#b7bdf8"
		};

		private readonly Harmony harmony = new Harmony("asta.lethalcompany.streamchats");

		private static readonly ConcurrentQueue<PendingChatMessage> PendingMessages = new ConcurrentQueue<PendingChatMessage>();

		private static int colorIndex;

		private static int displayedMessages;

		private static bool loggedHudWait;

		private static DateTime? firstQueueTime;

		private static Plugin? instance;

		private static bool gameLoopPumpLogged;

		private static DateTime nextGameLoopPumpStatusLog;

		private static int gameLoopPumpCalls;

		private static bool applicationQuitting;

		private static bool quitHookRegistered;

		private static ChzzkUnity? chzzkUnity;

		private static TwitchChatClient? twitchClient;

		private static YoutubeLiveChatClient? youtubeClient;

		internal static ManualLogSource? logger;

		private bool updatePumpLogged;

		private bool coroutinePumpLogged;

		private void Awake()
		{
			if ((Object)(object)instance != (Object)null && (Object)(object)instance != (Object)(object)this)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)"Duplicate StreamChats plugin instance detected. Disabling this instance.");
				((Behaviour)this).enabled = false;
				return;
			}
			instance = this;
			harmony.PatchAll();
			logger = ((BaseUnityPlugin)this).Logger;
			RegisterQuitHook();
			Config.Load();
			MainThreadDispatcher.Initialize();
			PatchGameLoopPump();
			((BaseUnityPlugin)this).Logger.LogInfo((object)"StreamChats loaded.");
			StartChatClients();
			((MonoBehaviour)this).StartCoroutine(MainThreadPump());
		}

		private void OnDestroy()
		{
			((BaseUnityPlugin)this).Logger.LogInfo((object)$"StreamChats plugin OnDestroy. applicationQuitting={applicationQuitting}");
			if (!applicationQuitting)
			{
				((BaseUnityPlugin)this).Logger.LogInfo((object)"Ignoring non-quit OnDestroy so the CHZZK websocket can keep running.");
				return;
			}
			StopChatClients("plugin destroyed during application quit");
			harmony.UnpatchSelf();
			if ((Object)(object)instance == (Object)(object)this)
			{
				instance = null;
			}
		}

		private void OnApplicationQuit()
		{
			applicationQuitting = true;
			StopChatClients("application quit");
		}

		private void Update()
		{
			if (!updatePumpLogged)
			{
				updatePumpLogged = true;
				((BaseUnityPlugin)this).Logger.LogInfo((object)"Plugin.Update main thread pump is running.");
			}
			ProcessMainThreadWork();
		}

		private static void RegisterQuitHook()
		{
			if (!quitHookRegistered)
			{
				quitHookRegistered = true;
				Application.quitting += delegate
				{
					applicationQuitting = true;
					StopChatClients("Application.quitting");
				};
			}
		}

		private void PatchGameLoopPump()
		{
			//IL_003d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0043: Expected O, but got Unknown
			MethodInfo method = typeof(Plugin).GetMethod("GameLoopPumpPostfix", BindingFlags.Static | BindingFlags.NonPublic);
			if (method == null)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)"Could not find StreamChats main thread pump postfix.");
				return;
			}
			HarmonyMethod val = new HarmonyMethod(method);
			int num = 0;
			string[] array = new string[7] { "HUDManager.Update", "HUDManager.LateUpdate", "MenuManager.Update", "QuickMenuManager.Update", "PlayerControllerB.Update", "StartOfRound.Update", "GameNetworkManager.Update" };
			string[] array2 = array;
			foreach (string text in array2)
			{
				int num2 = text.LastIndexOf('.');
				string text2 = text.Substring(0, num2);
				string text3 = text.Substring(num2 + 1);
				Type type = AccessTools.TypeByName(text2);
				MethodInfo methodInfo = ((type == null) ? null : AccessTools.Method(type, text3, (Type[])null, (Type[])null));
				if (!(methodInfo == null))
				{
					harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, val, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
					num++;
					((BaseUnityPlugin)this).Logger.LogInfo((object)("Patched StreamChats main thread pump into " + text2 + "." + text3 + "."));
				}
			}
			if (num == 0)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)"Could not find any game loop method for StreamChats main thread pump.");
			}
		}

		private static void GameLoopPumpPostfix()
		{
			gameLoopPumpCalls++;
			if (!gameLoopPumpLogged)
			{
				gameLoopPumpLogged = true;
				nextGameLoopPumpStatusLog = DateTime.UtcNow.AddSeconds(5.0);
				ManualLogSource? obj = logger;
				if (obj != null)
				{
					obj.LogInfo((object)"Game loop main thread pump is running.");
				}
			}
			ProcessMainThreadWork();
			if (DateTime.UtcNow >= nextGameLoopPumpStatusLog)
			{
				ManualLogSource? obj2 = logger;
				if (obj2 != null)
				{
					obj2.LogInfo((object)($"Game loop pump alive. queued callbacks: {MainThreadDispatcher.Count}, " + $"pending HUD messages: {PendingMessages.Count}, hudReady: {IsHudReady()}, " + $"calls: {gameLoopPumpCalls}"));
				}
				nextGameLoopPumpStatusLog = DateTime.UtcNow.AddSeconds(5.0);
			}
		}

		private IEnumerator MainThreadPump()
		{
			DateTime nextStatusLog = DateTime.UtcNow.AddSeconds(5.0);
			while (((Behaviour)this).enabled)
			{
				if (!coroutinePumpLogged)
				{
					coroutinePumpLogged = true;
					((BaseUnityPlugin)this).Logger.LogInfo((object)"Coroutine main thread pump is running.");
				}
				ProcessMainThreadWork();
				if (DateTime.UtcNow >= nextStatusLog)
				{
					((BaseUnityPlugin)this).Logger.LogInfo((object)($"Main thread pump alive. queued callbacks: {MainThreadDispatcher.Count}, " + $"pending HUD messages: {PendingMessages.Count}, hudReady: {IsHudReady()}"));
					nextStatusLog = DateTime.UtcNow.AddSeconds(5.0);
				}
				yield return null;
			}
		}

		private static void ProcessMainThreadWork()
		{
			for (int i = 0; i < 100; i++)
			{
				if (!MainThreadDispatcher.TryDequeue(out Action action))
				{
					break;
				}
				if (action == null)
				{
					break;
				}
				try
				{
					action();
				}
				catch (Exception arg)
				{
					ManualLogSource? obj = logger;
					if (obj != null)
					{
						obj.LogError((object)$"Main thread callback failed: {arg}");
					}
				}
			}
			FlushPendingMessages();
		}

		private void StartChatClients()
		{
			StopChatClients("restarting chat clients");
			bool flag = !string.IsNullOrWhiteSpace(Config.ConfigChannelId);
			bool flag2 = !string.IsNullOrWhiteSpace(Config.TwitchHandle);
			bool flag3 = !string.IsNullOrWhiteSpace(Config.YoutubeHandle);
			((BaseUnityPlugin)this).Logger.LogInfo((object)$"Configured stream chats: CHZZK={flag}, Twitch={flag2}, YouTube={flag3}");
			if (flag)
			{
				StartChzzkClient();
			}
			if (flag2)
			{
				StartTwitchClient();
			}
			if (flag3)
			{
				StartYoutubeClient();
			}
			if (!flag && !flag2 && !flag3)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)"No stream chat target configured. Fill ChannelId, TwitchHandle, or YoutubeHandle in StreamChats.cfg.");
			}
		}

		private void StartChzzkClient()
		{
			if (string.IsNullOrWhiteSpace(Config.ConfigChannelId))
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)"ChannelId is empty. Set it in BepInEx/config/StreamChats.cfg.");
				return;
			}
			if (chzzkUnity != null)
			{
				chzzkUnity.StopListening("restarting CHZZK client");
				chzzkUnity = null;
			}
			chzzkUnity = new ChzzkUnity();
			((BaseUnityPlugin)this).Logger.LogInfo((object)"Starting CHZZK chat client.");
			chzzkUnity.OnMessage += delegate(ChzzkUnity.Profile profile, string msg)
			{
				ShowPlatformMessage("CHZZK", profile.nickname, msg);
			};
			chzzkUnity.OnDonation += delegate(ChzzkUnity.Profile profile, string msg, ChzzkUnity.DonationExtras donation)
			{
				ShowDonation(profile.nickname, donation.payAmount, msg);
			};
			chzzkUnity.OnSubscription += delegate(ChzzkUnity.Profile profile, ChzzkUnity.SubscriptionExtras subscription)
			{
				ShowPlatformMessage("CHZZK", "CHZZK", $"{profile.nickname}님이 {subscription.month}개월 구독했어요!");
			};
			chzzkUnity.OnClose += delegate
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)"Disconnected from CHZZK chat. Reconnecting soon.");
			};
			chzzkUnity.Connect(Config.ConfigChannelId);
		}

		private void StartTwitchClient()
		{
			if (string.IsNullOrWhiteSpace(Config.TwitchHandle))
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)"TwitchHandle is empty. Set it in BepInEx/config/StreamChats.cfg.");
				return;
			}
			twitchClient = new TwitchChatClient();
			((BaseUnityPlugin)this).Logger.LogInfo((object)"Starting Twitch chat client.");
			twitchClient.OnMessage += delegate(string name, string msg)
			{
				ShowPlatformMessage("Twitch", name, msg);
			};
			twitchClient.Connect(Config.TwitchHandle);
		}

		private void StartYoutubeClient()
		{
			if (string.IsNullOrWhiteSpace(Config.YoutubeHandle))
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)"YoutubeHandle is empty. Set it in BepInEx/config/StreamChats.cfg.");
				return;
			}
			youtubeClient = new YoutubeLiveChatClient();
			((BaseUnityPlugin)this).Logger.LogInfo((object)"Starting YouTube chat client.");
			youtubeClient.OnMessage += delegate(string name, string msg)
			{
				ShowPlatformMessage("YouTube", name, msg);
			};
			youtubeClient.Connect(Config.YoutubeHandle);
		}

		private static void StopChatClients(string reason)
		{
			if (chzzkUnity != null)
			{
				chzzkUnity.StopListening(reason);
				chzzkUnity = null;
			}
			if (twitchClient != null)
			{
				twitchClient.StopListening(reason);
				twitchClient = null;
			}
			if (youtubeClient != null)
			{
				youtubeClient.StopListening(reason);
				youtubeClient = null;
			}
		}

		public static void ShowMessage(string name, string msg)
		{
			try
			{
				if (!IsHudReady())
				{
					QueuePendingMessage(PendingChatMessage.Chat(name, msg));
				}
				else
				{
					ShowMessageNow(name, msg);
				}
			}
			catch (Exception ex)
			{
				ManualLogSource? obj = logger;
				if (obj != null)
				{
					obj.LogError((object)("Error showing message: " + ex.Message));
				}
			}
		}

		public static void ShowPlatformMessage(string platform, string name, string msg)
		{
			string name2 = ((!Config.ShowPlatformPrefix || string.IsNullOrWhiteSpace(platform)) ? name : ("[" + platform + "] " + name));
			ShowMessage(name2, msg);
		}

		public static void ShowDonation(string name, int amount, string msg)
		{
			try
			{
				if (!IsHudReady())
				{
					QueuePendingMessage(PendingChatMessage.Donation(name, msg, amount));
				}
				else
				{
					ShowDonationNow(name, amount, msg);
				}
			}
			catch (Exception ex)
			{
				ManualLogSource? obj = logger;
				if (obj != null)
				{
					obj.LogError((object)("Error showing donation: " + ex.Message));
				}
			}
		}

		internal static void FlushPendingMessages()
		{
			if (!IsHudReady())
			{
				return;
			}
			int num = 0;
			PendingChatMessage result;
			while (PendingMessages.TryDequeue(out result))
			{
				try
				{
					if (result.IsDonation)
					{
						ShowDonationNow(result.Name, result.Amount, result.Message);
					}
					else
					{
						ShowMessageNow(result.Name, result.Message);
					}
					num++;
				}
				catch (Exception ex)
				{
					ManualLogSource? obj = logger;
					if (obj != null)
					{
						obj.LogError((object)("Error showing queued CHZZK message: " + ex.Message));
					}
					break;
				}
			}
			if (num > 0)
			{
				loggedHudWait = false;
				firstQueueTime = null;
				ManualLogSource? obj2 = logger;
				if (obj2 != null)
				{
					obj2.LogInfo((object)$"Displayed {num} queued CHZZK chat message(s).");
				}
			}
		}

		private static void ShowMessageNow(string name, string msg)
		{
			string text = (string.IsNullOrWhiteSpace(name) ? "CHZZK" : name);
			string text2 = NextColor();
			string item = "<color=" + text2 + ">" + EscapeRichText(text) + "</color>: <color=#FFFFFF>" + EscapeRichText(msg) + "</color>";
			HUDManager.Instance.ChatMessageHistory.Add(item);
			if (HUDManager.Instance.ChatMessageHistory.Count > 30)
			{
				HUDManager.Instance.ChatMessageHistory.RemoveAt(0);
			}
			((TMP_Text)HUDManager.Instance.chatText).text = "\n" + string.Join("\n", HUDManager.Instance.ChatMessageHistory);
			HUDManager.Instance.PingHUDElement(HUDManager.Instance.Chat, 4f, 1f, 0.2f);
			LogDisplayedMessage(text);
		}

		private static void ShowDonationNow(string name, int amount, string msg)
		{
			string arg = EscapeRichText(string.IsNullOrWhiteSpace(name) ? "익명" : name);
			HUDManager.Instance.DisplayTip($"{arg}님이 {amount:N0}원 후원하셨어요!", EscapeRichText(msg) ?? "", false, false, "LC_Tip1");
			HUDManager.Instance.PingHUDElement(HUDManager.Instance.Chat, 2f, 1f, 0.2f);
		}

		private static bool IsHudReady()
		{
			return (Object)(object)HUDManager.Instance != (Object)null && (Object)(object)HUDManager.Instance.chatText != (Object)null && HUDManager.Instance.Chat != null && HUDManager.Instance.ChatMessageHistory != null;
		}

		private static string NextColor()
		{
			string result = Colors[colorIndex++];
			if (colorIndex >= Colors.Length)
			{
				colorIndex = 0;
			}
			return result;
		}

		private static void LogDisplayedMessage(string name)
		{
			displayedMessages++;
			if (displayedMessages <= 10 || displayedMessages % 25 == 0)
			{
				ManualLogSource? obj = logger;
				if (obj != null)
				{
					obj.LogInfo((object)$"Displayed CHZZK chat #{displayedMessages} in HUD: {name}");
				}
			}
		}

		private static void QueuePendingMessage(PendingChatMessage message)
		{
			firstQueueTime = firstQueueTime ?? DateTime.UtcNow;
			PendingMessages.Enqueue(message);
			PendingChatMessage result;
			while (PendingMessages.Count > 30)
			{
				PendingMessages.TryDequeue(out result);
			}
			if (firstQueueTime.HasValue && (DateTime.UtcNow - firstQueueTime.Value).TotalSeconds > 120.0)
			{
				while (PendingMessages.TryDequeue(out result))
				{
				}
				firstQueueTime = null;
				loggedHudWait = false;
				ManualLogSource? obj = logger;
				if (obj != null)
				{
					obj.LogWarning((object)"HUD did not become ready in time. Dropped queued CHZZK messages.");
				}
			}
			else if (!loggedHudWait)
			{
				loggedHudWait = true;
				ManualLogSource? obj2 = logger;
				if (obj2 != null)
				{
					obj2.LogInfo((object)"HUD is not ready yet; queueing CHZZK chat until it can be displayed.");
				}
			}
		}

		private static string EscapeRichText(string value)
		{
			if (string.IsNullOrEmpty(value))
			{
				return string.Empty;
			}
			return value.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;");
		}
	}
}
namespace ChzzkChat.Utils
{
	internal static class MainThreadDispatcher
	{
		private static readonly ConcurrentQueue<Action> Actions = new ConcurrentQueue<Action>();

		internal static int Count => Actions.Count;

		internal static void Initialize()
		{
		}

		internal static void Enqueue(Action action)
		{
			Actions.Enqueue(action);
		}

		internal static bool TryDequeue(out Action? action)
		{
			return Actions.TryDequeue(out action);
		}
	}
}
namespace ChzzkChat.chzzk
{
	public sealed class ChzzkUnity
	{
		private enum SslProtocolsHack
		{
			Tls = 192,
			Tls11 = 768,
			Tls12 = 3072
		}

		[Serializable]
		public class LiveDetail
		{
			[Serializable]
			public class Content
			{
				public string liveTitle = string.Empty;

				public string status = string.Empty;

				public string chatChannelId = string.Empty;

				public bool chatActive;
			}

			public int code;

			public string message = string.Empty;

			public Content? content;
		}

		[Serializable]
		public class LiveStatus
		{
			[Serializable]
			public class Content
			{
				public string liveTitle = string.Empty;

				public string status = string.Empty;

				public int concurrentUserCount;

				public int accumulateCount;

				public bool paidPromotion;

				public bool adult;

				public string chatChannelId = string.Empty;

				public string categoryType = string.Empty;

				public string liveCategory = string.Empty;

				public string liveCategoryValue = string.Empty;

				public string livePollingStatusJson = string.Empty;

				public string faultStatus = string.Empty;

				public string userAdultStatus = string.Empty;

				public bool chatActive;

				public string chatAvailableGroup = string.Empty;

				public string chatAvailableCondition = string.Empty;

				public int minFollowerMinute;
			}

			public int code;

			public string message = string.Empty;

			public Content? content;
		}

		[Serializable]
		public class AccessTokenResult
		{
			[Serializable]
			public class Content
			{
				public string accessToken = string.Empty;

				public bool realNameAuth;

				public string extraToken = string.Empty;
			}

			public int code;

			public string message = string.Empty;

			public Content? content;
		}

		[Serializable]
		public class Profile
		{
			[Serializable]
			public class StreamingProperty
			{
			}

			[Serializable]
			public class ActivityBadge
			{
				public int badgeNo;

				public string badgeId = string.Empty;

				public string imageUrl = string.Empty;

				public bool activated;
			}

			public string userIdHash = string.Empty;

			public string nickname = string.Empty;

			public string profileImageUrl = string.Empty;

			public string userRoleCode = string.Empty;

			public string badge = string.Empty;

			public string title = string.Empty;

			public bool verifiedMark;

			public List<ActivityBadge> activityBadges = new List<ActivityBadge>();

			public StreamingProperty streamingProperty = new StreamingProperty();

			public static Profile Unknown()
			{
				return FromNickname("알 수 없음");
			}

			public static Profile FromNickname(string value)
			{
				return new Profile
				{
					nickname = (string.IsNullOrWhiteSpace(value) ? "알 수 없음" : value)
				};
			}
		}

		[Serializable]
		public class SubscriptionExtras
		{
			public int month;

			public string tierName = string.Empty;

			public string nickname = string.Empty;

			public int tierNo;
		}

		[Serializable]
		public class DonationExtras
		{
			[Serializable]
			public class WeeklyRank
			{
				public string userIdHash = string.Empty;

				public string nickName = string.Empty;

				public bool verifiedMark;

				public int donationAmount;

				public int ranking;
			}

			public bool isAnonymous;

			public string payType = string.Empty;

			public int payAmount;

			public string streamingChannelId = string.Empty;

			public string nickname = string.Empty;

			public string osType = string.Empty;

			public string donationType = string.Empty;

			public List<WeeklyRank> weeklyRankList = new List<WeeklyRank>();

			public WeeklyRank? donationUserWeeklyRank;
		}

		[Serializable]
		public class ChannelInfo
		{
			[Serializable]
			public class Content
			{
				public string channelId = string.Empty;

				public string channelName = string.Empty;

				public string channelImageUrl = string.Empty;

				public bool verifiedMark;

				public string channelType = string.Empty;

				public string channelDescription = string.Empty;

				public int followerCount;

				public bool openLive;
			}

			public int code;

			public string message = string.Empty;

			public Content? content;
		}

		private const string ChzzkApiBaseUrl = "https://api.chzzk.naver.com";

		private const string GameApiBaseUrl = "https://comm-api.game.naver.com/nng_main";

		private const string ClientPing = "{\"ver\":\"2\",\"cmd\":0}";

		private const string ServerPingPong = "{\"ver\":\"2\",\"cmd\":10000}";

		private const int InitialHeartbeatDelayMilliseconds = 2000;

		private const int HeartbeatIntervalMilliseconds = 20000;

		private const int HttpRequestTimeoutMilliseconds = 15000;

		private const int ReconnectDelayMilliseconds = 5000;

		private const int SocketInactivityTimeoutMilliseconds = 30000;

		private const int WatchdogIntervalMilliseconds = 3000;

		private static int nextInstanceId;

		private readonly ConcurrentQueue<string> sendQueue = new ConcurrentQueue<string>();

		private readonly int instanceId = Interlocked.Increment(ref nextInstanceId);

		private CancellationTokenSource? cancellation;

		private Thread? worker;

		private WebSocket? socket;

		private string channel = string.Empty;

		private int receivedChatMessages;

		private int heartbeatResponses;

		private long lastFrameTicks;

		private bool openEventRaised;

		public int connected;

		public event Action<Profile, string>? OnMessage;

		public event Action<Profile, string, DonationExtras>? OnDonation;

		public event Action<Profile, SubscriptionExtras>? OnSubscription;

		public event Action? OnClose;

		public event Action? OnOpen;

		public void RemoveAllOnMessageListener()
		{
			this.OnMessage = null;
		}

		public void RemoveAllOnDonationListener()
		{
			this.OnDonation = null;
		}

		public void RemoveAllOnSubscriptionListener()
		{
			this.OnSubscription = null;
		}

		public void Connect(string channelId)
		{
			StopListening("connect reset");
			channel = ((channelId == null) ? string.Empty : channelId.Trim());
			cancellation = new CancellationTokenSource();
			receivedChatMessages = 0;
			heartbeatResponses = 0;
			lastFrameTicks = DateTime.UtcNow.Ticks;
			openEventRaised = false;
			connected = 0;
			ClearSendQueue();
			CancellationToken token = cancellation.Token;
			LogInfo($"Starting CHZZK chat worker #{instanceId}.");
			worker = StartBackgroundThread($"ChzzkWorker#{instanceId}", delegate
			{
				Run(channel, token);
			});
		}

		public void StopListening(string reason = "manual")
		{
			CancellationTokenSource cancellationTokenSource = cancellation;
			cancellation = null;
			connected = 0;
			if (cancellationTokenSource != null && !cancellationTokenSource.IsCancellationRequested)
			{
				LogInfo("Stopping CHZZK chat client: " + reason);
				cancellationTokenSource.Cancel();
			}
			CloseSocket();
		}

		private void Run(string requestedChannel, CancellationToken cancellationToken)
		{
			if (string.IsNullOrWhiteSpace(requestedChannel))
			{
				LogError("ChannelId is empty. Please set it in StreamChats.cfg.");
				connected = -1;
				return;
			}
			while (!cancellationToken.IsCancellationRequested)
			{
				try
				{
					connected = 0;
					openEventRaised = false;
					LogInfo("Fetching CHZZK live status for channel: " + requestedChannel);
					LiveStatus liveStatus = FetchJson<LiveStatus>("https://api.chzzk.naver.com/polling/v2/channels/" + Uri.EscapeDataString(requestedChannel) + "/live-status", cancellationToken);
					string text = liveStatus.content?.chatChannelId ?? string.Empty;
					if (string.IsNullOrEmpty(text))
					{
						LogWarning("live-status did not include chatChannelId. Trying live-detail fallback.");
						LiveDetail liveDetail = FetchJson<LiveDetail>("https://api.chzzk.naver.com/service/v1/channels/" + Uri.EscapeDataString(requestedChannel) + "/live-detail", cancellationToken);
						text = liveDetail.content?.chatChannelId ?? string.Empty;
					}
					if (string.IsNullOrEmpty(text))
					{
						string text2 = liveStatus.content?.status ?? "unknown";
						throw new InvalidOperationException("Could not find chatChannelId for channel '" + requestedChannel + "'. Live status: " + text2 + ".");
					}
					LogInfo("Resolved CHZZK chatChannelId: " + text);
					AccessTokenResult accessTokenResult = FetchJson<AccessTokenResult>("https://comm-api.game.naver.com/nng_main/v1/chats/access-token?channelId=" + Uri.EscapeDataString(text) + "&chatType=STREAMING", cancellationToken);
					string text3 = accessTokenResult.content?.accessToken ?? string.Empty;
					if (string.IsNullOrEmpty(text3))
					{
						throw new InvalidOperationException("Could not get CHZZK chat access token for chat channel '" + text + "'.");
					}
					LogInfo("Received CHZZK chat access token.");
					RunSocket(text, text3, cancellationToken);
				}
				catch (OperationCanceledException)
				{
					break;
				}
				catch (Exception ex2)
				{
					connected = -1;
					LogError("CHZZK chat connection failed: " + ex2.Message);
				}
				if (cancellationToken.IsCancellationRequested)
				{
					break;
				}
				LogWarning($"Reconnecting to CHZZK chat in {5} seconds.");
				if (cancellationToken.WaitHandle.WaitOne(5000))
				{
					break;
				}
			}
		}

		private void RunSocket(string cid, string accessToken, CancellationToken cancellationToken)
		{
			//IL_0074: Unknown result type (might be due to invalid IL or missing references)
			//IL_0079: Unknown result type (might be due to invalid IL or missing references)
			//IL_0093: Expected O, but got Unknown
			//IL_0159: Unknown result type (might be due to invalid IL or missing references)
			//IL_015f: Invalid comparison between Unknown and I4
			//IL_0176: Unknown result type (might be due to invalid IL or missing references)
			string cid2 = cid;
			string accessToken2 = accessToken;
			string text = $"wss://kr-ss{SelectChatServerNumber(cid2)}.chat.naver.com/chat";
			TaskCompletionSource<bool> closed = new TaskCompletionSource<bool>();
			CancellationTokenSource heartbeatCancellation = null;
			CancellationTokenSource watchdogCancellation = null;
			CancellationTokenSource senderCancellation = null;
			WebSocket localSocket = null;
			try
			{
				ClearSendQueue();
				localSocket = new WebSocket(text, Array.Empty<string>())
				{
					WaitTime = TimeSpan.FromSeconds(60.0)
				};
				SslProtocols enabledSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12;
				localSocket.SslConfiguration.EnabledSslProtocols = enabledSslProtocols;
				localSocket.OnOpen += delegate
				{
					connected = 1;
					lastFrameTicks = DateTime.UtcNow.Ticks;
					LogInfo("CHZZK websocket opened. Sending read-only connect payload.");
					heartbeatCancellation = CancellationTokenSource.CreateLinkedTokenSource(new CancellationToken[1] { cancellationToken });
					watchdogCancellation = CancellationTokenSource.CreateLinkedTokenSource(new CancellationToken[1] { cancellationToken });
					senderCancellation = CancellationTokenSource.CreateLinkedTokenSource(new CancellationToken[1] { cancellationToken });
					QueueSocketMessage(BuildConnectPayload(cid2, accessToken2));
					StartBackgroundThread("ChzzkSender", delegate
					{
						SenderLoop(localSocket, senderCancellation.Token);
					});
					StartBackgroundThread("ChzzkHeartbeat", delegate
					{
						HeartbeatLoop(localSocket, heartbeatCancellation.Token);
					});
					StartBackgroundThread("ChzzkWatchdog", delegate
					{
						WatchdogLoop(localSocket, watchdogCancellation.Token);
					});
				};
				localSocket.OnMessage += delegate(object sender, MessageEventArgs args)
				{
					ParseMessage(localSocket, args);
				};
				localSocket.OnClose += delegate(object sender, CloseEventArgs args)
				{
					bool flag = connected == 1;
					connected = -1;
					heartbeatCancellation?.Cancel();
					watchdogCancellation?.Cancel();
					senderCancellation?.Cancel();
					if (!cancellationToken.IsCancellationRequested)
					{
						LogWarning($"CHZZK websocket closed: {args.Code} {args.Reason}");
						if (flag)
						{
							MainThreadDispatcher.Enqueue(delegate
							{
								this.OnClose?.Invoke();
							});
						}
					}
					closed.TrySetResult(result: true);
				};
				localSocket.OnError += delegate(object sender, ErrorEventArgs args)
				{
					if (!cancellationToken.IsCancellationRequested)
					{
						LogError("CHZZK websocket error: " + args.Message);
					}
					connected = -1;
					heartbeatCancellation?.Cancel();
					watchdogCancellation?.Cancel();
					senderCancellation?.Cancel();
					closed.TrySetException(new InvalidOperationException(args.Message));
				};
				Interlocked.Exchange(ref socket, localSocket);
				LogInfo("Connecting to CHZZK chat: " + text);
				using (cancellationToken.Register(delegate
				{
					heartbeatCancellation?.Cancel();
					watchdogCancellation?.Cancel();
					senderCancellation?.Cancel();
					CloseSocket(localSocket);
					closed.TrySetCanceled();
				}))
				{
					localSocket.Connect();
					if ((int)localSocket.ReadyState != 1)
					{
						throw new InvalidOperationException($"WebSocket connect failed: {localSocket.ReadyState}");
					}
					closed.Task.GetAwaiter().GetResult();
				}
			}
			finally
			{
				heartbeatCancellation?.Cancel();
				heartbeatCancellation?.Dispose();
				watchdogCancellation?.Cancel();
				watchdogCancellation?.Dispose();
				senderCancellation?.Cancel();
				senderCancellation?.Dispose();
				if (localSocket != null)
				{
					Interlocked.CompareExchange(ref socket, null, localSocket);
					CloseSocket(localSocket);
				}
			}
		}

		private static T FetchJson<T>(string url, CancellationToken cancellationToken) where T : class
		{
			cancellationToken.ThrowIfCancellationRequested();
			LogInfo("HTTP GET " + url);
			try
			{
				ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
				HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(url);
				httpWebRequest.Method = "GET";
				httpWebRequest.Accept = "application/json, text/plain, */*";
				httpWebRequest.UserAgent = "Mozilla/5.0 StreamChats-LethalCompany";
				httpWebRequest.Timeout = 15000;
				httpWebRequest.ReadWriteTimeout = 15000;
				httpWebRequest.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
				HttpStatusCode statusCode;
				string text;
				using (cancellationToken.Register(httpWebRequest.Abort))
				{
					using HttpWebResponse httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse();
					statusCode = httpWebResponse.StatusCode;
					text = ReadResponseBody(httpWebResponse);
				}
				LogInfo($"HTTP OK {(int)statusCode} {url}");
				T val = JsonConvert.DeserializeObject<T>(text);
				if (val == null)
				{
					throw new InvalidOperationException("Empty response from " + url);
				}
				return val;
			}
			catch (WebException ex)
			{
				if (cancellationToken.IsCancellationRequested)
				{
					throw new OperationCanceledException(cancellationToken);
				}
				string arg = string.Empty;
				if (ex.Response != null)
				{
					using WebResponse response = ex.Response;
					arg = ReadResponseBody(response);
				}
				throw new InvalidOperationException($"HTTP request failed: {ex.Status} {ex.Message} {arg}");
			}
		}

		private static string ReadResponseBody(WebResponse response)
		{
			using Stream stream = response.GetResponseStream();
			if (stream == null)
			{
				return string.Empty;
			}
			using StreamReader streamReader = new StreamReader(stream);
			return streamReader.ReadToEnd();
		}

		private void HeartbeatLoop(WebSocket targetSocket, CancellationToken cancellationToken)
		{
			//IL_00d3: Unknown result type (might be due to invalid IL or missing references)
			//IL_0096: Unknown result type (might be due to invalid IL or missing references)
			//IL_009c: Invalid comparison between Unknown and I4
			//IL_003a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0040: Invalid comparison between Unknown and I4
			try
			{
				while (!cancellationToken.IsCancellationRequested && (int)targetSocket.ReadyState == 1)
				{
					TimeSpan timeSpan = TimeSpan.FromTicks(DateTime.UtcNow.Ticks - Interlocked.Read(ref lastFrameTicks));
					if (timeSpan.TotalMilliseconds >= 20000.0 && (int)targetSocket.ReadyState == 1)
					{
						QueueSocketMessage("{\"ver\":\"2\",\"cmd\":0}");
						LogInfo($"Queued CHZZK heartbeat ping after {timeSpan.TotalSeconds:0}s without a frame.");
					}
					if (cancellationToken.WaitHandle.WaitOne(2000))
					{
						break;
					}
				}
			}
			catch (Exception ex)
			{
				LogError("CHZZK heartbeat failed: " + ex.Message);
			}
			finally
			{
				LogInfo($"CHZZK heartbeat loop stopped. socketState={targetSocket.ReadyState}, cancelled={cancellationToken.IsCancellationRequested}");
			}
		}

		private void WatchdogLoop(WebSocket targetSocket, CancellationToken cancellationToken)
		{
			//IL_00ff: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c8: Invalid comparison between Unknown and I4
			int num = 0;
			try
			{
				while (!cancellationToken.IsCancellationRequested && (int)targetSocket.ReadyState == 1 && !cancellationToken.WaitHandle.WaitOne(3000))
				{
					num++;
					TimeSpan timeSpan = TimeSpan.FromTicks(DateTime.UtcNow.Ticks - Interlocked.Read(ref lastFrameTicks));
					if (num % 5 == 0)
					{
						LogInfo($"Watchdog alive #{num}, last frame {timeSpan.TotalSeconds:0}s ago.");
					}
					if (timeSpan.TotalMilliseconds < 30000.0)
					{
						continue;
					}
					LogWarning($"No CHZZK websocket frame for {timeSpan.TotalSeconds:0}s. Forcing reconnect.");
					CloseSocket(targetSocket);
					break;
				}
			}
			catch (Exception ex)
			{
				LogError("CHZZK watchdog failed: " + ex.Message);
			}
			finally
			{
				LogInfo($"CHZZK watchdog loop stopped. socketState={targetSocket.ReadyState}, cancelled={cancellationToken.IsCancellationRequested}");
			}
		}

		private void ParseMessage(WebSocket targetSocket, MessageEventArgs e)
		{
			try
			{
				Interlocked.Exchange(ref lastFrameTicks, DateTime.UtcNow.Ticks);
				JObject val = JObject.Parse(e.Data);
				switch (((JToken)val).Value<int?>((object)"cmd").GetValueOrDefault(-1))
				{
				case 0:
					QueueSocketMessage("{\"ver\":\"2\",\"cmd\":10000}");
					break;
				case 93101:
				{
					JToken obj2 = val["bdy"];
					HandleChat((JArray?)(object)((obj2 is JArray) ? obj2 : null));
					break;
				}
				case 93102:
				{
					JToken obj = val["bdy"];
					HandleSpecialMessage((JArray?)(object)((obj is JArray) ? obj : null));
					break;
				}
				case 10000:
					heartbeatResponses++;
					LogInfo($"Received CHZZK heartbeat pong #{heartbeatResponses}.");
					break;
				case 10100:
					if (!openEventRaised)
					{
						openEventRaised = true;
						LogInfo("Connected to CHZZK chat.");
						MainThreadDispatcher.Enqueue(delegate
						{
							this.OnOpen?.Invoke();
						});
					}
					break;
				}
			}
			catch (Exception arg)
			{
				LogError($"Failed to parse CHZZK websocket message: {arg}");
			}
		}

		private void HandleChat(JArray? body)
		{
			if (body == null)
			{
				return;
			}
			foreach (JToken item in body)
			{
				JObject val = (JObject)(object)((item is JObject) ? item : null);
				if (val == null)
				{
					continue;
				}
				Profile profile = ParseJsonPayload<Profile>(val["profile"]) ?? Profile.Unknown();
				string text = ((JToken)val).Value<string>((object)"msg")?.Trim() ?? string.Empty;
				if (text.Length != 0)
				{
					receivedChatMessages++;
					LogInfo($"Received CHZZK chat #{receivedChatMessages}: {profile.nickname}: {text}");
					Profile capturedProfile = profile;
					string capturedMessage = text;
					MainThreadDispatcher.Enqueue(delegate
					{
						this.OnMessage?.Invoke(capturedProfile, capturedMessage);
					});
				}
			}
		}

		private void HandleSpecialMessage(JArray? body)
		{
			if (body == null)
			{
				return;
			}
			foreach (JToken item in body)
			{
				JObject val = (JObject)(object)((item is JObject) ? item : null);
				if (val == null || !int.TryParse(((JToken)val).Value<string>((object)"msgTypeCode"), out var result))
				{
					continue;
				}
				Profile profile = ParseJsonPayload<Profile>(val["profile"]);
				string message = ((JToken)val).Value<string>((object)"msg") ?? string.Empty;
				JToken token = val["extra"] ?? val["extras"];
				switch (result)
				{
				case 10:
				{
					DonationExtras donation = ParseJsonPayload<DonationExtras>(token) ?? new DonationExtras();
					Profile donationProfile = profile ?? Profile.FromNickname(donation.isAnonymous ? "익명" : donation.nickname);
					MainThreadDispatcher.Enqueue(delegate
					{
						this.OnDonation?.Invoke(donationProfile, message, donation);
					});
					break;
				}
				case 11:
				{
					SubscriptionExtras subscription = ParseJsonPayload<SubscriptionExtras>(token) ?? new SubscriptionExtras();
					Profile subscriptionProfile = profile ?? Profile.FromNickname(subscription.nickname);
					MainThreadDispatcher.Enqueue(delegate
					{
						this.OnSubscription?.Invoke(subscriptionProfile, subscription);
					});
					break;
				}
				default:
					LogInfo($"Unsupported CHZZK message type: {result}");
					break;
				}
			}
		}

		private static T? ParseJsonPayload<T>(JToken? token) where T : class
		{
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_000c: Invalid comparison between Unknown and I4
			//IL_0023: Unknown result type (might be due to invalid IL or missing references)
			//IL_0029: Invalid comparison between Unknown and I4
			if (token == null || (int)token.Type == 10)
			{
				return null;
			}
			string text = (((int)token.Type == 8) ? Extensions.Value<string>((IEnumerable<JToken>)token) : token.ToString((Formatting)0, Array.Empty<JsonConverter>()));
			if (string.IsNullOrWhiteSpace(text) || text == "null")
			{
				return null;
			}
			try
			{
				return JsonConvert.DeserializeObject<T>(text);
			}
			catch (Exception ex)
			{
				LogError("CHZZK JSON payload parse failed: " + ex.Message);
				return null;
			}
		}

		private static string BuildConnectPayload(string cid, string accessToken)
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_001c: Unknown result type (might be due to invalid IL or missing references)
			//IL_002f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0045: Unknown result type (might be due to invalid IL or missing references)
			//IL_0057: Unknown result type (might be due to invalid IL or missing references)
			//IL_005d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0062: Unknown result type (might be due to invalid IL or missing references)
			//IL_0073: Unknown result type (might be due to invalid IL or missing references)
			//IL_0089: Unknown result type (might be due to invalid IL or missing references)
			//IL_009b: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b6: Expected O, but got Unknown
			//IL_00b7: Unknown result type (might be due to invalid IL or missing references)
			return ((JToken)new JObject
			{
				["ver"] = JToken.op_Implicit("2"),
				["cmd"] = JToken.op_Implicit(100),
				["svcid"] = JToken.op_Implicit("game"),
				["cid"] = JToken.op_Implicit(cid),
				["bdy"] = (JToken)new JObject
				{
					["uid"] = (JToken)(object)JValue.CreateNull(),
					["devType"] = JToken.op_Implicit(2001),
					["accTkn"] = JToken.op_Implicit(accessToken),
					["auth"] = JToken.op_Implicit("READ")
				},
				["tid"] = JToken.op_Implicit(1)
			}).ToString((Formatting)0, Array.Empty<JsonConverter>());
		}

		private void QueueSocketMessage(string message)
		{
			sendQueue.Enqueue(message);
		}

		private void SenderLoop(WebSocket targetSocket, CancellationToken cancellationToken)
		{
			//IL_009a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0054: Unknown result type (might be due to invalid IL or missing references)
			//IL_005a: Invalid comparison between Unknown and I4
			//IL_002c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0032: Invalid comparison between Unknown and I4
			try
			{
				while (!cancellationToken.IsCancellationRequested && (int)targetSocket.ReadyState == 1)
				{
					if (!sendQueue.TryDequeue(out string result))
					{
						cancellationToken.WaitHandle.WaitOne(10);
					}
					else if ((int)targetSocket.ReadyState == 1)
					{
						targetSocket.Send(result);
						LogSocketSend(result);
					}
				}
			}
			catch (Exception ex)
			{
				LogError("CHZZK sender failed: " + ex.Message);
				connected = -1;
				CloseSocket(targetSocket);
			}
			finally
			{
				LogInfo($"CHZZK sender loop stopped. socketState={targetSocket.ReadyState}, cancelled={cancellationToken.IsCancellationRequested}");
			}
		}

		private static void LogSocketSend(string message)
		{
			if (message.Contains("\"cmd\":100"))
			{
				LogInfo("Sent CHZZK connect payload.");
			}
			else if (message.Contains("\"cmd\":0"))
			{
				LogInfo("Sent CHZZK heartbeat ping.");
			}
			else if (message.Contains("\"cmd\":10000"))
			{
				LogInfo("Sent CHZZK server-ping pong.");
			}
		}

		private static Thread StartBackgroundThread(string name, Action action)
		{
			Action action2 = action;
			string name2 = name;
			Thread thread = new Thread((ThreadStart)delegate
			{
				try
				{
					action2();
				}
				catch (Exception ex)
				{
					LogError(name2 + " thread crashed: " + ex.Message);
				}
			})
			{
				Name = name2,
				IsBackground = true
			};
			thread.Start();
			LogInfo("Started background thread: " + name2);
			return thread;
		}

		private void CloseSocket()
		{
			WebSocket val = Interlocked.Exchange(ref socket, null);
			if (val != null)
			{
				CloseSocket(val);
			}
		}

		private static void CloseSocket(WebSocket targetSocket)
		{
			//IL_0003: Unknown result type (might be due to invalid IL or missing references)
			//IL_0009: Invalid comparison between Unknown and I4
			//IL_000c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0012: Invalid comparison between Unknown and I4
			try
			{
				if ((int)targetSocket.ReadyState == 1 || (int)targetSocket.ReadyState == 0)
				{
					targetSocket.Close();
				}
			}
			catch (Exception ex)
			{
				LogWarning("Failed to close CHZZK websocket cleanly: " + ex.Message);
			}
		}

		private void ClearSendQueue()
		{
			string result;
			while (sendQueue.TryDequeue(out result))
			{
			}
		}

		private static int SelectChatServerNumber(string chatChannelId)
		{
			int num = 0;
			foreach (char c in chatChannelId)
			{
				num += c;
			}
			return Math.Abs(num) % 9 + 1;
		}

		private static void LogInfo(string message)
		{
			ManualLogSource? logger = Plugin.logger;
			if (logger != null)
			{
				logger.LogInfo((object)message);
			}
		}

		private static void LogWarning(string message)
		{
			ManualLogSource? logger = Plugin.logger;
			if (logger != null)
			{
				logger.LogWarning((object)message);
			}
		}

		private static void LogError(string message)
		{
			ManualLogSource? logger = Plugin.logger;
			if (logger != null)
			{
				logger.LogError((object)message);
			}
		}
	}
	internal sealed class TwitchChatClient
	{
		private const string Server = "irc.chat.twitch.tv";

		private const int Port = 6667;

		private static readonly Random Random = new Random();

		private CancellationTokenSource? cancellation;

		private Thread? worker;

		private TcpClient? client;

		private StreamWriter? writer;

		public event Action<string, string>? OnMessage;

		public void Connect(string channelHandle)
		{
			StopListening("connect reset");
			string channel = NormalizeChannel(channelHandle);
			if (string.IsNullOrWhiteSpace(channel))
			{
				LogError("TwitchHandle is empty.");
				return;
			}
			cancellation = new CancellationTokenSource();
			CancellationToken token = cancellation.Token;
			worker = StartBackgroundThread("TwitchChatWorker", delegate
			{
				Run(channel, token);
			});
		}

		public void StopListening(string reason = "manual")
		{
			CancellationTokenSource cancellationTokenSource = cancellation;
			cancellation = null;
			if (cancellationTokenSource != null && !cancellationTokenSource.IsCancellationRequested)
			{
				LogInfo("Stopping Twitch chat client: " + reason);
				cancellationTokenSource.Cancel();
			}
			CloseSocket();
		}

		private void Run(string channel, CancellationToken cancellationToken)
		{
			while (!cancellationToken.IsCancellationRequested)
			{
				TcpClient tcpClient = null;
				StreamReader streamReader = null;
				StreamWriter streamWriter = null;
				try
				{
					LogInfo(string.Format("Connecting to Twitch IRC over TCP {0}:{1}: #{2}", "irc.chat.twitch.tv", 6667, channel));
					tcpClient = new TcpClient();
					tcpClient.Connect("irc.chat.twitch.tv", 6667);
					tcpClient.ReceiveTimeout = 1000;
					tcpClient.SendTimeout = 5000;
					NetworkStream stream = tcpClient.GetStream();
					streamReader = new StreamReader(stream);
					streamWriter = new StreamWriter(stream)
					{
						NewLine = "\r\n",
						AutoFlush = true
					};
					client = tcpClient;
					writer = streamWriter;
					string text = NextAnonymousNick();
					LogInfo("Connected to Twitch IRC as " + text + ". Joining #" + channel + ".");
					SendIrc(streamWriter, "PASS SCHMOOPIIE");
					SendIrc(streamWriter, "NICK " + text);
					SendIrc(streamWriter, "CAP REQ :twitch.tv/tags twitch.tv/commands");
					SendIrc(streamWriter, "JOIN #" + channel);
					while (!cancellationToken.IsCancellationRequested && tcpClient.Connected)
					{
						string text2;
						try
						{
							text2 = streamReader.ReadLine();
						}
						catch (IOException)
						{
							continue;
						}
						if (text2 == null)
						{
							throw new IOException("Twitch IRC connection closed.");
						}
						HandleIrcLine(streamWriter, text2);
					}
				}
				catch (Exception ex2)
				{
					if (!cancellationToken.IsCancellationRequested)
					{
						LogError("Twitch chat failed: " + ex2.Message);
					}
				}
				finally
				{
					streamReader?.Dispose();
					streamWriter?.Dispose();
					tcpClient?.Close();
					if (client == tcpClient)
					{
						client = null;
						writer = null;
					}
				}
				if (cancellationToken.IsCancellationRequested)
				{
					break;
				}
				LogWarning("Reconnecting to Twitch chat in 15 seconds.");
				if (cancellationToken.WaitHandle.WaitOne(15000))
				{
					break;
				}
			}
		}

		private void HandleIrcLine(StreamWriter targetWriter, string line)
		{
			line = line.Trim();
			if (line.Length == 0)
			{
				return;
			}
			if (line.StartsWith("PING", StringComparison.OrdinalIgnoreCase))
			{
				SendIrc(targetWriter, "PONG :tmi.twitch.tv");
				return;
			}
			string input = (line.StartsWith("@", StringComparison.Ordinal) ? line.Substring(line.IndexOf(' ') + 1) : line);
			Match match = Regex.Match(input, ":([^!]+)![^ ]+ PRIVMSG #[^ ]+ :(.+)$");
			if (match.Success)
			{
				string name = match.Groups[1].Value;
				string message = match.Groups[2].Value;
				LogInfo("Received Twitch chat: " + name + ": " + message);
				MainThreadDispatcher.Enqueue(delegate
				{
					this.OnMessage?.Invoke(name, message);
				});
			}
		}

		private static void SendIrc(StreamWriter targetWriter, string message)
		{
			targetWriter.WriteLine(message);
		}

		private static string NormalizeChannel(string channelHandle)
		{
			string text = channelHandle.Trim();
			if (text.StartsWith("@", StringComparison.Ordinal))
			{
				text = text.Substring(1);
			}
			if (Uri.TryCreate(text, UriKind.Absolute, out Uri result))
			{
				text = result.AbsolutePath.Trim('/');
			}
			return text.Trim().TrimStart('#').ToLowerInvariant();
		}

		private static string NextAnonymousNick()
		{
			lock (Random)
			{
				return $"justinfan{Random.Next(10000, 999999)}";
			}
		}

		private static Thread StartBackgroundThread(string name, Action action)
		{
			Action action2 = action;
			string name2 = name;
			Thread thread = new Thread((ThreadStart)delegate
			{
				try
				{
					action2();
				}
				catch (Exception ex)
				{
					LogError(name2 + " crashed: " + ex.Message);
				}
			})
			{
				Name = name2,
				IsBackground = true
			};
			thread.Start();
			LogInfo("Started background thread: " + name2);
			return thread;
		}

		private void CloseSocket()
		{
			StreamWriter streamWriter = writer;
			TcpClient tcpClient = client;
			writer = null;
			client = null;
			try
			{
				streamWriter?.Dispose();
			}
			catch (Exception ex)
			{
				LogWarning("Failed to close Twitch IRC writer cleanly: " + ex.Message);
			}
			try
			{
				tcpClient?.Close();
			}
			catch (Exception ex2)
			{
				LogWarning("Failed to close Twitch IRC client cleanly: " + ex2.Message);
			}
		}

		private static void LogInfo(string message)
		{
			ManualLogSource? logger = Plugin.logger;
			if (logger != null)
			{
				logger.LogInfo((object)message);
			}
		}

		private static void LogWarning(string message)
		{
			ManualLogSource? logger = Plugin.logger;
			if (logger != null)
			{
				logger.LogWarning((object)message);
			}
		}

		private static void LogError(string message)
		{
			ManualLogSource? logger = Plugin.logger;
			if (logger != null)
			{
				logger.LogError((object)message);
			}
		}
	}
	internal sealed class YoutubeLiveChatClient
	{
		private readonly struct ResponseText
		{
			public string Body { get; }

			public Uri? ResponseUri { get; }

			public ResponseText(string body, Uri? responseUri)
			{
				Body = body;
				ResponseUri = responseUri;
			}
		}

		private const int HttpRequestTimeoutMilliseconds = 15000;

		private readonly HashSet<string> seenMessageIds = new HashSet<string>();

		private CancellationTokenSource? cancellation;

		private Thread? worker;

		public event Action<string, string>? OnMessage;

		public void Connect(string channelHandle)
		{
			StopListening("connect reset");
			string target = ((channelHandle == null) ? string.Empty : channelHandle.Trim());
			if (string.IsNullOrWhiteSpace(target))
			{
				LogError("YoutubeHandle is empty.");
				return;
			}
			cancellation = new CancellationTokenSource();
			CancellationToken token = cancellation.Token;
			worker = StartBackgroundThread("YoutubeChatWorker", delegate
			{
				Run(target, token);
			});
		}

		public void StopListening(string reason = "manual")
		{
			CancellationTokenSource cancellationTokenSource = cancellation;
			cancellation = null;
			if (cancellationTokenSource != null && !cancellationTokenSource.IsCancellationRequested)
			{
				LogInfo("Stopping YouTube chat client: " + reason);
				cancellationTokenSource.Cancel();
			}
		}

		private void Run(string target, CancellationToken cancellationToken)
		{
			while (!cancellationToken.IsCancellationRequested)
			{
				try
				{
					seenMessageIds.Clear();
					ResponseText responseText = FetchLivePage(target, cancellationToken);
					string text = ExtractVideoId(responseText.ResponseUri, responseText.Body);
					if (string.IsNullOrWhiteSpace(text))
					{
						throw new InvalidOperationException("Could not find a live YouTube video for the configured handle.");
					}
					LogInfo("Resolved YouTube live video: " + text);
					ResponseText @string = GetString("https://www.youtube.com/watch?v=" + Uri.EscapeDataString(text), cancellationToken);
					string apiKey = ExtractRequired(@string.Body, "\"INNERTUBE_API_KEY\"\\s*:\\s*\"([^\"]+)\"", "INNERTUBE_API_KEY");
					string clientVersion = ExtractOptional(@string.Body, "\"clientVersion\"\\s*:\\s*\"([^\"]+)\"") ?? "2.20240501.00.00";
					string text2 = ExtractLiveChatContinuation(@string.Body);
					if (string.IsNullOrWhiteSpace(text2))
					{
						throw new InvalidOperationException("Could not find YouTube live chat continuation.");
					}
					LogInfo("Connected to YouTube live chat polling.");
					PollLiveChat(apiKey, clientVersion, text2, cancellationToken);
				}
				catch (OperationCanceledException)
				{
					break;
				}
				catch (Exception ex2)
				{
					if (!cancellationToken.IsCancellationRequested)
					{
						LogError("YouTube chat failed: " + ex2.Message);
					}
				}
				if (cancellationToken.IsCancellationRequested)
				{
					break;
				}
				LogWarning("Reconnecting to YouTube chat in 10 seconds.");
				if (cancellationToken.WaitHandle.WaitOne(10000))
				{
					break;
				}
			}
		}

		private void PollLiveChat(string apiKey, string clientVersion, string continuation, CancellationToken cancellationToken)
		{
			//IL_0009: Unknown result type (might be due to invalid IL or missing references)
			//IL_000e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0014: Unknown result type (might be due to invalid IL or missing references)
			//IL_0019: Unknown result type (might be due to invalid IL or missing references)
			//IL_001f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0024: Unknown result type (might be due to invalid IL or missing references)
			//IL_003a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0051: Expected O, but got Unknown
			//IL_0057: Expected O, but got Unknown
			//IL_0058: Unknown result type (might be due to invalid IL or missing references)
			//IL_006b: Expected O, but got Unknown
			string text = continuation;
			while (!cancellationToken.IsCancellationRequested && !string.IsNullOrWhiteSpace(text))
			{
				JObject val = new JObject
				{
					["context"] = (JToken)new JObject { ["client"] = (JToken)new JObject
					{
						["clientName"] = JToken.op_Implicit("WEB"),
						["clientVersion"] = JToken.op_Implicit(clientVersion)
					} },
					["continuation"] = JToken.op_Implicit(text)
				};
				string text2 = PostJson("https://www.youtube.com/youtubei/v1/live_chat/get_live_chat?key=" + Uri.EscapeDataString(apiKey), ((JToken)val).ToString((Formatting)0, Array.Empty<JsonConverter>()), cancellationToken);
				JObject root = JObject.Parse(text2);
				EmitChatMessages(root);
				int timeoutMs;
				string text3 = FindContinuation(root, out timeoutMs);
				if (string.IsNullOrWhiteSpace(text3))
				{
					throw new InvalidOperationException("YouTube live chat response did not include a continuation.");
				}
				text = text3;
				int millisecondsTimeout = Math.Max(1000, Math.Min((timeoutMs <= 0) ? 3000 : timeoutMs, 10000));
				cancellationToken.WaitHandle.WaitOne(millisecondsTimeout);
			}
		}

		private void EmitChatMessages(JObject root)
		{
			foreach (JToken item in ((JToken)root).SelectTokens("$..liveChatTextMessageRenderer"))
			{
				string text = item.Value<string>((object)"id") ?? string.Empty;
				if (text.Length > 0 && !seenMessageIds.Add(text))
				{
					continue;
				}
				string name = ReadRunsOrSimpleText(item[(object)"authorName"]) ?? "YouTube";
				string message = ReadRunsOrSimpleText(item[(object)"message"]) ?? string.Empty;
				if (message.Length != 0)
				{
					LogInfo("Received YouTube chat: " + name + ": " + message);
					MainThreadDispatcher.Enqueue(delegate
					{
						this.OnMessage?.Invoke(name, message);
					});
				}
			}
		}

		private static string? FindContinuation(JObject root, out int timeoutMs)
		{
			timeoutMs = 3000;
			foreach (JToken item in ((JToken)root).SelectTokens("$..timedContinuationData"))
			{
				string text = item.Value<string>((object)"continuation");
				timeoutMs = item.Value<int?>((object)"timeoutMs") ?? timeoutMs;
				if (!string.IsNullOrWhiteSpace(text))
				{
					return text;
				}
			}
			foreach (JToken item2 in ((JToken)root).SelectTokens("$..invalidationContinuationData"))
			{
				string text2 = item2.Value<string>((object)"continuation");
				timeoutMs = item2.Value<int?>((object)"timeoutMs") ?? timeoutMs;
				if (!string.IsNullOrWhiteSpace(text2))
				{
					return text2;
				}
			}
			return null;
		}

		private static ResponseText FetchLivePage(string target, CancellationToken cancellationToken)
		{
			string text = NormalizeTargetUrl(target);
			LogInfo("Fetching YouTube live page: " + text);
			return GetString(text, cancellationToken);
		}

		private static string NormalizeTargetUrl(string target)
		{
			if (Uri.TryCreate(target, UriKind.Absolute, out Uri _))
			{
				return target;
			}
			string stringToEscape = target.Trim().TrimStart('@');
			return "https://www.youtube.com/@" + Uri.EscapeDataString(stringToEscape) + "/live";
		}

		private static string? ExtractVideoId(Uri? responseUri, string body)
		{
			if (responseUri != null)
			{
				Match match = Regex.Match(responseUri.Query, "(?:^|[?&])v=([A-Za-z0-9_-]{11})");
				if (match.Success)
				{
					return match.Groups[1].Value;
				}
				Match match2 = Regex.Match(responseUri.AbsolutePath, "/(?:watch|live)/([A-Za-z0-9_-]{11})");
				if (match2.Success)
				{
					return match2.Groups[1].Value;
				}
			}
			Match match3 = Regex.Match(body, "\"videoId\"\\s*:\\s*\"([A-Za-z0-9_-]{11})\"");
			return match3.Success ? match3.Groups[1].Value : null;
		}

		private static string? ExtractLiveChatContinuation(string body)
		{
			int num = body.IndexOf("liveChatRenderer", StringComparison.Ordinal);
			if (num < 0)
			{
				num = body.IndexOf("liveChat", StringComparison.Ordinal);
			}
			string input = ((num >= 0) ? body.Substring(num) : body);
			Match match = Regex.Match(input, "\"continuation\"\\s*:\\s*\"([^\"]+)\"");
			return match.Success ? Regex.Unescape(match.Groups[1].Value) : null;
		}

		private static string ExtractRequired(string body, string pattern, string name)
		{
			string text = ExtractOptional(body, pattern);
			if (string.IsNullOrWhiteSpace(text))
			{
				throw new InvalidOperationException("Could not find YouTube " + name + ".");
			}
			return text;
		}

		private static string? ExtractOptional(string body, string pattern)
		{
			Match match = Regex.Match(body, pattern);
			return match.Success ? Regex.Unescape(match.Groups[1].Value) : null;
		}

		private static string? ReadRunsOrSimpleText(JToken? token)
		{
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_000c: Invalid comparison between Unknown and I4
			if (token == null || (int)token.Type == 10)
			{
				return null;
			}
			string value = token.Value<string>((object)"simpleText");
			if (!string.IsNullOrEmpty(value))
			{
				return WebUtility.HtmlDecode(value);
			}
			StringBuilder stringBuilder = new StringBuilder();
			JToken obj = token[(object)"runs"];
			JArray val = (JArray)(object)((obj is JArray) ? obj : null);
			if (val == null)
			{
				return null;
			}
			foreach (JToken item in val)
			{
				string text = item.Value<string>((object)"text");
				if (text != null)
				{
					stringBuilder.Append(text);
					continue;
				}
				JToken obj2 = item[(object)"emoji"];
				object obj3;
				if (obj2 == null)
				{
					obj3 = null;
				}
				else
				{
					JToken obj4 = obj2[(object)"shortcuts"];
					if (obj4 == null)
					{
						obj3 = null;
					}
					else
					{
						JToken obj5 = obj4[(object)0];
						obj3 = ((obj5 != null) ? Extensions.Value<string>((IEnumerable<JToken>)obj5) : null);
					}
				}
				if (obj3 == null)
				{
					JToken obj6 = item[(object)"emoji"];
					if (obj6 == null)
					{
						obj3 = null;
					}
					else
					{
						JToken obj7 = obj6[(object)"image"];
						if (obj7 == null)
						{
							obj3 = null;
						}
						else
						{
							JToken obj8 = obj7[(object)"accessibility"];
							if (obj8 == null)
							{
								obj3 = null;
							}
							else
							{
								JToken obj9 = obj8[(object)"accessibilityData"];
								obj3 = ((obj9 != null) ? obj9.Value<string>((object)"label") : null);
							}
						}
					}
				}
				string value2 = (string)obj3;
				if (!string.IsNullOrEmpty(value2))
				{
					stringBuilder.Append(value2);
				}
			}
			return WebUtility.HtmlDecode(stringBuilder.ToString());
		}

		private static ResponseText GetString(string url, CancellationToken cancellationToken)
		{
			cancellationToken.ThrowIfCancellationRequested();
			ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
			HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(url);
			httpWebRequest.Method = "GET";
			httpWebRequest.Accept = "text/html,application/json,*/*";
			httpWebRequest.UserAgent = "Mozilla/5.0 StreamChats-LethalCompany";
			httpWebRequest.Timeout = 15000;
			httpWebRequest.ReadWriteTimeout = 15000;
			httpWebRequest.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
			httpWebRequest.AllowAutoRedirect = true;
			using (cancellationToken.Register(httpWebRequest.Abort))
			{
				using HttpWebResponse httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse();
				using Stream stream = httpWebResponse.GetResponseStream();
				using StreamReader streamReader = new StreamReader(stream ?? Stream.Null);
				return new ResponseText(streamReader.ReadToEnd(), httpWebResponse.ResponseUri);
			}
		}

		private static string PostJson(string url, string body, CancellationToken cancellationToken)
		{
			cancellationToken.ThrowIfCancellationRequested();
			ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
			byte[] bytes = Encoding.UTF8.GetBytes(body);
			HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(url);
			httpWebRequest.Method = "POST";
			httpWebRequest.Accept = "application/json";
			httpWebRequest.ContentType = "application/json";
			httpWebRequest.UserAgent = "Mozilla/5.0 StreamChats-LethalCompany";
			httpWebRequest.Timeout = 15000;
			httpWebRequest.ReadWriteTimeout = 15000;
			httpWebRequest.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
			using (cancellationToken.Register(httpWebRequest.Abort))
			{
				using Stream stream = httpWebRequest.GetRequestStream();
				stream.Write(bytes, 0, bytes.Length);
			}
			using HttpWebResponse httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse();
			using Stream stream2 = httpWebResponse.GetResponseStream();
			using StreamReader streamReader = new StreamReader(stream2 ?? Stream.Null);
			return streamReader.ReadToEnd();
		}

		private static Thread StartBackgroundThread(string name, Action action)
		{
			Action action2 = action;
			string name2 = name;
			Thread thread = new Thread((ThreadStart)delegate
			{
				try
				{
					action2();
				}
				catch (Exception ex)
				{
					LogError(name2 + " crashed: " + ex.Message);
				}
			})
			{
				Name = name2,
				IsBackground = true
			};
			thread.Start();
			LogInfo("Started background thread: " + name2);
			return thread;
		}

		private static void LogInfo(string message)
		{
			ManualLogSource? logger = Plugin.logger;
			if (logger != null)
			{
				logger.LogInfo((object)message);
			}
		}

		private static void LogWarning(string message)
		{
			ManualLogSource? logger = Plugin.logger;
			if (logger != null)
			{
				logger.LogWarning((object)message);
			}
		}

		private static void LogError(string message)
		{
			ManualLogSource? logger = Plugin.logger;
			if (logger != null)
			{
				logger.LogError((object)message);
			}
		}
	}
}
namespace ChzzkChat.Configuration
{
	internal static class Config
	{
		private static ConfigFile config;

		private static ConfigEntry<string> config_channel_id;

		private static ConfigEntry<string> twitch_handle;

		private static ConfigEntry<string> youtube_handle;

		private static ConfigEntry<bool> show_platform_prefix;

		public static string ConfigChannelId => config_channel_id.Value;

		public static string TwitchHandle => twitch_handle.Value;

		public static string YoutubeHandle => youtube_handle.Value;

		public static bool ShowPlatformPrefix => show_platform_prefix.Value;

		public static void Load()
		{
			//IL_0013: Unknown result type (might be due to invalid IL or missing references)
			//IL_001d: Expected O, but got Unknown
			string text = Path.Combine(Paths.ConfigPath, "StreamChats.cfg");
			config = new ConfigFile(text, true);
			InternalLoad();
		}

		public static void InternalLoad()
		{
			config_channel_id = config.Bind<string>("Access", "ChannelId", "", "CHZZK channel ID for chat");
			twitch_handle = config.Bind<string>("Access", "TwitchHandle", "", "Twitch channel handle/login, without @");
			youtube_handle = config.Bind<string>("Access", "YoutubeHandle", "", "YouTube channel handle, channel URL, or live video URL");
			show_platform_prefix = config.Bind<bool>("Display", "ShowPlatformPrefix", true, "Show [CHZZK], [Twitch], or [YouTube] before chat names");
		}
	}
}
namespace System.Runtime.CompilerServices
{
	[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
	internal sealed class IgnoresAccessChecksToAttribute : Attribute
	{
		public IgnoresAccessChecksToAttribute(string assemblyName)
		{
		}
	}
}