Decompiled source of RepoLiveChat v1.0.0

plugins/RepoLiveChat.dll

Decompiled a day ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using System.Threading;
using BepInEx;
using BepInEx.Logging;
using ExitGames.Client.Photon;
using Newtonsoft.Json;
using Photon.Pun;
using Photon.Realtime;
using RepoLiveChat;
using RepoLiveChat.Actions;
using RepoLiveChat.Config;
using RepoLiveChat.Core;
using RepoLiveChat.Models;
using RepoLiveChat.Net;
using RepoLiveChat.Platform;
using UnityEngine;
using UnityEngine.SceneManagement;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("RepoLiveChat_New")]
[assembly: AssemblyConfiguration("Debug")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("RepoLiveChat_New")]
[assembly: AssemblyTitle("RepoLiveChat_New")]
[assembly: AssemblyVersion("1.0.0.0")]
public static class RLog
{
	public enum Level
	{
		ERROR,
		WARN,
		INFO,
		DEBUG
	}

	private static Level _level = Level.DEBUG;

	private static string JstPrefix()
	{
		DateTime dateTime = DateTime.UtcNow.AddHours(9.0);
		return $"[{dateTime:yyyy-MM-dd HH:mm:ss.fff} JST] ";
	}

	public static void SetLevel(string levelString)
	{
		switch ((levelString ?? "DEBUG").Trim().ToUpperInvariant())
		{
		case "ERROR":
			_level = Level.ERROR;
			break;
		case "WARN":
		case "WARNING":
			_level = Level.WARN;
			break;
		case "INFO":
			_level = Level.INFO;
			break;
		default:
			_level = Level.DEBUG;
			break;
		}
		ManualLogSource logS = RepoLiveChatPlugin.LogS;
		if (logS != null)
		{
			logS.LogInfo((object)(JstPrefix() + "[RLog] Level = " + _level));
		}
	}

	public static string GetLevelString()
	{
		return _level.ToString();
	}

	public static void Error(string msg)
	{
		if (_level >= Level.ERROR)
		{
			ManualLogSource logS = RepoLiveChatPlugin.LogS;
			if (logS != null)
			{
				logS.LogError((object)(JstPrefix() + msg));
			}
		}
	}

	public static void Warn(string msg)
	{
		if (_level >= Level.WARN)
		{
			ManualLogSource logS = RepoLiveChatPlugin.LogS;
			if (logS != null)
			{
				logS.LogWarning((object)(JstPrefix() + msg));
			}
		}
	}

	public static void Info(string msg)
	{
		if (_level >= Level.INFO)
		{
			ManualLogSource logS = RepoLiveChatPlugin.LogS;
			if (logS != null)
			{
				logS.LogInfo((object)(JstPrefix() + msg));
			}
		}
	}

	public static void Debug(string msg)
	{
		if (_level >= Level.DEBUG)
		{
			ManualLogSource logS = RepoLiveChatPlugin.LogS;
			if (logS != null)
			{
				logS.LogInfo((object)(JstPrefix() + "[DEBUG] " + msg));
			}
		}
	}

	public static void Error(string head, Exception ex)
	{
		Error(head + " :: " + ex);
	}

	public static void Warn(string head, Exception ex)
	{
		Warn(head + " :: " + ex);
	}
}
internal static class ExternalMods
{
	private static bool _installed;

	private static string _pluginsDir;

	public static void Init()
	{
		if (_installed)
		{
			return;
		}
		try
		{
			_pluginsDir = Paths.PluginPath;
			AppDomain.CurrentDomain.AssemblyResolve -= OnResolve;
			AppDomain.CurrentDomain.AssemblyResolve += OnResolve;
			LoadIfExists("REPOLib.dll");
			LoadIfExists("ItemSpawner.dll");
			LoadIfExists("EnemySpawning.dll");
			_installed = true;
		}
		catch (Exception ex)
		{
			RLog.Warn("[Ext] init failed: " + ex.Message);
		}
	}

	private static Assembly OnResolve(object sender, ResolveEventArgs args)
	{
		try
		{
			string text = new AssemblyName(args.Name).Name + ".dll";
			string text2 = Path.Combine(_pluginsDir, text);
			if (File.Exists(text2))
			{
				Assembly result = Assembly.LoadFrom(text2);
				RLog.Info("[Ext] resolve: " + text + " -> OK");
				return result;
			}
		}
		catch (Exception ex)
		{
			RLog.Warn("[Ext] resolve failed: " + ex.Message);
		}
		return null;
	}

	private static void LoadIfExists(string file)
	{
		try
		{
			string text = Path.Combine(_pluginsDir, file);
			if (File.Exists(text))
			{
				string shortName = Path.GetFileNameWithoutExtension(file);
				if (!AppDomain.CurrentDomain.GetAssemblies().Any(delegate(Assembly a)
				{
					try
					{
						return string.Equals(a.GetName().Name, shortName, StringComparison.OrdinalIgnoreCase);
					}
					catch
					{
						return false;
					}
				}))
				{
					Assembly assembly = Assembly.LoadFrom(text);
					AssemblyName name = assembly.GetName();
					RLog.Info($"[Ext] loaded: {file} / {name.Name}, Version={name.Version}");
				}
			}
		}
		catch (Exception ex)
		{
			RLog.Warn("[Ext] load '" + file + "' failed: " + ex.Message);
		}
	}
}
namespace RepoLiveChat
{
	public static class MiniJSON
	{
		private sealed class Parser : IDisposable
		{
			private enum TOKEN
			{
				NONE,
				CURLY_OPEN,
				CURLY_CLOSE,
				SQUARE_OPEN,
				SQUARE_CLOSE,
				COLON,
				COMMA,
				STRING,
				NUMBER,
				TRUE,
				FALSE,
				NULL
			}

			private const string WORD_BREAK = "{}[],:\"";

			private StringReader json;

			private char PeekChar => Convert.ToChar(json.Peek());

			private char NextChar => Convert.ToChar(json.Read());

			private string NextWord
			{
				get
				{
					StringBuilder stringBuilder = new StringBuilder();
					while (json.Peek() != -1 && "{}[],:\"".IndexOf(PeekChar) == -1 && !char.IsWhiteSpace(PeekChar))
					{
						stringBuilder.Append(NextChar);
					}
					return stringBuilder.ToString();
				}
			}

			private TOKEN NextToken
			{
				get
				{
					EatWhitespace();
					if (json.Peek() == -1)
					{
						return TOKEN.NONE;
					}
					switch (PeekChar)
					{
					case '{':
						return TOKEN.CURLY_OPEN;
					case '}':
						return TOKEN.CURLY_CLOSE;
					case '[':
						return TOKEN.SQUARE_OPEN;
					case ']':
						return TOKEN.SQUARE_CLOSE;
					case ',':
						return TOKEN.COMMA;
					case '"':
						return TOKEN.STRING;
					case ':':
						return TOKEN.COLON;
					case '-':
					case '0':
					case '1':
					case '2':
					case '3':
					case '4':
					case '5':
					case '6':
					case '7':
					case '8':
					case '9':
						return TOKEN.NUMBER;
					default:
						return NextWord switch
						{
							"false" => TOKEN.FALSE, 
							"true" => TOKEN.TRUE, 
							"null" => TOKEN.NULL, 
							_ => TOKEN.NONE, 
						};
					}
				}
			}

			private Parser(string jsonString)
			{
				json = new StringReader(jsonString);
			}

			public static object Parse(string jsonString)
			{
				using Parser parser = new Parser(jsonString);
				return parser.ParseValue();
			}

			public void Dispose()
			{
				json.Dispose();
			}

			private object ParseValue()
			{
				return NextToken switch
				{
					TOKEN.STRING => ParseString(), 
					TOKEN.NUMBER => ParseNumber(), 
					TOKEN.CURLY_OPEN => ParseObject(), 
					TOKEN.SQUARE_OPEN => ParseArray(), 
					TOKEN.TRUE => true, 
					TOKEN.FALSE => false, 
					TOKEN.NULL => null, 
					_ => null, 
				};
			}

			private Dictionary<string, object> ParseObject()
			{
				Dictionary<string, object> dictionary = new Dictionary<string, object>();
				json.Read();
				while (true)
				{
					switch (NextToken)
					{
					case TOKEN.NONE:
						return null;
					case TOKEN.CURLY_CLOSE:
						json.Read();
						return dictionary;
					}
					string key = ParseString();
					if (NextToken != TOKEN.COLON)
					{
						return null;
					}
					json.Read();
					dictionary[key] = ParseValue();
					switch (NextToken)
					{
					case TOKEN.COMMA:
						json.Read();
						break;
					case TOKEN.CURLY_CLOSE:
						json.Read();
						return dictionary;
					}
				}
			}

			private List<object> ParseArray()
			{
				List<object> list = new List<object>();
				json.Read();
				while (true)
				{
					switch (NextToken)
					{
					case TOKEN.NONE:
						return null;
					case TOKEN.SQUARE_CLOSE:
						json.Read();
						break;
					default:
						list.Add(ParseValue());
						switch (NextToken)
						{
						case TOKEN.COMMA:
							json.Read();
							continue;
						case TOKEN.SQUARE_CLOSE:
							break;
						default:
							continue;
						}
						json.Read();
						break;
					}
					break;
				}
				return list;
			}

			private string ParseString()
			{
				StringBuilder stringBuilder = new StringBuilder();
				json.Read();
				while (json.Peek() != -1)
				{
					char nextChar = NextChar;
					switch (nextChar)
					{
					case '\\':
						if (json.Peek() == -1)
						{
							break;
						}
						switch (NextChar)
						{
						case '"':
							stringBuilder.Append('"');
							break;
						case '\\':
							stringBuilder.Append('\\');
							break;
						case '/':
							stringBuilder.Append('/');
							break;
						case 'b':
							stringBuilder.Append('\b');
							break;
						case 'f':
							stringBuilder.Append('\f');
							break;
						case 'n':
							stringBuilder.Append('\n');
							break;
						case 'r':
							stringBuilder.Append('\r');
							break;
						case 't':
							stringBuilder.Append('\t');
							break;
						case 'u':
						{
							char[] array = new char[4];
							for (int i = 0; i < 4; i++)
							{
								array[i] = NextChar;
							}
							stringBuilder.Append((char)Convert.ToInt32(new string(array), 16));
							break;
						}
						}
						continue;
					default:
						stringBuilder.Append(nextChar);
						continue;
					case '"':
						break;
					}
					break;
				}
				return stringBuilder.ToString();
			}

			private object ParseNumber()
			{
				string nextWord = NextWord;
				if (nextWord.IndexOf('.') != -1 && double.TryParse(nextWord, NumberStyles.Float, CultureInfo.InvariantCulture, out var result))
				{
					return result;
				}
				if (long.TryParse(nextWord, out var result2))
				{
					return result2;
				}
				return 0;
			}

			private void EatWhitespace()
			{
				while (char.IsWhiteSpace(PeekChar))
				{
					json.Read();
				}
			}
		}

		private sealed class Serializer
		{
			private StringBuilder sb = new StringBuilder();

			public static string Serialize(object obj)
			{
				Serializer serializer = new Serializer();
				serializer.SerializeValue(obj);
				return serializer.sb.ToString();
			}

			private void SerializeValue(object value)
			{
				if (value == null)
				{
					sb.Append("null");
				}
				else if (value is string str)
				{
					SerializeString(str);
				}
				else if (value is bool flag)
				{
					sb.Append(flag ? "true" : "false");
				}
				else if (value is IDictionary obj)
				{
					SerializeObject(obj);
				}
				else if (value is IEnumerable array)
				{
					SerializeArray(array);
				}
				else if (value is double || value is float || value is long || value is int)
				{
					sb.Append(Convert.ToString(value, CultureInfo.InvariantCulture));
				}
				else
				{
					SerializeString(value.ToString());
				}
			}

			private void SerializeObject(IDictionary obj)
			{
				bool flag = true;
				sb.Append('{');
				foreach (DictionaryEntry item in obj)
				{
					if (!flag)
					{
						sb.Append(',');
					}
					flag = false;
					SerializeString(item.Key.ToString());
					sb.Append(':');
					SerializeValue(item.Value);
				}
				sb.Append('}');
			}

			private void SerializeArray(IEnumerable array)
			{
				bool flag = true;
				sb.Append('[');
				foreach (object item in array)
				{
					if (!flag)
					{
						sb.Append(',');
					}
					flag = false;
					SerializeValue(item);
				}
				sb.Append(']');
			}

			private void SerializeString(string str)
			{
				sb.Append('"');
				foreach (char c in str)
				{
					switch (c)
					{
					case '"':
						sb.Append("\\\"");
						continue;
					case '\\':
						sb.Append("\\\\");
						continue;
					case '\b':
						sb.Append("\\b");
						continue;
					case '\f':
						sb.Append("\\f");
						continue;
					case '\n':
						sb.Append("\\n");
						continue;
					case '\r':
						sb.Append("\\r");
						continue;
					case '\t':
						sb.Append("\\t");
						continue;
					}
					if (c < ' ' || c > '~')
					{
						StringBuilder stringBuilder = sb;
						int num = c;
						stringBuilder.Append("\\u" + num.ToString("x4"));
					}
					else
					{
						sb.Append(c);
					}
				}
				sb.Append('"');
			}
		}

		public static object Deserialize(string json)
		{
			if (json == null)
			{
				return null;
			}
			return Parser.Parse(json);
		}

		public static string Serialize(object obj)
		{
			return Serializer.Serialize(obj);
		}
	}
	public class PhotonCallbackHandler : MonoBehaviourPunCallbacks, IOnEventCallback
	{
		public const byte kModEventCode = 42;

		public override void OnEnable()
		{
			((MonoBehaviourPunCallbacks)this).OnEnable();
			RLog.Info("[PhotonCallbackHandler] OnEnable: Callbacks registered.");
			PhotonNetwork.AddCallbackTarget((object)this);
		}

		public override void OnDisable()
		{
			((MonoBehaviourPunCallbacks)this).OnDisable();
			RLog.Info("[PhotonCallbackHandler] OnDisable: Callbacks unregistered.");
			PhotonNetwork.RemoveCallbackTarget((object)this);
		}

		public void OnEvent(EventData photonEvent)
		{
			if (photonEvent.Code != 42)
			{
				return;
			}
			RLog.Debug($"[PhotonCallbackHandler] OnEvent received (Code={photonEvent.Code}). IsMasterClient={PhotonNetwork.IsMasterClient}");
			if (!PhotonNetwork.IsMasterClient)
			{
				RLog.Debug("[PhotonCallbackHandler] Event ignored (not MasterClient).");
				return;
			}
			try
			{
				int sender = photonEvent.Sender;
				RLog.Debug($"[PhotonCallbackHandler][Host] Event received from ActorNumber: {sender}");
				object customData = photonEvent.CustomData;
				if (customData is string text)
				{
					RLog.Info($"[PhotonCallbackHandler][Host] Event OK from SenderID: {sender}. Processing...");
					Dictionary<string, object> dictionary = null;
					try
					{
						dictionary = JsonConvert.DeserializeObject<Dictionary<string, object>>(text);
						if (dictionary != null)
						{
							dictionary["senderActorNumber"] = sender;
							string json = JsonConvert.SerializeObject((object)dictionary);
							PhotonRpcBridge.LocalExecute_Static(json, $"event_from_{sender}");
						}
						else
						{
							RLog.Warn("[PhotonCallbackHandler][Host] Event ignored (JSON deserialization failed). JSON: " + text);
						}
						return;
					}
					catch (Exception ex)
					{
						RLog.Error("[PhotonCallbackHandler][Host] JSON processing error: " + ex.Message + ". JSON: " + text);
						UIOverlay.TryToast("ホスト実行エラー(JSON)");
						return;
					}
				}
				RLog.Warn("[PhotonCallbackHandler][Host] Event ignored (CustomData is not string). Type=" + (customData?.GetType().Name ?? "null"));
			}
			catch (Exception ex2)
			{
				RLog.Error("[PhotonCallbackHandler][Host] Exception in OnEvent :: " + ex2);
				UIOverlay.TryToast("ホスト実行エラー(Event)");
			}
		}

		public override void OnJoinedRoom()
		{
			((MonoBehaviourPunCallbacks)this).OnJoinedRoom();
			RLog.Info("[PhotonCallbackHandler] OnJoinedRoom received.");
		}

		public override void OnLeftRoom()
		{
			((MonoBehaviourPunCallbacks)this).OnLeftRoom();
			RLog.Info("[PhotonCallbackHandler] OnLeftRoom received.");
		}

		public override void OnPlayerEnteredRoom(Player newPlayer)
		{
			((MonoBehaviourPunCallbacks)this).OnPlayerEnteredRoom(newPlayer);
			RLog.Debug(string.Format("[PhotonCallbackHandler] OnPlayerEnteredRoom: {0} (ActorNr: {1})", ((newPlayer != null) ? newPlayer.NickName : null) ?? "NullPlayer", (newPlayer != null) ? newPlayer.ActorNumber : (-1)));
		}

		public override void OnPlayerLeftRoom(Player otherPlayer)
		{
			((MonoBehaviourPunCallbacks)this).OnPlayerLeftRoom(otherPlayer);
			RLog.Debug(string.Format("[PhotonCallbackHandler] OnPlayerLeftRoom: {0} (ActorNr: {1})", ((otherPlayer != null) ? otherPlayer.NickName : null) ?? "NullPlayer", (otherPlayer != null) ? otherPlayer.ActorNumber : (-1)));
		}

		public override void OnRoomPropertiesUpdate(Hashtable propertiesThatChanged)
		{
			((MonoBehaviourPunCallbacks)this).OnRoomPropertiesUpdate(propertiesThatChanged);
			RLog.Debug("[PhotonCallbackHandler] OnRoomPropertiesUpdate called.");
		}

		public override void OnPlayerPropertiesUpdate(Player targetPlayer, Hashtable changedProps)
		{
			((MonoBehaviourPunCallbacks)this).OnPlayerPropertiesUpdate(targetPlayer, changedProps);
			RLog.Debug(string.Format("[PhotonCallbackHandler] OnPlayerPropertiesUpdate for {0} (ActorNr: {1}).", ((targetPlayer != null) ? targetPlayer.NickName : null) ?? "NullPlayer", (targetPlayer != null) ? targetPlayer.ActorNumber : (-1)));
		}

		public override void OnMasterClientSwitched(Player newMasterClient)
		{
			((MonoBehaviourPunCallbacks)this).OnMasterClientSwitched(newMasterClient);
			RLog.Info(string.Format("[PhotonCallbackHandler] OnMasterClientSwitched. New Master: {0} (ActorNr: {1})", ((newMasterClient != null) ? newMasterClient.NickName : null) ?? "None", (newMasterClient != null) ? newMasterClient.ActorNumber : (-1)));
			RLog.Info("[PhotonCallbackHandler] MasterClient switched. RoleService needs manual update via UI button if needed.");
			UIOverlay.TryToast("ホストが切り替わりました。", 3f);
		}
	}
	[BepInPlugin("com.nacho.repo.livechat", "RepoLiveChat", "1.6.0")]
	public sealed class RepoLiveChatPlugin : BaseUnityPlugin
	{
		public static ManualLogSource LogS;

		private static GameObject _photonRpcBridgeGO;

		private static GameObject _photonCallbackHandlerGO;

		private bool _initialized = false;

		public static RepoLiveChatPlugin Instance { get; private set; }

		public static string ConfigDir { get; private set; } = "";

		public static TableIndex Tables { get; private set; }

		public static RulesConfig Rules { get; private set; }

		public static ControlConfig Control { get; private set; }

		public static PlatformConfig PlatformCfg { get; private set; }

		public static ChatRouter Router { get; private set; }

		private void Awake()
		{
			if ((Object)(object)Instance != (Object)null && (Object)(object)Instance != (Object)(object)this)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)"[RepoLiveChat] Duplicate instance of RepoLiveChatPlugin detected. Destroying self.");
				Object.Destroy((Object)(object)this);
				return;
			}
			Instance = this;
			LogS = ((BaseUnityPlugin)this).Logger;
			((BaseUnityPlugin)this).Logger.LogInfo((object)"[RepoLiveChat] Plugin Awake starting...");
			try
			{
				ExternalMods.Init();
				string configPath = Paths.ConfigPath;
				ConfigDir = Path.Combine(configPath, "repo-livechat");
				Directory.CreateDirectory(ConfigDir);
				LogS.LogInfo((object)("[RepoLiveChat] ConfigDir = " + ConfigDir));
				LoadConfigsAndTables();
				Router = new ChatRouter(Rules, Tables, Control, PlatformCfg, ConfigDir);
				if (PlatformConnector.I == null)
				{
					PlatformConnector.I = new PlatformConnector(Rules, Tables, Control, PlatformCfg, ConfigDir);
					PlatformConnector.I.Configure(PlatformCfg);
					PlatformConnector.I.ConfigureControl(Control);
					LogS.LogInfo((object)"[RepoLiveChat] PlatformConnector (Logic) initialized");
				}
				SceneManager.sceneLoaded += OnSceneLoaded;
				LogS.LogInfo((object)"[RepoLiveChat] Bootstrap (Awake) OK. Waiting for scene load to initialize components.");
			}
			catch (Exception ex)
			{
				LogS.LogError((object)("[RepoLiveChat] Bootstrap failed during Awake: " + ex));
			}
		}

		private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
		{
			if (_initialized)
			{
				return;
			}
			if (((Scene)(ref scene)).name == "Level - Lobby Menu" || ((Scene)(ref scene)).name == "MainMenu" || ((Scene)(ref scene)).name == "Main")
			{
				LogS.LogInfo((object)("[RepoLiveChat] Safe scene '" + ((Scene)(ref scene)).name + "' loaded. Initializing components..."));
				_initialized = true;
				try
				{
					EnsurePhotonRpcBridge();
					EnsurePhotonCallbackHandler();
					UIOverlay.Bootstrap(Tables, Rules, PlatformCfg, Control, ConfigDir);
					LogS.LogInfo((object)"[RepoLiveChat] All components initialized successfully.");
				}
				catch (Exception ex)
				{
					LogS.LogError((object)("[RepoLiveChat] Failed to initialize components on scene load: " + ex));
				}
				SceneManager.sceneLoaded -= OnSceneLoaded;
			}
			else
			{
				LogS.LogInfo((object)("[RepoLiveChat] Scene loaded: " + ((Scene)(ref scene)).name + " (Waiting for main menu/lobby)."));
			}
		}

		private static void EnsurePhotonRpcBridge()
		{
			//IL_0054: Unknown result type (might be due to invalid IL or missing references)
			//IL_005e: Expected O, but got Unknown
			if ((Object)(object)_photonRpcBridgeGO != (Object)null)
			{
				return;
			}
			PhotonRpcBridge photonRpcBridge = Object.FindObjectOfType<PhotonRpcBridge>();
			if ((Object)(object)photonRpcBridge != (Object)null)
			{
				_photonRpcBridgeGO = ((Component)photonRpcBridge).gameObject;
				RLog.Info("[RepoLiveChat] Found existing PhotonRpcBridge instance.");
				Object.DontDestroyOnLoad((Object)(object)_photonRpcBridgeGO);
				return;
			}
			try
			{
				_photonRpcBridgeGO = new GameObject("__RepoLiveChat_PhotonRpcBridge__");
				Object.DontDestroyOnLoad((Object)(object)_photonRpcBridgeGO);
				((Object)_photonRpcBridgeGO).hideFlags = (HideFlags)61;
				_photonRpcBridgeGO.AddComponent<PhotonRpcBridge>();
				LogS.LogInfo((object)"[RepoLiveChat] PhotonRpcBridge (Queue Manager) GameObject created.");
			}
			catch (Exception arg)
			{
				LogS.LogError((object)$"[RepoLiveChat] Failed to create PhotonRpcBridge: {arg}");
				if ((Object)(object)_photonRpcBridgeGO != (Object)null)
				{
					Object.Destroy((Object)(object)_photonRpcBridgeGO);
				}
				_photonRpcBridgeGO = null;
			}
		}

		private void EnsurePhotonCallbackHandler()
		{
			//IL_0054: Unknown result type (might be due to invalid IL or missing references)
			//IL_005e: Expected O, but got Unknown
			if ((Object)(object)_photonCallbackHandlerGO != (Object)null)
			{
				return;
			}
			PhotonCallbackHandler photonCallbackHandler = Object.FindObjectOfType<PhotonCallbackHandler>();
			if ((Object)(object)photonCallbackHandler != (Object)null)
			{
				_photonCallbackHandlerGO = ((Component)photonCallbackHandler).gameObject;
				RLog.Info("[RepoLiveChat] Found existing PhotonCallbackHandler instance.");
				Object.DontDestroyOnLoad((Object)(object)_photonCallbackHandlerGO);
				return;
			}
			try
			{
				_photonCallbackHandlerGO = new GameObject("__RepoLiveChat_PhotonCallbackHandler__");
				Object.DontDestroyOnLoad((Object)(object)_photonCallbackHandlerGO);
				((Object)_photonCallbackHandlerGO).hideFlags = (HideFlags)61;
				_photonCallbackHandlerGO.AddComponent<PhotonCallbackHandler>();
				LogS.LogInfo((object)"[RepoLiveChat] PhotonCallbackHandler GameObject created.");
			}
			catch (Exception arg)
			{
				LogS.LogError((object)$"[RepoLiveChat] Failed to create PhotonCallbackHandler: {arg}");
				if ((Object)(object)_photonCallbackHandlerGO != (Object)null)
				{
					Object.Destroy((Object)(object)_photonCallbackHandlerGO);
				}
				_photonCallbackHandlerGO = null;
			}
		}

		private static void LoadConfigsAndTables()
		{
			ManualLogSource val = LogS ?? Logger.CreateLogSource("RepoLiveChat_Temp");
			try
			{
				Rules = ConfigFiles.LoadRules(ConfigDir);
				Control = ConfigFiles.LoadControl(ConfigDir);
				PlatformCfg = ConfigFiles.LoadPlatform(ConfigDir);
				RLog.SetLevel(Control?.logLevel);
				Tables = TableIndex.LoadFromDisk(ConfigDir, log: true);
				Router?.SetRules(Rules);
				Router?.SetTables(Tables);
				PlatformConnector.I?.Configure(PlatformCfg);
				PlatformConnector.I?.ConfigureControl(Control);
				val.LogInfo((object)"[RepoLiveChat] Configs and tables loaded/reloaded.");
			}
			catch (Exception arg)
			{
				val.LogError((object)$"[RepoLiveChat] Error loading configs/tables: {arg}");
			}
		}

		public static void ReloadAll()
		{
			ManualLogSource val = LogS ?? Logger.CreateLogSource("RepoLiveChat_Temp");
			try
			{
				ExternalMods.Init();
				LoadConfigsAndTables();
				val.LogInfo((object)"[RepoLiveChat] ReloadAll executed.");
				if ((Object)(object)UIOverlay.Instance != (Object)null)
				{
					UIOverlay.Instance.Tables = Tables ?? new TableIndex();
					UIOverlay.Instance.CurrentRules = Rules ?? new RulesConfig
					{
						rules = new List<RulesConfig.Rule>()
					};
					UIOverlay.Instance.CurrentPlat = PlatformCfg ?? new PlatformConfig();
					UIOverlay.Instance.CurrentCtrl = Control ?? new ControlConfig();
					UIOverlay.Instance.ConfigDir = ConfigDir ?? "";
					UIOverlay.Instance.ResetAuthFields(PlatformCfg);
					UIOverlay.Instance.RefreshCandidates();
				}
				UIOverlay.TryToast("設定とテーブルを再読み込みしました", 2f);
			}
			catch (Exception ex)
			{
				val.LogError((object)("[RepoLiveChat] ReloadAll failed: " + ex));
				UIOverlay.TryToast("再読み込みエラー", 2.5f);
			}
		}
	}
	[DefaultExecutionOrder(int.MaxValue)]
	public sealed class UIOverlay : MonoBehaviour
	{
		private struct Toast
		{
			public string text;

			public float start;

			public float until;
		}

		public static UIOverlay Instance;

		public GameObject RootGO;

		public TableIndex Tables = new TableIndex();

		public RulesConfig CurrentRules = new RulesConfig
		{
			rules = new List<RulesConfig.Rule>()
		};

		public PlatformConfig CurrentPlat = new PlatformConfig();

		public ControlConfig CurrentCtrl = new ControlConfig();

		public string ConfigDir = "";

		private bool _open = false;

		private Rect _btnRect;

		private Vector2 _scroll;

		private int _prevW = -1;

		private int _prevH = -1;

		private GUIStyle _btnStyle;

		private GUIStyle _hdrStyle;

		private GUIStyle _labStyle;

		private int _activeTab = 0;

		private readonly string[] _tabs = new string[3] { "ルール", "配信連携", "状態・制御" };

		private string _tmpMatch = "";

		private int _tmpTypeIdx = 0;

		private int _tmpCD = 10;

		private bool _tmpEnabled = true;

		private int _editIndex = -1;

		private string _tmpSelectedName = "";

		private string[] _itemNames = Array.Empty<string>();

		private string[] _enemyNames = Array.Empty<string>();

		private bool _popupOpen = false;

		private Rect _popupRect;

		private GUIStyle _popupBtnStyle;

		private GUIStyle _popupHdrStyle;

		private int _popupPage = 0;

		private const int POPUP_PAGE_SIZE = 10;

		private string[] _authMsg = new string[3] { "", "", "" };

		private string[] _twClientId = new string[3] { "", "", "" };

		private string[] _twVerifyUrl = new string[3] { "", "", "" };

		private string[] _twUserCode = new string[3] { "", "", "" };

		private string[] _ytClientId = new string[3] { "", "", "" };

		private string[] _ytClientSecret = new string[3] { "", "", "" };

		private string[] _ytVerifyUrl = new string[3] { "", "", "" };

		private string[] _ytUserCode = new string[3] { "", "", "" };

		private readonly List<Toast> _toasts = new List<Toast>();

		private static readonly object _toastLock = new object();

		private GUIStyle _toastStyle;

		private Texture2D _iconTexture;

		private string _iconPath = "";

		private bool _iconLoadAttempted = false;

		public static void TryToast(string message, float seconds = 5f)
		{
			if (string.IsNullOrEmpty(message))
			{
				return;
			}
			UIOverlay instance = Instance;
			if ((Object)(object)instance == (Object)null)
			{
				return;
			}
			lock (_toastLock)
			{
				instance._toasts.Add(new Toast
				{
					text = message,
					start = Time.realtimeSinceStartup,
					until = Time.realtimeSinceStartup + Mathf.Max(0.5f, seconds)
				});
			}
		}

		public static void Bootstrap(TableIndex tables, RulesConfig rules, PlatformConfig plat, ControlConfig ctrl, string cfgDir)
		{
			//IL_00bb: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c1: Expected O, but got Unknown
			if ((Object)(object)Instance != (Object)null)
			{
				Instance.Tables = tables ?? new TableIndex();
				Instance.CurrentRules = rules ?? new RulesConfig
				{
					rules = new List<RulesConfig.Rule>()
				};
				Instance.CurrentPlat = plat ?? new PlatformConfig();
				Instance.CurrentCtrl = ctrl ?? new ControlConfig();
				Instance.ConfigDir = cfgDir ?? "";
				Instance._iconPath = Path.Combine(cfgDir, "icon.png");
				Instance.RefreshCandidates();
				Instance.ResetAuthFields(plat);
				return;
			}
			GameObject val = new GameObject("RepoLiveChat.UI");
			((Object)val).hideFlags = (HideFlags)61;
			Object.DontDestroyOnLoad((Object)(object)val);
			UIOverlay uIOverlay = val.AddComponent<UIOverlay>();
			uIOverlay.RootGO = val;
			uIOverlay.Tables = tables ?? new TableIndex();
			uIOverlay.CurrentRules = rules ?? new RulesConfig
			{
				rules = new List<RulesConfig.Rule>()
			};
			uIOverlay.CurrentPlat = plat ?? new PlatformConfig();
			uIOverlay.CurrentCtrl = ctrl ?? new ControlConfig();
			uIOverlay.ConfigDir = cfgDir ?? "";
			Instance = uIOverlay;
			uIOverlay._iconPath = Path.Combine(cfgDir, "icon.png");
			uIOverlay.ResetAuthFields(plat);
			uIOverlay.RefreshCandidates();
			ManualLogSource logS = RepoLiveChatPlugin.LogS;
			if (logS != null)
			{
				logS.LogInfo((object)"[UI] IMGUI overlay (always-on) built.");
			}
		}

		public void ResetAuthFields(PlatformConfig plat)
		{
			plat = plat ?? new PlatformConfig();
			ResetAuthFieldForSlot(1, plat.Slot1);
			ResetAuthFieldForSlot(2, plat.Slot2);
		}

		private void ResetAuthFieldForSlot(int slotIndex, ConnectionSlot slot)
		{
			if (slot == null)
			{
				slot = new ConnectionSlot();
			}
			_twClientId[slotIndex] = (string.IsNullOrEmpty(slot.twClientId) ? "" : "********");
			_ytClientId[slotIndex] = (string.IsNullOrEmpty(slot.ytClientId) ? "" : "********");
			_ytClientSecret[slotIndex] = (string.IsNullOrEmpty(slot.ytClientSecret) ? "" : "********");
		}

		private void Awake()
		{
			if ((Object)(object)Instance != (Object)null && (Object)(object)Instance != (Object)(object)this)
			{
				Object.Destroy((Object)(object)((Component)this).gameObject);
				return;
			}
			Instance = this;
			((Object)((Component)this).gameObject).hideFlags = (HideFlags)61;
			Object.DontDestroyOnLoad((Object)(object)((Component)this).gameObject);
			if ((Object)(object)Instance != (Object)null && !string.IsNullOrEmpty(Instance.ConfigDir) && string.IsNullOrEmpty(_iconPath))
			{
				_iconPath = Path.Combine(Instance.ConfigDir, "icon.png");
			}
		}

		private void Update()
		{
			try
			{
				if (RoleService.Current == RoleService.Role.Host || RoleService.Current == RoleService.Role.Single)
				{
					PlatformConnector.I?.TickTimer(Time.deltaTime);
				}
			}
			catch (Exception ex)
			{
				RLog.Error("[UI] Update/TickTimer exception: " + ex.Message);
			}
		}

		public void RefreshCandidates()
		{
			_itemNames = ((Tables?.Items != null) ? Tables.Items.FindAll((ItemRow x) => x.enabled).ConvertAll((ItemRow x) => x.nameJa ?? x.id).ToArray() : Array.Empty<string>());
			_enemyNames = ((Tables?.Enemies != null) ? Tables.Enemies.FindAll((EnemyRow x) => x.enabled).ConvertAll((EnemyRow x) => x.nameJa ?? x.id).ToArray() : Array.Empty<string>());
			if (!IsCurrentNameInCandidates(_tmpSelectedName))
			{
				_tmpSelectedName = "";
			}
		}

		private bool IsCurrentNameInCandidates(string name)
		{
			if (string.IsNullOrEmpty(name))
			{
				return false;
			}
			string[] itemNames = _itemNames;
			foreach (string text in itemNames)
			{
				if (text == name)
				{
					return true;
				}
			}
			string[] enemyNames = _enemyNames;
			foreach (string text2 in enemyNames)
			{
				if (text2 == name)
				{
					return true;
				}
			}
			return false;
		}

		private void InitIfNeeded()
		{
			//IL_005f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0064: Unknown result type (might be due to invalid IL or missing references)
			//IL_0082: Unknown result type (might be due to invalid IL or missing references)
			//IL_0087: Unknown result type (might be due to invalid IL or missing references)
			//IL_0095: Expected O, but got Unknown
			//IL_00ad: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bb: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c8: Expected O, but got Unknown
			//IL_00e0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e5: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f3: Expected O, but got Unknown
			//IL_010d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0112: Unknown result type (might be due to invalid IL or missing references)
			//IL_011b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0123: Unknown result type (might be due to invalid IL or missing references)
			//IL_0134: Expected O, but got Unknown
			//IL_014e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0153: Unknown result type (might be due to invalid IL or missing references)
			//IL_015c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0169: Expected O, but got Unknown
			//IL_0184: Unknown result type (might be due to invalid IL or missing references)
			//IL_0189: Unknown result type (might be due to invalid IL or missing references)
			//IL_0192: Unknown result type (might be due to invalid IL or missing references)
			//IL_019a: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a1: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ab: Expected O, but got Unknown
			//IL_01b1: Expected O, but got Unknown
			//IL_020d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0217: Expected O, but got Unknown
			if (_prevW != Screen.width || _prevH != Screen.height)
			{
				_prevW = Screen.width;
				_prevH = Screen.height;
				_btnRect = new Rect((float)Screen.width - 230f, (float)Screen.height - 54f, 220f, 44f);
			}
			if (_btnStyle == null)
			{
				_btnStyle = new GUIStyle(GUI.skin.button)
				{
					fontSize = 16
				};
			}
			if (_hdrStyle == null)
			{
				_hdrStyle = new GUIStyle(GUI.skin.label)
				{
					fontSize = 20,
					fontStyle = (FontStyle)1
				};
			}
			if (_labStyle == null)
			{
				_labStyle = new GUIStyle(GUI.skin.label)
				{
					fontSize = 14
				};
			}
			if (_popupBtnStyle == null)
			{
				_popupBtnStyle = new GUIStyle(GUI.skin.button)
				{
					fontSize = 15,
					alignment = (TextAnchor)3,
					fixedHeight = 28f
				};
			}
			if (_popupHdrStyle == null)
			{
				_popupHdrStyle = new GUIStyle(GUI.skin.label)
				{
					fontSize = 18,
					fontStyle = (FontStyle)1
				};
			}
			if (_toastStyle == null)
			{
				_toastStyle = new GUIStyle(GUI.skin.box)
				{
					fontSize = 14,
					alignment = (TextAnchor)3,
					padding = new RectOffset(10, 10, 6, 6)
				};
			}
			if (!((Object)(object)_iconTexture == (Object)null) || _iconLoadAttempted || string.IsNullOrEmpty(_iconPath))
			{
				return;
			}
			_iconLoadAttempted = true;
			try
			{
				if (File.Exists(_iconPath))
				{
					byte[] array = File.ReadAllBytes(_iconPath);
					_iconTexture = new Texture2D(2, 2);
					if (ImageConversion.LoadImage(_iconTexture, array))
					{
						LogInfo("Icon loaded successfully from: " + _iconPath);
						return;
					}
					LogErr("Icon failed to load (LoadImage returned false).");
					_iconTexture = null;
				}
				else
				{
					LogErr("Icon file not found at: " + _iconPath + " (Must be named 'icon.png')");
				}
			}
			catch (Exception ex)
			{
				LogErr("Icon load error: " + ex.Message);
				_iconTexture = null;
			}
		}

		private void OnGUI()
		{
			//IL_0023: Unknown result type (might be due to invalid IL or missing references)
			//IL_002e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0039: Unknown result type (might be due to invalid IL or missing references)
			//IL_009c: 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_0189: Unknown result type (might be due to invalid IL or missing references)
			//IL_01e5: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ff: Unknown result type (might be due to invalid IL or missing references)
			//IL_0204: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				if ((Object)(object)Instance == (Object)null)
				{
					return;
				}
				GUI.depth = int.MinValue;
				GUI.color = Color.white;
				GUI.backgroundColor = Color.white;
				GUI.contentColor = Color.white;
				InitIfNeeded();
				PumpAuthProgressToUI(1);
				PumpAuthProgressToUI(2);
				DrawToasts();
				if ((Object)(object)_iconTexture != (Object)null)
				{
					float num = 100f;
					float num2 = 15f;
					Rect val = default(Rect);
					((Rect)(ref val))..ctor(num2, (float)Screen.height - num - num2, num, num);
					GUI.DrawTexture(val, (Texture)(object)_iconTexture, (ScaleMode)2);
				}
				string text = (_open ? "LIVECHAT ▲" : "LIVECHAT ▼");
				if (GUI.Button(_btnRect, text, _btnStyle))
				{
					_open = !_open;
					if (_open)
					{
						ReloadAll();
					}
				}
				if (_open)
				{
					float num3 = 880f;
					float num4 = Mathf.Min((float)Screen.height * 0.85f, 740f);
					float num5 = Mathf.Clamp((float)Screen.width - num3 - 30f, 10f, (float)Screen.width - num3 - 10f);
					float num6 = Mathf.Clamp((float)Screen.height - num4 - 70f, 10f, (float)Screen.height - num4 - 10f);
					Rect val2 = default(Rect);
					((Rect)(ref val2))..ctor(num5, num6, num3, num4);
					GUILayout.BeginArea(val2, GUI.skin.window);
					GUILayout.Label("Repo Live Chat", _hdrStyle, Array.Empty<GUILayoutOption>());
					GUILayout.Space(4f);
					_activeTab = GUILayout.Toolbar(_activeTab, _tabs, Array.Empty<GUILayoutOption>());
					GUILayout.Space(6f);
					_scroll = GUILayout.BeginScrollView(_scroll, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(num4 - 160f) });
					if (_activeTab == 0)
					{
						DrawRulesTab();
					}
					else if (_activeTab == 1)
					{
						DrawPlatformTab();
					}
					else
					{
						DrawStatusTab();
					}
					GUILayout.EndScrollView();
					GUILayout.FlexibleSpace();
					if (GUILayout.Button("閉じる", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(140f) }))
					{
						_open = false;
					}
					GUILayout.EndArea();
					if (_popupOpen)
					{
						DrawPopup();
					}
				}
			}
			catch (Exception ex)
			{
				ManualLogSource logS = RepoLiveChatPlugin.LogS;
				if (logS != null)
				{
					logS.LogError((object)("[UI] OnGUI exception: " + ex));
				}
			}
		}

		private void ReloadAll()
		{
			try
			{
				RepoLiveChatPlugin.ReloadAll();
				LogInfo($"Reload tables/items={(Tables?.Items?.Count).GetValueOrDefault()}, enemies={(Tables?.Enemies?.Count).GetValueOrDefault()} / rules={(CurrentRules?.rules?.Count).GetValueOrDefault()} / platform=Slot1({CurrentPlat?.Slot1.Platform}), Slot2({CurrentPlat?.Slot2.Platform})");
			}
			catch (Exception ex)
			{
				LogErr("ReloadAll failed: " + ex);
			}
		}

		private void ReloadPlatformOnly(int slotIndex)
		{
			try
			{
				PlatformConfig currentPlat = ConfigFiles.LoadPlatform(ConfigDir) ?? new PlatformConfig();
				CurrentPlat = currentPlat;
				ResetAuthFields(CurrentPlat);
				PlatformConnector.I?.Configure(CurrentPlat);
				ConnectionSlot s = ((slotIndex == 1) ? CurrentPlat.Slot1 : CurrentPlat.Slot2);
				ManualLogSource logS = RepoLiveChatPlugin.LogS;
				if (logS != null)
				{
					logS.LogInfo((object)($"[UI] Platform reloaded for Slot {slotIndex} (linked=" + PlatformConnector.IsLinked(s) + ")"));
				}
			}
			catch (Exception ex)
			{
				ManualLogSource logS2 = RepoLiveChatPlugin.LogS;
				if (logS2 != null)
				{
					logS2.LogError((object)("[UI] ReloadPlatformOnly failed: " + ex));
				}
			}
		}

		private void DrawPlatformTab()
		{
			GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>());
			GUILayout.Label("総合状態: " + BuildUnifiedStatus(), _labStyle, Array.Empty<GUILayoutOption>());
			GUILayout.FlexibleSpace();
			if (GUILayout.Button("監視開始 (全スロット)", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(180f) }))
			{
				try
				{
					PlatformConnector.I?.StartWatching();
				}
				catch (Exception ex)
				{
					RLog.Error("[UI] StartWatching failed: " + ex);
				}
			}
			if (GUILayout.Button("監視停止 (全スロット)", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(180f) }))
			{
				try
				{
					PlatformConnector.I?.StopWatching();
				}
				catch (Exception ex2)
				{
					RLog.Error("[UI] StopWatching failed: " + ex2);
				}
			}
			GUILayout.EndHorizontal();
			DrawRoleControls();
			GUILayout.Space(10f);
			GUILayout.BeginVertical(GUIStyle.op_Implicit("box"), Array.Empty<GUILayoutOption>());
			DrawSlotUI(CurrentPlat.Slot1, 1, "接続スロット 1");
			GUILayout.EndVertical();
			GUILayout.Space(10f);
			GUILayout.BeginVertical(GUIStyle.op_Implicit("box"), Array.Empty<GUILayoutOption>());
			DrawSlotUI(CurrentPlat.Slot2, 2, "接続スロット 2");
			GUILayout.EndVertical();
		}

		private void DrawSlotUI(ConnectionSlot slot, int slotIndex, string title)
		{
			if (slot == null)
			{
				return;
			}
			slot.Enabled = GUILayout.Toggle(slot.Enabled, title, GUI.skin.button, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(200f) });
			if (!slot.Enabled)
			{
				GUILayout.Label("(無効)", _labStyle, Array.Empty<GUILayoutOption>());
				return;
			}
			GUILayout.Space(4f);
			GUILayout.Label("プラットフォーム選択", _labStyle, Array.Empty<GUILayoutOption>());
			GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>());
			if (GUILayout.Toggle(slot.Platform == "None", "なし", GUI.skin.button, Array.Empty<GUILayoutOption>()))
			{
				slot.Platform = "None";
			}
			if (GUILayout.Toggle(slot.Platform == "Twitch", "Twitch", GUI.skin.button, Array.Empty<GUILayoutOption>()))
			{
				slot.Platform = "Twitch";
			}
			if (GUILayout.Toggle(slot.Platform == "YouTube", "YouTube", GUI.skin.button, Array.Empty<GUILayoutOption>()))
			{
				slot.Platform = "YouTube";
			}
			GUILayout.FlexibleSpace();
			GUILayout.EndHorizontal();
			GUILayout.Space(4f);
			if (string.Equals(slot.Platform, "Twitch", StringComparison.OrdinalIgnoreCase))
			{
				GUILayout.Label("クライアント設定(BYOK / Twitch)", _labStyle, Array.Empty<GUILayoutOption>());
				GUILayout.Label("Twitch Client ID", Array.Empty<GUILayoutOption>());
				_twClientId[slotIndex] = GUILayout.TextField(_twClientId[slotIndex], (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(400f) });
				if (GUILayout.Button("保存(クライアント設定)", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(200f) }))
				{
					try
					{
						if (!string.IsNullOrEmpty(_twClientId[slotIndex]) && _twClientId[slotIndex] != "********")
						{
							slot.twClientId = _twClientId[slotIndex];
						}
						ConfigFiles.SavePlatform(ConfigDir, CurrentPlat);
						LogInfo($"保存しました(Twitch Slot {slotIndex})。");
					}
					catch (Exception ex)
					{
						LogErr($"保存エラー(Twitch Slot {slotIndex}): " + ex);
					}
				}
				GUILayout.Space(10f);
				GUILayout.Label("認可(デバイスフロー / Twitch)", _labStyle, Array.Empty<GUILayoutOption>());
				if (GUILayout.Button("認可URLを生成/コピー", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(220f) }))
				{
					_authMsg[slotIndex] = "認可URLを生成中...(Twitch)";
					_twVerifyUrl[slotIndex] = (_twUserCode[slotIndex] = "");
					PlatformConnector.I?.BeginAuthRequest(slotIndex);
				}
				DrawAuthFields(slotIndex, "Twitch");
			}
			else if (string.Equals(slot.Platform, "YouTube", StringComparison.OrdinalIgnoreCase))
			{
				GUILayout.Label("クライアント設定(BYOK / YouTube)", _labStyle, Array.Empty<GUILayoutOption>());
				GUILayout.Label("YouTube Client ID", Array.Empty<GUILayoutOption>());
				_ytClientId[slotIndex] = GUILayout.TextField(_ytClientId[slotIndex], (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(400f) });
				GUILayout.Label("YouTube Client Secret", Array.Empty<GUILayoutOption>());
				_ytClientSecret[slotIndex] = GUILayout.TextField(_ytClientSecret[slotIndex], (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(400f) });
				if (GUILayout.Button("保存(クライアント設定)", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(200f) }))
				{
					try
					{
						if (!string.IsNullOrEmpty(_ytClientId[slotIndex]) && _ytClientId[slotIndex] != "********")
						{
							slot.ytClientId = _ytClientId[slotIndex];
						}
						if (!string.IsNullOrEmpty(_ytClientSecret[slotIndex]) && _ytClientSecret[slotIndex] != "********")
						{
							slot.ytClientSecret = _ytClientSecret[slotIndex];
						}
						ConfigFiles.SavePlatform(ConfigDir, CurrentPlat);
						LogInfo($"保存しました(YouTube Slot {slotIndex})。");
					}
					catch (Exception ex2)
					{
						LogErr($"保存エラー(YouTube Slot {slotIndex}): " + ex2);
					}
				}
				GUILayout.Space(6f);
				GUILayout.Label("使用したいチャンネル(UC または /channel/UC… のURL)", _labStyle, Array.Empty<GUILayoutOption>());
				slot.ytPreferredChannelUC = GUILayout.TextField(slot.ytPreferredChannelUC ?? "", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(400f) });
				GUILayout.Space(10f);
				GUILayout.Label("認可(デバイスフロー / YouTube)", _labStyle, Array.Empty<GUILayoutOption>());
				if (GUILayout.Button("認可URLを生成/コピー", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(220f) }))
				{
					_authMsg[slotIndex] = "認可URLを生成中...(YouTube)";
					_ytVerifyUrl[slotIndex] = (_ytUserCode[slotIndex] = "");
					if (PlatformConnector.I != null)
					{
						if (!PlatformConnector.I.TrySetYouTubePreferredChannel(slotIndex, slot.ytPreferredChannelUC, out var normalized, out var err))
						{
							_authMsg[slotIndex] = "YouTube: " + (string.IsNullOrEmpty(err) ? "UC形式(UCxxxxx)または /channel/UC… を入力してください。" : err);
							return;
						}
						_authMsg[slotIndex] = "YouTube: 事前指定チャンネル UC を設定しました: " + normalized;
					}
					PlatformConnector.I?.BeginAuthRequest(slotIndex);
				}
				DrawAuthFields(slotIndex, "YouTube");
			}
			GUILayout.Space(8f);
			GUILayout.Label(_authMsg[slotIndex] ?? "", _labStyle, Array.Empty<GUILayoutOption>());
		}

		private void PumpAuthProgressToUI(int slotIndex)
		{
			PlatformConnector i = PlatformConnector.I;
			if (i == null)
			{
				return;
			}
			if (i.TryConsumeAuthResult(slotIndex, out var err))
			{
				_authMsg[slotIndex] = (string.IsNullOrEmpty(err) ? "認可成功。監視開始できます。" : ("認可エラー: " + err));
				if (string.IsNullOrEmpty(err))
				{
					ReloadPlatformOnly(slotIndex);
				}
			}
			if (!i.GetLatestAuthPrompt(slotIndex, out var verifyUrl, out var userCode, out var err2, out var platform))
			{
				return;
			}
			if (string.Equals(platform, "Twitch", StringComparison.OrdinalIgnoreCase))
			{
				if (!string.IsNullOrEmpty(verifyUrl))
				{
					_twVerifyUrl[slotIndex] = verifyUrl;
				}
				if (!string.IsNullOrEmpty(userCode))
				{
					_twUserCode[slotIndex] = userCode;
				}
			}
			else if (string.Equals(platform, "YouTube", StringComparison.OrdinalIgnoreCase))
			{
				if (!string.IsNullOrEmpty(verifyUrl))
				{
					_ytVerifyUrl[slotIndex] = verifyUrl;
				}
				if (!string.IsNullOrEmpty(userCode))
				{
					_ytUserCode[slotIndex] = userCode;
				}
			}
			if (!string.IsNullOrEmpty(err2))
			{
				_authMsg[slotIndex] = (platform ?? "Auth") + ": 認可エラー: " + err2;
			}
		}

		private void DrawAuthFields(int slotIndex, string labelPrefix)
		{
			string text = ((labelPrefix == "Twitch") ? _twUserCode[slotIndex] : _ytUserCode[slotIndex]);
			string text2 = ((labelPrefix == "Twitch") ? _twVerifyUrl[slotIndex] : _ytVerifyUrl[slotIndex]);
			if (string.IsNullOrEmpty(text) && string.IsNullOrEmpty(text2))
			{
				return;
			}
			GUILayout.Space(6f);
			if (!string.IsNullOrEmpty(text))
			{
				GUILayout.Label("ユーザーコード(" + labelPrefix + ")", _labStyle, Array.Empty<GUILayoutOption>());
				GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>());
				GUILayout.TextField(text, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(360f) });
				if (GUILayout.Button("コードをコピー", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(120f) }))
				{
					GUIUtility.systemCopyBuffer = text;
				}
				GUILayout.EndHorizontal();
			}
			if (!string.IsNullOrEmpty(text2))
			{
				GUILayout.Label("認可URL(" + labelPrefix + ")", _labStyle, Array.Empty<GUILayoutOption>());
				GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>());
				GUILayout.TextField(text2, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(540f) });
				if (GUILayout.Button("URLをコピー", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(100f) }))
				{
					GUIUtility.systemCopyBuffer = text2;
				}
				GUILayout.EndHorizontal();
			}
		}

		private void DrawStatusTab()
		{
			int num = TableIndex.ItemsEnabled(Tables);
			int num2 = TableIndex.EnemiesEnabled(Tables);
			int num3 = Tables.Items?.Count ?? 0;
			int num4 = Tables.Enemies?.Count ?? 0;
			GUILayout.Label("テーブル件数", _labStyle, Array.Empty<GUILayoutOption>());
			GUILayout.BeginVertical(GUIStyle.op_Implicit("box"), Array.Empty<GUILayoutOption>());
			GUILayout.Label("アイテム: 有効 " + num + " / 総数 " + num3, Array.Empty<GUILayoutOption>());
			GUILayout.Label("敵\u3000\u3000\u3000: 有効 " + num2 + " / 総数 " + num4, Array.Empty<GUILayoutOption>());
			GUILayout.EndVertical();
			GUILayout.Space(6f);
			GUILayout.Label("敵スポーン予約制御", _labStyle, Array.Empty<GUILayoutOption>());
			GUILayout.BeginVertical(GUIStyle.op_Implicit("box"), Array.Empty<GUILayoutOption>());
			int enemyQueueCount = ActionExecutorsCompat.GetEnemyQueueCount();
			GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>());
			GUILayout.Label($"現在の待機数: {enemyQueueCount} 件", Array.Empty<GUILayoutOption>());
			GUILayout.FlexibleSpace();
			if (GUILayout.Button("敵予約を全てクリア", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(160f) }))
			{
				ActionExecutorsCompat.ClearEnemyQueue();
			}
			GUILayout.EndHorizontal();
			GUILayout.EndVertical();
			GUILayout.Space(6f);
			GUILayout.Label("レート制限・重複抑止", _labStyle, Array.Empty<GUILayoutOption>());
			GUILayout.BeginVertical(GUIStyle.op_Implicit("box"), Array.Empty<GUILayoutOption>());
			GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>());
			GUILayout.Label("全体 最大実行/秒", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(180f) });
			CurrentCtrl.rate.allPerSec = IntField(CurrentCtrl.rate.allPerSec, 0, 999);
			GUILayout.EndHorizontal();
			GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>());
			GUILayout.Label("視聴者ごと 最大実行/秒", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(180f) });
			CurrentCtrl.rate.perViewerPerSec = IntField(CurrentCtrl.rate.perViewerPerSec, 0, 999);
			GUILayout.EndHorizontal();
			GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>());
			GUILayout.Label("重複抑止ウィンドウ(秒)", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(180f) });
			CurrentCtrl.dedupeWindowSec = IntField(CurrentCtrl.dedupeWindowSec, 1, 999);
			GUILayout.EndHorizontal();
			GUILayout.EndVertical();
			GUILayout.Space(6f);
			GUILayout.Label("コメント無しタイマー(ホスト/シングル時のみ動作)", _labStyle, Array.Empty<GUILayoutOption>());
			GUILayout.BeginVertical(GUIStyle.op_Implicit("box"), Array.Empty<GUILayoutOption>());
			GUILayout.Label("指定時間コメントが無い場合、ランダムな敵をスポーンします (0=無効)", Array.Empty<GUILayoutOption>());
			GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>());
			GUILayout.Label("N1 (1回目): コメント無しで敵をスポーン (分)", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(280f) });
			CurrentCtrl.NoCommentSpawnMinutes1 = IntField(CurrentCtrl.NoCommentSpawnMinutes1, 0, 999);
			GUILayout.EndHorizontal();
			GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>());
			GUILayout.Label("N2 (継続): N1実行後、さらにN2分毎にスポーン (分)", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(280f) });
			CurrentCtrl.NoCommentSpawnMinutes2 = IntField(CurrentCtrl.NoCommentSpawnMinutes2, 0, 999);
			GUILayout.EndHorizontal();
			GUILayout.EndVertical();
			GUILayout.Space(6f);
			if (GUILayout.Button("保存", Array.Empty<GUILayoutOption>()))
			{
				try
				{
					ConfigFiles.SaveControl(ConfigDir, CurrentCtrl);
					LogInfo("制御設定を保存しました。");
					RepoLiveChatPlugin.ReloadAll();
				}
				catch (Exception ex)
				{
					LogErr("制御設定の保存に失敗: " + ex);
				}
			}
		}

		private void DrawRulesTab()
		{
			//IL_0436: Unknown result type (might be due to invalid IL or missing references)
			//IL_043b: Unknown result type (might be due to invalid IL or missing references)
			List<RulesConfig.Rule> list = CurrentRules?.rules ?? new List<RulesConfig.Rule>();
			GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>());
			GUILayout.FlexibleSpace();
			if (GUILayout.Button("テーブル再読み込み", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(160f) }))
			{
				ReloadAll();
			}
			GUILayout.EndHorizontal();
			GUILayout.Label("ルール(コメントのトリガーワードで実行)", _labStyle, Array.Empty<GUILayoutOption>());
			GUILayout.Space(4f);
			if (list.Count == 0)
			{
				GUILayout.Label("(ルールはありません)", _labStyle, Array.Empty<GUILayoutOption>());
			}
			for (int i = 0; i < list.Count; i++)
			{
				RulesConfig.Rule rule = list[i];
				GUILayout.BeginVertical(GUIStyle.op_Implicit("box"), Array.Empty<GUILayoutOption>());
				GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>());
				GUILayout.Label("トリガーワード: " + rule.name, Array.Empty<GUILayoutOption>());
				GUILayout.FlexibleSpace();
				if (GUILayout.Button("編集", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(50f) }))
				{
					_editIndex = i;
					_tmpMatch = rule.name;
					_tmpTypeIdx = ((rule.type == "enemy") ? 1 : 0);
					_tmpSelectedName = rule.nameJa;
					_tmpCD = rule.cooldownSec;
					_tmpEnabled = rule.enabled;
				}
				if (_editIndex < 0 && GUILayout.Button("削除", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(60f) }))
				{
					list.RemoveAt(i);
					i--;
					GUILayout.EndHorizontal();
					GUILayout.EndVertical();
					continue;
				}
				GUILayout.EndHorizontal();
				GUILayout.Label("種類: " + rule.type + " / 対象: " + rule.nameJa + " / ID: " + rule.id, Array.Empty<GUILayoutOption>());
				GUILayout.Label("prefab: " + (string.IsNullOrEmpty(rule.prefab) ? "(未設定)" : rule.prefab), Array.Empty<GUILayoutOption>());
				GUILayout.Label(string.Format("クールダウン: {0} 秒   有効: {1}", rule.cooldownSec, rule.enabled ? "はい" : "いいえ"), Array.Empty<GUILayoutOption>());
				GUILayout.EndVertical();
			}
			GUILayout.Space(6f);
			GUILayout.Label((_editIndex >= 0) ? "ルールを編集" : "新しいルールを追加", _labStyle, Array.Empty<GUILayoutOption>());
			GUILayout.BeginVertical(GUIStyle.op_Implicit("box"), Array.Empty<GUILayoutOption>());
			GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>());
			GUILayout.Label("トリガーワード", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(140f) });
			_tmpMatch = GUILayout.TextField(_tmpMatch, Array.Empty<GUILayoutOption>());
			GUILayout.EndHorizontal();
			GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>());
			GUILayout.Label("種類", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(140f) });
			_tmpTypeIdx = GUILayout.Toolbar(_tmpTypeIdx, new string[2] { "アイテム", "敵" }, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(220f) });
			GUILayout.EndHorizontal();
			GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>());
			GUILayout.Label("対象(日本語名)", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(140f) });
			string text = (string.IsNullOrEmpty(_tmpSelectedName) ? "(未選択)" : _tmpSelectedName);
			if (GUILayout.Button(text, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(320f) }))
			{
				_popupOpen = true;
				_popupPage = 0;
				_popupRect = new Rect((float)Screen.width * 0.5f - 300f, (float)Screen.height * 0.5f - 210f, 600f, 400f);
			}
			GUILayout.EndHorizontal();
			GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>());
			GUILayout.Label("クールダウン(秒)", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(140f) });
			_tmpCD = IntField(_tmpCD, 0, 999);
			GUILayout.EndHorizontal();
			GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>());
			_tmpEnabled = GUILayout.Toggle(_tmpEnabled, "有効にする", Array.Empty<GUILayoutOption>());
			GUILayout.EndHorizontal();
			if (_editIndex >= 0)
			{
				if (GUILayout.Button("ルールを更新", Array.Empty<GUILayoutOption>()))
				{
					List<RulesConfig.Rule> rules = CurrentRules.rules;
					RulesConfig.Rule rule2 = rules[_editIndex];
					rule2.name = (string.IsNullOrEmpty(_tmpMatch) ? _tmpSelectedName : _tmpMatch);
					rule2.type = ((_tmpTypeIdx == 1) ? "enemy" : "item");
					rule2.nameJa = _tmpSelectedName;
					rule2.id = ResolveIdFromName(_tmpSelectedName, rule2.type);
					rule2.cooldownSec = _tmpCD;
					rule2.enabled = _tmpEnabled;
					FillPrefabForRule(rule2);
					rules[_editIndex] = rule2;
					_editIndex = -1;
					_tmpMatch = "";
					_tmpSelectedName = "";
					_tmpTypeIdx = 0;
					_tmpCD = 10;
					_tmpEnabled = true;
				}
			}
			else if (GUILayout.Button("この内容でルールを追加", Array.Empty<GUILayoutOption>()))
			{
				string type = ((_tmpTypeIdx == 1) ? "enemy" : "item");
				string id = ResolveIdFromName(_tmpSelectedName, type);
				RulesConfig.Rule rule3 = new RulesConfig.Rule
				{
					name = (string.IsNullOrEmpty(_tmpMatch) ? _tmpSelectedName : _tmpMatch),
					type = type,
					nameJa = _tmpSelectedName,
					id = id,
					cooldownSec = _tmpCD,
					enabled = _tmpEnabled
				};
				FillPrefabForRule(rule3);
				CurrentRules.rules.Add(rule3);
				_tmpMatch = "";
				_tmpSelectedName = "";
				_tmpTypeIdx = 0;
				_tmpCD = 10;
				_tmpEnabled = true;
			}
			GUILayout.EndVertical();
			GUILayout.Space(6f);
			if (GUILayout.Button("保存", Array.Empty<GUILayoutOption>()))
			{
				try
				{
					ConfigFiles.SaveRules(ConfigDir, CurrentRules ?? new RulesConfig());
					LogInfo("ルールを保存しました。");
					RepoLiveChatPlugin.ReloadAll();
				}
				catch (Exception ex)
				{
					LogErr("ルール保存に失敗: " + ex);
				}
			}
		}

		private void DrawPopup()
		{
			//IL_006f: Unknown result type (might be due to invalid IL or missing references)
			//IL_01c1: Unknown result type (might be due to invalid IL or missing references)
			//IL_01d0: Unknown result type (might be due to invalid IL or missing references)
			string[] array = ((_tmpTypeIdx == 1) ? _enemyNames : _itemNames);
			if (array == null)
			{
				array = Array.Empty<string>();
			}
			int num = array.Length;
			int num2 = Math.Max(1, (int)Math.Ceiling((float)num / 10f));
			_popupPage = Mathf.Clamp(_popupPage, 0, num2 - 1);
			int num3 = _popupPage * 10;
			int num4 = Math.Min(num, num3 + 10);
			GUILayout.BeginArea(_popupRect, GUI.skin.window);
			GUILayout.Label("候補を選択", _popupHdrStyle, Array.Empty<GUILayoutOption>());
			GUILayout.Space(6f);
			for (int i = num3; i < num4; i++)
			{
				if (GUILayout.Button(array[i], _popupBtnStyle, Array.Empty<GUILayoutOption>()))
				{
					_tmpSelectedName = array[i];
					_popupOpen = false;
				}
			}
			GUILayout.FlexibleSpace();
			GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>());
			if (GUILayout.Button("← 前へ", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(120f) }))
			{
				_popupPage = Math.Max(0, _popupPage - 1);
			}
			GUILayout.FlexibleSpace();
			if (GUILayout.Button("閉じる", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(120f) }))
			{
				_popupOpen = false;
			}
			GUILayout.FlexibleSpace();
			if (GUILayout.Button("次へ →", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(120f) }))
			{
				_popupPage = Math.Min(num2 - 1, _popupPage + 1);
			}
			GUILayout.EndHorizontal();
			GUILayout.EndArea();
			Event current = Event.current;
			if (current != null && (int)current.type == 0 && !((Rect)(ref _popupRect)).Contains(current.mousePosition))
			{
				_popupOpen = false;
			}
		}

		private int IntField(int value, int min, int max)
		{
			string s = GUILayout.TextField(value.ToString(), (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(120f) });
			if (!int.TryParse(s, out var result))
			{
				result = value;
			}
			return Mathf.Clamp(result, min, max);
		}

		private string ResolveIdFromName(string nameJa, string type)
		{
			if (type == "enemy")
			{
				return (Tables?.Enemies?.Find((EnemyRow e) => (e.nameJa ?? e.id) == nameJa && e.enabled))?.id ?? "";
			}
			return (Tables?.Items?.Find((ItemRow i) => (i.nameJa ?? i.id) == nameJa && i.enabled))?.id ?? "";
		}

		private string BuildUnifiedStatus()
		{
			PlatformConnector i = PlatformConnector.I;
			if (i == null)
			{
				return "未初期化";
			}
			string text = (CurrentPlat.Slot1.Enabled ? ("S1[" + PlatTxt(CurrentPlat.Slot1.Platform) + "] " + StatusTxt(i.Status1)) : "S1[Off]");
			string text2 = (CurrentPlat.Slot2.Enabled ? ("S2[" + PlatTxt(CurrentPlat.Slot2.Platform) + "] " + StatusTxt(i.Status2)) : "S2[Off]");
			return text + " | " + text2;
			static string PlatTxt(string p)
			{
				if (string.IsNullOrEmpty(p))
				{
					p = "None";
				}
				if (p.Equals("YouTube", StringComparison.OrdinalIgnoreCase))
				{
					return "YT";
				}
				if (p.Equals("Twitch", StringComparison.OrdinalIgnoreCase))
				{
					return "TW";
				}
				return "Off";
			}
			static string StatusTxt(WatchStatus s)
			{
				if (1 == 0)
				{
				}
				string result = s switch
				{
					WatchStatus.Connecting => "接続中", 
					WatchStatus.Watching => "監視中", 
					_ => "未監視", 
				};
				if (1 == 0)
				{
				}
				return result;
			}
		}

		private void FillPrefabForRule(RulesConfig.Rule r)
		{
			if (r == null)
			{
				return;
			}
			if (r.type == "enemy")
			{
				string text = (Tables?.GetEnemyById(r.id) ?? Tables?.GetEnemyByJa(r.nameJa))?.prefab;
				if (!string.IsNullOrEmpty(text))
				{
					r.prefab = text;
					if (r.args == null)
					{
						r.args = new Dictionary<string, object>();
					}
					r.args["prefabFullPath"] = (text.StartsWith("Enemies/") ? text : ("Enemies/" + text));
				}
			}
			else
			{
				string text2 = (Tables?.GetItemById(r.id) ?? Tables?.GetItemByJa(r.nameJa))?.prefab;
				if (!string.IsNullOrEmpty(text2))
				{
					r.prefab = text2;
					if (r.args == null)
					{
						r.args = new Dictionary<string, object>();
					}
					r.args["prefabFullPath"] = (text2.StartsWith("Items/") ? text2 : ("Items/" + text2));
				}
			}
			string text3 = ((r.args == null || !r.args.ContainsKey("prefabFullPath")) ? "" : (r.args["prefabFullPath"]?.ToString() ?? ""));
			ManualLogSource logS = RepoLiveChatPlugin.LogS;
			if (logS != null)
			{
				logS.LogInfo((object)("[UI] FillPrefabForRule: id=" + r.id + " type=" + r.type + " nameJa='" + r.nameJa + "' -> prefab='" + r.prefab + "' full='" + text3 + "'"));
			}
		}

		private void DrawRoleControls()
		{
			GUILayout.Space(6f);
			GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>());
			GUILayout.Label($"Role: {RoleService.Current}", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.MinWidth(120f) });
			if (GUILayout.Button("役割を取得/更新", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(140f) }))
			{
				RoleService.Role role = RoleService.ForceProbe();
				RLog.Info($"[UIOverlay] Role probe -> {role}");
				switch (role)
				{
				case RoleService.Role.Host:
					TryToast("役割をホストとして認識しました", 2f);
					break;
				case RoleService.Role.Client:
					TryToast("役割をクライアントとして認識しました", 2f);
					break;
				default:
					TryToast("シングルプレイヤーモードです", 2f);
					break;
				}
			}
			GUILayout.FlexibleSpace();
			GUILayout.EndHorizontal();
		}

		private void DrawToasts()
		{
			//IL_00c4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00cb: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e2: Unknown result type (might be due to invalid IL or missing references)
			//IL_0115: Unknown result type (might be due to invalid IL or missing references)
			//IL_012a: Unknown result type (might be due to invalid IL or missing references)
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			lock (_toastLock)
			{
				for (int num = _toasts.Count - 1; num >= 0; num--)
				{
					if (_toasts[num].until <= realtimeSinceStartup)
					{
						_toasts.RemoveAt(num);
					}
				}
				float num2 = 12f;
				Rect val = default(Rect);
				for (int i = 0; i < _toasts.Count; i++)
				{
					Toast toast = _toasts[i];
					float num3 = Mathf.Max(0f, toast.until - realtimeSinceStartup);
					float num4 = 1f;
					if (num3 < 0.3f)
					{
						num4 = Mathf.Clamp01(num3 / 0.3f);
					}
					Color color = GUI.color;
					GUI.color = new Color(color.r, color.g, color.b, num4);
					float num5 = (float)Screen.width - 420f - 12f;
					((Rect)(ref val))..ctor(num5, num2, 420f, 32f);
					GUI.Box(val, toast.text, _toastStyle);
					GUI.color = color;
					num2 += 38f;
				}
			}
		}

		private void LogInfo(string msg)
		{
			ManualLogSource logS = RepoLiveChatPlugin.LogS;
			if (logS != null)
			{
				logS.LogInfo((object)("[UI] " + msg));
			}
		}

		private void LogErr(string msg)
		{
			ManualLogSource logS = RepoLiveChatPlugin.LogS;
			if (logS != null)
			{
				logS.LogError((object)("[UI] " + msg));
			}
		}
	}
}
namespace RepoLiveChat.Platform
{
	public class PhotonRpcBridge : MonoBehaviour
	{
		private struct PendingCmd
		{
			public string json;

			public string key;

			public int attempts;

			public float firstEnqueuedAt;
		}

		private static PhotonRpcBridge _localInstance;

		private static readonly Queue<PendingCmd> _pending = new Queue<PendingCmd>(64);

		private static readonly LinkedList<(string key, float seenAt)> _seenOrder = new LinkedList<(string, float)>();

		private static readonly HashSet<string> _seenSet = new HashSet<string>();

		private const int kSeenCap = 2048;

		private const float kFlushInterval = 1f;

		private const int kMaxAttemptsPerItem = 50;

		private static bool _flushRunning = false;

		private static Coroutine _flushCoroutine;

		private static readonly RaiseEventOptions _raiseEventOptions = new RaiseEventOptions
		{
			Receivers = (ReceiverGroup)2
		};

		private static readonly SendOptions _sendOptions = SendOptions.SendReliable;

		private void Awake()
		{
			if ((Object)(object)_localInstance != (Object)null && (Object)(object)_localInstance != (Object)(object)this && (Object)(object)((Component)_localInstance).gameObject != (Object)null)
			{
				RLog.Warn("[RPC] Destroying old local Player Bridge (Queue Manager) instance: " + ((Object)((Component)_localInstance).gameObject).name);
				Object.Destroy((Object)(object)((Component)_localInstance).gameObject);
			}
			_localInstance = this;
			RLog.Info("[RPC] Local Player Bridge (Queue Manager) instance assigned. GameObject: " + ((Object)((Component)this).gameObject).name);
			((MonoBehaviour)this).StartCoroutine(DelayedEnsureFlush());
		}

		private void OnDestroy()
		{
			RLog.Info("[RPC] Player Bridge (Queue Manager) OnDestroy.");
			if (!((Object)(object)_localInstance == (Object)(object)this))
			{
				return;
			}
			if (_flushCoroutine != null)
			{
				try
				{
					((MonoBehaviour)this).StopCoroutine(_flushCoroutine);
				}
				catch
				{
				}
				_flushCoroutine = null;
			}
			_flushRunning = false;
			_localInstance = null;
			RLog.Info("[RPC] Local Player Bridge (Queue Manager) instance reference cleared.");
		}

		public static void SendToHost(string json)
		{
			if ((Object)(object)_localInstance == (Object)null)
			{
				RLog.Error("[RPC] SendToHost called but no local Player Bridge (Queue Manager) instance exists!");
				Enqueue(json, TryMakeMsgKey(json), "no_local_player_bridge");
				return;
			}
			string text = TryMakeMsgKey(json);
			if (IsDuplicateAndMark(text))
			{
				RLog.Warn("[RPC] SendToHost duplicate drop key=" + text);
				UIOverlay.TryToast("(重複) 送信スキップ");
				return;
			}
			RoleService.Role current = RoleService.Current;
			if (current == RoleService.Role.Single || current == RoleService.Role.Host)
			{
				RLog.Info("[RPC] SendToHost -> local (role=Host/Single)");
				LocalExecute_Static(json, "local:host_or_single");
			}
		}

		public static void SendEnemySpawn(string prefabPath)
		{
			Dictionary<string, object> dictionary = new Dictionary<string, object>
			{
				{ "type", "enemy" },
				{ "prefabFullPath", prefabPath }
			};
			string json = JsonConvert.SerializeObject((object)dictionary);
			string key = TryMakeMsgKey(json);
			if ((Object)(object)_localInstance == (Object)null)
			{
				Enqueue(json, key, "no_local_instance_spawn");
				return;
			}
			RoleService.Role current = RoleService.Current;
			if (current == RoleService.Role.Single || current == RoleService.Role.Host)
			{
				LocalExecute_Static(json, "local:host_or_single_spawn");
			}
		}

		private static void Enqueue(string json, string key, string reason)
		{
			lock (_pending)
			{
				if (_pending.Count >= 256)
				{
					_pending.Dequeue();
				}
				_pending.Enqueue(new PendingCmd
				{
					json = json,
					key = key,
					attempts = 0,
					firstEnqueuedAt = Time.realtimeSinceStartup
				});
				EnsureFlushLoop();
			}
		}

		private static void EnsureFlushLoop()
		{
			if (!_flushRunning && (Object)(object)_localInstance != (Object)null)
			{
				_flushCoroutine = ((MonoBehaviour)_localInstance).StartCoroutine(_localInstance.CoFlush());
				_flushRunning = true;
			}
		}

		private IEnumerator CoFlush()
		{
			while (true)
			{
				bool queueIsEmpty;
				lock (_pending)
				{
					queueIsEmpty = _pending.Count == 0;
				}
				if (queueIsEmpty)
				{
					yield return (object)new WaitForSeconds(1f);
					continue;
				}
				break;
			}
		}

		private IEnumerator DelayedEnsureFlush()
		{
			yield return (object)new WaitForSeconds(0.5f);
			EnsureFlushLoop();
		}

		private static string TryMakeMsgKey(string json)
		{
			try
			{
				return json.Substring(0, Math.Min(json.Length, 100));
			}
			catch
			{
				return "unknown";
			}
		}

		private static bool IsDuplicateAndMark(string key)
		{
			lock (_seenOrder)
			{
				if (_seenSet.Contains(key))
				{
					return true;
				}
				_seenOrder.AddFirst((key, Time.realtimeSinceStartup));
				_seenSet.Add(key);
				if (_seenOrder.Count > 2048)
				{
					(string, float) value = _seenOrder.Last.Value;
					_seenSet.Remove(value.Item1);
					_seenOrder.RemoveLast();
				}
			}
			return false;
		}

		public static void LocalExecute_Static(string json, string source)
		{
			try
			{
				Dictionary<string, object> dictionary = JsonConvert.DeserializeObject<Dictionary<string, object>>(json);
				if (dictionary != null)
				{
					dictionary["__host_invoke"] = true;
					if (!dictionary.ContainsKey("senderActorNumber") || Convert.ToInt32(dictionary["senderActorNumber"]) <= 0)
					{
						dictionary["senderActorNumber"] = -1;
					}
					ActionExecutorsCompat.TryExecute(dictionary);
				}
			}
			catch (Exception ex)
			{
				RLog.Error("[RPC] LocalExecute_Static exception :: " + ex);
			}
		}
	}
	public enum WatchStatus
	{
		NotWatching,
		Connecting,
		Watching
	}
	public sealed class PlatformConnector
	{
		public static PlatformConnector I;

		private readonly string _configDir;

		private RulesConfig _rules;

		private TableIndex _tables;

		private volatile ControlConfig _ctrl;

		private PlatformConfig _plat;

		private IDisposable _connection1;

		private IDisposable _connection2;

		private DateTime _startedAtUtc = DateTime.MinValue;

		private readonly LinkedList<(string key, DateTime seenAtUtc)> _seenMsgOrder = new LinkedList<(string, DateTime)>();

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

		private const int kSeenCap = 2048;

		private readonly object _seenLock = new object();

		private const double kResetSkipWindowSec = 30.0;

		private float _lastCommentTime = -1f;

		private float _lastNoCommentSpawnTime = -1f;

		private bool _noCommentSpawn1Triggered = false;

		private Thread _authThread;

		private volatile bool _authRunning;

		private volatile string _authErr;

		private volatile int _authSlotIndex;

		private volatile string _authPlatform;

		private volatile string _authVerifyUrl;

		private volatile string _authUserCode;

		private volatile bool _authSucceeded;

		private int _authPollIntervalSec = 5;

		public WatchStatus Status1 { get; private set; } = WatchStatus.NotWatching;

		public WatchStatus Status2 { get; private set; } = WatchStatus.NotWatching;

		public WatchStatus CombinedStatus => (Status1 == WatchStatus.Watching || Status2 == WatchStatus.Watching) ? WatchStatus.Watching : ((Status1 == WatchStatus.Connecting || Status2 == WatchStatus.Connecting) ? WatchStatus.Connecting : WatchStatus.NotWatching);

		public bool IsWatching => CombinedStatus == WatchStatus.Watching;

		public PlatformConnector(RulesConfig rules, TableIndex tables, ControlConfig ctrl, PlatformConfig plat, string configDir)
		{
			_rules = rules ?? new RulesConfig();
			_tables = tables ?? new TableIndex();
			_ctrl = ctrl ?? new ControlConfig();
			_plat = plat ?? new PlatformConfig();
			_configDir = configDir ?? "";
		}

		public void Configure(PlatformConfig plat)
		{
			_plat = plat ?? new PlatformConfig();
			RLog.Info("[PLAT] Configure: Slot1=(" + _plat.Slot1.Platform + "), Slot2=(" + _plat.Slot2.Platform + ")");
			LogRoleSnapshot("[PLAT] Configure");
		}

		public void ConfigureControl(ControlConfig ctrl)
		{
			_ctrl = ctrl ?? new ControlConfig();
			RLog.Info($"[PLAT] ControlConfig reloaded (Timer1={_ctrl.NoCommentSpawnMinutes1}min, Timer2={_ctrl.NoCommentSpawnMinutes2}min).");
		}

		public static bool IsLinked(ConnectionSlot s)
		{
			if (s == null || !s.Enabled)
			{
				return false;
			}
			if (string.Equals(s.Platform, "YouTube", StringComparison.OrdinalIgnoreCase))
			{
				return !string.IsNullOrEmpty(s.ytAccessToken);
			}
			if (string.Equals(s.Platform, "Twitch", StringComparison.OrdinalIgnoreCase))
			{
				return !string.IsNullOrEmpty(s.twitchOAuthToken);
			}
			return false;
		}

		public void StartWatching()
		{
			try
			{
				LogRoleSnapshot("[PLAT] StartWatching: pre");
				if (CombinedStatus != WatchStatus.NotWatching)
				{
					StopWatching();
					RLog.Info("[PLAT] Stopping existing connections before starting...");
					Thread.Sleep(500);
				}
				Status1 = WatchStatus.NotWatching;
				Status2 = WatchStatus.NotWatching;
				if (_plat == null)
				{
					RLog.Warn("[PLAT] StartWatching ignored: platform config is null");
					return;
				}
				if (_plat.Slot1.Enabled && string.Equals(_plat.Slot1.Platform, "YouTube", StringComparison.OrdinalIgnoreCase) && string.IsNullOrEmpty(_plat.Slot1.ytLiveChatId) && _plat.Slot2.Enabled && string.Equals(_plat.Slot2.Platform, "YouTube", StringComparison.OrdinalIgnoreCase) && string.IsNullOrEmpty(_plat.Slot2.ytLiveChatId))
				{
					RLog.Info("[PLAT] YouTube dual-slot detected. Trying to auto-assign different liveChatIds...");
					List<string> allActiveChatIds = YouTubeLiveChatPoller.GetAllActiveChatIds(_plat.Slot1);
					if (allActiveChatIds.Count >= 2)
					{
						_plat.Slot1.ytLiveChatId = allActiveChatIds[0];
						_plat.Slot2.ytLiveChatId = allActiveChatIds[1];
						RLog.Info("[PLAT] Auto-assigned: Slot1=" + allActiveChatIds[0] + ", Slot2=" + allActiveChatIds[1]);
						UIOverlay.TryToast("2つの配信を検出し、自動割り当てしました");
					}
					else if (allActiveChatIds.Count == 1)
					{
						RLog.Warn("[PLAT] Only 1 active stream found. Assigning to Slot1 only.");
						_plat.Slot1.ytLiveChatId = allActiveChatIds[0];
					}
					else
					{
						RLog.Warn("[PLAT] No active streams found for auto-assignment.");
					}
				}
				lock (_seenLock)
				{
					_seenMsgOrder.Clear();
					_seenMsgSet.Clear();
				}
				_startedAtUtc = DateTime.MinValue;
				_lastCommentTime = Time.realtimeSinceStartup;
				_lastNoCommentSpawnTime = -1f;
				_noCommentSpawn1Triggered = false;
				StartSlot(1, _plat.Slot1);
				StartSlot(2, _plat.Slot2);
			}
			catch (Exception ex)
			{
				Status1 = WatchStatus.NotWatching;
				Status2 = WatchStatus.NotWatching;
				RLog.Error("[PLAT] StartWatching fatal: " + ex);
				UIOverlay.TryToast("配信監視の開始に失敗しました");
			}
		}

		public void StopWatching()
		{
			try
			{
				if (CombinedStatus == WatchStatus.NotWatching)
				{
					RLog.Info("[PLAT] StopWatching ignored: already stopped.");
					return;
				}
				try
				{
					_connection1?.Dispose();
				}
				catch
				{
				}
				try
				{
					_connection2?.Dispose();
				}
				catch
				{
				}
				_connection1 = null;
				_connection2 = null;
				Status1 = WatchStatus.NotWatching;
				Status2 = WatchStatus.NotWatching;
				_startedAtUtc = DateTime.MinValue;
				_lastCommentTime = -1f;
				_lastNoCommentSpawnTime = -1f;
				_noCommentSpawn1Triggered = false;
				lock (_seenLock)
				{
					_seenMsgOrder.Clear();
					_seenMsgSet.Clear();
				}
				RLog.Info("[PLAT] StopWatching (All slots)");
			}
			catch (Exception ex)
			{
				RLog.Error("[PLAT] StopWatching fatal: " + ex);
				UIOverlay.TryToast("配信監視の停止に失敗しました");
			}
		}

		private void StartSlot(int slotIndex, ConnectionSlot slot)
		{
			if (slot == null || !slot.Enabled || string.Equals(slot.Platform, "None", StringComparison.OrdinalIgnoreCase))
			{
				RLog.Info($"[PLAT][Slot{slotIndex}] Disabled or platform=None. Skipping.");
			}
			else if (string.Equals(slot.Platform, "YouTube", StringComparison.OrdinalIgnoreCase))
			{
				StartYouTube(slotIndex, slot);
			}
			else if (string.Equals(slot.Platform, "Twitch", StringComparison.OrdinalIgnoreCase))
			{
				StartTwitch(slotIndex, slot);
			}
			else
			{
				RLog.Warn($"[PLAT][Slot{slotIndex}] Unknown platform: {slot.Platform}");
			}
		}

		private void ProcessReceivedComment(string platform, string user, string text, string msgId, int slotIndex, YouTubeLiveChatPoller associatedPoller)
		{
			try
			{
				RLog.Info($"[FLOW][Slot{slotIndex}] comment recv platform={platform} user=@{user} text='{text}' msgId={msgId} {RoleSnapshotInlineBG()}");
				ResetNoCommentTimer();
				if (_startedAtUtc == DateTime.MinValue)
				{
					RLog.Debug($"[PLAT][Slot{slotIndex}] message ignored (central _startedAtUtc not set)");
					return;
				}
				string key = BuildMsgKey(platform, user, text);
				if (IsDuplicateAndRemember(key))
				{
					RLog.Debug($"[PLAT][Slot{slotIndex}] duplicate message ignored");
					return;
				}
				if (associatedPoller != null && associatedPoller.LastResetUtc != DateTime.MinValue)
				{
					TimeSpan timeSpan = DateTime.UtcNow - associatedPoller.LastResetUtc;
					if (timeSpan.TotalSeconds < 30.0)
					{
						if (TryRouteCurrent(platform, user, text, out var _))
						{
							RLog.Warn($"[PLAT][Slot{slotIndex}] Command skipped (Reset Avoidance: {timeSpan.TotalSeconds:F1}s). Trigger: {text}");
							UIOverlay.TryToast("過去コマンドを一時スキップしました", 1f);
							return;
						}
						if (timeSpan.TotalSeconds >= 30.0)
						{
							associatedPoller.LastResetUtc = DateTime.MinValue;
							RLog.Info($"[PLAT][Slot{slotIndex}] Reset avoidance window closed.");
						}
					}
				}
				if (!TryRouteCurrent(platform, user, text, out var cmd2))
				{
					RLog.Debug("[Router] comment did not match any rule (text='" + text + "')");
					return;
				}
				cmd2["senderActorNumber"] = -1;
				try
				{
					if (!ActionExecutorsCompat.TryExecute(cmd2))
					{
						RLog.Warn("[Action] Execute returned false");
						UIOverlay.TryToast("コマンド実行に失敗しました");
					}
				}
				catch (Exception ex)
				{
					RLog.Warn("[Action] Execute failed: " + ex.Message);
					UIOverlay.TryToast("実行時エラー(詳細はログ)");
				}
			}
			catch (Exception ex2)
			{
				RLog.Error($"[PLAT][Slot{slotIndex}] onMessage exception: " + ex2);
				UIOverlay.TryToast("コメント処理に失敗(" + platform + ")");
			}
		}

		private void StartYouTube(int slotIndex, ConnectionSlot slot)
		{
			if (slotIndex == 1)
			{
				Status1 = WatchStatus.Connecting;
			}
			else
			{
				Status2 = WatchStatus.Connecting;
			}
			RLog.Info(string.Format("[YT][Slot{0}] StartWatching: liveChatId='{1}'", slotIndex, slot.ytLiveChatId ?? "(null)"));
			LogRoleSnapshot($"[YT][Slot{slotIndex}] StartWatching");
			if (string.IsNullOrEmpty(slot.ytAccessToken) && string.IsNullOrEmpty(slot.ytRefreshToken))
			{
				RLog.Warn($"[YT][Slot{slotIndex}] ytAccessToken/RefreshToken is empty. Link account first.");
				if (slotIndex == 1)
				{
					Status1 = WatchStatus.NotWatching;
				}
				else
				{
					Status2 = WatchStatus.NotWatching;
				}
				UIOverlay.TryToast($"YouTube[${slotIndex}] 未連携");
				return;
			}
			YouTubeLiveChatPoller youTubeLiveChatPoller = new YouTubeLiveChatPoller(slot, delegate(string id, string user, string text)
			{
				ProcessReceivedComment("YouTube", user, text, id, slotIndex, (slotIndex == 1) ? (_connection1 as YouTubeLiveChatPoller) : (_connection2 as YouTubeLiveChatPoller));
			}, delegate(string access, string refresh, string liveId)
			{
				try
				{
					bool flag = false;
					if (!string.IsNullOrEmpty(access) && access != slot.ytAccessToken)
					{
						slot.ytAccessToken = access;
						flag = true;
						RLog.Info($"[YT][Slot{slotIndex}] AccessToken refreshed");
					}
					if (!string.IsNullOrEmpty(refresh) && refresh != slot.ytRefreshToken)
					{
						slot.ytRefreshToken = refresh;
						flag = true;
						RLog.Info($"[YT][Slot{slotIndex}] RefreshToken updated");
					}
					if (!string.IsNullOrEmpty(liveId) && liveId != slot.ytLiveChatId)
					{
						slot.ytLiveChatId = liveId;
						flag = true;
						RLog.Info($"[YT][Slot{slotIndex}] liveChatId resolved: " + liveId);
					}
					if (flag)
					{
						ConfigFiles.SavePlatform(_configDir, _plat);
					}
				}
				catch (Exception ex)
				{
					RLog.Warn($"[YT][Slot{slotIndex}] onTokenRefresh error: " + ex.Message);
				}
			}, delegate
			{
				try
				{
					if (slotIndex == 1)
					{
						Status1 = WatchStatus.Watching;
					}
					else
					{
						Status2 = WatchStatus.Watching;
					}
					_startedAtUtc = DateTime.UtcNow;
					RLog.Info($"[YT][Slot{slotIndex}] connected");
					LogRoleSnapshot($"[YT][Slot{slotIndex}] connected");
					UIOverlay.TryToast($"YouTube[${slotIndex}] 監視開始");
				}
				catch (Exception ex)
				{
					RLog.Warn($"[YT][Slot{slotIndex}] onConnected error: " + ex.Message);
				}
			}, delegate
			{
				try
				{
					if (slotIndex == 1)
					{
						Status1 = WatchStatus.NotWatching;
					}
					else
					{
						Status2 = WatchStatus.NotWatching;
					}
					RLog.Warn($"[YT][Slot{slotIndex}] disconnected");
					UIOverlay.TryToast($"YouTube[${slotIndex}] 切断");
				}
				catch (Exception ex)
				{
					RLog.Warn($"[YT][Slot{slotIndex}] onDisconnected error: " + ex.Message);
				}
			});
			if (slotIndex == 1)
			{
				_connection1 = youTubeLiveChatPoller;
			}
			else
			{
				_connection2 = youTubeLiveChatPoller;
			}
			youTubeLiveChatPoller.Start();
		}

		private void StartTwitch(int slotIndex, ConnectionSlot slot)
		{
			if (slotIndex == 1)
			{
				Status1 = WatchStatus.Connecting;
			}
			else
			{
				Status2 = WatchStatus.Connecting;
			}
			string twitchChannel = slot.twitchChannel;
			string twitchOAuthToken = slot.twitchOAuthToken;
			if (string.IsNullOrEmpty(twitchChannel) || string.IsNullOrEmpty(twitchOAuthToken))
			{
				RLog.Warn($"[TW][Slot{slotIndex}] channel or oauth token is empty.");
				if (slotIndex == 1)
				{
					Status1 = WatchStatus.NotWatching;
				}
				else
				{
					Status2 = WatchStatus.NotWatching;
				}
				UIOverlay.TryToast($"Twitch[${slotIndex}] 未連携");
				return;
			}
			string text = twitchChannel;
			TwitchIrcClient twitchIrcClient = new TwitchIrcClient(slot, delegate(string user, string text2)
			{
				ProcessReceivedComment("Twitch", user, text2, "", slotIndex, null);
			}, delegate
			{
				try
				{
					if (slotIndex == 1)
					{
						Status1 = WatchStatus.Watching;
					}
					else
					{
						Status2 = WatchStatus.Watching;
					}
					_startedAtUtc = DateTime.UtcNow;
					RLog.Info($"[TW][Slot{slotIndex}] connected");
					LogRoleSnapshot($"[TW][Slot{slotIndex}] connected");
					UIOverlay.TryToast($"Twitch[${slotIndex}] 監視開始");
				}
				catch (Exception ex)
				{
					RLog.Warn($"[TW][Slot{slotIndex}] onConnected error: " + ex.Message);
				}
			}, delegate
			{
				try
				{
					if (slotIndex == 1)
					{
						Status1 = WatchStatus.NotWatching;
					}
					else
					{
						Status2 = WatchStatus.NotWatching;
					}
					RLog.Warn($"[TW][Slot{slotIndex}] disconnected");
					UIOverlay.TryToast($"Twitch[${slotIndex}] 切断");
				}
				catch (Exception ex)
				{
					RLog.Warn($"[TW][Slot{slotIndex}] onClosed error: " + ex.Message);
				}
			});
			if (slotIndex == 1)
			{
				_connection1 = twitchIrcClient;
			}
			else
			{
				_connection2 = twitchIrcClient;
			}
			twitchIrcClient.Start();
		}

		public void BeginAuthRequest(int slotIndex)
		{
			try
			{
				if (_authRunning)
				{
					_authRunning = false;
					try
					{
						_authThread?.Join(100);
					}
					catch
					{
					}
				}
				_authSlotIndex = slotIndex;
				ConnectionSlot connectionSlot = ((slotIndex != 1) ? _plat?.Slot2 : _plat?.Slot1);
				if (connectionSlot == null)
				{
					RLog.Warn($"[Auth][Slot{slotIndex}] BeginAuthRequest failed: Slot config is null.");
					_authErr = "Slot config missing.";
					return;
				}
				_authPlatform = connectionSlot.Platform;
				_authVerifyUrl = null;
				_authUserCode = null;
				_authErr = null;
				_authSucceeded = false;
				_authPollIntervalSec = 5;
				if (string.Equals(_authPlatform, "Twitch", StringComparison.OrdinalIgnoreCase))
				{
					BeginAuthTwitch(slotIndex, connectionSlot);
				}
				else if (string.Equals(_authPlatform, "YouTube", StringComparison.OrdinalIgnoreCase))
				{
					BeginAuthYouTube(slotIndex, connectionSlot);
				}
				else
				{
					RLog.Warn($"[Auth][Slot{slotIndex}] BeginAuthRequest ignored: platform=None");
				}
			}
			catch (Exception ex)
			{
				RLog.Warn($"[Auth][Slot{slotIndex}] BeginAuthRequest error: " + ex.Message);
				_authErr = ex.Message;
			}
		}

		private void BeginAuthTwitch(int slotIndex, ConnectionSlot slot)
		{
			string clientId = slot.twClientId;
			if (string.IsNullOrWhiteSpace(clientId))
			{
				_authErr = "Twitch Client ID is empty.";
				RLog.Warn($"[Auth/TW][Slot{slotIndex}] twClientId is empty.");
				return;
			}
			string url = "https://id.twitch.tv/oauth2/device";
			string body = "client_id=" + Uri.EscapeDataString(clientId) + "&scope=" + Uri.EscapeDataString("chat:read");
			if (!HttpPostForm(url, body, out var text, out var err, out var status))
			{
				_authErr = err;
				RLog.Warn($"[Auth/TW][Slot{slotIndex}] device_code request failed: " + err);
				return;
			}
			if (status != HttpStatusCode.OK)
			{
				_authErr = $"device_code http={(int)status} body={text}";
				RLog.Warn($"[Auth/TW][Slot{slotIndex}] {_authErr}");
				return;
			}
			Dictionary<string, object> dictionary = MiniJSON.Deserialize(text) as Dictionary<string, object>;
			object value;
			string deviceCode = ((dictionary == null || !dictionary.TryGetValue("device_code", out value)) ? null : value?.ToString());
			object value2;
			string text2 = ((dictionary == null || !dictionary.TryGetValue("verification_uri", out value2)) ? null : value2?.ToString());
			object value3;
			string text3 = ((dictionary == null || !dictionary.TryGetValue("user_code", out value3)) ? null : value3?.ToString());
			if (dictionary != null && dictionary.TryGetValue("interval", out var value4) && int.TryParse(value4?.ToString() ?? "", out var result) && result > 0)
			{
				_authPollIntervalSec = result;
			}
			if (string.IsNullOrEmpty(deviceCode) || string.IsNullOrEmpty(text2) || string.IsNullOrEmpty(text3))
			{
				_authErr = "invalid device_code response.";
				RLog.Warn($"[Auth/TW][Slot{slotIndex}] " + _authErr);
				return;
			}
			_authVerifyUrl = text2;
			_authUserCode = text3;
			_authErr = null;
			_authSucceeded = false;
			_authRunning = true;
			_authThread = new Thread((ThreadStart)delegate
			{
				AuthPollLoopTwitch(slotIndex, slot, clientId, deviceCode);
			})
			{
				IsBackground = true,
				Name = $"TW-AuthPoll-{slotIndex}"
			};
			_authThread.Start();
			RLog.Info($"[Auth/TW][Slot{slotIndex}] ブラウザで '{_authVerifyUrl}' を開き、コード '{_authUserCode}' を入力して許可してください.");
		}

		private void AuthPollLoopTwitch(int slotIndex, ConnectionSlot slot, string clientId, string deviceCode)
		{
			try
			{
				while (_authRunning)
				{
					string url = "https://id.twitch.tv/oauth2/token";
					string body = "client_id=" + Uri.EscapeDataString(clientId) + "&grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=" + Uri.EscapeDataString(deviceCode);
					if (!HttpPostForm(url, body, out var text, out var err, out var status))
					{
						_authErr = err;
						Thread.Sleep(Math.Max(5, _authPollIntervalSec) * 1000);
						continue;
					}
					if (status == HttpStatusCode.BadRequest && text.IndexOf("authorization_pending", StringComparison.OrdinalIgnoreCase) >= 0)
					{
						Thread.Sleep(Math.Max(5, _authPollIntervalSec) * 1000);
						continue;
					}
					if (status == HttpStatusCode.BadRequest && text.IndexOf("slow_down", StringComparison.OrdinalIgnoreCase) >= 0)
					{
						Thread.Sleep(Math.Max(8, _authPollIntervalSec + 3) * 1000);
						continue;
					}
					if (status != HttpStatusCode.OK)
					{
						_authErr = $"token http={(int)status} body={text}";
						Thread.Sleep(Math.Max(5, _authPollIntervalSec) * 1000);
						continue;
					}
					object value;
					string text2 = ((!(MiniJSON.Deserialize(text) is Dictionary<string, object> dictionary) || !dictionary.TryGetValue("access_token", out value)) ? null : value?.ToString());
					if (string.IsNullOrEmpty(text2))
					{
						_authErr = "no access_token in response";
						Thread.Sleep(Math.Max(5, _authPollIntervalSec) * 1000);
						continue;
					}
					string text3 = null;
					if (HttpGetJson("https://api.twitch.tv/helix/users", text2, clientId, out var text4, out var _, out var status2) && status2 == HttpStatusCode.OK && MiniJSON.Deserialize(text4) is Dictionary<string, object> dictionary2 && dictionary2.TryGetValue("data", out var value2) && value2 is List<object> { Count: >0 } list && list[0] is Dictionary<string, object> dictionary3 && dictionary3.TryGetValue("login", out var value3) && value3 != null)
					{
						text3 = value3.ToString();
					}
					if (string.IsNullOrEmpty(text3))
					{
						text3 = slot.twitchChannel;
					}
					slot.twitchOAuthToken = text2;
					if (!string.IsNullOrEmpty(text3))
					{
						slot.twitchChannel = text3;
					}
					ConfigFiles.SavePlatform(_configDir, _plat);
					RLog.Info($"[Auth/TW][Slot{slotIndex}] 認可成功: channel={slot.twitchChannel}");
					_authSucceeded = true;
					_authRunning = false;
				}
			}
			catch (Exception ex)
			{
				_authErr = ex.Message;
				_authRunning = false;
			}
		}

		public bool TrySetYouTubePreferredChannel(int slotIndex, string ucOrUrl, out string normalized, out string err)
		{
			ConnectionSlot connectionSlot = ((slotIndex != 1) ? _plat?.Slot2 : _plat?.Slot1);
			normalized = null;
			err = null;
			if (connectionSlot == null)
			{
				err = "Internal error: slot is null";
				return false;
			}
			string text = ParseChannelUC(ucOrUrl);
			if (string.IsNullOrEmpty(text))
			{
				err = "UC形式(UCxxxxx から始まる)または /channel/UC… のURLを入力してください。";
				return false;
			}
			connectionSlot.ytPreferredChannelUC = text;
			normalized = text;
			RLog.Info($"[Auth/YT][Slot{slotIndex}] preferred UC set: " + text);
			return true;
		}

		private static string ParseChannelUC(string ucOrUrl)
		{
			if (string.IsNullOrWhiteSpace(ucOrUrl))
			{
				return null;
			}
			string text = ucOrUrl.Trim();
			int num = text.IndexOf("UC", StringComparison.OrdinalIgnoreCase);
			if (num >= 0)
			{
				text = text.Substring(num);
			}
			if (text.StartsWith("UC") && text.Length >= 8)
			{
				return text;
			}
			return null;
		}

		private void BeginAuthYouTube(int slotIndex, ConnectionSlot slot)
		{
			string clientId = slot.ytClientId;
			string clientSecret = slot.ytClientSecret;
			if (string.IsNullOrWhiteSpace(clientId) || string.IsNullOrWhiteSpace(clientSecret))
			{
				_authErr = "YouTube Client ID/Secret is empty.";
				RLog.Warn($"[Auth/YT][Slot{slotIndex}] clientId/clientSecret is empty.");
				return;
			}
			string stringToEscape = "https://www.googleapis.com/auth/youtube.readonly";
			string url = "https://oauth2.googleapis.com/device/code";
			string body = "client_id=" + Uri.EscapeDataString(clientId) + "&scope=" + Uri.EscapeDataString(stringToEscape);
			if (!HttpPostForm(url, body, out var text, out var err, out var status))
			{
				_authErr = err;
				RLog.Warn($"[Auth/YT][Slot{slotIndex}] device_code request failed: " + err);
				return;
			}
			if (status != HttpStatusCode.OK)
			{
				_authErr = $"device_code http={(int)status} body={text}";
				RLog.Warn($"[Auth/YT][Slot{slotIndex}] {_authErr}");
				return;
			}
			Dictionary<string, object> dictionary = MiniJSON.Deserialize(text) as Dictionary<string, object>;
			object value;
			string deviceCode = ((dictionary == null || !dictionary.TryGetValue("device_code", out value)) ? null : value?.ToString());
			string text2 = null;
			if (dictionary != null && dictionary.TryGetValue("verification_url", out var value2))
			{
				text2 = value2?.ToString();
			}
			if (string.IsNullOrEmpty(text2) && dictionary != null && dictionary.TryGetValue("verification_uri", out var value3))
			{
				text2 = value3?.ToString();
			}
			if (dictionary != null && dictionary.TryGetValue("verification_uri_complete", out var value4) && !string.IsNullOrEmpty(value4?.ToString()))
			{
				text2 = value4.ToString();
			}
			object value5;
			string text3 = ((dictionary == null || !dictionary.TryGetValue("user_code", out value5)) ? null : value5?.ToString());
			if (dictionary != null && dictionary.TryGetValue("interval", out var value6) && int.TryParse(value6?.ToString() ?? "", out var result) && result > 0)
			{
				_authPollIntervalSec = result;
			}
			if (string.IsNullOrEmpty(deviceCode) || string.IsNullOrEmpty(text2) || string.IsNullOrEmpty(text3))
			{
				_authErr = "invalid device_code response.";
				RLog.Warn($"[Auth/YT][Slot{slotIndex}] " + _authErr);
				return;
			}
			_authVerifyUrl = text2;
			_authUserCode = text3;
			_authErr = null;
			_authSucceeded = false;
			_authRunning = true;
			_authThread = new Thread((ThreadStart)delegate
			{
				AuthPollLoopYouTube(slotIndex, slot, clientId, clientSecret, deviceCode);
			})
			{
				IsBackground = true,
				Name = $"YT-AuthPoll-{slotIndex}"
			};
			_authThread.Start();
			RLog.Info($"[Auth/YT][Slot{slotIndex}] ブラウザで '{_authVerifyUrl}' を開き、コード '{_authUserCode}' を入力して許可してください.");
		}

		private void AuthPollLoopYouTube(int slotIndex, ConnectionSlot slot, string clientId, string clientSecret, string deviceCode)
		{
			try
			{
				while (_authRunning)
				{
					string url = "https://oauth2.googleapis.com/token";
					string body = "client_id=" + Uri.EscapeDataString(clientId) + "&client_secret=" + Uri.EscapeDataString(clientSecret) + "&grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=" + Uri.EscapeDataString(deviceCode);
					if (!HttpPostForm(url, body, out var text, out var err, out var status))
					{
						_authErr = err;
						Thread.Sleep(Math.Max(5, _authPollIntervalSec) * 1000);
						continue;
					}
					bool flag = text.IndexOf("authorization_pending", StringComparison.OrdinalIgnoreCase) >= 0;
					bool flag2 = text.IndexOf("slow_down", StringComparison.OrdinalIgnoreCase) >= 0;
					if ((status == HttpStatusCode.PreconditionRequired || status == HttpStatusCode.BadRequest) && flag)
					{
						Thread.Sleep(Math.Max(5, _authPollIntervalSec) * 1000);
						continue;
					}
					if ((status == HttpStatusCode.Forbidden || status == HttpStatusCode.BadRequest) && flag2)
					{
						Thread.Sleep(Math.Max(8, _authPollIntervalSec + 3) * 1000);
						continue;
					}
					if (status != HttpStatusCode.OK)
					{
						_authErr = $"token http={(int)status} body={text}";
						Thread.Sleep(Math.Max(5, _authPollIntervalSec) * 1000);
						continue;
					}
					Dictionary<string, object> dictionary = MiniJSON.Deserialize(text) as Dictionary<string, object>;
					object value;
					string text2 = ((dictionary == null || !dictionary.TryGetValue("access_token", out value)) ? null : value?.ToString());
					object value2;
					string text3 = ((dictionary == null || !dictionary.TryGetValue("refresh_token", out value2)) ? null : value2?.ToString());
					if (string.IsNullOrEmpty(text2))
					{
						_authErr = "no access_token in response";
						Thread.Sleep(Math.Max(5, _authPollIntervalSec) * 1000);
						continue;
					}
					string text4 = null;
					string text5 = null;
					if (HttpGetJson("https://www.googleapis.com/youtube/v3/channels?part=id%2Csnippet&mine=true", text2, null, out var text6, out var _, out var status2) && status2 == HttpStatusCode.OK)
					{
						try
						{
							if (MiniJSON.Deserialize(text6) is Dictionary<string, object> dictionary2 && dictionary2.TryGetValue("items", out var value3) && value3 is List<object> { Count: >0 } list && list[0] is Dictionary<string, object> dictionary3)
							{
								if (dictionary3.TryGetValue("id", out var value4) && value4 != null)
								{
									text4 = value4.ToString();
								}
								if (dictionary3.TryGetValue("snippet", out var value5) && value5 is Dictionary<string, object> dictionary4 && dictionary4.TryGetValue("title", out var value6) && value6 != null)
								{
									text5 = value6.ToString();
								}
							}
						}
						catch
						{
						}
					}
					string ytPreferredChannelUC = slot.ytPreferredChannelUC;
					if (!string.IsNullOrEmpty(ytPreferredChannelUC) && !string.IsNullOrEmpty(text4) && !string.Equals(ytPreferredChannelUC, text4, StringComparison.OrdinalIgnoreCase))
					{
						_authErr = "YouTube: 認可されたCHが指定と異なります。now=" + text5 + "(" + text4 + ") / wanted=" + ytPreferredChannelUC;
						RLog.Warn($"[Auth/YT][Slot{slotIndex}] mismatch: authorized=" + text4 + " wanted=" + ytPreferredChannelUC + " → re-issue device code");
						_authRunning = false;
						MainThreadDispatcher.Enqueue(delegate
						{
							BeginAuthYouTube(slotIndex, slot);
						});
						break;
					}
					slot.ytAccessToken = text2;
					if (!string.IsNullOrEmpty(text3))
					{
						slot.ytRefreshToken = text3;
					}
					ConfigFiles.SavePlatform(_configDir, _plat);
					RLog.Info($"[Auth/YT][Slot{slotIndex}] 認可成功(access/refresh 保存)");
					_authSucceeded = true;
					_authRunning = false;
				}
			}
			catch (Exception ex)
			{
				_authErr = ex.Message;
				_authRunning = false;
			}
		}

		public bool GetLatestAuthPrompt(int slotIndex, out string verifyUrl, out string userCode, out string err, out string platform)
		{
			if (_authRunning && _authSlotIndex == slotIndex)
			{
				verifyUrl = _authVerifyUrl;
				userCode = _authUserCode;
				err = _authErr;
				platform = _authPlatform;
				return true;
			}
			verifyUrl = null;
			userCode = null;
			err = null;
			platform = null;
			return false;
		}

		public bool TryConsumeAuthResult(int slotIndex, out string err)
		{
			err = null;
			if (_authSucceeded && _authSlotIndex == slotIndex)
			{
				err = _authErr;
				_authSucceeded = false;
				return true;
			}
			return false;
		}

		private static bool HttpPostForm(string url, string body, out string text, out string err, out HttpStatusCode status)
		{
			text = "";
			err = null;
			status = (HttpStatusCode)0;
			try
			{
				byte[] bytes = Encoding.UTF8.GetBytes(body);
				HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(url);
				httpWebRequest.Method = "POST";
				httpWebRequest.Timeout = 20000;
				httpWebRequest.ContentType = "application/x-www-form-url