Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of RepoLiveChat v1.0.0
plugins/RepoLiveChat.dll
Decompiled a day ago
The result has been truncated due to the large size, download it to view full contents!
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