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 ParseObject() { Dictionary dictionary = new Dictionary(); 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 ParseArray() { List list = new List(); 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 dictionary = null; try { dictionary = JsonConvert.DeserializeObject>(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(); 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(); 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(); 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(); 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() }; 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() }; 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(); private string[] _enemyNames = Array.Empty(); 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 _toasts = new List(); 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() }; 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.RootGO = val; uIOverlay.Tables = tables ?? new TableIndex(); uIOverlay.CurrentRules = rules ?? new RulesConfig { rules = new List() }; 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()); _enemyNames = ((Tables?.Enemies != null) ? Tables.Enemies.FindAll((EnemyRow x) => x.enabled).ConvertAll((EnemyRow x) => x.nameJa ?? x.id).ToArray() : Array.Empty()); 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()); GUILayout.Space(4f); _activeTab = GUILayout.Toolbar(_activeTab, _tabs, Array.Empty()); 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()); GUILayout.Label("総合状態: " + BuildUnifiedStatus(), _labStyle, Array.Empty()); 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()); DrawSlotUI(CurrentPlat.Slot1, 1, "接続スロット 1"); GUILayout.EndVertical(); GUILayout.Space(10f); GUILayout.BeginVertical(GUIStyle.op_Implicit("box"), Array.Empty()); 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()); return; } GUILayout.Space(4f); GUILayout.Label("プラットフォーム選択", _labStyle, Array.Empty()); GUILayout.BeginHorizontal(Array.Empty()); if (GUILayout.Toggle(slot.Platform == "None", "なし", GUI.skin.button, Array.Empty())) { slot.Platform = "None"; } if (GUILayout.Toggle(slot.Platform == "Twitch", "Twitch", GUI.skin.button, Array.Empty())) { slot.Platform = "Twitch"; } if (GUILayout.Toggle(slot.Platform == "YouTube", "YouTube", GUI.skin.button, Array.Empty())) { 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()); GUILayout.Label("Twitch Client ID", Array.Empty()); _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()); 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()); GUILayout.Label("YouTube Client ID", Array.Empty()); _ytClientId[slotIndex] = GUILayout.TextField(_ytClientId[slotIndex], (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(400f) }); GUILayout.Label("YouTube Client Secret", Array.Empty()); _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()); slot.ytPreferredChannelUC = GUILayout.TextField(slot.ytPreferredChannelUC ?? "", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(400f) }); GUILayout.Space(10f); GUILayout.Label("認可(デバイスフロー / YouTube)", _labStyle, Array.Empty()); 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()); } 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()); GUILayout.BeginHorizontal(Array.Empty()); 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()); GUILayout.BeginHorizontal(Array.Empty()); 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()); GUILayout.BeginVertical(GUIStyle.op_Implicit("box"), Array.Empty()); GUILayout.Label("アイテム: 有効 " + num + " / 総数 " + num3, Array.Empty()); GUILayout.Label("敵\u3000\u3000\u3000: 有効 " + num2 + " / 総数 " + num4, Array.Empty()); GUILayout.EndVertical(); GUILayout.Space(6f); GUILayout.Label("敵スポーン予約制御", _labStyle, Array.Empty()); GUILayout.BeginVertical(GUIStyle.op_Implicit("box"), Array.Empty()); int enemyQueueCount = ActionExecutorsCompat.GetEnemyQueueCount(); GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label($"現在の待機数: {enemyQueueCount} 件", Array.Empty()); 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()); GUILayout.BeginVertical(GUIStyle.op_Implicit("box"), Array.Empty()); GUILayout.BeginHorizontal(Array.Empty()); 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()); 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()); 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()); GUILayout.BeginVertical(GUIStyle.op_Implicit("box"), Array.Empty()); GUILayout.Label("指定時間コメントが無い場合、ランダムな敵をスポーンします (0=無効)", Array.Empty()); GUILayout.BeginHorizontal(Array.Empty()); 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()); 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())) { 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 list = CurrentRules?.rules ?? new List(); GUILayout.BeginHorizontal(Array.Empty()); GUILayout.FlexibleSpace(); if (GUILayout.Button("テーブル再読み込み", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(160f) })) { ReloadAll(); } GUILayout.EndHorizontal(); GUILayout.Label("ルール(コメントのトリガーワードで実行)", _labStyle, Array.Empty()); GUILayout.Space(4f); if (list.Count == 0) { GUILayout.Label("(ルールはありません)", _labStyle, Array.Empty()); } for (int i = 0; i < list.Count; i++) { RulesConfig.Rule rule = list[i]; GUILayout.BeginVertical(GUIStyle.op_Implicit("box"), Array.Empty()); GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label("トリガーワード: " + rule.name, Array.Empty()); 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()); GUILayout.Label("prefab: " + (string.IsNullOrEmpty(rule.prefab) ? "(未設定)" : rule.prefab), Array.Empty()); GUILayout.Label(string.Format("クールダウン: {0} 秒 有効: {1}", rule.cooldownSec, rule.enabled ? "はい" : "いいえ"), Array.Empty()); GUILayout.EndVertical(); } GUILayout.Space(6f); GUILayout.Label((_editIndex >= 0) ? "ルールを編集" : "新しいルールを追加", _labStyle, Array.Empty()); GUILayout.BeginVertical(GUIStyle.op_Implicit("box"), Array.Empty()); GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label("トリガーワード", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(140f) }); _tmpMatch = GUILayout.TextField(_tmpMatch, Array.Empty()); GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(Array.Empty()); 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()); 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()); GUILayout.Label("クールダウン(秒)", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(140f) }); _tmpCD = IntField(_tmpCD, 0, 999); GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(Array.Empty()); _tmpEnabled = GUILayout.Toggle(_tmpEnabled, "有効にする", Array.Empty()); GUILayout.EndHorizontal(); if (_editIndex >= 0) { if (GUILayout.Button("ルールを更新", Array.Empty())) { List 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())) { 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())) { 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(); } 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()); GUILayout.Space(6f); for (int i = num3; i < num4; i++) { if (GUILayout.Button(array[i], _popupBtnStyle, Array.Empty())) { _tmpSelectedName = array[i]; _popupOpen = false; } } GUILayout.FlexibleSpace(); GUILayout.BeginHorizontal(Array.Empty()); 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(); } 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(); } 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()); 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 _pending = new Queue(64); private static readonly LinkedList<(string key, float seenAt)> _seenOrder = new LinkedList<(string, float)>(); private static readonly HashSet _seenSet = new HashSet(); 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 dictionary = new Dictionary { { "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 dictionary = JsonConvert.DeserializeObject>(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 _seenMsgSet = new HashSet(); 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 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 dictionary = MiniJSON.Deserialize(text) as Dictionary; 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 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 dictionary2 && dictionary2.TryGetValue("data", out var value2) && value2 is List { Count: >0 } list && list[0] is Dictionary 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 dictionary = MiniJSON.Deserialize(text) as Dictionary; 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 dictionary = MiniJSON.Deserialize(text) as Dictionary; 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 dictionary2 && dictionary2.TryGetValue("items", out var value3) && value3 is List { Count: >0 } list && list[0] is Dictionary dictionary3) { if (dictionary3.TryGetValue("id", out var value4) && value4 != null) { text4 = value4.ToString(); } if (dictionary3.TryGetValue("snippet", out var value5) && value5 is Dictionary 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-urlencoded"; httpWebRequest.ContentLength = bytes.Length; using (Stream stream = httpWebRequest.GetRequestStream()) { stream.Write(bytes, 0, bytes.Length); } HttpWebResponse httpWebResponse = null; try { httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse(); } catch (WebException ex) { httpWebResponse = ex.Response as HttpWebResponse; if (httpWebResponse == null) { err = ex.Message; text = ""; return false; } } status = httpWebResponse.StatusCode; using StreamReader streamReader = new StreamReader(httpWebResponse.GetResponseStream(), Encoding.UTF8); text = streamReader.ReadToEnd(); return true; } catch (Exception ex2) { err = ex2.Message; text = ""; return false; } } private static bool HttpGetJson(string url, string bearer, string clientId, out string text, out string err, out HttpStatusCode status) { text = ""; err = null; status = (HttpStatusCode)0; try { HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(url); httpWebRequest.Method = "GET"; httpWebRequest.Timeout = 20000; if (!string.IsNullOrEmpty(bearer)) { httpWebRequest.Headers["Authorization"] = "Bearer " + bearer; } if (!string.IsNullOrEmpty(clientId)) { httpWebRequest.Headers["Client-Id"] = clientId; } using HttpWebResponse httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse(); status = httpWebResponse.StatusCode; using StreamReader streamReader = new StreamReader(httpWebResponse.GetResponseStream(), Encoding.UTF8); text = streamReader.ReadToEnd(); return true; } catch (Exception ex) { err = ex.Message; text = ""; return false; } } private static void LogRoleSnapshot(string head) { try { RLog.Info(head + " [Role:unknown]"); } catch { } } private static string RoleSnapshotInlineBG() { return "[Thread:BG]"; } private string BuildMsgKey(string platform, string user, string text) { ControlConfig ctrl = _ctrl; return platform + "|" + user + "|" + text; } private bool IsDuplicateAndRemember(string key) { if (string.IsNullOrEmpty(key)) { return false; } DateTime utcNow = DateTime.UtcNow; int num = Math.Max(1, _ctrl?.dedupeWindowSec ?? 10); lock (_seenLock) { LinkedListNode<(string, DateTime)> linkedListNode = _seenMsgOrder.Last; while (linkedListNode != null) { LinkedListNode<(string, DateTime)> previous = linkedListNode.Previous; if ((utcNow - linkedListNode.Value.Item2).TotalSeconds > (double)num) { _seenMsgSet.Remove(linkedListNode.Value.Item1); _seenMsgOrder.Remove(linkedListNode); linkedListNode = previous; continue; } break; } if (_seenMsgSet.Contains(key)) { return true; } _seenMsgOrder.AddFirst((key, utcNow)); _seenMsgSet.Add(key); while (_seenMsgOrder.Count > 2048) { LinkedListNode<(string, DateTime)> last = _seenMsgOrder.Last; _seenMsgSet.Remove(last.Value.Item1); _seenMsgOrder.RemoveLast(); } } return false; } private bool TryRouteCurrent(string platform, string user, string text, out Dictionary cmd) { cmd = null; try { ChatRouter router = RepoLiveChatPlugin.Router; if (router != null) { return router.TryRoute(platform, user, text, out cmd); } RLog.Warn("[Router] TryRouteCurrent failed: Router instance is null."); return false; } catch (Exception ex) { RLog.Warn("[Router] TryRouteCurrent exception: " + ex.Message); return false; } } private string ResolvePrefabFull(RulesConfig.Rule r) { if (r == null) { return null; } if (r.args != null && r.args.TryGetValue("prefabFullPath", out var value) && value != null) { string text = value.ToString(); if (!string.IsNullOrEmpty(text)) { return text; } } if (!string.IsNullOrEmpty(r.prefab)) { if (string.Equals(r.type, "item", StringComparison.OrdinalIgnoreCase)) { return r.prefab.StartsWith("Items/") ? r.prefab : ("Items/" + r.prefab); } if (string.Equals(r.type, "enemy", StringComparison.OrdinalIgnoreCase)) { return r.prefab.StartsWith("Enemies/") ? r.prefab : ("Enemies/" + r.prefab); } } TableIndex tables = _tables; if (tables != null && tables.TryGetPrefabFullPath(r.type, r.id, out var fullPath)) { return fullPath; } return null; } private void ResetNoCommentTimer() { _lastCommentTime = Time.realtimeSinceStartup; _noCommentSpawn1Triggered = false; _lastNoCommentSpawnTime = -1f; } public void TickTimer(float deltaTime) { RoleService.Role current = RoleService.Current; if (CombinedStatus != WatchStatus.Watching || (current != RoleService.Role.Host && current != RoleService.Role.Single)) { return; } ControlConfig ctrl = _ctrl; float num = (float)(ctrl?.NoCommentSpawnMinutes1 ?? 0) * 60f; float num2 = (float)(ctrl?.NoCommentSpawnMinutes2 ?? 0) * 60f; if (num <= 0f) { return; } if (_lastCommentTime < 0f) { _lastCommentTime = Time.realtimeSinceStartup; } float num3 = Time.realtimeSinceStartup - _lastCommentTime; if (!_noCommentSpawn1Triggered) { if (num3 >= num) { RLog.Info($"[PLAT][Timer] NoCommentTimer1 triggered ({num3:F0}s >= {num:F0}s)"); _noCommentSpawn1Triggered = true; _lastNoCommentSpawnTime = Time.realtimeSinceStartup; TriggerRandomEnemySpawn("NoCommentTimer1"); } } else if (!(num2 <= 0f)) { if (_lastNoCommentSpawnTime < 0f) { _lastNoCommentSpawnTime = Time.realtimeSinceStartup; } float num4 = Time.realtimeSinceStartup - _lastNoCommentSpawnTime; if (num4 >= num2) { RLog.Info($"[PLAT][Timer] NoCommentTimer2 (Repeat) triggered ({num4:F0}s >= {num2:F0}s)"); _lastNoCommentSpawnTime = Time.realtimeSinceStartup; TriggerRandomEnemySpawn("NoCommentTimer2_Repeat"); } } } private void TriggerRandomEnemySpawn(string reason) { RLog.Info("[PLAT][Timer] Triggering random enemy spawn (Reason: " + reason + ")"); try { List list = _tables?.Enemies?.FindAll((EnemyRow e) => e.enabled && !string.IsNullOrEmpty(e.prefab)); if (list == null || list.Count == 0) { RLog.Warn("[PLAT][Timer] No enabled enemies found in table."); return; } EnemyRow enemyRow = list[Random.Range(0, list.Count)]; string value = ResolvePrefabFull(new RulesConfig.Rule { type = "enemy", id = enemyRow.id, prefab = enemyRow.prefab }); if (string.IsNullOrEmpty(value)) { RLog.Warn("[PLAT][Timer] Failed to resolve prefab for random enemy: " + enemyRow.id + " / " + enemyRow.nameJa); return; } int num = -1; Dictionary cmd = new Dictionary { ["type"] = "enemy", ["ruleId"] = "TIMER_" + reason, ["trigger"] = reason, ["prefabFullPath"] = value, ["at"] = DateTime.UtcNow.ToString("o"), ["platform"] = "Timer", ["user"] = "System", ["displayName"] = enemyRow.nameJa ?? enemyRow.name, ["senderActorNumber"] = num, ["__host_invoke"] = true }; ActionExecutorsCompat.TryExecute(cmd); } catch (Exception arg) { RLog.Error($"[PLAT][Timer] TriggerRandomEnemySpawn exception: {arg}"); UIOverlay.TryToast("タイマー実行エラー"); } } } public sealed class TwitchIrcClient : IDisposable { private readonly string _channel; private readonly string _username; private readonly string _oauth; private readonly Action _onMsg; private readonly Action _onConnected; private readonly Action _onClosed; private Thread _th; private volatile bool _run; public TwitchIrcClient(ConnectionSlot slot, Action onMsg, Action onConnected, Action onClosed) { _channel = (slot.twitchChannel ?? "").Trim().ToLowerInvariant(); _username = (slot.twitchChannel ?? "").Trim().ToLowerInvariant(); _oauth = (slot.twitchOAuthToken ?? "").Trim(); _onMsg = onMsg; _onConnected = onConnected; _onClosed = onClosed; } public void Start() { if (!_run) { if (string.IsNullOrEmpty(_channel) || string.IsNullOrEmpty(_oauth)) { RLog.Warn("[Twitch] Start failed: channel or oauth is empty."); _onClosed?.Invoke(); return; } _run = true; _th = new Thread(Loop) { IsBackground = true, Name = "TwitchIRC" }; _th.Start(); } } public void Dispose() { _run = false; try { _th?.Join(200); } catch { } } private void Loop() { try { while (_run) { try { using TcpClient tcpClient = new TcpClient(); tcpClient.ReceiveTimeout = 20000; tcpClient.SendTimeout = 20000; tcpClient.Connect("irc.chat.twitch.tv", 6667); using NetworkStream stream = tcpClient.GetStream(); using StreamReader streamReader = new StreamReader(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); using StreamWriter streamWriter = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)) { NewLine = "\r\n", AutoFlush = true }; string text = (_oauth.StartsWith("oauth:", StringComparison.OrdinalIgnoreCase) ? _oauth : ("oauth:" + _oauth)); string text2 = (string.IsNullOrEmpty(_username) ? _channel : _username); string text3 = (_channel.StartsWith("#") ? _channel : ("#" + _channel)); streamWriter.WriteLine("PASS " + text); streamWriter.WriteLine("NICK " + text2); streamWriter.WriteLine("CAP REQ :twitch.tv/tags twitch.tv/commands twitch.tv/membership"); bool flag = false; bool flag2 = false; bool flag3 = false; int tickCount = Environment.TickCount; while (_run && tcpClient.Connected) { string text4 = streamReader.ReadLine(); if (text4 == null) { if (Environment.TickCount - tickCount > 10000) { streamWriter.WriteLine("PING :keepalive"); tickCount = Environment.TickCount; } Thread.Sleep(10); continue; } tickCount = Environment.TickCount; ManualLogSource logS = RepoLiveChatPlugin.LogS; if (logS != null) { logS.LogDebug((object)("[Twitch<<] " + ((text4.Length > 240) ? (text4.Substring(0, 240) + "...") : text4))); } if (text4.StartsWith("PING ", StringComparison.OrdinalIgnoreCase)) { streamWriter.WriteLine("PONG :tmi.twitch.tv"); continue; } if (!flag2 && text4.Contains(" 001 ")) { flag2 = true; } if (flag2 && !flag && (text4.Contains(" 376 ") || text4.Contains(" 422 "))) { streamWriter.WriteLine("JOIN " + text3); ManualLogSource logS2 = RepoLiveChatPlugin.LogS; if (logS2 != null) { logS2.LogInfo((object)("[Twitch] JOIN " + text3)); } continue; } if (!flag && text4.Contains(" 366 ")) { flag = true; ManualLogSource logS3 = RepoLiveChatPlugin.LogS; if (logS3 != null) { logS3.LogInfo((object)"[Twitch] JOIN completed"); } } if (flag && !flag3) { flag3 = true; try { _onConnected?.Invoke(); } catch { } } int num = text4.IndexOf(" PRIVMSG "); if (num <= 0) { continue; } string text5 = "?"; int num2 = text4.IndexOf('!'); if (text4.StartsWith("@")) { int num3 = text4.IndexOf(" :"); if (num3 > 0) { int num4 = text4.IndexOf('!', num3 + 2); if (num4 > 0) { text5 = text4.Substring(num3 + 2, num4 - (num3 + 2)); } } } else if (num2 > 1) { text5 = text4.Substring(1, num2 - 1); } int num5 = text4.IndexOf(" :", num); string text6 = ((num5 >= 0 && num5 + 2 <= text4.Length) ? text4.Substring(num5 + 2) : ""); if (!string.IsNullOrEmpty(text6)) { ManualLogSource logS4 = RepoLiveChatPlugin.LogS; if (logS4 != null) { logS4.LogDebug((object)("[TW] @" + text5 + ": " + text6)); } try { _onMsg?.Invoke(text5, text6); } catch { } } } if (_run) { Thread.Sleep(8000); } } catch (Exception ex) { ManualLogSource logS5 = RepoLiveChatPlugin.LogS; if (logS5 != null) { logS5.LogWarning((object)("[Twitch] loop error: " + ex.Message)); } if (_run) { Thread.Sleep(4000); } } } } catch (Exception ex2) { ManualLogSource logS6 = RepoLiveChatPlugin.LogS; if (logS6 != null) { logS6.LogWarning((object)("[Twitch] fatal: " + ex2.Message)); } } finally { try { _onClosed?.Invoke(); } catch { } } } } public sealed class YouTubeLiveChatPoller : IDisposable { private readonly ConnectionSlot _slot; private readonly Action _onMessage; private readonly Action _onTokenRefresh; private readonly Action _onConnected; private readonly Action _onDisconnected; private Thread _th; private volatile bool _run; private string _pageToken = ""; private int _apiRecommendedMs = 2000; private bool _connected = false; private int _zeroStreak = 0; private const int ZERO_STREAK_RESET = 8; private readonly Dictionary _seen = new Dictionary(1024); private const int SEEN_MAX = 1000; private static readonly TimeSpan SEEN_TTL = TimeSpan.FromMinutes(30.0); public DateTime LastResetUtc { get; internal set; } = DateTime.MinValue; public YouTubeLiveChatPoller(ConnectionSlot slot, Action onMessage, Action onTokenRefresh, Action onConnected, Action onDisconnected) { _slot = slot ?? new ConnectionSlot(); _onMessage = onMessage; _onTokenRefresh = onTokenRefresh; _onConnected = onConnected; _onDisconnected = onDisconnected; } public void Start() { if (_th == null) { if (string.IsNullOrEmpty(_slot.ytAccessToken) && string.IsNullOrEmpty(_slot.ytRefreshToken)) { RLog.Warn("[YouTube] Start failed: AccessToken and RefreshToken are both empty."); _onDisconnected?.Invoke(); return; } ActionExecutorsCompat.ClearEnemyQueue(); _run = true; _th = new Thread(Run) { IsBackground = true, Name = "RepoLiveChat.YouTubePoller" }; _th.Start(); } } public void Dispose() { _run = false; ActionExecutorsCompat.ClearEnemyQueue(); try { _th?.Join(300); } catch { } _th = null; } private void Run() { RLog.Info("[YouTube] poller thread start"); while (_run) { try { if (string.IsNullOrEmpty(_slot.ytAccessToken) && !string.IsNullOrEmpty(_slot.ytRefreshToken)) { TryRefreshAccessToken(_slot, out var _, out var _); } if (!string.IsNullOrEmpty(_slot.ytLiveChatId)) { goto IL_0177; } if (!HasValidAccessToken(_slot)) { RLog.Debug("[YouTube] waiting token."); Thread.Sleep(15000); continue; } if (TryResolveLiveChatIdRobust(_slot, out var liveChatId, out var error2)) { _slot.ytLiveChatId = liveChatId; _onTokenRefresh?.Invoke(_slot.ytAccessToken, _slot.ytRefreshToken, _slot.ytLiveChatId); RLog.Info("[YouTube] liveChatId resolved: " + liveChatId); _pageToken = ""; _zeroStreak = 0; PruneSeen(hard: true); goto IL_0177; } if (_connected) { _connected = false; _onDisconnected?.Invoke(); } RLog.Info("[YouTube] waiting live start. (" + (error2 ?? "no active") + ")"); Thread.Sleep(15000); goto end_IL_0012; IL_0177: string fatalErr; int httpStatus; int num = FetchMessagesOnce(_slot, _slot.ytLiveChatId, ref _pageToken, ref _apiRecommendedMs, _onMessage, out fatalErr, out httpStatus); if (httpStatus == 200) { if (!_connected) { _connected = true; _onConnected?.Invoke(); } if (num == 0) { _zeroStreak++; if (_zeroStreak >= 8) { RLog.Warn("[YouTube] zero items streak -> reset pageToken and retry"); _pageToken = ""; _zeroStreak = 0; LastResetUtc = DateTime.UtcNow; PruneSeen(); } } else { _zeroStreak = 0; } Thread.Sleep(15000); continue; } if (_connected) { _connected = false; _onDisconnected?.Invoke(); } RLog.Warn($"[YouTube] poll failed: http={httpStatus} err={fatalErr}"); if (httpStatus == 401 || (fatalErr != null && fatalErr.IndexOf("401", StringComparison.OrdinalIgnoreCase) >= 0)) { RLog.Warn("[YouTube] 401 Unauthorized → try refresh token"); if (TryRefreshAccessToken(_slot, out var newAccess2, out var error3)) { RLog.Info("[YouTube] access token refreshed"); _onTokenRefresh?.Invoke(newAccess2, _slot.ytRefreshToken, null); } else { RLog.Warn("[YouTube] token refresh failed: " + error3); } } else { _slot.ytLiveChatId = ""; } _pageToken = ""; _zeroStreak = 0; Thread.Sleep(15000); end_IL_0012:; } catch (Exception ex) { if (_connected) { _connected = false; _onDisconnected?.Invoke(); } RLog.Warn("[YouTube] poll error: " + ex.Message); _pageToken = ""; _zeroStreak = 0; Thread.Sleep(15000); } } RLog.Info("[YouTube] poller thread end"); } private static bool HasValidAccessToken(ConnectionSlot s) { return !string.IsNullOrEmpty(s.ytAccessToken); } private int FetchMessagesOnce(ConnectionSlot s, string liveChatId, ref string pageToken, ref int apiRecommendedMs, Action onMsg, out string fatalErr, out int httpStatus) { fatalErr = null; httpStatus = 0; if (string.IsNullOrEmpty(s.ytAccessToken)) { fatalErr = "no access token"; return 0; } string value = "https://www.googleapis.com/youtube/v3/liveChat/messages"; StringBuilder stringBuilder = new StringBuilder(value); stringBuilder.Append("?part=").Append(UE("snippet,authorDetails")); stringBuilder.Append("&liveChatId=").Append(UE(liveChatId)); if (!string.IsNullOrEmpty(pageToken)) { stringBuilder.Append("&pageToken=").Append(UE(pageToken)); } string requestUriString = stringBuilder.ToString(); try { HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(requestUriString); httpWebRequest.Method = "GET"; httpWebRequest.Timeout = 20000; httpWebRequest.Headers["Authorization"] = "Bearer " + s.ytAccessToken; using HttpWebResponse httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse(); httpStatus = (int)httpWebResponse.StatusCode; using StreamReader streamReader = new StreamReader(httpWebResponse.GetResponseStream(), Encoding.UTF8); string json = streamReader.ReadToEnd(); Dictionary dictionary = MiniJSON.Deserialize(json) as Dictionary; int num = 0; if (dictionary != null && dictionary.TryGetValue("nextPageToken", out var value2) && value2 != null) { pageToken = value2.ToString(); } if (dictionary != null && dictionary.TryGetValue("pollingIntervalMillis", out var value3) && int.TryParse(value3?.ToString() ?? "0", out var result) && result > 0) { apiRecommendedMs = result; } if (dictionary != null && dictionary.TryGetValue("items", out var value4) && value4 is List list) { foreach (object item in list) { if (!(item is Dictionary dictionary2)) { continue; } object value5; string id = ((!dictionary2.TryGetValue("id", out value5)) ? "" : (value5?.ToString() ?? "")); object value6; Dictionary dictionary3 = (dictionary2.TryGetValue("snippet", out value6) ? (value6 as Dictionary) : null); object value7; Dictionary dictionary4 = (dictionary2.TryGetValue("authorDetails", out value7) ? (value7 as Dictionary) : null); object value8; string msg = ((dictionary3 == null || !dictionary3.TryGetValue("displayMessage", out value8)) ? "" : (value8?.ToString() ?? "")); object value9; string author = ((dictionary4 == null || !dictionary4.TryGetValue("displayName", out value9)) ? "" : (value9?.ToString() ?? "")); if (!string.IsNullOrEmpty(msg) && MarkIfUnseen(id)) { num++; RLog.Debug("[YouTube] raw msg: @" + author + ": " + msg); MainThreadDispatcher.EnqueueOrNow(delegate { onMsg?.Invoke(id, author, msg); }); } } } return num; } catch (WebException ex) { try { httpStatus = (int)(((HttpWebResponse)ex.Response)?.StatusCode ?? ((HttpStatusCode)0)); } catch { httpStatus = 0; } fatalErr = ex.Message; return 0; } static string UE(string str) { return Uri.EscapeDataString(str ?? ""); } } private bool MarkIfUnseen(string id) { if (string.IsNullOrEmpty(id)) { id = Guid.NewGuid().ToString("N"); } DateTime utcNow = DateTime.UtcNow; if (_seen.TryGetValue(id, out var value)) { if (utcNow - value < SEEN_TTL) { return false; } _seen[id] = utcNow; return true; } if (_seen.Count >= 1000) { PruneSeen(); } _seen[id] = utcNow; return true; } private void PruneSeen(bool hard = false) { try { if (hard) { _seen.Clear(); return; } DateTime utcNow = DateTime.UtcNow; List list = new List(); foreach (KeyValuePair item in _seen) { if (utcNow - item.Value >= SEEN_TTL) { list.Add(item.Key); } } for (int i = 0; i < list.Count; i++) { _seen.Remove(list[i]); } while (_seen.Count > 1000) { string text = null; DateTime dateTime = DateTime.MaxValue; foreach (KeyValuePair item2 in _seen) { if (item2.Value < dateTime) { dateTime = item2.Value; text = item2.Key; } } if (text == null) { break; } _seen.Remove(text); } } catch { } } private static bool TryResolveLiveChatIdRobust(ConnectionSlot s, out string liveChatId, out string error) { if (TryResolveViaSearchVideos(s, out liveChatId, out error)) { return true; } if (TryResolveViaChannelIdThenBroadcasts(s, out liveChatId, out error)) { return true; } if (TryResolveViaLiveBroadcastsMineOnly(s, out liveChatId, out error)) { return true; } return false; } private static bool TryResolveViaSearchVideos(ConnectionSlot s, out string liveChatId, out string error) { liveChatId = ""; error = null; try { string requestUriString = "https://www.googleapis.com/youtube/v3/search?part=id&eventType=live&type=video&mine=true"; HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(requestUriString); httpWebRequest.Method = "GET"; httpWebRequest.Timeout = 20000; httpWebRequest.Headers["Authorization"] = "Bearer " + s.ytAccessToken; using HttpWebResponse httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse(); using StreamReader streamReader = new StreamReader(httpWebResponse.GetResponseStream(), Encoding.UTF8); string json = streamReader.ReadToEnd(); if (!(MiniJSON.Deserialize(json) is Dictionary dictionary)) { error = "parse(search)"; return false; } string text = ""; if (dictionary.TryGetValue("items", out var value) && value is List { Count: >0 } list && list[0] is Dictionary dictionary2 && dictionary2.TryGetValue("id", out var value2) && value2 is Dictionary dictionary3) { text = ((!dictionary3.TryGetValue("videoId", out var value3)) ? "" : (value3?.ToString() ?? "")); } if (string.IsNullOrEmpty(text)) { error = "no live video"; return false; } string requestUriString2 = "https://www.googleapis.com/youtube/v3/videos?part=liveStreamingDetails&id=" + Uri.EscapeDataString(text); HttpWebRequest httpWebRequest2 = (HttpWebRequest)WebRequest.Create(requestUriString2); httpWebRequest2.Method = "GET"; httpWebRequest2.Timeout = 20000; httpWebRequest2.Headers["Authorization"] = "Bearer " + s.ytAccessToken; using HttpWebResponse httpWebResponse2 = (HttpWebResponse)httpWebRequest2.GetResponse(); using StreamReader streamReader2 = new StreamReader(httpWebResponse2.GetResponseStream(), Encoding.UTF8); string json2 = streamReader2.ReadToEnd(); if (!(MiniJSON.Deserialize(json2) is Dictionary dictionary4)) { error = "parse(videos)"; return false; } if (dictionary4.TryGetValue("items", out var value4) && value4 is List { Count: >0 } list2 && list2[0] is Dictionary dictionary5 && dictionary5.TryGetValue("liveStreamingDetails", out var value5) && value5 is Dictionary dictionary6) { liveChatId = ((!dictionary6.TryGetValue("activeLiveChatId", out var value6)) ? "" : (value6?.ToString() ?? "")); return !string.IsNullOrEmpty(liveChatId); } error = "no activeLiveChatId"; return false; } catch (WebException ex) { error = ex.Message; return false; } catch (Exception ex2) { error = ex2.Message; return false; } } private static bool TryResolveViaChannelIdThenBroadcasts(ConnectionSlot s, out string liveChatId, out string error) { liveChatId = ""; error = null; try { string requestUriString = "https://www.googleapis.com/youtube/v3/channels?part=id&mine=true"; HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(requestUriString); httpWebRequest.Method = "GET"; httpWebRequest.Timeout = 20000; httpWebRequest.Headers["Authorization"] = "Bearer " + s.ytAccessToken; using HttpWebResponse httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse(); using StreamReader streamReader = new StreamReader(httpWebResponse.GetResponseStream(), Encoding.UTF8); string json = streamReader.ReadToEnd(); Dictionary dictionary = MiniJSON.Deserialize(json) as Dictionary; string text = ""; if (dictionary != null && dictionary.TryGetValue("items", out var value) && value is List { Count: >0 } list) { text = ((!(list[0] is Dictionary dictionary2)) ? null : dictionary2["id"]?.ToString()) ?? ""; } if (string.IsNullOrEmpty(text)) { error = "no channelId"; return false; } string requestUriString2 = "https://www.googleapis.com/youtube/v3/liveBroadcasts?part=snippet&broadcastStatus=active&channelId=" + Uri.EscapeDataString(text); HttpWebRequest httpWebRequest2 = (HttpWebRequest)WebRequest.Create(requestUriString2); httpWebRequest2.Method = "GET"; httpWebRequest2.Timeout = 20000; httpWebRequest2.Headers["Authorization"] = "Bearer " + s.ytAccessToken; using HttpWebResponse httpWebResponse2 = (HttpWebResponse)httpWebRequest2.GetResponse(); using StreamReader streamReader2 = new StreamReader(httpWebResponse2.GetResponseStream(), Encoding.UTF8); string json2 = streamReader2.ReadToEnd(); if (!(MiniJSON.Deserialize(json2) is Dictionary dictionary3)) { error = "parse(liveBroadcasts+channelId)"; return false; } if (dictionary3.TryGetValue("items", out var value2) && value2 is List { Count: >0 } list2 && list2[0] is Dictionary dictionary4 && dictionary4.TryGetValue("snippet", out var value3) && value3 is Dictionary dictionary5) { liveChatId = ((!dictionary5.TryGetValue("liveChatId", out var value4)) ? "" : (value4?.ToString() ?? "")); return !string.IsNullOrEmpty(liveChatId); } error = "no active broadcast"; return false; } catch (WebException ex) { error = ex.Message; return false; } catch (Exception ex2) { error = ex2.Message; return false; } } private static bool TryResolveViaLiveBroadcastsMineOnly(ConnectionSlot s, out string liveChatId, out string error) { liveChatId = ""; error = null; try { string requestUriString = "https://www.googleapis.com/youtube/v3/liveBroadcasts?part=snippet&mine=true"; HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(requestUriString); httpWebRequest.Method = "GET"; httpWebRequest.Timeout = 20000; httpWebRequest.Headers["Authorization"] = "Bearer " + s.ytAccessToken; using HttpWebResponse httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse(); using StreamReader streamReader = new StreamReader(httpWebResponse.GetResponseStream(), Encoding.UTF8); string json = streamReader.ReadToEnd(); if (!(MiniJSON.Deserialize(json) is Dictionary dictionary)) { error = "parse(liveBroadcasts mine)"; return false; } if (dictionary.TryGetValue("items", out var value) && value is List { Count: >0 } list) { foreach (object item in list) { if (item is Dictionary dictionary2 && dictionary2.TryGetValue("snippet", out var value2) && value2 is Dictionary dictionary3) { object value3; string text = ((!dictionary3.TryGetValue("liveChatId", out value3)) ? "" : (value3?.ToString() ?? "")); if (!string.IsNullOrEmpty(text)) { liveChatId = text; return true; } } } } error = "no liveChatId found"; return false; } catch (WebException ex) { error = ex.Message; return false; } catch (Exception ex2) { error = ex2.Message; return false; } } public static void BeginDeviceFlow(string clientId, string clientSecret, Action onBegin, Action onComplete, Action onError) { } private static bool TryRefreshAccessToken(ConnectionSlot s, out string newAccess, out string error) { newAccess = ""; error = null; try { if (string.IsNullOrEmpty(s.ytRefreshToken) || string.IsNullOrEmpty(s.ytClientId) || string.IsNullOrEmpty(s.ytClientSecret)) { error = "refresh_token/client not set"; return false; } string url = "https://oauth2.googleapis.com/token"; string body = "client_id=" + Uri.EscapeDataString(s.ytClientId) + "&client_secret=" + Uri.EscapeDataString(s.ytClientSecret) + "&refresh_token=" + Uri.EscapeDataString(s.ytRefreshToken) + "&grant_type=refresh_token"; if (!HttpPostForm(url, body, out var text, out var err, out var _)) { error = err; return false; } if (!(MiniJSON.Deserialize(text) is Dictionary dictionary)) { error = "refresh parse error"; return false; } object value; string text2 = ((!dictionary.TryGetValue("access_token", out value)) ? "" : (value?.ToString() ?? "")); if (string.IsNullOrEmpty(text2)) { error = "no access_token in response"; return false; } s.ytAccessToken = (newAccess = text2); return true; } catch (Exception ex) { error = ex.Message; 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-urlencoded"; httpWebRequest.ContentLength = bytes.Length; using (Stream stream = httpWebRequest.GetRequestStream()) { stream.Write(bytes, 0, bytes.Length); } HttpWebResponse httpWebResponse = null; try { httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse(); } catch (WebException ex) { httpWebResponse = ex.Response as HttpWebResponse; if (httpWebResponse == null) { err = ex.Message; text = ""; return false; } } status = httpWebResponse.StatusCode; using StreamReader streamReader = new StreamReader(httpWebResponse.GetResponseStream(), Encoding.UTF8); text = streamReader.ReadToEnd(); return true; } catch (Exception ex2) { err = ex2.Message; text = ""; return false; } } public static List GetAllActiveChatIds(ConnectionSlot s) { List list = new List(); if (s == null || string.IsNullOrEmpty(s.ytAccessToken)) { return list; } try { string requestUriString = "https://www.googleapis.com/youtube/v3/liveBroadcasts?part=snippet&broadcastStatus=active&mine=true"; HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(requestUriString); httpWebRequest.Method = "GET"; httpWebRequest.Timeout = 20000; httpWebRequest.Headers["Authorization"] = "Bearer " + s.ytAccessToken; using HttpWebResponse httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse(); using StreamReader streamReader = new StreamReader(httpWebResponse.GetResponseStream(), Encoding.UTF8); string json = streamReader.ReadToEnd(); if (MiniJSON.Deserialize(json) is Dictionary dictionary && dictionary.TryGetValue("items", out var value) && value is List list2) { foreach (object item in list2) { if (item is Dictionary dictionary2 && dictionary2.TryGetValue("snippet", out var value2) && value2 is Dictionary dictionary3 && dictionary3.TryGetValue("liveChatId", out var value3) && value3 != null) { string text = value3.ToString(); if (!string.IsNullOrEmpty(text) && !list.Contains(text)) { list.Add(text); } } } } } catch (Exception ex) { RLog.Warn("[YouTube] GetAllActiveChatIds failed: " + ex.Message); } return list; } } } namespace RepoLiveChat.Net { public class ChatRouter { private RulesConfig _rules; private TableIndex _tables; private ControlConfig _ctrl; private PlatformConfig _plat; private string _configDir; private RateLimiter _limiter; public ChatRouter(RulesConfig rules, TableIndex tables, ControlConfig ctrl, PlatformConfig plat, string configDir) { _rules = rules; _tables = tables; _ctrl = ctrl; _plat = plat; _configDir = configDir; _limiter = new RateLimiter(_ctrl?.rate); } public void SetRules(RulesConfig rules) { _rules = rules; } public void SetTables(TableIndex tables) { _tables = tables; } public void Reload(RulesConfig rules, TableIndex tables, ControlConfig ctrl) { _rules = rules; _tables = tables; _ctrl = ctrl; _limiter = new RateLimiter(_ctrl?.rate); RLog.Info($"[Router] Configs reloaded ({(_rules?.rules?.Count).GetValueOrDefault()} rules)."); } public void OnChatMessage(string platform, string user, string text, string msgId) { if (!TryRoute(platform, user, text, out var _)) { } } public bool TryRoute(string platform, string user, string text, out Dictionary cmd) { cmd = null; if (_rules == null || _rules.rules == null) { return false; } if (_limiter != null && !_limiter.Allow(DateTime.UtcNow.Ticks, user)) { return false; } foreach (RulesConfig.Rule rule in _rules.rules) { if (rule.enabled && !string.IsNullOrEmpty(rule.name) && text.Contains(rule.name)) { RLog.Info("[Router] match rule='" + rule.name + "' type=" + rule.type + " id=" + rule.id); if (rule.type == "item") { cmd = BuildItemCommand(rule, platform, user, text); return true; } if (rule.type == "enemy") { cmd = BuildEnemyCommand(rule, platform, user, text); return true; } } } return false; } private Dictionary BuildItemCommand(RulesConfig.Rule rule, string platform, string user, string text) { string prefab = rule.prefab; if (string.IsNullOrEmpty(prefab)) { ItemRow itemById = _tables.GetItemById(rule.id); if (itemById != null) { prefab = itemById.prefab; } } string value = ((!string.IsNullOrEmpty(prefab) && !prefab.StartsWith("Items/")) ? ("Items/" + prefab) : prefab); Dictionary dictionary = new Dictionary(); dictionary["type"] = "item"; dictionary["ruleId"] = rule.id; dictionary["trigger"] = text; dictionary["prefabFullPath"] = value; dictionary["displayName"] = rule.nameJa; dictionary["user"] = user; return dictionary; } private Dictionary BuildEnemyCommand(RulesConfig.Rule rule, string platform, string user, string text) { string prefab = rule.prefab; if (string.IsNullOrEmpty(prefab)) { EnemyRow enemyById = _tables.GetEnemyById(rule.id); if (enemyById != null) { prefab = enemyById.prefab; } } string value = ((!string.IsNullOrEmpty(prefab) && !prefab.StartsWith("Enemies/")) ? ("Enemies/" + prefab) : prefab); Dictionary dictionary = new Dictionary(); dictionary["type"] = "enemy"; dictionary["ruleId"] = rule.id; dictionary["trigger"] = text; dictionary["prefabFullPath"] = value; dictionary["displayName"] = rule.nameJa; dictionary["user"] = user; return dictionary; } } } namespace RepoLiveChat.Models { public static class TableHelpers { public static ItemRow ResolveItemByIdOrName(TableIndex t, string key) { if (t == null || string.IsNullOrWhiteSpace(key)) { return null; } ItemRow itemById = t.GetItemById(key); if (itemById != null) { return itemById; } return t.GetItemByJa(key); } public static EnemyRow ResolveEnemyByIdOrName(TableIndex t, string key) { if (t == null || string.IsNullOrWhiteSpace(key)) { return null; } EnemyRow enemyById = t.GetEnemyById(key); if (enemyById != null) { return enemyById; } return t.GetEnemyByJa(key); } } public sealed class ItemRow { public string id = ""; public string name = ""; public string nameJa = ""; public string prefab = ""; public bool enabled = true; } public sealed class EnemyRow { public string id = ""; public string name = ""; public string nameJa = ""; public string prefab = ""; public bool enabled = true; } public sealed class TableIndex { public List Items = new List(); public List Enemies = new List(); private const string FILE_ITEMS = "repo-livechat.items.table.json"; private const string FILE_ENEMIES = "repo-livechat.enemies.table.json"; public static TableIndex LoadFromDisk(string dir, bool log) { TableIndex tableIndex = new TableIndex(); try { string path = Path.Combine(dir, "repo-livechat.items.table.json"); string path2 = Path.Combine(dir, "repo-livechat.enemies.table.json"); if (File.Exists(path)) { string json = File.ReadAllText(path, Encoding.UTF8); if (MiniJSON.Deserialize(json) is List list) { foreach (object item3 in list) { if (item3 is Dictionary d) { ItemRow item = new ItemRow { id = GetS(d, "id", GetS(d, "ID")), name = GetS(d, "name", GetS(d, "英語名")), nameJa = GetS(d, "nameJa", GetS(d, "日本語名")), prefab = GetS(d, "prefab"), enabled = GetB(d, "enabled", GetB(d, "有効", def: true)) }; tableIndex.Items.Add(item); } } } } if (File.Exists(path2)) { string json2 = File.ReadAllText(path2, Encoding.UTF8); if (MiniJSON.Deserialize(json2) is List list2) { foreach (object item4 in list2) { if (item4 is Dictionary d2) { EnemyRow item2 = new EnemyRow { id = GetS(d2, "id", GetS(d2, "ID")), name = GetS(d2, "name", GetS(d2, "英語名")), nameJa = GetS(d2, "nameJa", GetS(d2, "日本語名")), prefab = GetS(d2, "prefab"), enabled = GetB(d2, "enabled", GetB(d2, "有効", def: true)) }; tableIndex.Enemies.Add(item2); } } } } if (log) { ManualLogSource logS = RepoLiveChatPlugin.LogS; if (logS != null) { logS.LogInfo((object)$"[TableIndex] Load OK: Items={tableIndex.Items.Count}, Enemies={tableIndex.Enemies.Count}"); } for (int i = 0; i < Math.Min(3, tableIndex.Items.Count); i++) { ItemRow itemRow = tableIndex.Items[i]; ManualLogSource logS2 = RepoLiveChatPlugin.LogS; if (logS2 != null) { logS2.LogInfo((object)$"[TableIndex] Item[{i}] id={itemRow.id} ja='{itemRow.nameJa}' prefab='{itemRow.prefab}' enabled={itemRow.enabled}"); } } for (int j = 0; j < Math.Min(3, tableIndex.Enemies.Count); j++) { EnemyRow enemyRow = tableIndex.Enemies[j]; ManualLogSource logS3 = RepoLiveChatPlugin.LogS; if (logS3 != null) { logS3.LogInfo((object)$"[TableIndex] Enemy[{j}] id={enemyRow.id} ja='{enemyRow.nameJa}' prefab='{enemyRow.prefab}' enabled={enemyRow.enabled}"); } } } } catch (Exception ex) { ManualLogSource logS4 = RepoLiveChatPlugin.LogS; if (logS4 != null) { logS4.LogWarning((object)("[TableIndex] Load failed: " + ex)); } } return tableIndex; } public ItemRow GetItemById(string id) { return Items.Find((ItemRow x) => string.Equals(x.id, id, StringComparison.OrdinalIgnoreCase)); } public EnemyRow GetEnemyById(string id) { return Enemies.Find((EnemyRow x) => string.Equals(x.id, id, StringComparison.OrdinalIgnoreCase)); } public ItemRow GetItemByJa(string nJa) { return Items.Find((ItemRow x) => x.enabled && string.Equals(x.nameJa ?? "", nJa ?? "", StringComparison.Ordinal)); } public EnemyRow GetEnemyByJa(string nJa) { return Enemies.Find((EnemyRow x) => x.enabled && string.Equals(x.nameJa ?? "", nJa ?? "", StringComparison.Ordinal)); } public bool TryGetPrefabFullPath(string type, string id, out string fullPath) { fullPath = null; if (string.Equals(type, "item", StringComparison.OrdinalIgnoreCase)) { ItemRow itemById = GetItemById(id); if (!string.IsNullOrEmpty(itemById?.prefab)) { fullPath = Prefix("Items/", itemById.prefab); return true; } } else if (string.Equals(type, "enemy", StringComparison.OrdinalIgnoreCase)) { EnemyRow enemyById = GetEnemyById(id); if (!string.IsNullOrEmpty(enemyById?.prefab)) { fullPath = Prefix("Enemies/", enemyById.prefab); return true; } } return false; } private static string Prefix(string root, string prefab) { return string.IsNullOrEmpty(prefab) ? prefab : (prefab.StartsWith(root, StringComparison.Ordinal) ? prefab : (root + prefab)); } private static string GetS(Dictionary d, string k, string def = "") { object value; return (d != null && d.TryGetValue(k, out value) && value != null) ? value.ToString() : def; } private static bool GetB(Dictionary d, string k, bool def = false) { if (d == null || !d.TryGetValue(k, out var value) || value == null) { return def; } if (value is bool result) { return result; } string text = value.ToString().Trim().ToLowerInvariant(); if (text == "true" || text == "1") { return true; } if (text == "false" || text == "0") { return false; } return def; } public static int ItemsEnabled(TableIndex t) { return (t?.Items?.FindAll((ItemRow x) => x.enabled).Count).GetValueOrDefault(); } public static int EnemiesEnabled(TableIndex t) { return (t?.Enemies?.FindAll((EnemyRow x) => x.enabled).Count).GetValueOrDefault(); } } } namespace RepoLiveChat.Core { public static class MainThreadDispatcher { private sealed class Runner : MonoBehaviour { private void Update() { Action action = null; lock (_q) { if (_q.Count > 0) { action = _q.Dequeue(); } } while (action != null) { try { action(); } catch (Exception ex) { ManualLogSource logS = RepoLiveChatPlugin.LogS; if (logS != null) { logS.LogError((object)("[Core] dispatch error: " + ex)); } } lock (_q) { action = ((_q.Count > 0) ? _q.Dequeue() : null); } } } } private static GameObject _go; private static readonly Queue _q = new Queue(); private static bool _inited; public static void Ensure() { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Expected O, but got Unknown if (!_inited) { _inited = true; _go = new GameObject("RepoLiveChat.MainThreadDispatcher"); ((Object)_go).hideFlags = (HideFlags)61; Object.DontDestroyOnLoad((Object)(object)_go); _go.AddComponent(); ManualLogSource logS = RepoLiveChatPlugin.LogS; if (logS != null) { logS.LogInfo((object)"[Core] MainThreadDispatcher ready"); } } } public static void Enqueue(Action a) { if (a == null) { return; } lock (_q) { _q.Enqueue(a); } } public static void EnqueueOrNow(Action a) { if (a == null) { return; } if (_inited) { Enqueue(a); return; } try { a(); } catch (Exception ex) { ManualLogSource logS = RepoLiveChatPlugin.LogS; if (logS != null) { logS.LogError((object)("[Core] EnqueueOrNow error: " + ex)); } } } } public static class Normalizer { public static string Canonical(string s) { if (string.IsNullOrWhiteSpace(s)) { return ""; } s = s.Trim(); s = ToHalfWidthAscii(s).ToLowerInvariant(); while (s.StartsWith("!") || s.StartsWith("/") || s.StartsWith("/") || s.StartsWith("、") || s.StartsWith("!")) { s = s.Substring(1).TrimStart(); } while (s.Contains(" ")) { s = s.Replace(" ", " "); } return s; } public static string Key(string s) { return Canonical(s); } private static string ToHalfWidthAscii(string s) { char[] array = s.ToCharArray(); for (int i = 0; i < array.Length; i++) { int num = array[i]; if (num >= 65281 && num <= 65374) { array[i] = (char)(num - 65248); } else if (num == 12288) { array[i] = ' '; } } return new string(array); } } public sealed class RateLimiter { private readonly int _allPerSec; private readonly int _perViewerPerSec; private readonly Queue _global = new Queue(); private readonly Dictionary> _perViewer = new Dictionary>(); private static readonly long OneSec = TimeSpan.FromSeconds(1.0).Ticks; public RateLimiter(object rateSettings) { if (rateSettings != null) { Type type = rateSettings.GetType(); _allPerSec = GetIntField(type, rateSettings, "allPerSec"); _perViewerPerSec = GetIntField(type, rateSettings, "perViewerPerSec"); } } private int GetIntField(Type t, object o, string name) { PropertyInfo property = t.GetProperty(name); if (property != null && property.GetValue(o, null) is int result) { return result; } FieldInfo field = t.GetField(name); if (field != null && field.GetValue(o) is int result2) { return result2; } return 0; } public bool Allow(long nowTicks, string viewer) { if (_allPerSec > 0) { Prune(_global, nowTicks); if (_global.Count >= _allPerSec) { return false; } } if (_perViewerPerSec > 0 && !string.IsNullOrEmpty(viewer)) { if (!_perViewer.TryGetValue(viewer, out var value)) { value = (_perViewer[viewer] = new Queue()); } Prune(value, nowTicks); if (value.Count >= _perViewerPerSec) { return false; } value.Enqueue(nowTicks); } if (_allPerSec > 0) { _global.Enqueue(nowTicks); } return true; } private void Prune(Queue q, long now) { while (q.Count > 0 && now - q.Peek() > OneSec) { q.Dequeue(); } } } public sealed class RoleService : MonoBehaviour { public enum Role { Single, Host, Client } private static RoleService _instance; private static Type _pnType; private static PropertyInfo _piIsConnected; private static PropertyInfo _piInRoom; private static PropertyInfo _piIsMasterClient; private static bool _pnLookupTried; public static RoleService Instance { get { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Expected O, but got Unknown if ((Object)(object)_instance == (Object)null) { GameObject val = new GameObject("__RepoLiveChat.RoleService"); _instance = val.AddComponent(); Object.DontDestroyOnLoad((Object)(object)val); } return _instance; } } public static Role Current { get; private set; } public static event Action OnChanged; 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.DontDestroyOnLoad((Object)(object)((Component)this).gameObject); Set(Role.Single, "init"); } public static Role ForceProbe(string reason = "ui") { if (EnsurePhotonReflection()) { try { bool flag = SafeGetBool(_piIsConnected); bool flag2 = SafeGetBool(_piInRoom); bool flag3 = SafeGetBool(_piIsMasterClient); Role role = ((flag && flag2) ? (flag3 ? Role.Host : Role.Client) : Role.Single); if (role != Current) { Set(role, reason); } else { RLog.Info($"[ROLE] {role} (no change, {reason})"); } return role; } catch (Exception ex) { RLog.Warn("[ROLE] Photon reflection read failed: " + ex.Message); } } else { RLog.Warn("[ROLE] PhotonNetwork not found (return Single)."); } if (Current != Role.Single) { Set(Role.Single, "fallback"); } return Role.Single; } private static void Set(Role r, string why) { Current = r; RLog.Info($"[ROLE] {r} ({why})"); try { RoleService.OnChanged?.Invoke(r); } catch { } } private static bool EnsurePhotonReflection() { if (_pnType != null) { return true; } if (_pnLookupTried) { return false; } _pnLookupTried = true; try { _pnType = Type.GetType("Photon.Pun.PhotonNetwork, PhotonUnityNetworking", throwOnError: false); if (_pnType == null) { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { _pnType = assembly.GetType("Photon.Pun.PhotonNetwork", throwOnError: false); if (_pnType != null) { break; } } } if (_pnType == null) { return false; } BindingFlags bindingAttr = BindingFlags.Static | BindingFlags.Public; _piIsConnected = _pnType.GetProperty("IsConnected", bindingAttr); _piInRoom = _pnType.GetProperty("InRoom", bindingAttr); _piIsMasterClient = _pnType.GetProperty("IsMasterClient", bindingAttr); return _piIsConnected != null && _piInRoom != null && _piIsMasterClient != null; } catch { return false; } } private static bool SafeGetBool(PropertyInfo pi) { if (pi == null) { return false; } object value = pi.GetValue(null, null); bool flag = default(bool); int num; if (value is bool) { flag = (bool)value; num = 1; } else { num = 0; } return (byte)((uint)num & (flag ? 1u : 0u)) != 0; } } } namespace RepoLiveChat.Config { public sealed class ConnectionSlot { public string twitchChannel = ""; public string twitchOAuthToken = ""; public string twClientId = ""; public string ytClientId = ""; public string ytClientSecret = ""; public string ytAccessToken = ""; public string ytRefreshToken = ""; public string ytLiveChatId = ""; public string ytPreferredChannelUC = ""; public bool Enabled { get; set; } = false; public string Platform { get; set; } = "None"; } public sealed class PlatformConfig { public ConnectionSlot Slot1 { get; set; } = new ConnectionSlot(); public ConnectionSlot Slot2 { get; set; } = new ConnectionSlot(); } public sealed class RateConfig { public int allPerSec = 3; public int perViewerPerSec = 0; } public sealed class ControlConfig { public RateConfig rate = new RateConfig(); public int dedupeWindowSec = 10; public bool allowInMenu = true; public bool allowInGameOnly = false; public string logLevel = "DEBUG"; public int NoCommentSpawnMinutes1 = 0; public int NoCommentSpawnMinutes2 = 0; } public sealed class RulesConfig { public sealed class Rule { public string id = ""; public string name = ""; public string nameJa = ""; public string type = "item"; public int cooldownSec = 10; public bool enabled = true; public string prefab = ""; public Dictionary args; } public List rules = new List(); } public static class ConfigFiles { private const string FILE_PLATFORM = "repo-livechat.platform.json"; private const string FILE_CONTROL = "repo-livechat.control.json"; private const string FILE_RULES = "repo-livechat.commands.json"; private static Dictionary SerializeSlot(ConnectionSlot s) { if (s == null) { s = new ConnectionSlot(); } return new Dictionary { ["Enabled"] = s.Enabled, ["Platform"] = s.Platform ?? "None", ["twitchChannel"] = s.twitchChannel ?? "", ["twitchOAuthToken"] = s.twitchOAuthToken ?? "", ["twClientId"] = s.twClientId ?? "", ["ytClientId"] = s.ytClientId ?? "", ["ytClientSecret"] = s.ytClientSecret ?? "", ["ytAccessToken"] = s.ytAccessToken ?? "", ["ytRefreshToken"] = s.ytRefreshToken ?? "", ["ytLiveChatId"] = s.ytLiveChatId ?? "", ["ytPreferredChannelUC"] = s.ytPreferredChannelUC ?? "" }; } private static ConnectionSlot DeserializeSlot(Dictionary obj) { if (obj == null) { return new ConnectionSlot(); } ConnectionSlot connectionSlot = new ConnectionSlot(); connectionSlot.Enabled = GetB(obj, "Enabled", connectionSlot.Enabled); connectionSlot.Platform = GetS(obj, "Platform", connectionSlot.Platform); connectionSlot.twitchChannel = GetS(obj, "twitchChannel", connectionSlot.twitchChannel); connectionSlot.twitchOAuthToken = GetS(obj, "twitchOAuthToken", connectionSlot.twitchOAuthToken); connectionSlot.twClientId = GetS(obj, "twClientId", connectionSlot.twClientId); connectionSlot.ytClientId = GetS(obj, "ytClientId", connectionSlot.ytClientId); connectionSlot.ytClientSecret = GetS(obj, "ytClientSecret", connectionSlot.ytClientSecret); connectionSlot.ytAccessToken = GetS(obj, "ytAccessToken", connectionSlot.ytAccessToken); connectionSlot.ytRefreshToken = GetS(obj, "ytRefreshToken", connectionSlot.ytRefreshToken); connectionSlot.ytLiveChatId = GetS(obj, "ytLiveChatId", connectionSlot.ytLiveChatId); connectionSlot.ytPreferredChannelUC = GetS(obj, "ytPreferredChannelUC", connectionSlot.ytPreferredChannelUC); return connectionSlot; } public static PlatformConfig LoadPlatform(string dir) { string path = Path.Combine(dir, "repo-livechat.platform.json"); try { if (!File.Exists(path)) { return new PlatformConfig(); } string json = File.ReadAllText(path, Encoding.UTF8); if (!(MiniJSON.Deserialize(json) is Dictionary dictionary)) { return new PlatformConfig(); } PlatformConfig platformConfig = new PlatformConfig(); platformConfig.Slot1 = DeserializeSlot(GetD(dictionary, "Slot1")); platformConfig.Slot2 = DeserializeSlot(GetD(dictionary, "Slot2")); if (dictionary.ContainsKey("platform") && !dictionary.ContainsKey("Slot1")) { RLog.Info("[Config] Migrating old platform config to Slot1..."); platformConfig.Slot1.Enabled = true; platformConfig.Slot1.Platform = GetS(dictionary, "platform", "None"); platformConfig.Slot1.twitchChannel = GetS(dictionary, "twChannel"); platformConfig.Slot1.twitchOAuthToken = GetS(dictionary, "twOAuthToken"); platformConfig.Slot1.twClientId = GetS(dictionary, "twClientId"); platformConfig.Slot1.ytClientId = GetS(dictionary, "ytClientId"); platformConfig.Slot1.ytClientSecret = GetS(dictionary, "ytClientSecret"); platformConfig.Slot1.ytAccessToken = GetS(dictionary, "ytAccessToken"); platformConfig.Slot1.ytRefreshToken = GetS(dictionary, "ytRefreshToken"); platformConfig.Slot1.ytLiveChatId = GetS(dictionary, "ytLiveChatId"); } return platformConfig; } catch (Exception ex) { ManualLogSource logS = RepoLiveChatPlugin.LogS; if (logS != null) { logS.LogWarning((object)("[Config] Load platform failed: " + ex.Message)); } return new PlatformConfig(); } } public static void SavePlatform(string dir, PlatformConfig p) { string text = Path.Combine(dir, "repo-livechat.platform.json"); try { Dictionary data = new Dictionary { ["Slot1"] = SerializeSlot(p.Slot1), ["Slot2"] = SerializeSlot(p.Slot2) }; WriteJson(text, data); ManualLogSource logS = RepoLiveChatPlugin.LogS; if (logS != null) { logS.LogInfo((object)("[Config] Save platform OK: " + text)); } } catch (Exception ex) { ManualLogSource logS2 = RepoLiveChatPlugin.LogS; if (logS2 != null) { logS2.LogWarning((object)("[Config] Save platform failed: " + ex.Message)); } } } public static ControlConfig LoadControl(string dir) { string path = Path.Combine(dir, "repo-livechat.control.json"); try { if (!File.Exists(path)) { return new ControlConfig(); } string json = File.ReadAllText(path, Encoding.UTF8); if (!(MiniJSON.Deserialize(json) is Dictionary d)) { return new ControlConfig(); } ControlConfig controlConfig = new ControlConfig(); Dictionary d2 = GetD(d, "rate"); if (d2 != null) { controlConfig.rate.allPerSec = GetI(d2, "allPerSec", controlConfig.rate.allPerSec); controlConfig.rate.perViewerPerSec = GetI(d2, "perViewerPerSec", controlConfig.rate.perViewerPerSec); string s = GetS(d2, "per"); int i = GetI(d2, "count", -1); if (i >= 0) { if (s == "sec") { controlConfig.rate.allPerSec = i; } else if (s == "min") { controlConfig.rate.allPerSec = Math.Max(0, i / 60); } } } controlConfig.dedupeWindowSec = GetI(d, "dedupeWindowSec", controlConfig.dedupeWindowSec); controlConfig.allowInMenu = GetB(d, "allowInMenu", controlConfig.allowInMenu); controlConfig.allowInGameOnly = GetB(d, "allowInGameOnly", controlConfig.allowInGameOnly); controlConfig.logLevel = GetS(d, "logLevel", controlConfig.logLevel); controlConfig.NoCommentSpawnMinutes1 = GetI(d, "NoCommentSpawnMinutes1", controlConfig.NoCommentSpawnMinutes1); controlConfig.NoCommentSpawnMinutes2 = GetI(d, "NoCommentSpawnMinutes2", controlConfig.NoCommentSpawnMinutes2); return controlConfig; } catch (Exception ex) { ManualLogSource logS = RepoLiveChatPlugin.LogS; if (logS != null) { logS.LogWarning((object)("[Config] Load control failed: " + ex.Message)); } return new ControlConfig(); } } public static void SaveControl(string dir, ControlConfig c) { string text = Path.Combine(dir, "repo-livechat.control.json"); try { Dictionary data = new Dictionary { ["rate"] = new Dictionary { ["allPerSec"] = c.rate?.allPerSec ?? 0, ["perViewerPerSec"] = c.rate?.perViewerPerSec ?? 0 }, ["dedupeWindowSec"] = c.dedupeWindowSec, ["allowInMenu"] = c.allowInMenu, ["allowInGameOnly"] = c.allowInGameOnly, ["logLevel"] = (c.logLevel ?? "DEBUG").ToUpperInvariant(), ["NoCommentSpawnMinutes1"] = c.NoCommentSpawnMinutes1, ["NoCommentSpawnMinutes2"] = c.NoCommentSpawnMinutes2 }; WriteJson(text, data); ManualLogSource logS = RepoLiveChatPlugin.LogS; if (logS != null) { logS.LogInfo((object)("[Config] Save control OK: " + text)); } } catch (Exception ex) { ManualLogSource logS2 = RepoLiveChatPlugin.LogS; if (logS2 != null) { logS2.LogWarning((object)("[Config] Save control failed: " + ex.Message)); } } } public static RulesConfig LoadRules(string dir) { string path = Path.Combine(dir, "repo-livechat.commands.json"); try { if (!File.Exists(path)) { return new RulesConfig(); } string json = File.ReadAllText(path, Encoding.UTF8); if (!(MiniJSON.Deserialize(json) is Dictionary dictionary)) { return new RulesConfig(); } RulesConfig rulesConfig = new RulesConfig(); if (dictionary.TryGetValue("rules", out var value) && value is List list) { foreach (object item in list) { if (item is Dictionary dictionary2) { RulesConfig.Rule rule = new RulesConfig.Rule { id = GetS(dictionary2, "id"), name = GetS(dictionary2, "name"), nameJa = GetS(dictionary2, "nameJa"), type = GetS(dictionary2, "type", "item"), cooldownSec = GetI(dictionary2, "cooldownSec", 10), enabled = GetB(dictionary2, "enabled", def: true), prefab = GetS(dictionary2, "prefab") }; if (dictionary2.TryGetValue("args", out var value2) && value2 is Dictionary args) { rule.args = args; } rulesConfig.rules.Add(rule); } } } return rulesConfig; } catch (Exception ex) { ManualLogSource logS = RepoLiveChatPlugin.LogS; if (logS != null) { logS.LogWarning((object)("[Config] Load rules failed: " + ex.Message)); } return new RulesConfig(); } } public static void SaveRules(string dir, RulesConfig r) { string text = Path.Combine(dir, "repo-livechat.commands.json"); try { List list = new List(); foreach (RulesConfig.Rule rule in r.rules) { Dictionary dictionary = new Dictionary { ["id"] = rule.id ?? "", ["name"] = rule.name ?? "", ["nameJa"] = rule.nameJa ?? "", ["type"] = rule.type ?? "item", ["cooldownSec"] = rule.cooldownSec, ["enabled"] = rule.enabled, ["prefab"] = rule.prefab ?? "" }; if (rule.args != null) { dictionary["args"] = rule.args; } list.Add(dictionary); } Dictionary data = new Dictionary { ["rules"] = list }; WriteJson(text, data); ManualLogSource logS = RepoLiveChatPlugin.LogS; if (logS != null) { logS.LogInfo((object)("[Config] Save rules OK: " + text)); } } catch (Exception ex) { ManualLogSource logS2 = RepoLiveChatPlugin.LogS; if (logS2 != null) { logS2.LogWarning((object)("[Config] Save rules failed: " + ex.Message)); } } } private static void WriteJson(string path, object data) { string contents = MiniJSON.Serialize(data); File.WriteAllText(path, contents, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); } private static string GetS(Dictionary d, string k, string def = "") { object value; return (d != null && d.TryGetValue(k, out value) && value != null) ? value.ToString() : def; } private static int GetI(Dictionary d, string k, int def = 0) { if (d == null || !d.TryGetValue(k, out var value) || value == null) { return def; } int result; return int.TryParse(value.ToString(), out result) ? result : def; } private static bool GetB(Dictionary d, string k, bool def = false) { if (d == null || !d.TryGetValue(k, out var value) || value == null) { return def; } if (value is bool result) { return result; } string text = value.ToString().Trim().ToLowerInvariant(); if (text == "true" || text == "1") { return true; } if (text == "false" || text == "0") { return false; } return def; } private static Dictionary GetD(Dictionary d, string k) { if (d != null && d.TryGetValue(k, out var value) && value is Dictionary result) { return result; } return null; } } } namespace RepoLiveChat.Actions { public static class ActionExecutorsCompat { private class EnemyRequest { public Dictionary cmd; public string displayName; public string ruleId; public string user; } private class _SpawnNudger : MonoBehaviour { public int frames = 3; public float baseLift = 0.5f; private void LateUpdate() { //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: 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) if (frames-- <= 0) { Object.Destroy((Object)(object)this); return; } Transform transform = ((Component)this).transform; Vector3 val = transform.position + Vector3.up * 3f; int num = ~LayerMask.GetMask(new string[2] { "Player", "Ignore Raycast" }); RaycastHit val2 = default(RaycastHit); if (Physics.Raycast(val, Vector3.down, ref val2, 6f, num, (QueryTriggerInteraction)1)) { transform.position = new Vector3(transform.position.x, ((RaycastHit)(ref val2)).point.y + baseLift, transform.position.z); } } } private struct Anchor { public Transform t; public string src; public float footY; } private class _RetryHub : MonoBehaviour { } private const float kItemLiftY = 0.5f; private const int kNudgeFrames = 3; private const float kEnemyMinDist = 5f; private const float kEnemyMaxDist = 7f; private const float kEnemyMinIntervalSec = 10f; private static readonly Dictionary> _enemyUserQueues = new Dictionary>(); private static readonly List _enemyUserOrder = new List(); private static int _rrIndex = 0; private static int _totalEnemyCount = 0; private static readonly object _queueLock = new object(); private static bool _enemyRunnerActive = false; private static float _enemyLastExecAt = -9999f; private static _RetryHub _retryHub; public static void ClearEnemyQueue() { lock (_queueLock) { _enemyUserQueues.Clear(); _enemyUserOrder.Clear(); _totalEnemyCount = 0; _rrIndex = 0; } RLog.Info("[GUARD][Enemy] Queue cleared manually."); UIOverlay.TryToast("敵スポーン予約を全て削除しました"); } public static int GetEnemyQueueCount() { return _totalEnemyCount; } private static Vector3? GetCmdPosition(Dictionary cmd) { //IL_0068: Unknown result type (might be due to invalid IL or missing references) if (cmd != null && cmd.ContainsKey("pos_x") && cmd.ContainsKey("pos_y") && cmd.ContainsKey("pos_z")) { try { float num = Convert.ToSingle(cmd["pos_x"]); float num2 = Convert.ToSingle(cmd["pos_y"]); float num3 = Convert.ToSingle(cmd["pos_z"]); return new Vector3(num, num2, num3); } catch { } } return null; } public static bool TryExecute(Dictionary cmd) { try { if (cmd == null) { RLog.Warn("[Action] cmd is null"); return false; } bool b = GetB(cmd, "__host_invoke"); string type = GetS(cmd, "type"); string prefabFull = GetS(cmd, "prefabFullPath"); if (string.IsNullOrEmpty(prefabFull)) { prefabFull = GetS(GetD(cmd, "args"), "prefabFullPath"); } string rid = GetS(cmd, "ruleId"); string displayName = GetS(cmd, "displayName"); string text = GetS(cmd, "user"); if (string.IsNullOrEmpty(text)) { text = "Unknown"; } int senderActorNumber = GetI(cmd, "senderActorNumber", -1); if (!b) { MainThreadDispatcher.Ensure(); MainThreadDispatcher.Enqueue(delegate { //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_007b: 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_00bd: Unknown result type (might be due to invalid IL or missing references) try { if (senderActorNumber == -1 && PhotonNetwork.LocalPlayer != null) { senderActorNumber = PhotonNetwork.LocalPlayer.ActorNumber; } cmd["senderActorNumber"] = senderActorNumber; if ((Object)(object)Camera.main != (Object)null) { Vector3 position = ((Component)Camera.main).transform.position; cmd["pos_x"] = position.x; cmd["pos_y"] = position.y; cmd["pos_z"] = position.z; } string json = JsonConvert.SerializeObject((object)cmd); PhotonRpcBridge.SendToHost(json); } catch (Exception ex2) { RLog.Warn("[Action] forward failed: " + ex2.Message); } }); return true; } if (string.IsNullOrEmpty(type) || string.IsNullOrEmpty(prefabFull)) { UIOverlay.TryToast("コマンド不正", 2.5f); return false; } type = type.ToLowerInvariant(); if (type == "enemy") { lock (_queueLock) { if (!_enemyUserQueues.ContainsKey(text)) { _enemyUserQueues[text] = new Queue(); _enemyUserOrder.Add(text); } _enemyUserQueues[text].Enqueue(new EnemyRequest { cmd = cmd, displayName = displayName, ruleId = rid, user = text }); _totalEnemyCount++; } RLog.Info($"[GUARD][Enemy] ENQUEUE ruleId={rid} user={text} total={_totalEnemyCount}"); UIOverlay.TryToast($"敵生成 予約 ({_totalEnemyCount}件待ち)", 1f); EnsureEnemyRunner(); return true; } bool ok = false; Vector3? explicitPos = GetCmdPosition(cmd); MainThreadDispatcher.Ensure(); MainThreadDispatcher.Enqueue(delegate { try { if (type == "item") { ok = ExecItem_InventoryOrDrop(prefabFull, displayName, rid, senderActorNumber, explicitPos); } else { ok = false; } } catch (Exception ex2) { RLog.Error("[Action] exception: " + ex2); ok = false; } }); return true; } catch (Exception ex) { RLog.Error("[Action] TryExecute exception: " + ex); return false; } } private static void EnsureEnemyRunner() { if (!_enemyRunnerActive) { _enemyRunnerActive = true; ((MonoBehaviour)EnsureRetryHub()).StartCoroutine(CoEnemyRunner()); } } private static IEnumerator CoEnemyRunner() { while (true) { EnemyRequest req = null; lock (_queueLock) { if (_enemyUserOrder.Count == 0) { break; } int loopCount = 0; while (loopCount < _enemyUserOrder.Count) { if (_rrIndex >= _enemyUserOrder.Count) { _rrIndex = 0; } string currentUser = _enemyUserOrder[_rrIndex]; if (_enemyUserQueues.TryGetValue(currentUser, out var q) && q.Count > 0) { req = q.Dequeue(); _totalEnemyCount--; if (q.Count == 0) { _enemyUserQueues.Remove(currentUser); _enemyUserOrder.RemoveAt(_rrIndex); } else { _rrIndex++; } break; } _enemyUserQueues.Remove(currentUser); _enemyUserOrder.RemoveAt(_rrIndex); loopCount++; q = null; } goto IL_01d7; } IL_01d7: if (req == null) { break; } float now = Time.realtimeSinceStartup; float wait = ((_enemyLastExecAt < 0f) ? 0f : Mathf.Max(0f, 10f - (now - _enemyLastExecAt))); if (wait > 0f) { yield return (object)new WaitForSeconds(wait); } string prefab = GetS(req.cmd, "prefabFullPath"); int senderActorNumber = GetI(req.cmd, "senderActorNumber", -1); Vector3? explicitPos = GetCmdPosition(req.cmd); RLog.Info("[GUARD][Enemy] BEGIN ruleId=" + req.ruleId + " user=" + req.user + " prefab='" + prefab + "'"); bool completed = false; bool okMain = false; float waitStart = Time.realtimeSinceStartup; try { MainThreadDispatcher.Ensure(); MainThreadDispatcher.Enqueue(delegate { try { bool decided = false; okMain = SpawnEnemyVanillaWay(prefab, senderActorNumber, explicitPos, out decided); if (okMain && decided) { UIOverlay.TryToast("敵生成: " + (req.displayName ?? TrimLastSegment(prefab)), 2f); } else { RLog.Error("[GUARD][Enemy][Runner] スポーンに失敗しました。Prefab: '" + prefab + "'"); } } catch (Exception ex) { RLog.Error("[GUARD][Enemy][Runner] ランナーループ内全体で重大な例外を検知しました:\n" + ex.ToString()); okMain = false; } finally { completed = true; } }); } catch { completed = true; } while (!completed && Time.realtimeSinceStartup - waitStart < 10f) { yield return null; } _enemyLastExecAt = Time.realtimeSinceStartup; RLog.Info($"[GUARD][Enemy] LEAVE ok={okMain}"); } _enemyRunnerActive = false; } private static bool ExecItem_InventoryOrDrop(string prefabFullPath, string displayName, string ruleId, int senderActorNumber, Vector3? explicitPos) { //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Unknown result type (might be due to invalid IL or missing references) //IL_00ed: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Unknown result type (might be due to invalid IL or missing references) //IL_00fc: Unknown result type (might be due to invalid IL or missing references) //IL_0101: Unknown result type (might be due to invalid IL or missing references) //IL_0106: Unknown result type (might be due to invalid IL or missing references) //IL_0135: Unknown result type (might be due to invalid IL or missing references) //IL_013a: Unknown result type (might be due to invalid IL or missing references) //IL_0144: Unknown result type (might be due to invalid IL or missing references) //IL_0149: Unknown result type (might be due to invalid IL or missing references) //IL_014e: Unknown result type (might be due to invalid IL or missing references) //IL_0156: Unknown result type (might be due to invalid IL or missing references) //IL_015b: Unknown result type (might be due to invalid IL or missing references) //IL_0188: 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_016e: Unknown result type (might be due to invalid IL or missing references) //IL_016f: Unknown result type (might be due to invalid IL or missing references) try { string text = TrimLastSegment(prefabFullPath); List list = new List { prefabFullPath, "Items/" + text, text }; string text2 = null; foreach (string item in list) { if (string.IsNullOrEmpty(item) || !((Object)(object)Resources.Load(item) != (Object)null)) { continue; } text2 = item; break; } if (string.IsNullOrEmpty(text2)) { RLog.Error("[Mod] Item prefab not found in Resources: " + prefabFullPath); return false; } Quaternion val = Quaternion.identity; Vector3 val2; if (explicitPos.HasValue && explicitPos.Value != Vector3.zero) { val2 = FindGroundOrFallback(explicitPos.Value, 2f, 10f) + Vector3.up * 0.5f; } else { Anchor playerAnchorByActorNumber = GetPlayerAnchorByActorNumber(senderActorNumber); if ((Object)(object)playerAnchorByActorNumber.t == (Object)null) { return false; } val2 = playerAnchorByActorNumber.t.position + Vector3.up * 0.5f; val = playerAnchorByActorNumber.t.rotation; } GameObject val3 = null; if (IsMultiplayer()) { val3 = PhotonNetwork.Instantiate(text2, val2, val, (byte)0, (object[])null); } else { GameObject val4 = Resources.Load(text2); val3 = Object.Instantiate(val4, val2, val); } if ((Object)(object)val3 == (Object)null) { return false; } AttachNudger(val3, 0.05f); string text3 = ((!string.IsNullOrEmpty(displayName)) ? displayName : text); UIOverlay.TryToast("アイテム生成: " + text3, 2f); return true; } catch (Exception ex) { RLog.Error("[Action][Item] spawn failed: " + ex); return false; } } private static bool SpawnEnemyVanillaWay(string prefabFullPath, int senderActorNumber, Vector3? explicitPos, out bool decided) { //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00b9: Unknown result type (might be due to invalid IL or missing references) //IL_00c6: 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_00e1: Unknown result type (might be due to invalid IL or missing references) //IL_00e6: Unknown result type (might be due to invalid IL or missing references) //IL_0110: Unknown result type (might be due to invalid IL or missing references) //IL_0102: 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_0148: Unknown result type (might be due to invalid IL or missing references) //IL_0149: Unknown result type (might be due to invalid IL or missing references) //IL_0130: Unknown result type (might be due to invalid IL or missing references) //IL_0135: Unknown result type (might be due to invalid IL or missing references) //IL_0170: Unknown result type (might be due to invalid IL or missing references) //IL_0171: Unknown result type (might be due to invalid IL or missing references) //IL_0329: Unknown result type (might be due to invalid IL or missing references) decided = false; try { string text = TrimLastSegment(prefabFullPath); List list = new List { prefabFullPath, "Enemies/" + text, text }; string text2 = null; foreach (string item in list) { if (string.IsNullOrEmpty(item) || !((Object)(object)Resources.Load(item) != (Object)null)) { continue; } text2 = item; break; } if (string.IsNullOrEmpty(text2)) { RLog.Error("[Mod] Enemy prefab not found in Resources: " + prefabFullPath); return false; } Quaternion val = Quaternion.identity; Vector3 val2; if (explicitPos.HasValue && explicitPos.Value != Vector3.zero) { val2 = explicitPos.Value; } else { Anchor playerAnchorByActorNumber = GetPlayerAnchorByActorNumber(senderActorNumber); val2 = (((Object)(object)playerAnchorByActorNumber.t != (Object)null) ? playerAnchorByActorNumber.t.position : Vector3.zero); if ((Object)(object)playerAnchorByActorNumber.t != (Object)null) { val = playerAnchorByActorNumber.t.rotation; } } GameObject val3 = null; if (IsMultiplayer()) { val3 = PhotonNetwork.InstantiateRoomObject(text2, val2, val, (byte)0, (object[])null); } else { GameObject val4 = Resources.Load(text2); if ((Object)(object)val4 != (Object)null) { val3 = Object.Instantiate(val4, val2, val); } } if ((Object)(object)val3 != (Object)null) { RLog.Info("[Mod] SUCCESS: Instantiated " + ((Object)val3).name + ". Injecting into Vanilla logic..."); Type type = Type.GetType("LevelGenerator, Assembly-CSharp") ?? FindType("LevelGenerator"); if (type != null) { object obj = GetStaticField(type, "Instance") ?? Object.FindObjectOfType(type); if (obj != null) { int num = (int)(GetFieldOrProperty(type, obj, "EnemiesSpawned") ?? ((object)0)); SetFieldOrProperty(type, obj, "EnemiesSpawned", num + 1); int num2 = (int)(GetFieldOrProperty(type, obj, "EnemiesSpawnTarget") ?? ((object)0)); SetFieldOrProperty(type, obj, "EnemiesSpawnTarget", num2 + 1); } } Type type2 = Type.GetType("EnemyParent, Assembly-CSharp") ?? FindType("EnemyParent"); if (type2 != null) { Component component = val3.GetComponent(type2); if ((Object)(object)component != (Object)null) { SetFieldOrProperty(type2, component, "SetupDone", true); Type type3 = Type.GetType("Enemy, Assembly-CSharp") ?? FindType("Enemy"); if (type3 != null) { Component componentInChildren = val3.GetComponentInChildren(type3); if ((Object)(object)componentInChildren != (Object)null) { type3.GetMethod("EnemyTeleported", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.Invoke(componentInChildren, new object[1] { val2 }); } } Type type4 = Type.GetType("EnemyDirector, Assembly-CSharp") ?? FindType("EnemyDirector"); if (type4 != null) { object obj2 = GetStaticField(type4, "instance") ?? Object.FindObjectOfType(type4); if (obj2 != null) { type4.GetMethod("FirstSpawnPointAdd", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.Invoke(obj2, new object[1] { component }); } } } } decided = true; return true; } RLog.Error("[Mod] PhotonNetwork.Instantiate failed."); return false; } catch (Exception ex) { RLog.Error("[Action][Enemy] Vanilla Style Spawn failed: " + ex); return false; } } private static GameObject FindLocalPlayerRobust() { try { PhotonView[] array = Object.FindObjectsOfType(); PhotonView[] array2 = array; foreach (PhotonView val in array2) { if ((Object)(object)val != (Object)null && (Object)(object)((Component)val).gameObject != (Object)null && val.IsMine && ((Component)val).gameObject.activeInHierarchy) { CharacterController componentInParent = ((Component)val).GetComponentInParent(); if ((Object)(object)componentInParent != (Object)null) { return ((Component)componentInParent).gameObject; } if (((Object)((Component)val).gameObject).name.IndexOf("Player", StringComparison.OrdinalIgnoreCase) != -1 || ((Component)val).gameObject.CompareTag("Player")) { return ((Component)val).gameObject; } } } Camera main = Camera.main; if ((Object)(object)main != (Object)null) { Transform root = ((Component)main).transform.root; if (((Component)root).CompareTag("Player") || ((Object)root).name.Contains("Player")) { return ((Component)root).gameObject; } return ((Component)main).gameObject; } GameObject val2 = GameObject.FindWithTag("Player"); if ((Object)(object)val2 != (Object)null) { return val2; } } catch { } return null; } private static float SampleGroundY_SphereDown(Vector3 baseXZ, float startY, float castUp, float castDown, float radius, out bool hit) { //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) int num = GroundMask(); Vector3 val = default(Vector3); ((Vector3)(ref val))..ctor(baseXZ.x, startY + castUp, baseXZ.z); float num2 = castUp + castDown; RaycastHit val2 = default(RaycastHit); if (Physics.SphereCast(val, radius, Vector3.down, ref val2, num2, num, (QueryTriggerInteraction)1)) { hit = true; return ((RaycastHit)(ref val2)).point.y; } hit = false; return float.MinValue; } private static int GroundMask() { int num = 0; try { string[] array = new string[7] { "Default", "Ground", "Terrain", "Environment", "Map", "World", "StaticGeometry" }; num = LayerMask.GetMask(array); if (num == 0) { num = -1; } } catch { num = -1; } return num; } private static void AttachNudger(GameObject go, float baseLift) { if (!((Object)(object)go == (Object)null)) { _SpawnNudger spawnNudger = go.GetComponent<_SpawnNudger>(); if ((Object)(object)spawnNudger == (Object)null) { spawnNudger = go.AddComponent<_SpawnNudger>(); } spawnNudger.frames = 3; spawnNudger.baseLift = baseLift; } } private static Vector3 FindGroundOrFallback(Vector3 around, float rayUp = 3f, float rayDown = 30f) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) Vector3 val = around + Vector3.up * rayUp; int num = GroundMask(); RaycastHit val2 = default(RaycastHit); if (Physics.Raycast(val, Vector3.down, ref val2, rayUp + rayDown, num, (QueryTriggerInteraction)1)) { return ((RaycastHit)(ref val2)).point + Vector3.up * 0.05f; } return around; } private static Anchor GetPlayerAnchorByActorNumber(int actorNumber) { //IL_01a7: Unknown result type (might be due to invalid IL or missing references) if (actorNumber <= 0) { return GetPlayerAnchor_LocalOrFallback(); } try { Player val = ((PhotonNetwork.InRoom && PhotonNetwork.CurrentRoom != null) ? PhotonNetwork.CurrentRoom.GetPlayer(actorNumber, false) : null); if (val == null) { return GetPlayerAnchor_LocalOrFallback(); } PhotonView[] array = Object.FindObjectsOfType(); PhotonView val2 = null; int num = -1; PhotonView[] array2 = array; foreach (PhotonView val3 in array2) { if ((Object)(object)val3 != (Object)null && (Object)(object)((Component)val3).gameObject != (Object)null && val3.OwnerActorNr == actorNumber && ((Component)val3).gameObject.activeInHierarchy) { int num2 = 0; GameObject gameObject = ((Component)val3).gameObject; num2 = (((Object)(object)gameObject.GetComponentInParent() != (Object)null) ? 100 : ((((Object)gameObject).name.IndexOf("Player", StringComparison.OrdinalIgnoreCase) != -1) ? 50 : ((!gameObject.CompareTag("Player")) ? 1 : 20))); if (num2 > num) { num = num2; val2 = val3; } } } if ((Object)(object)val2 != (Object)null) { Transform transform = ((Component)val2).transform; if (TryFootYUpwards(transform, out var bestT, out var footY)) { return new Anchor { t = bestT, src = "PhotonView Owner", footY = footY }; } return new Anchor { t = transform, src = "PhotonView Owner (Fallback)", footY = transform.position.y }; } } catch { } return GetPlayerAnchor_LocalOrFallback(); } private static Anchor GetPlayerAnchor_LocalOrFallback() { //IL_0161: Unknown result type (might be due to invalid IL or missing references) //IL_01b4: Unknown result type (might be due to invalid IL or missing references) //IL_01c9: Unknown result type (might be due to invalid IL or missing references) //IL_01df: Unknown result type (might be due to invalid IL or missing references) //IL_01ee: Unknown result type (might be due to invalid IL or missing references) float footY = 0f; try { PhotonView[] array = Object.FindObjectsOfType(); PhotonView val = null; int num = -1; PhotonView[] array2 = array; foreach (PhotonView val2 in array2) { if ((Object)(object)val2 != (Object)null && (Object)(object)((Component)val2).gameObject != (Object)null && val2.IsMine && ((Component)val2).gameObject.activeInHierarchy) { int num2 = 0; GameObject gameObject = ((Component)val2).gameObject; num2 = (((Object)(object)gameObject.GetComponentInParent() != (Object)null) ? 100 : ((((Object)gameObject).name.IndexOf("Player", StringComparison.OrdinalIgnoreCase) != -1) ? 50 : ((!gameObject.CompareTag("Player")) ? 1 : 20))); if (num2 > num) { num = num2; val = val2; } } } if ((Object)(object)val != (Object)null) { Transform transform = ((Component)val).transform; if (TryFootYUpwards(transform, out var bestT, out footY)) { return new Anchor { t = bestT, src = "PhotonView.IsMine", footY = footY }; } return new Anchor { t = transform, src = "PhotonView.IsMine", footY = transform.position.y }; } } catch { } try { Camera main = Camera.main; if ((Object)(object)main != (Object)null && ((Component)main).gameObject.activeInHierarchy) { float y = ((Component)main).transform.position.y; Vector3 baseXZ = default(Vector3); ((Vector3)(ref baseXZ))..ctor(((Component)main).transform.position.x, 0f, ((Component)main).transform.position.z); footY = SampleGroundY_SphereDown(baseXZ, y, 1f, 50f, 0.2f, out var hit); if (!hit || footY < -1000f) { footY = y - 1.5f; } return new Anchor { t = ((Component)main).transform, src = "Camera.main", footY = footY }; } } catch { } return new Anchor { t = null, src = "None", footY = 0f }; } private static bool TryFootY(Transform t, out float footY) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_00ca: Unknown result type (might be due to invalid IL or missing references) //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00d3: Unknown result type (might be due to invalid IL or missing references) footY = ((t != null) ? t.position.y : 0f); if ((Object)(object)t == (Object)null) { return false; } CharacterController component = ((Component)t).GetComponent(); Bounds bounds; if ((Object)(object)component != (Object)null && ((Collider)component).enabled) { bounds = ((Collider)component).bounds; footY = ((Bounds)(ref bounds)).min.y + component.skinWidth; return true; } CapsuleCollider component2 = ((Component)t).GetComponent(); if ((Object)(object)component2 != (Object)null && ((Collider)component2).enabled) { bounds = ((Collider)component2).bounds; footY = ((Bounds)(ref bounds)).min.y; return true; } Collider component3 = ((Component)t).GetComponent(); if ((Object)(object)component3 != (Object)null && component3.enabled) { bounds = component3.bounds; footY = ((Bounds)(ref bounds)).min.y; return true; } return false; } private static bool TryFootYUpwards(Transform start, out Transform bestT, out float footY) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) bestT = start; footY = ((start != null) ? start.position.y : 0f); if ((Object)(object)start == (Object)null) { return false; } Transform val = start; int num = 0; while ((Object)(object)val != (Object)null && num < 10) { if (TryFootY(val, out var footY2)) { bestT = val; footY = footY2; return true; } val = val.parent; num++; } return false; } private static string TrimLastSegment(string path) { if (string.IsNullOrEmpty(path)) { return path; } int num = Math.Max(path.LastIndexOf('/'), path.LastIndexOf('\\')); return (num >= 0 && num + 1 < path.Length) ? path.Substring(num + 1) : path; } private static void SetFieldOrProperty(Type t, object obj, string name, object value) { if (t == null) { return; } BindingFlags bindingAttr = BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; FieldInfo field = t.GetField(name, bindingAttr); if (field != null) { field.SetValue(obj, value); return; } PropertyInfo property = t.GetProperty(name, bindingAttr); if (property != null && property.CanWrite) { property.SetValue(obj, value); } } private static object GetFieldOrProperty(Type t, object obj, string name) { if (t == null) { return null; } BindingFlags bindingAttr = (BindingFlags)(0x30 | ((obj == null) ? 8 : 4) | 1); FieldInfo field = t.GetField(name, bindingAttr); if (field != null) { return field.GetValue(obj); } PropertyInfo property = t.GetProperty(name, bindingAttr); if (property != null && property.CanRead) { return property.GetValue(obj); } return null; } private static Dictionary GetD(Dictionary d, string k) { object value; return (d != null && d.TryGetValue(k, out value) && value is Dictionary dictionary) ? dictionary : null; } private static string GetS(Dictionary d, string k, string def = null) { object value; return (d != null && d.TryGetValue(k, out value) && value != null) ? value.ToString() : def; } private static int GetI(Dictionary d, string k, int def = 0) { if (d != null && d.TryGetValue(k, out var value)) { if (value is int result) { return result; } if (value is long num) { return (int)num; } if (value != null && int.TryParse(value.ToString(), out var result2)) { return result2; } } return def; } private static bool GetB(Dictionary d, string k, bool def = false) { if (d != null && d.TryGetValue(k, out var value)) { if (value is bool result) { return result; } if (value != null) { string text = value.ToString().ToLowerInvariant(); if (text == "true" || text == "1") { return true; } if (text == "false" || text == "0") { return false; } } } return def; } private static Type FindType(string fullName) { try { return AppDomain.CurrentDomain.GetAssemblies().SelectMany(delegate(Assembly a) { try { return a.GetTypes(); } catch { return Type.EmptyTypes; } }).FirstOrDefault((Type t) => t.FullName != null && t.FullName.Equals(fullName, StringComparison.Ordinal)); } catch { return null; } } private static Type FindTypeSuffix(string suffix) { try { return AppDomain.CurrentDomain.GetAssemblies().SelectMany(delegate(Assembly a) { try { return a.GetTypes(); } catch { return Type.EmptyTypes; } }).FirstOrDefault((Type t) => t.FullName != null && t.FullName.EndsWith(suffix, StringComparison.Ordinal)); } catch { return null; } } private static object GetStaticField(Type t, string name) { if (t == null || string.IsNullOrEmpty(name)) { return null; } try { FieldInfo field = t.GetField(name, BindingFlags.IgnoreCase | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (field != null) { return field.GetValue(null); } PropertyInfo property = t.GetProperty(name, BindingFlags.IgnoreCase | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (property != null && property.CanRead) { return property.GetValue(null); } return null; } catch { return null; } } private static bool IsMultiplayer() { try { return PhotonNetwork.InRoom; } catch { return false; } } private static _RetryHub EnsureRetryHub() { //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Expected O, but got Unknown if ((Object)(object)_retryHub != (Object)null) { return _retryHub; } _retryHub = Object.FindObjectOfType<_RetryHub>(); if ((Object)(object)_retryHub != (Object)null) { return _retryHub; } GameObject val = new GameObject("RepoLiveChat.RetryHub"); Object.DontDestroyOnLoad((Object)(object)val); ((Object)val).hideFlags = (HideFlags)61; _retryHub = val.AddComponent<_RetryHub>(); return _retryHub; } } }