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.Bootstrap; 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 UnityEngine.UI; 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+bffca9cfcd2432652bfb4cbc9338f4a83acf5469")] [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); } } [CompilerGenerated] private sealed class d__42 : IEnumerator, IEnumerator, IDisposable { private int <>1__state; private object <>2__current; public Plugin <>4__this; private DateTime 5__1; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__42(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>1__state = -2; } private bool MoveNext() { switch (<>1__state) { default: return false; case 0: <>1__state = -1; 5__1 = DateTime.UtcNow.AddSeconds(5.0); break; case 1: <>1__state = -1; break; } if (((Behaviour)<>4__this).enabled) { if (!<>4__this.coroutinePumpLogged) { <>4__this.coroutinePumpLogged = true; ((BaseUnityPlugin)<>4__this).Logger.LogInfo((object)"Coroutine main thread pump is running."); } ProcessMainThreadWork(); if (DateTime.UtcNow >= 5__1) { ((BaseUnityPlugin)<>4__this).Logger.LogInfo((object)($"Main thread pump alive. queued callbacks: {MainThreadDispatcher.Count}, " + $"pending HUD messages: {PendingMessages.Count}, hudReady: {IsHudReady()}")); 5__1 = DateTime.UtcNow.AddSeconds(5.0); } <>2__current = null; <>1__state = 1; return true; } return false; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } private const string PluginGuid = "asta.lethalcompany.streamchats"; private const string NiceChatGuid = "taffyko.NiceChat"; private const int MaxMainThreadActionsPerFrame = 100; private const int MaxPendingMessages = 30; private const int HudWaitTimeoutSeconds = 120; private const float FallbackChatFontSize = 11f; 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 PendingMessages = new ConcurrentQueue(); 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; private static bool niceChatLoadChecked; private static bool niceChatLoaded; private static bool niceChatStateLogged; private static bool fallbackNiceChatSetupLogged; private static RectTransform? fallbackChatTextRect; private static RectTransform? fallbackChatTextBgRect; private static RectTransform? fallbackChatTextFieldRect; private static RectTransform? fallbackClipContainerRect; private static float fallbackPreviousChatTextHeight; private static int lastProcessedFrame = -1; 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); } } [IteratorStateMachine(typeof(d__42))] private IEnumerator MainThreadPump() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__42(0) { <>4__this = this }; } private static void ProcessMainThreadWork() { if (lastProcessedFrame == Time.frameCount) { return; } lastProcessedFrame = Time.frameCount; 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}"); } } } ApplyFallbackNiceChat(); FlushPendingMessages(); } private void StartChatClients() { StopChatClients("restarting chat clients"); bool flag = !string.IsNullOrWhiteSpace(Config.ChzzkChannelId); 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 TwitchHandle, YoutubeHandle, or ChzzkChannelId in StreamChats.cfg."); } } private void StartChzzkClient() { if (string.IsNullOrWhiteSpace(Config.ChzzkChannelId)) { ((BaseUnityPlugin)this).Logger.LogWarning((object)"ChzzkChannelId 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.ChzzkChannelId); } 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 ApplyFallbackNiceChat() { if (!IsHudReady()) { return; } if (IsNiceChatLoaded()) { if (!niceChatStateLogged) { niceChatStateLogged = true; ManualLogSource? obj = logger; if (obj != null) { obj.LogInfo((object)"NiceChat is loaded; StreamChats fallback chat UI tweaks are disabled."); } } return; } if (!niceChatStateLogged) { niceChatStateLogged = true; ManualLogSource? obj2 = logger; if (obj2 != null) { obj2.LogInfo((object)"NiceChat is not loaded; applying StreamChats fallback chat UI tweaks."); } } if (!((Object)(object)fallbackClipContainerRect == (Object)null) || TrySetupFallbackNiceChat()) { UpdateFallbackNiceChat(); } } private static bool IsNiceChatLoaded() { if (niceChatLoadChecked) { return niceChatLoaded; } niceChatLoadChecked = true; niceChatLoaded = Chainloader.PluginInfos.ContainsKey("taffyko.NiceChat") || AccessTools.TypeByName("NiceChat.Plugin") != null; return niceChatLoaded; } private static bool TrySetupFallbackNiceChat() { //IL_0159: Unknown result type (might be due to invalid IL or missing references) //IL_0173: Unknown result type (might be due to invalid IL or missing references) //IL_018d: Unknown result type (might be due to invalid IL or missing references) //IL_01a7: Unknown result type (might be due to invalid IL or missing references) //IL_01d1: Unknown result type (might be due to invalid IL or missing references) //IL_01d7: Expected O, but got Unknown //IL_01f8: Unknown result type (might be due to invalid IL or missing references) //IL_0208: Unknown result type (might be due to invalid IL or missing references) //IL_0222: Unknown result type (might be due to invalid IL or missing references) //IL_023c: Unknown result type (might be due to invalid IL or missing references) //IL_026e: Unknown result type (might be due to invalid IL or missing references) //IL_0288: Unknown result type (might be due to invalid IL or missing references) //IL_02a2: Unknown result type (might be due to invalid IL or missing references) //IL_02b2: Unknown result type (might be due to invalid IL or missing references) //IL_0306: Unknown result type (might be due to invalid IL or missing references) //IL_030b: Unknown result type (might be due to invalid IL or missing references) HUDManager val = HUDManager.Instance; if ((Object)(object)val == (Object)null || (Object)(object)val.chatText == (Object)null || (Object)(object)val.chatTextField == (Object)null) { return false; } RectTransform val2 = default(RectTransform); if ((Object)(object)fallbackChatTextRect == (Object)null && ((Component)val.chatText).TryGetComponent(ref val2)) { fallbackChatTextRect = val2; } RectTransform val3 = default(RectTransform); if ((Object)(object)fallbackChatTextFieldRect == (Object)null && ((Component)val.chatTextField).TryGetComponent(ref val3)) { fallbackChatTextFieldRect = val3; } if ((Object)(object)fallbackChatTextBgRect == (Object)null) { Transform parent = ((Component)val.chatTextField).transform.parent; if (parent != null) { Transform obj = parent.Find("Image"); RectTransform val4 = default(RectTransform); if (((obj != null) ? new bool?(((Component)obj).TryGetComponent(ref val4)) : null).GetValueOrDefault()) { fallbackChatTextBgRect = val4; } } } if ((Object)(object)fallbackChatTextRect == (Object)null || (Object)(object)fallbackChatTextFieldRect == (Object)null || (Object)(object)fallbackChatTextBgRect == (Object)null) { return false; } ((TMP_Text)val.chatText).fontSize = 11f; fallbackChatTextBgRect.anchorMin = new Vector2(0.35f, 0.5f); fallbackChatTextBgRect.anchorMax = new Vector2(0.8f, 0.5f); fallbackChatTextFieldRect.anchorMin = new Vector2(0.3f, 0.5f); fallbackChatTextFieldRect.anchorMax = new Vector2(0.8f, 0.5f); if ((Object)(object)fallbackClipContainerRect != (Object)null) { return true; } GameObject val5 = new GameObject("StreamChatsFallbackClipContainer"); fallbackClipContainerRect = val5.AddComponent(); ((Transform)fallbackClipContainerRect).SetParent((Transform)(object)fallbackChatTextBgRect, false); fallbackClipContainerRect.anchorMin = Vector2.zero; fallbackClipContainerRect.anchorMax = Vector2.one; fallbackClipContainerRect.offsetMin = new Vector2(0f, 40f); fallbackClipContainerRect.offsetMax = new Vector2(0f, -3f); val5.AddComponent(); ((Transform)fallbackChatTextRect).SetParent((Transform)(object)fallbackClipContainerRect, false); fallbackChatTextRect.anchorMin = new Vector2(0f, 0f); fallbackChatTextRect.anchorMax = new Vector2(1f, 0f); fallbackChatTextRect.pivot = new Vector2(0.5f, 0f); fallbackChatTextRect.anchoredPosition = Vector2.zero; ((TMP_Text)val.chatText).alignment = (TextAlignmentOptions)1025; LayoutElement val6 = default(LayoutElement); if (((Component)val.chatText).gameObject.TryGetComponent(ref val6)) { Object.Destroy((Object)(object)val6); } LayoutElement val7 = ((Component)val.chatText).gameObject.AddComponent(); Rect rect = fallbackClipContainerRect.rect; val7.minHeight = ((Rect)(ref rect)).height; ContentSizeFitter val8 = default(ContentSizeFitter); if (((Component)val.chatText).gameObject.TryGetComponent(ref val8)) { Object.Destroy((Object)(object)val8); } ContentSizeFitter val9 = ((Component)val.chatText).gameObject.AddComponent(); val9.verticalFit = (FitMode)2; val9.horizontalFit = (FitMode)0; if (!fallbackNiceChatSetupLogged) { fallbackNiceChatSetupLogged = true; ManualLogSource? obj2 = logger; if (obj2 != null) { obj2.LogInfo((object)"StreamChats fallback chat UI clipping is ready."); } } return true; } private static void UpdateFallbackNiceChat() { //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Unknown result type (might be due to invalid IL or missing references) //IL_00e3: Unknown result type (might be due to invalid IL or missing references) //IL_00e8: Unknown result type (might be due to invalid IL or missing references) //IL_0106: Unknown result type (might be due to invalid IL or missing references) //IL_0110: Unknown result type (might be due to invalid IL or missing references) //IL_0120: Unknown result type (might be due to invalid IL or missing references) HUDManager val = HUDManager.Instance; if (!((Object)(object)val == (Object)null) && !((Object)(object)val.chatText == (Object)null) && !((Object)(object)val.chatTextField == (Object)null) && !((Object)(object)fallbackChatTextRect == (Object)null) && !((Object)(object)fallbackClipContainerRect == (Object)null)) { if (Math.Abs(((TMP_Text)val.chatText).fontSize - 11f) > 0.01f) { ((TMP_Text)val.chatText).fontSize = 11f; } Rect rect = fallbackChatTextRect.rect; float height = ((Rect)(ref rect)).height; LayoutElement val2 = default(LayoutElement); if (Math.Abs(height - fallbackPreviousChatTextHeight) > 0.01f && ((Component)val.chatText).gameObject.TryGetComponent(ref val2) && (Object)(object)fallbackClipContainerRect != (Object)null) { LayoutElement obj = val2; rect = fallbackClipContainerRect.rect; obj.minHeight = ((Rect)(ref rect)).height; fallbackChatTextRect.sizeDelta = new Vector2(-12f, fallbackChatTextRect.sizeDelta.y); fallbackChatTextRect.anchoredPosition = Vector2.zero; } fallbackPreviousChatTextHeight = height; } } private static void ShowMessageNow(string name, string msg) { string text = (string.IsNullOrWhiteSpace(name) ? "CHZZK" : name); string text2 = NextColor(); string item = "" + EscapeRichText(text) + ": " + EscapeRichText(msg) + ""; 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("&", "&").Replace("<", "<").Replace(">", ">"); } } } namespace ChzzkChat.Utils { internal static class MainThreadDispatcher { private static readonly ConcurrentQueue Actions = new ConcurrentQueue(); 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 JToken? badge; public JToken? title; public bool verifiedMark; public List activityBadges = new List(); 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 weeklyRankList = new List(); 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 sendQueue = new ConcurrentQueue(); 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? OnMessage; public event Action? OnDonation; public event Action? 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("ChzzkChannelId 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("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("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("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 closed = new TaskCompletionSource(); CancellationTokenSource heartbeatCancellation = null; CancellationTokenSource watchdogCancellation = null; CancellationTokenSource senderCancellation = null; WebSocket localSocket = null; try { ClearSendQueue(); localSocket = new WebSocket(text, Array.Empty()) { 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(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(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((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(val["profile"]) ?? Profile.Unknown(); string text = ((JToken)val).Value((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((object)"msgTypeCode"), out var result)) { continue; } Profile profile = ParseJsonPayload(val["profile"]); string message = ((JToken)val).Value((object)"msg") ?? string.Empty; JToken token = val["extra"] ?? val["extras"]; switch (result) { case 10: { DonationExtras donation = ParseJsonPayload(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(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(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((IEnumerable)token) : token.ToString((Formatting)0, Array.Empty())); if (string.IsNullOrWhiteSpace(text) || text == "null") { return null; } try { return JsonConvert.DeserializeObject(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()); } 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? 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 seenMessageIds = new HashSet(); private CancellationTokenSource? cancellation; private Thread? worker; public event Action? 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()), 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((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((object)"continuation"); timeoutMs = item.Value((object)"timeoutMs") ?? timeoutMs; if (!string.IsNullOrWhiteSpace(text)) { return text; } } foreach (JToken item2 in ((JToken)root).SelectTokens("$..invalidationContinuationData")) { string text2 = item2.Value((object)"continuation"); timeoutMs = item2.Value((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((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((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((IEnumerable)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((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 readonly ConfigDefinition legacy_channel_id_definition = new ConfigDefinition("Access", "ChannelId"); private static readonly ConfigDefinition chzzk_channel_id_definition = new ConfigDefinition("Access", "ChzzkChannelId"); private static ConfigFile config = null; private static ConfigEntry config_chzzk_channel_id = null; private static ConfigEntry twitch_handle = null; private static ConfigEntry youtube_handle = null; private static ConfigEntry show_platform_prefix = null; public static string ChzzkChannelId => config_chzzk_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); bool saveOnConfigSet = config.SaveOnConfigSet; config.SaveOnConfigSet = false; try { InternalLoad(); config.Save(); } finally { config.SaveOnConfigSet = saveOnConfigSet; } } public static void InternalLoad() { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Expected O, but got Unknown //IL_00a5: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Expected O, but got Unknown ConfigEntry val = config.Bind(legacy_channel_id_definition, "", new ConfigDescription("Deprecated. Use ChzzkChannelId instead.", (AcceptableValueBase)null, Array.Empty())); twitch_handle = config.Bind("Access", "TwitchHandle", "", "Twitch channel handle/login, without @"); youtube_handle = config.Bind("Access", "YoutubeHandle", "", "YouTube channel handle, channel URL, or live video URL"); show_platform_prefix = config.Bind("Display", "ShowPlatformPrefix", true, "Show [CHZZK], [Twitch], or [YouTube] before chat names"); config_chzzk_channel_id = config.Bind(chzzk_channel_id_definition, "", new ConfigDescription("CHZZK channel ID for chat", (AcceptableValueBase)null, Array.Empty())); if (string.IsNullOrWhiteSpace(config_chzzk_channel_id.Value) && !string.IsNullOrWhiteSpace(val.Value)) { config_chzzk_channel_id.Value = val.Value; } config.Remove(legacy_channel_id_definition); } } } namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] internal sealed class IgnoresAccessChecksToAttribute : Attribute { public IgnoresAccessChecksToAttribute(string assemblyName) { } } }