Decompiled source of BlueSage QoL Tweaks Beta v0.0.3
BepInEx\plugins\BlueSage_QoL_Tweaks_Beta\BlueSage_QoL_Tweaks_Beta.dll
Decompiled 6 days ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using BlueSage.QoLTweaks.Core; using HarmonyLib; using Microsoft.CodeAnalysis; using PurrNet; using Steamworks; using TMPro; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.6", FrameworkDisplayName = ".NET Framework 4.6")] [assembly: AssemblyCompany("BlueSage_QoL_Tweaks_Beta")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.0.3.0")] [assembly: AssemblyInformationalVersion("0.0.3+beb00bc563a9599284cac5ddf8e96c64020715b1")] [assembly: AssemblyProduct("BlueSage_QoL_Tweaks_Beta")] [assembly: AssemblyTitle("BlueSage_QoL_Tweaks_Beta")] [assembly: AssemblyVersion("0.0.3.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace BlueSage.QoLTweaks { [BepInPlugin("com.bluesage.ontogether.qoltweaks.beta", "BlueSage QoL Tweaks - Beta", "0.0.3")] [BepInProcess("OnTogether.exe")] public sealed class Plugin : BaseUnityPlugin { public const string PluginGuid = "com.bluesage.ontogether.qoltweaks.beta"; public const string PluginName = "BlueSage QoL Tweaks - Beta"; public const string PluginVersion = "0.0.3"; private readonly Harmony _harmony = new Harmony("com.bluesage.ontogether.qoltweaks.beta"); private Coroutine _sweepCoroutine; private Coroutine _compatibilityRefreshCoroutine; private Coroutine _hostHealthCoroutine; private Coroutine _managedReconnectCoroutine; private Coroutine _reconnectAnnouncementCoroutine; private PluginCompatibilityResult _compatibility; private BlueSageCommandRegistry _commandRegistry; private float _lastLobbyVisibleAt = -1f; private float _lastManagedReconnectAttemptAt = -1f; private int _managedReconnectAttempt; private bool _managedReconnectActive; private bool _managedReconnectSuccessAnnounced; private float _hostHealthStartedAt = -1f; private HostHealthDecision _lastHostHealthDecision; internal const int LockedMaxIdCardCharacters = 750; internal const int LockedMaxSessionNameCharacters = 750; internal const int LockedMaxChatCharacters = 750; internal const int LockedHostHealthIntervalSeconds = 300; internal const bool LockedHostHealthRefreshLobbyMetadata = true; internal const bool LockedHostHealthRequestPersonaRefresh = true; internal const bool LockedHostHealthShowNotifications = true; internal const int LockedHostHealthWarnAfterHours = 12; internal const bool LockedReconnectRequireVisibleLobby = true; internal const bool LockedReconnectManageHostKick = false; internal const bool LockedReconnectUseLobbyGrace = true; internal const int LockedReconnectLobbyGraceSeconds = 20; internal const int LockedReconnectRetryIntervalSeconds = 60; internal const int LockedReconnectMaxAttempts = 300; internal const bool LockedReconnectShowNotifications = true; internal const string LockedReconnectAnnouncementMessage = "Auto Reconnected by BlueSage QoL Tweaks - Thank you Blue <3"; internal const int LockedReconnectAnnouncementDelaySeconds = 3; internal static ManualLogSource Log { get; private set; } internal static Plugin Instance { get; private set; } internal static ConfigEntry<bool> EnableAutoSweep { get; private set; } internal static ConfigEntry<bool> AutoDisableSweepWhenAndrewSweepInstalled { get; private set; } internal static ConfigEntry<bool> AllowManualSweepWhenAndrewSweepInstalled { get; private set; } internal static ConfigEntry<int> SweepIntervalMinutes { get; private set; } internal static ConfigEntry<bool> NotifyOnSweep { get; private set; } internal static ConfigEntry<bool> ForceManagedGcCollect { get; private set; } internal static ConfigEntry<bool> EnableManualSweepHotkey { get; private set; } internal static ConfigEntry<KeyCode> ManualSweepKey { get; private set; } internal static ConfigEntry<bool> EnableChatTimestamps { get; private set; } internal static ConfigEntry<bool> EnableNotificationTimestamps { get; private set; } internal static ConfigEntry<bool> Use24HourTime { get; private set; } internal static ConfigEntry<int> TimestampSizePercent { get; private set; } internal static ConfigEntry<string> TimestampColor { get; private set; } internal static ConfigEntry<bool> AutoDisableChatTweaksWhenSimpleQoLInstalled { get; private set; } internal static ConfigEntry<string> StatusBaseName { get; private set; } internal static ConfigEntry<string> StatusMessage { get; private set; } internal static ConfigEntry<string> StatusColor { get; private set; } internal static ConfigEntry<string> StatusBrackets { get; private set; } internal static ConfigEntry<bool> EnableLeaveNotifications { get; private set; } internal static ConfigEntry<bool> EnableReconnectGuard { get; private set; } internal static ConfigEntry<bool> EnableReconnectAnnouncement { get; private set; } internal static ConfigEntry<bool> EnableHostHealthMonitor { get; private set; } internal static ConfigEntry<bool> EnableFocusAnywhere { get; private set; } internal static ConfigEntry<bool> EnableEnhancedPlayerPanel { get; private set; } internal static ConfigEntry<bool> EnableHiddenDiagnostics { get; private set; } internal static bool AutoSweepAllowed { get { if ((Object)(object)Instance != (Object)null) { return Instance._compatibility.AutoSweepAllowed; } return false; } } internal static bool ManualSweepAllowed { get { if ((Object)(object)Instance != (Object)null) { return Instance._compatibility.ManualSweepAllowed; } return false; } } internal static bool ChatTweaksAllowed { get { if ((Object)(object)Instance != (Object)null) { return Instance._compatibility.ChatTweaksAllowed; } return false; } } internal static bool ShouldApplyChatTimestamps { get { if (ChatTweaksAllowed) { return EnableChatTimestamps.Value; } return false; } } internal static bool ShouldApplyNotificationTimestamps { get { if (ChatTweaksAllowed) { return EnableNotificationTimestamps.Value; } return false; } } internal static bool ShouldApplyLeaveNotifications { get { if (ChatTweaksAllowed) { return EnableLeaveNotifications.Value; } return false; } } internal static bool ShouldApplyFocusAnywhere { get { if (EnableFocusAnywhere != null) { return EnableFocusAnywhere.Value; } return false; } } internal static bool ShouldApplyEnhancedPlayerPanel { get { if (EnableEnhancedPlayerPanel != null) { return EnableEnhancedPlayerPanel.Value; } return false; } } private void Awake() { Instance = this; Log = ((BaseUnityPlugin)this).Logger; BindConfig(); SyncNotificationTimestampsWithChatTimestamps("startup config normalization"); RegisterBlueSageCommands(); RefreshCompatibility("initial BepInEx loaded-plugin scan"); _harmony.PatchAll(); RestartSweepLoop(); RestartHostHealthLoop(); _compatibilityRefreshCoroutine = ((MonoBehaviour)this).StartCoroutine(RefreshCompatibilityAfterLoadSettles()); Log.LogInfo((object)string.Format("{0} {1} loaded. AutoSweep={2}, ChatTweaksAllowed={3}.", "BlueSage QoL Tweaks - Beta", "0.0.3", EnableAutoSweep.Value && AutoSweepAllowed, ChatTweaksAllowed)); } private void Update() { //IL_0012: Unknown result type (might be due to invalid IL or missing references) if (EnableManualSweepHotkey.Value && Input.GetKeyDown(ManualSweepKey.Value)) { RunSweep("manual hotkey", requireManualPermission: true); } } private void OnDestroy() { if (_sweepCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_sweepCoroutine); _sweepCoroutine = null; } if (_compatibilityRefreshCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_compatibilityRefreshCoroutine); _compatibilityRefreshCoroutine = null; } if (_managedReconnectCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_managedReconnectCoroutine); _managedReconnectCoroutine = null; } if (_hostHealthCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_hostHealthCoroutine); _hostHealthCoroutine = null; } if (_reconnectAnnouncementCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_reconnectAnnouncementCoroutine); _reconnectAnnouncementCoroutine = null; } _harmony.UnpatchSelf(); ManualLogSource log = Log; if (log != null) { log.LogInfo((object)"BlueSage QoL Tweaks - Beta unloaded and Harmony patches removed."); } Instance = null; } private void BindConfig() { EnableAutoSweep = ((BaseUnityPlugin)this).Config.Bind<bool>("Asset Sweep", "EnableAutoSweep", true, "Automatically unload unused Unity assets on a safe timer."); AutoDisableSweepWhenAndrewSweepInstalled = ((BaseUnityPlugin)this).Config.Bind<bool>("Asset Sweep", "AutoDisableSweepWhenAndrewSweepInstalled", true, "Disable BlueSage auto sweep if AndrewLin Sweep is installed. On by default to avoid double sweeping."); AllowManualSweepWhenAndrewSweepInstalled = ((BaseUnityPlugin)this).Config.Bind<bool>("Asset Sweep", "AllowManualSweepWhenAndrewSweepInstalled", false, "Allow F8 manual BlueSage sweeps even when AndrewLin Sweep is installed. Off by default to avoid double-sweep confusion."); SweepIntervalMinutes = ((BaseUnityPlugin)this).Config.Bind<int>("Asset Sweep", "SweepIntervalMinutes", 60, "Minutes between automatic sweeps. Safe range 30 to 180."); NotifyOnSweep = ((BaseUnityPlugin)this).Config.Bind<bool>("Asset Sweep", "NotifyOnSweep", false, "Show an in-game notification when BlueSage runs a sweep."); ForceManagedGcCollect = ((BaseUnityPlugin)this).Config.Bind<bool>("Asset Sweep", "ForceManagedGcCollect", false, "Also run GC.Collect after Resources.UnloadUnusedAssets. Off by default for smoother play."); EnableManualSweepHotkey = ((BaseUnityPlugin)this).Config.Bind<bool>("Asset Sweep", "EnableManualSweepHotkey", true, "Enable manual sweep hotkey."); ManualSweepKey = ((BaseUnityPlugin)this).Config.Bind<KeyCode>("Asset Sweep", "ManualSweepKey", (KeyCode)289, "Hotkey for manual sweep."); EnableChatTimestamps = ((BaseUnityPlugin)this).Config.Bind<bool>("Chat", "EnableChatTimestamps", false, "Add local timestamps before player names. Off by default to avoid chat clutter."); EnableNotificationTimestamps = ((BaseUnityPlugin)this).Config.Bind<bool>("Chat", "EnableNotificationTimestamps", false, "Add local timestamps before system notifications. Off by default."); Use24HourTime = ((BaseUnityPlugin)this).Config.Bind<bool>("Chat", "Use24HourTime", true, "Use 24-hour timestamps when timestamp features are enabled."); TimestampSizePercent = ((BaseUnityPlugin)this).Config.Bind<int>("Chat", "TimestampSizePercent", 60, "Timestamp text size percentage. Clamped from 40 to 100."); TimestampColor = ((BaseUnityPlugin)this).Config.Bind<string>("Chat", "TimestampColor", "F5EDE1", "Timestamp color as a six-character hex code."); AutoDisableChatTweaksWhenSimpleQoLInstalled = ((BaseUnityPlugin)this).Config.Bind<bool>("Chat", "AutoDisableChatTweaksWhenSimpleQoLInstalled", true, "Disable BlueSage timestamps and leave notices if Simple_QOL/ChatTweaks-style mods are installed."); StatusBaseName = ((BaseUnityPlugin)this).Config.Bind<string>("Status Helper", "StatusBaseName", string.Empty, "Base display name used by BlueSage /setname and /status."); StatusMessage = ((BaseUnityPlugin)this).Config.Bind<string>("Status Helper", "StatusMessage", string.Empty, "Current BlueSage status text appended to your display name."); StatusColor = ((BaseUnityPlugin)this).Config.Bind<string>("Status Helper", "StatusColor", "FFD45D", "Six-character hex color for BlueSage status text."); StatusBrackets = ((BaseUnityPlugin)this).Config.Bind<string>("Status Helper", "StatusBrackets", "()", "Two characters used around BlueSage status text."); EnableLeaveNotifications = ((BaseUnityPlugin)this).Config.Bind<bool>("Notifications", "EnableLeaveNotifications", true, "Show a local notification when a player leaves. Auto-disabled when overlapping chat mods are detected."); EnableReconnectGuard = ((BaseUnityPlugin)this).Config.Bind<bool>("Reconnect Guard", "EnableReconnectGuard", true, "Beta: use BlueSage-managed reconnect retries. Players usually want this on; hosts can turn it off."); EnableReconnectAnnouncement = ((BaseUnityPlugin)this).Config.Bind<bool>("Reconnect Guard", "EnableReconnectAnnouncement", true, "Send a small in-game chat message after BlueSage auto reconnect succeeds."); EnableHostHealthMonitor = ((BaseUnityPlugin)this).Config.Bind<bool>("Host Health", "EnableHostHealthMonitor", true, "Host-only beta monitor for long-running lobby health. Clients do nothing."); EnableFocusAnywhere = ((BaseUnityPlugin)this).Config.Bind<bool>("Focus Anywhere", "EnableFocusAnywhere", true, "Allow focus mode from most places instead of only vanilla focus surfaces."); EnableEnhancedPlayerPanel = ((BaseUnityPlugin)this).Config.Bind<bool>("Enhanced Player Panel", "EnableEnhancedPlayerPanel", true, "Add BlueSage ping and ID-card helper buttons to the player panel."); EnableHiddenDiagnostics = ((BaseUnityPlugin)this).Config.Bind<bool>("Developer", "EnableHiddenDiagnostics", false, "Internal diagnostics toggle. Leave off unless asked."); } private void RefreshCompatibility(string source) { string[] array = Chainloader.PluginInfos.Keys.ToArray(); PluginCompatibilityResult compatibility = _compatibility; _compatibility = PluginCompatibility.Evaluate(array, AutoDisableSweepWhenAndrewSweepInstalled.Value, AllowManualSweepWhenAndrewSweepInstalled.Value, AutoDisableChatTweaksWhenSimpleQoLInstalled.Value); Log.LogInfo((object)$"Compatibility scan ({source}) checked {array.Length} loaded BepInEx plugin IDs only. Disabled, renamed, old, or backup DLL files are ignored."); foreach (string reason in _compatibility.Reasons) { Log.LogWarning((object)reason); } if (compatibility != null && (compatibility.AutoSweepAllowed != _compatibility.AutoSweepAllowed || compatibility.ManualSweepAllowed != _compatibility.ManualSweepAllowed || compatibility.ChatTweaksAllowed != _compatibility.ChatTweaksAllowed)) { Log.LogInfo((object)("Compatibility changed after " + source + ". Restarting BlueSage-controlled sweep loop with updated settings.")); RestartSweepLoop(); } } private IEnumerator RefreshCompatibilityAfterLoadSettles() { yield return (object)new WaitForSecondsRealtime(3f); RefreshCompatibility("delayed post-load recheck"); _compatibilityRefreshCoroutine = null; } private void RestartSweepLoop() { if (_sweepCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_sweepCoroutine); _sweepCoroutine = null; } if (EnableAutoSweep.Value && AutoSweepAllowed) { _sweepCoroutine = ((MonoBehaviour)this).StartCoroutine(SweepLoop()); } } private IEnumerator SweepLoop() { while (true) { int num = SweepSettingsPolicy.ClampIntervalMinutes(SweepIntervalMinutes.Value); yield return (object)new WaitForSecondsRealtime((float)num * 60f); RunSweep("automatic timer", requireManualPermission: false); } } internal void RunSweep(string reason, bool requireManualPermission) { if (requireManualPermission && !ManualSweepAllowed) { Log.LogWarning((object)("Skipping asset sweep via " + reason + ": AndrewLin Sweep compatibility guard is active.")); return; } Log.LogInfo((object)("Running asset sweep via " + reason + ": Resources.UnloadUnusedAssets().")); Resources.UnloadUnusedAssets(); if (ForceManagedGcCollect.Value) { GC.Collect(); } if (NotifyOnSweep.Value) { AddLocalNotification("BlueSage QoL swept unused assets."); } } internal static string BuildTimestampPrefix() { return TimestampFormatter.BuildPrefix(DateTime.Now, Use24HourTime.Value, TimestampSizePercent.Value, TimestampColor.Value); } internal static void AddLocalNotification(string text) { try { TextChannelManager i = NetworkSingleton<TextChannelManager>.I; if (i != null) { i.AddNotification(text); } } catch (Exception ex) { ManualLogSource log = Log; if (log != null) { log.LogWarning((object)("Could not show local notification: " + ex.GetType().Name + ": " + ex.Message)); } } } internal static bool HandleOutgoingChatCommand(byte[] message) { if ((Object)(object)Instance == (Object)null || message == null || Instance._commandRegistry == null) { return true; } string text; try { text = Encoding.Unicode.GetString(message).Trim(); } catch (Exception ex) { ManualLogSource log = Log; if (log != null) { log.LogWarning((object)("BlueSage command decode failed: " + ex.GetType().Name + ": " + ex.Message)); } return true; } BlueSageCommandResult blueSageCommandResult = Instance._commandRegistry.TryExecute(text); if (!blueSageCommandResult.Handled) { return true; } foreach (string message2 in blueSageCommandResult.Messages) { AddLocalNotification(message2); } ManualLogSource log2 = Log; if (log2 != null) { log2.LogInfo((object)("BlueSage command handled locally: " + text)); } return false; } private void RegisterBlueSageCommands() { _commandRegistry = new BlueSageCommandRegistry(); _commandRegistry.Register(new BlueSageCommand("bsqol", "BlueSage QoL help and status", new string[1] { "qol" }, HandleBlueSageQoLCommand)); _commandRegistry.Register(new BlueSageCommand("assetsweep", "Toggle BlueSage Asset Sweep or run it now", new string[1] { "as" }, (BlueSageCommandContext context) => HandleToggleCommand(context, "Asset Sweep", EnableAutoSweep, RestartSweepLoop, "now", delegate { RunSweep("manual command", requireManualPermission: true); return "Asset Sweep: ran now."; }))); _commandRegistry.Register(new BlueSageCommand("hosthealth", "Toggle Host Health or run a check now", new string[1] { "hh" }, (BlueSageCommandContext context) => HandleToggleCommand(context, "Host Health", EnableHostHealthMonitor, RestartHostHealthLoop, "run", () => RunHostHealthCheck("manual command")))); _commandRegistry.Register(new BlueSageCommand("leavenotices", "Toggle leave notices", new string[1] { "ln" }, (BlueSageCommandContext context) => HandleToggleCommand(context, "Leave Notices", EnableLeaveNotifications))); _commandRegistry.Register(new BlueSageCommand("timestamps", "Toggle chat and notification timestamps", new string[1] { "tt" }, HandleTimestampCommand)); _commandRegistry.Register(new BlueSageCommand("reconnectguard", "Toggle Reconnect Guard", new string[1] { "rg" }, (BlueSageCommandContext context) => HandleToggleCommand(context, "Reconnect Guard", EnableReconnectGuard, delegate { if (!EnableReconnectGuard.Value) { StopManagedReconnect("disabled by command"); } }))); _commandRegistry.Register(new BlueSageCommand("reconnectmessage", "Toggle reconnect success message", new string[1] { "rm" }, (BlueSageCommandContext context) => HandleToggleCommand(context, "Reconnect Message", EnableReconnectAnnouncement))); _commandRegistry.Register(new BlueSageCommand("focusanywhere", "Toggle Focus Anywhere", new string[1] { "fa" }, (BlueSageCommandContext context) => HandleToggleCommand(context, "Focus Anywhere", EnableFocusAnywhere))); _commandRegistry.Register(new BlueSageCommand("style", "Build paste-ready TMP name/status styling", new string[1] { "sty" }, HandleStyleCommand)); _commandRegistry.Register(new BlueSageCommand("setname", "Set your BlueSage base display name", Array.Empty<string>(), HandleSetNameCommand)); _commandRegistry.Register(new BlueSageCommand("status", "Set your BlueSage status message", Array.Empty<string>(), HandleStatusCommand)); _commandRegistry.Register(new BlueSageCommand("clearstatus", "Clear your BlueSage status message", Array.Empty<string>(), HandleClearStatusCommand)); _commandRegistry.Register(new BlueSageCommand("statuscolor", "Set your BlueSage status color", Array.Empty<string>(), HandleStatusColorCommand)); string name = DecodeCommandName("cmvfefwefcvh"); _commandRegistry.Register(new BlueSageCommand(name, "BlueSage internal", Array.Empty<string>(), HandleHiddenDiagnosticsCommand, isHidden: true)); _commandRegistry.Register(new BlueSageCommand(BuildBlueSageClueCommandName(), "BlueSage internal", Array.Empty<string>(), HandleBlueSageClueCommand, isHidden: true)); } private BlueSageCommandResult HandleBlueSageQoLCommand(BlueSageCommandContext context) { string text = ((context.Tokens.Count > 0) ? context.Tokens[0].ToLowerInvariant() : "help"); if (text == "status") { return BlueSageCommandResult.HandledWithMessages("BlueSage QoL: Sweep=" + FormatToggleState(EnableAutoSweep.Value && AutoSweepAllowed) + ", HostHealth=" + FormatToggleState(EnableHostHealthMonitor.Value) + ", LeaveNotices=" + FormatToggleState(ShouldApplyLeaveNotifications) + ", Timestamps=" + FormatToggleState(ShouldApplyChatTimestamps) + ", ReconnectGuard=" + FormatToggleState(EnableReconnectGuard.Value) + ", ReconnectMessage=" + FormatToggleState(EnableReconnectAnnouncement.Value) + ", LongerText=always on."); } if (text == "diag" && string.Equals(context.CommandName, "bsqol", StringComparison.OrdinalIgnoreCase)) { return HandleHiddenDiagnosticsCommand(context); } if (text == "hosthealth") { return BlueSageCommandResult.HandledWithMessages(RunHostHealthCheck("manual command")); } return BlueSageCommandResult.HandledWithMessages("BlueSage QoL commands: /as [on|off|now], /hh [on|off|run], /ln [on|off], /tt [on|off], /rg [on|off], /rm [on|off], /fa [on|off], /style [...], /setname [...], /status [...], /qol status."); } private BlueSageCommandResult HandleStyleCommand(BlueSageCommandContext context) { RichTextStyleResult richTextStyleResult = RichTextStyleBuilder.TryBuild(context.Arguments); if (!richTextStyleResult.Success) { return BlueSageCommandResult.HandledWithMessages(richTextStyleResult.Message); } GUIUtility.systemCopyBuffer = richTextStyleResult.GeneratedText; return BlueSageCommandResult.HandledWithMessages($"Style Helper: copied {richTextStyleResult.RawLength}/{750} chars. Paste into an ID/name/profile/room field."); } private BlueSageCommandResult HandleBlueSageClueCommand(BlueSageCommandContext context) { string text = GetStatusBaseName(); if (string.IsNullOrWhiteSpace(text)) { text = "You"; } return BlueSageCommandResult.HandledWithMessages("♥☻ " + text + " found a Clue! ☻♥ Screenshot this to @jollyblue (Blues) in Discord to make him giggle."); } private BlueSageCommandResult HandleSetNameCommand(BlueSageCommandContext context) { string value = context.Arguments.Trim(); if (string.IsNullOrWhiteSpace(value)) { return BlueSageCommandResult.HandledWithMessages("Status Helper name: " + GetStatusBaseName() + "."); } StatusBaseName.Value = value; ((BaseUnityPlugin)this).Config.Save(); return ApplyStatusNameToPlayerData("Status Helper: name updated."); } private BlueSageCommandResult HandleStatusCommand(BlueSageCommandContext context) { string value = context.Arguments.Trim(); if (string.IsNullOrWhiteSpace(value)) { return BlueSageCommandResult.HandledWithMessages(string.IsNullOrWhiteSpace(StatusMessage.Value) ? "Status Helper: no status set. Use /status BRB or /clearstatus." : ("Status Helper status: " + StatusMessage.Value + ".")); } StatusMessage.Value = value; ((BaseUnityPlugin)this).Config.Save(); return ApplyStatusNameToPlayerData("Status Helper: status updated."); } private BlueSageCommandResult HandleClearStatusCommand(BlueSageCommandContext context) { StatusMessage.Value = string.Empty; ((BaseUnityPlugin)this).Config.Save(); return ApplyStatusNameToPlayerData("Status Helper: status cleared."); } private BlueSageCommandResult HandleStatusColorCommand(BlueSageCommandContext context) { string value = context.Arguments.Trim(); if (string.IsNullOrWhiteSpace(value)) { return BlueSageCommandResult.HandledWithMessages("Status Helper color: #" + GetStatusColor() + "."); } if (!RichTextStyleBuilder.TryNormalizeHex(value, out var hex)) { return BlueSageCommandResult.HandledWithMessages("Status Helper: expected a hex color like #FFD45D."); } StatusColor.Value = hex; ((BaseUnityPlugin)this).Config.Save(); return ApplyStatusNameToPlayerData("Status Helper color: #" + hex + "."); } private BlueSageCommandResult ApplyStatusNameToPlayerData(string successMessage) { //IL_00ee: Unknown result type (might be due to invalid IL or missing references) //IL_00f5: Unknown result type (might be due to invalid IL or missing references) //IL_00fb: Unknown result type (might be due to invalid IL or missing references) string statusBaseName = GetStatusBaseName(); if (string.IsNullOrWhiteSpace(statusBaseName)) { return BlueSageCommandResult.HandledWithMessages("Status Helper: player name is not ready yet."); } if (string.IsNullOrWhiteSpace(StatusBaseName.Value)) { StatusBaseName.Value = statusBaseName; } string text = BuildStatusDisplayName(statusBaseName); if (text.Length > 750) { return BlueSageCommandResult.HandledWithMessages($"Status Helper: display name is too long ({text.Length}/{750}). Shorten your name or status."); } try { DataManager i = MonoSingleton<DataManager>.I; if ((Object)(object)i == (Object)null || i.PlayerData == null) { return BlueSageCommandResult.HandledWithMessages("Status Helper: player data is not ready yet."); } i.PlayerData.Name = text; TextChannelManager i2 = NetworkSingleton<TextChannelManager>.I; if ((Object)(object)i2 != (Object)null) { i2.UserName = text; if ((Object)(object)i2.MainCustomizationController != (Object)null) { i2.MainCustomizationController.UpdatePlayerInfo(i.PlayerData.GetPlayerIdInfo(), default(RPCInfo)); } } UIManager i3 = MonoSingleton<UIManager>.I; if ((Object)(object)i3 != (Object)null && (Object)(object)i3.PlayerText != (Object)null) { ((TMP_Text)i3.PlayerText).text = text; } ((BaseUnityPlugin)this).Config.Save(); return BlueSageCommandResult.HandledWithMessages(successMessage); } catch (Exception ex) { Log.LogWarning((object)("Status Helper update failed: " + ex.GetType().Name + ": " + ex.Message)); return BlueSageCommandResult.HandledWithMessages("Status Helper: update failed; check BepInEx console."); } } private static string BuildStatusDisplayName(string baseName) { string text = (StatusMessage.Value ?? string.Empty).Trim(); if (string.IsNullOrWhiteSpace(text)) { return baseName; } string text2 = StatusBrackets.Value ?? "()"; if (text2.Length < 2) { text2 = "()"; } string text3 = text2.Substring(0, 1); string text4 = text2.Substring(1, 1); string statusColor = GetStatusColor(); return baseName + " <color=#" + statusColor + ">" + text3 + text + text4 + "</color>"; } private static string GetStatusBaseName() { string text = (StatusBaseName.Value ?? string.Empty).Trim(); if (!string.IsNullOrWhiteSpace(text)) { return text; } try { return MonoSingleton<DataManager>.I?.PlayerData?.Name?.Trim() ?? string.Empty; } catch { return string.Empty; } } private static string GetStatusColor() { if (!RichTextStyleBuilder.TryNormalizeHex(StatusColor.Value, out var hex)) { return "FFD45D"; } return hex; } private static string BuildBlueSageClueCommandName() { return DecodeCommandName("cmvftdmvft"); } private static string DecodeCommandName(string encoded) { char[] array = encoded.ToCharArray(); for (int i = 0; i < array.Length; i++) { array[i] = (char)(array[i] - 1); } return new string(array); } private BlueSageCommandResult HandleToggleCommand(BlueSageCommandContext context, string label, ConfigEntry<bool> configEntry, Action afterChange = null, string actionToken = null, Func<string> actionHandler = null) { string text = ((context.Tokens.Count > 0) ? context.Tokens[0].Trim().ToLowerInvariant() : "toggle"); if (text == "status" || text == "?") { return BlueSageCommandResult.HandledWithMessages(label + ": " + FormatToggleState(configEntry.Value) + "."); } if (!string.IsNullOrWhiteSpace(actionToken) && (text == actionToken || (actionToken == "run" && text == "now") || (actionToken == "now" && text == "run"))) { return BlueSageCommandResult.HandledWithMessages(actionHandler?.Invoke() ?? (label + ": action complete.")); } bool? flag = ParseToggleRequestWithCurrent(text, configEntry.Value); if (!flag.HasValue) { return BlueSageCommandResult.HandledWithMessages(label + ": use on, off, toggle, or status."); } configEntry.Value = flag.Value; ((BaseUnityPlugin)this).Config.Save(); afterChange?.Invoke(); return BlueSageCommandResult.HandledWithMessages(label + ": " + FormatToggleState(configEntry.Value) + "."); } private BlueSageCommandResult HandleTimestampCommand(BlueSageCommandContext context) { string text = ((context.Tokens.Count > 0) ? context.Tokens[0].Trim().ToLowerInvariant() : "toggle"); if (text == "status" || text == "?") { return BlueSageCommandResult.HandledWithMessages("Timestamps: chat=" + FormatToggleState(EnableChatTimestamps.Value) + ", join/leave=" + FormatToggleState(EnableNotificationTimestamps.Value) + "."); } bool? flag = ParseToggleRequestWithCurrent(text, EnableChatTimestamps.Value && EnableNotificationTimestamps.Value); if (!flag.HasValue) { return BlueSageCommandResult.HandledWithMessages("Timestamps: use on, off, toggle, or status."); } EnableChatTimestamps.Value = flag.Value; SyncNotificationTimestampsWithChatTimestamps("timestamp command"); ((BaseUnityPlugin)this).Config.Save(); return BlueSageCommandResult.HandledWithMessages("Timestamps: " + FormatToggleState(EnableChatTimestamps.Value) + "."); } private static void SyncNotificationTimestampsWithChatTimestamps(string reason) { if (EnableNotificationTimestamps.Value != EnableChatTimestamps.Value) { EnableNotificationTimestamps.Value = EnableChatTimestamps.Value; Plugin instance = Instance; if (instance != null) { ((BaseUnityPlugin)instance).Config.Save(); } ManualLogSource log = Log; if (log != null) { log.LogInfo((object)("Timestamps: synced join/leave/system notices with chat timestamps via " + reason + ".")); } } } private static bool? ParseToggleRequest(string token) { switch (token) { case "toggle": case "t": case "": return null; case "enable": case "1": case "on": case "yes": case "enabled": case "true": return true; case "0": case "no": case "off": case "disable": case "false": case "disabled": return false; default: { if (!bool.TryParse(token, out var result)) { return null; } return result; } } } private bool? ParseToggleRequestWithCurrent(string token, bool current) { bool? flag = ParseToggleRequest(token); if (flag.HasValue) { return flag.Value; } if (string.IsNullOrWhiteSpace(token) || token == "toggle" || token == "t") { return !current; } return null; } private static string FormatToggleState(bool enabled) { if (!enabled) { return "off"; } return "on"; } private static string FormatNullableCount(int? value) { if (!value.HasValue) { return "?"; } return value.Value.ToString(); } private void RestartHostHealthLoop() { if (_hostHealthCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_hostHealthCoroutine); _hostHealthCoroutine = null; } if (EnableHostHealthMonitor.Value) { _hostHealthCoroutine = ((MonoBehaviour)this).StartCoroutine(HostHealthLoop()); } } private IEnumerator HostHealthLoop() { yield return (object)new WaitForSecondsRealtime(30f); while (true) { RunHostHealthCheck("automatic timer"); int num = 300; yield return (object)new WaitForSecondsRealtime((float)num); } } private string RunHostHealthCheck(string reason) { if (!EnableHostHealthMonitor.Value) { return "Host Health is disabled."; } CSteamID? lobbyId; string[] playerSteamIds; HostHealthInput input = CaptureHostHealthInput(out lobbyId, out playerSteamIds); PlayerLimitCompanionStatus playerLimitCompanionStatus = CapturePlayerLimitCompanionStatus(); LogHiddenDiagnosticSnapshot(reason, input, lobbyId, playerSteamIds); HostHealthDecision hostHealthDecision = (_lastHostHealthDecision = HostHealthPolicy.Evaluate(input)); if (!hostHealthDecision.ShouldRunDiagnostics) { Log.LogInfo((object)("Host Health skipped via " + reason + ": local player is not the host/lobby owner.")); return "Host Health: client/no-op. Only the host runs lobby health checks."; } string text = ((hostHealthDecision.Issues.Count == 0) ? "healthy" : string.Join(", ", hostHealthDecision.Issues.Select((HostHealthIssue issue) => issue.ToString()).ToArray())); Log.LogInfo((object)("Host Health check via " + reason + ": " + text + ". " + playerLimitCompanionStatus.ToDiagnosticText() + ".")); if (hostHealthDecision.Actions.Contains(HostHealthAction.RefreshLobbyMetadata)) { RefreshLobbyMetadataHeartbeat(lobbyId, playerLimitCompanionStatus); } if (hostHealthDecision.Actions.Contains(HostHealthAction.RequestPersonaRefresh)) { RequestPersonaRefresh(playerSteamIds); } if (hostHealthDecision.Actions.Contains(HostHealthAction.WarnOnly)) { string text2 = "Host Health warning: " + text + ". If joins fail, recreate the lobby."; Log.LogWarning((object)text2); AddLocalNotification(text2); } return "Host Health: " + text + ". Metadata refresh=on, persona refresh=on. " + playerLimitCompanionStatus.ToDiagnosticText() + "."; } private BlueSageCommandResult HandleHiddenDiagnosticsCommand(BlueSageCommandContext context) { int num = (string.Equals(context.CommandName, "bsqol", StringComparison.OrdinalIgnoreCase) ? 1 : 0); switch ((context.Tokens.Count > num) ? context.Tokens[num].Trim().ToLowerInvariant() : "status") { case "on": EnableHiddenDiagnostics.Value = true; ((BaseUnityPlugin)this).Config.Save(); Log.LogInfo((object)("BlueSage diagnostics enabled. Watch for [BlueSageDiag] lines in BepInEx log: " + GetBepInExLogPath())); LogHiddenDiagnosticSnapshot("hidden diag enabled"); return BlueSageCommandResult.HandledWithMessages("BlueSage diagnostics: on. BepInEx log: " + GetBepInExLogPath()); case "off": EnableHiddenDiagnostics.Value = false; ((BaseUnityPlugin)this).Config.Save(); Log.LogInfo((object)("BlueSage diagnostics disabled. Prior [BlueSageDiag] lines are in BepInEx log: " + GetBepInExLogPath())); return BlueSageCommandResult.HandledWithMessages("BlueSage diagnostics: off. BepInEx log: " + GetBepInExLogPath()); case "status": case "?": return BlueSageCommandResult.HandledWithMessages("BlueSage diagnostics: " + FormatToggleState(EnableHiddenDiagnostics.Value) + ". BepInEx log: " + GetBepInExLogPath()); default: return BlueSageCommandResult.HandledWithMessages("BlueSage diagnostics: use on, off, or status. BepInEx log: " + GetBepInExLogPath()); } } private static string GetBepInExLogPath() { try { return Path.Combine(Paths.BepInExRootPath, "LogOutput.log"); } catch { return "BepInEx/LogOutput.log"; } } private void LogHiddenDiagnosticSnapshot(string reason) { CSteamID? lobbyId; string[] playerSteamIds; HostHealthInput input = CaptureHostHealthInput(out lobbyId, out playerSteamIds); LogHiddenDiagnosticSnapshot(reason, input, lobbyId, playerSteamIds); } private void LogHiddenDiagnosticSnapshot(string reason, HostHealthInput input, CSteamID? lobbyId, string[] playerSteamIds) { //IL_0033: Unknown result type (might be due to invalid IL or missing references) if (EnableHiddenDiagnostics != null && EnableHiddenDiagnostics.Value && input != null) { TryGetVisibleLobbyCounts(out var memberCount, out var maxPlayers); string text = (lobbyId.HasValue ? ((ulong)lobbyId.Value).ToString() : "none"); string text2 = ((_lastHostHealthDecision == null) ? "none" : ((_lastHostHealthDecision.Issues.Count == 0) ? "healthy" : string.Join("|", _lastHostHealthDecision.Issues.Select((HostHealthIssue issue) => issue.ToString()).ToArray()))); string text3 = ((playerSteamIds == null || playerSteamIds.Length == 0) ? "none" : string.Join(",", playerSteamIds)); PlayerLimitCompanionStatus playerLimitCompanionStatus = CapturePlayerLimitCompanionStatus(); Log.LogInfo((object)("[BlueSageDiag] reason=" + reason + "; " + $"netIsHost={input.IsHost}; " + $"isHostMachine={input.IsHostMachine}; " + $"lobbyValid={input.LobbyValid}; " + "lobbyId=" + text + "; localSteamId=" + input.LocalSteamId + "; lobbyOwnerSteamId=" + input.LobbyOwnerSteamId + "; " + $"panelSteamIds={input.PlayerSteamIds.Count}; " + $"serverFlagCount={input.ServerPlayerCount}; " + "hostPersona='" + input.HostPersonaName + "'; visibleLobby=" + FormatNullableCount(memberCount) + "/" + FormatNullableCount(maxPlayers) + "; " + $"loadedPlugins={Chainloader.PluginInfos.Count}; " + playerLimitCompanionStatus.ToDiagnosticText() + "; lastHostHealth=" + text2 + "; panelSteamIdList=" + text3)); } } private HostHealthInput CaptureHostHealthInput(out CSteamID? lobbyId, out string[] playerSteamIds) { //IL_00ea: Unknown result type (might be due to invalid IL or missing references) //IL_01f5: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: Unknown result type (might be due to invalid IL or missing references) lobbyId = null; playerSteamIds = Array.Empty<string>(); bool flag = false; bool lobbyValid = false; string localSteamId = string.Empty; string text = string.Empty; int serverPlayerCount = 0; string hostPersonaName = string.Empty; try { flag = (Object)(object)NetworkManager.main != (Object)null && NetworkManager.main.isHost; } catch { flag = false; } try { MultiplayerManager i = MonoSingleton<MultiplayerManager>.I; lobbyValid = (Object)(object)i != (Object)null && i.LobbyStatus; string text2 = ((i != null) ? i.LobbyCode : null); if (!string.IsNullOrWhiteSpace(text2) && ulong.TryParse(text2, out var result)) { lobbyId = new CSteamID(result); text = ((ulong)SteamMatchmaking.GetLobbyOwner(lobbyId.Value)).ToString(); } } catch (Exception ex) { Log.LogWarning((object)("Host Health could not read Steam lobby owner: " + ex.GetType().Name + ": " + ex.Message)); } try { localSteamId = ((ulong)SteamUser.GetSteamID()).ToString(); } catch (Exception ex2) { Log.LogWarning((object)("Host Health could not read local SteamID: " + ex2.GetType().Name + ": " + ex2.Message)); } try { PlayerPanelController i2 = NetworkSingleton<PlayerPanelController>.I; if ((Object)(object)i2 != (Object)null) { playerSteamIds = i2.PlayerSteamIDs?.Where((string value) => !string.IsNullOrWhiteSpace(value)).ToArray() ?? Array.Empty<string>(); serverPlayerCount = i2.PlayerIDs?.Count((PlayerID playerId) => ((PlayerID)(ref playerId)).isServer) ?? 0; } } catch (Exception ex3) { Log.LogWarning((object)("Host Health could not read player panel state: " + ex3.GetType().Name + ": " + ex3.Message)); } try { if (TryParseSteamId(text, out var steamId)) { hostPersonaName = SteamFriends.GetFriendPersonaName(steamId); } } catch (Exception ex4) { Log.LogWarning((object)("Host Health could not read host persona: " + ex4.GetType().Name + ": " + ex4.Message)); } if (_hostHealthStartedAt < 0f) { _hostHealthStartedAt = Time.unscaledTime; } double lobbyAgeHours = Math.Max(0.0, (double)(Time.unscaledTime - _hostHealthStartedAt) / 3600.0); return new HostHealthInput(flag, lobbyValid, localSteamId, text, playerSteamIds, serverPlayerCount, hostPersonaName, lobbyAgeHours, 12.0); } private static void RefreshLobbyMetadataHeartbeat(CSteamID? lobbyId, PlayerLimitCompanionStatus playerLimitStatus) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) try { if (!lobbyId.HasValue || (ulong)lobbyId.Value == 0L) { Log.LogWarning((object)"Host Health H1: cannot refresh lobby metadata because lobby ID is unavailable."); return; } string text = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(); bool num = SteamMatchmaking.SetLobbyData(lobbyId.Value, "bluesage_qol_heartbeat", text); if (playerLimitStatus != null && playerLimitStatus.IsLoaded && playerLimitStatus.EffectiveMaxLobbySize.HasValue) { SteamMatchmaking.SetLobbyData(lobbyId.Value, "bluesage_playerlimit_cap", playerLimitStatus.EffectiveMaxLobbySize.Value.ToString()); } if (num) { Log.LogInfo((object)("Host Health H1: refreshed Steam lobby heartbeat metadata at " + text + ".")); } else { Log.LogWarning((object)"Host Health H1: SteamMatchmaking.SetLobbyData returned false."); } } catch (Exception ex) { Log.LogWarning((object)("Host Health H1 refresh failed: " + ex.GetType().Name + ": " + ex.Message)); } } private static PlayerLimitCompanionStatus CapturePlayerLimitCompanionStatus() { if (!IsPluginLoaded("com.bluesage.ontogether.playerlimitlift")) { return PlayerLimitCompanionStatus.NotLoaded(); } try { Type type = (from assembly in AppDomain.CurrentDomain.GetAssemblies() select assembly.GetType("BlueSagePatched.PlayerLimitLift.Plugin", throwOnError: false)).FirstOrDefault((Type candidate) => candidate != null); if (type == null) { return PlayerLimitCompanionStatus.FromLoadedValues(null, null, null, null); } return PlayerLimitCompanionStatus.FromLoadedValues(ReadStaticNullableInt(type, "EffectiveMaxLobbySize"), ReadStaticNullableInt(type, "EffectiveDefaultLobbySize"), ReadStaticNullableInt(type, "EffectiveShiftSkipRate"), ReadStaticConfigEntryBool(type, "EnableChatRelayPatch")); } catch (Exception ex) { Log.LogWarning((object)("PlayerLimit companion probe failed: " + ex.GetType().Name + ": " + ex.Message)); return PlayerLimitCompanionStatus.FromLoadedValues(null, null, null, null); } } private static int? ReadStaticNullableInt(Type type, string propertyName) { object obj = type.GetProperty(propertyName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(null, null); if (!(obj is int)) { return null; } return (int)obj; } private static bool? ReadStaticConfigEntryBool(Type type, string propertyName) { object obj = type.GetProperty(propertyName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(null, null); object obj2 = obj?.GetType().GetProperty("Value")?.GetValue(obj, null); if (!(obj2 is bool)) { return null; } return (bool)obj2; } private static void RequestPersonaRefresh(IEnumerable<string> playerSteamIds) { //IL_0027: Unknown result type (might be due to invalid IL or missing references) int num = 0; foreach (string item in playerSteamIds ?? Array.Empty<string>()) { try { if (TryParseSteamId(item, out var steamId)) { SteamFriends.RequestUserInformation(steamId, true); num++; } } catch (Exception ex) { Log.LogWarning((object)("Host Health H4 persona refresh failed for " + item + ": " + ex.GetType().Name + ": " + ex.Message)); } } Log.LogInfo((object)$"Host Health H4: requested Steam persona refresh for {num} player(s)."); } private static bool TryParseSteamId(string value, out CSteamID steamId) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) steamId = default(CSteamID); if (string.IsNullOrWhiteSpace(value) || !ulong.TryParse(value, out var result) || result == 0L) { return false; } steamId = new CSteamID(result); return true; } internal static bool HandleConnectionLostForReconnectGuard(NotificationStatus notificationStatus) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)Instance == (Object)null) { return true; } return Instance.HandleConnectionLostForReconnectGuardInstance(notificationStatus); } private unsafe bool HandleConnectionLostForReconnectGuardInstance(NotificationStatus notificationStatus) { //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Invalid comparison between Unknown and I4 if (!EnableReconnectGuard.Value) { return true; } bool flag = IsPluginLoaded("com.andrewlin.ontogether.reconnect"); bool flag2 = false; bool flag3 = false; try { flag2 = (Object)(object)NetworkManager.main != (Object)null && NetworkManager.main.isHost; flag3 = (Object)(object)NetworkManager.main != (Object)null && (int)NetworkManager.main.clientState == 1; } catch { } if (flag2 || flag3) { StopManagedReconnect("host/local client is already connected"); return true; } TryGetVisibleLobbyCounts(out var memberCount, out var maxPlayers); if (memberCount.HasValue && memberCount.Value >= 1 && _lastLobbyVisibleAt < 0f) { _lastLobbyVisibleAt = Time.unscaledTime; } ReconnectGuardDecision reconnectGuardDecision = ReconnectGuardPolicy.Evaluate(new ReconnectGuardInput(EnableReconnectGuard.Value, flag, suppressHostKickReconnect: true, blockDuringInitialLobbyGrace: true, requireVisibleLobby: true, 60, 20, 300, flag2, isIntentionalLeave: false, _managedReconnectActive, ((object)(*(NotificationStatus*)(¬ificationStatus))/*cast due to .constrained prefix*/).ToString(), (_lastLobbyVisibleAt < 0f) ? ((float?)null) : new float?(Time.unscaledTime - _lastLobbyVisibleAt), (_lastManagedReconnectAttemptAt < 0f) ? ((float?)null) : new float?(Time.unscaledTime - _lastManagedReconnectAttemptAt), Math.Max(1, _managedReconnectAttempt + 1), memberCount, maxPlayers)); switch (reconnectGuardDecision.Action) { case ReconnectGuardAction.AllowReconnectMod: Log.LogInfo((object)("Reconnect Guard: " + reconnectGuardDecision.Reason)); return true; case ReconnectGuardAction.Noop: Log.LogInfo((object)("Reconnect Guard: " + reconnectGuardDecision.Reason)); return false; case ReconnectGuardAction.StopManagedRetry: Log.LogWarning((object)("Reconnect Guard: " + reconnectGuardDecision.Reason)); StopManagedReconnect(reconnectGuardDecision.Reason); ReturnToMenuAfterFailedReconnect(); return false; case ReconnectGuardAction.WaitForManagedRetryCadence: Log.LogInfo((object)("Reconnect Guard: " + reconnectGuardDecision.Reason)); return false; case ReconnectGuardAction.ScheduleManagedRetry: case ReconnectGuardAction.RunManagedRetryNow: Log.LogWarning((object)("Reconnect Guard: " + reconnectGuardDecision.Reason)); if (flag) { MarkAndrewReconnectIntentionalLeave(); } StartManagedReconnectLoop(((object)(*(NotificationStatus*)(¬ificationStatus))/*cast due to .constrained prefix*/).ToString(), reconnectGuardDecision.RecommendedAttemptIntervalSeconds, reconnectGuardDecision.RecommendedMaxAttempts); return false; default: Log.LogWarning((object)$"Reconnect Guard: unexpected policy action {reconnectGuardDecision.Action}; allowing base reconnect flow."); return true; } } private void StartManagedReconnectLoop(string source, int intervalSeconds, int maxAttempts) { if (_managedReconnectCoroutine == null) { _managedReconnectActive = true; _managedReconnectSuccessAnnounced = false; _managedReconnectCoroutine = ((MonoBehaviour)this).StartCoroutine(ManagedReconnectLoop(source, intervalSeconds, maxAttempts)); } } private IEnumerator ManagedReconnectLoop(string source, int intervalSeconds, int maxAttempts) { Log.LogWarning((object)$"Reconnect Guard started managed retry loop after {source}. Interval={intervalSeconds}s, max={maxAttempts}."); while (_managedReconnectAttempt < maxAttempts) { if ((Object)(object)NetworkManager.main != (Object)null && (int)NetworkManager.main.clientState == 1) { AnnounceManagedReconnectSuccess(); StopManagedReconnect("client connected"); yield break; } TryGetVisibleLobbyCounts(out var memberCount, out var maxPlayers); bool flag = memberCount.HasValue && memberCount.Value >= 1; bool flag2 = false; _managedReconnectAttempt++; _lastManagedReconnectAttemptAt = Time.unscaledTime; if (flag || flag2) { if (flag && _lastLobbyVisibleAt < 0f) { _lastLobbyVisibleAt = Time.unscaledTime; } string arg = (flag ? $"{memberCount}/{maxPlayers}" : "not confirmed"); Log.LogWarning((object)$"Reconnect Guard attempt {_managedReconnectAttempt}/{maxAttempts}: visible lobby {arg}; calling StartClient()."); AddLocalNotification($"BlueSage reconnect attempt {_managedReconnectAttempt}/{maxAttempts} ({arg})."); try { NetworkManager main = NetworkManager.main; if (main != null) { main.StartClient(); } } catch (Exception ex) { Log.LogWarning((object)("Reconnect Guard StartClient failed: " + ex.GetType().Name + ": " + ex.Message)); } } else { Log.LogWarning((object)$"Reconnect Guard attempt {_managedReconnectAttempt}/{maxAttempts}: waiting because visible lobby does not show at least 1/x players yet."); } float waited = 0f; while (waited < (float)intervalSeconds) { yield return null; waited += Time.unscaledDeltaTime; if ((Object)(object)NetworkManager.main != (Object)null && (int)NetworkManager.main.clientState == 1) { AnnounceManagedReconnectSuccess(); StopManagedReconnect("client connected during wait"); yield break; } } } StopManagedReconnect($"max attempts reached ({maxAttempts})"); ReturnToMenuAfterFailedReconnect(); } private void StopManagedReconnect(string reason) { if (_managedReconnectCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_managedReconnectCoroutine); _managedReconnectCoroutine = null; } if (_managedReconnectActive) { Log.LogWarning((object)("Reconnect Guard stopped: " + reason + ".")); } _managedReconnectActive = false; _managedReconnectAttempt = 0; _lastManagedReconnectAttemptAt = -1f; } private void AnnounceManagedReconnectSuccess() { if (_managedReconnectSuccessAnnounced) { return; } _managedReconnectSuccessAnnounced = true; if (EnableReconnectAnnouncement.Value) { string text = "Auto Reconnected by BlueSage QoL Tweaks - Thank you Blue <3".Trim(); if (!string.IsNullOrWhiteSpace(text)) { _reconnectAnnouncementCoroutine = ((MonoBehaviour)this).StartCoroutine(DelayedReconnectAnnouncement(text)); } } } private IEnumerator DelayedReconnectAnnouncement(string message) { int num = 3; if (num > 0) { yield return (object)new WaitForSecondsRealtime((float)num); } if (!TrySendReconnectChatMessage(message)) { AddLocalNotification(message); } _reconnectAnnouncementCoroutine = null; } private static bool TrySendReconnectChatMessage(string message) { //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_0090: 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_009a: Unknown result type (might be due to invalid IL or missing references) try { TextChannelManager i = NetworkSingleton<TextChannelManager>.I; if ((Object)(object)i == (Object)null) { return false; } string s = i.UserName ?? "BlueSage QoL"; string text = (AccessTools.Field(typeof(TextChannelManager), "_playerId")?.GetValue(i) as string) ?? string.Empty; Vector3 val = (((Object)(object)i.MainPlayer != (Object)null) ? i.MainPlayer.position : Vector3.zero); i.SendMessageAsync(Encoding.Unicode.GetBytes(message), Encoding.Unicode.GetBytes(s), false, val, text, default(RPCInfo)); return true; } catch (Exception ex) { Log.LogWarning((object)("Reconnect announcement chat send failed: " + ex.GetType().Name + ": " + ex.Message)); return false; } } private static int Clamp(int value, int min, int max) { if (value < min) { return min; } if (value > max) { return max; } return value; } private static bool IsPluginLoaded(string pluginGuid) { return Chainloader.PluginInfos.Keys.Any((string key) => string.Equals(key, pluginGuid, StringComparison.OrdinalIgnoreCase)); } private static void ReturnToMenuAfterFailedReconnect() { //IL_0048: Unknown result type (might be due to invalid IL or missing references) try { MainSceneManager i = MonoSingleton<MainSceneManager>.I; if (!((Object)(object)i == (Object)null)) { AccessTools.Field(typeof(MainSceneManager), "_returnMenuStarted")?.SetValue(i, false); MultiplayerManager i2 = MonoSingleton<MultiplayerManager>.I; if ((Object)(object)i2 != (Object)null) { i2._notificationState = (NotificationStatus)3; } i.ReturnMenu(false); } } catch (Exception ex) { Log.LogWarning((object)("Reconnect Guard fallback to menu failed: " + ex.GetType().Name + ": " + ex.Message)); } } private static void MarkAndrewReconnectIntentionalLeave() { try { PropertyInfo propertyInfo = (from assembly in AppDomain.CurrentDomain.GetAssemblies() select assembly.GetType("Reconnect.ReconnectPlugin", throwOnError: false)).FirstOrDefault((Type candidate) => candidate != null)?.GetProperty("IsIntentionalLeave", BindingFlags.Static | BindingFlags.Public); if ((object)propertyInfo != null && propertyInfo.CanWrite) { propertyInfo.SetValue(null, true, null); Log.LogInfo((object)"Reconnect Guard marked AndrewLin Reconnect intentional-leave flag to prevent immediate retry."); } } catch (Exception ex) { Log.LogWarning((object)("Reconnect Guard could not mark AndrewLin Reconnect intentional leave: " + ex.GetType().Name + ": " + ex.Message)); } } private static bool TryGetVisibleLobbyCounts(out int? memberCount, out int? maxPlayers) { memberCount = null; maxPlayers = null; try { MultiplayerManager i = MonoSingleton<MultiplayerManager>.I; object obj = AccessTools.Field(typeof(MultiplayerManager), "_lobbyManager")?.GetValue(i); object obj2 = obj?.GetType().GetProperty("CurrentLobby")?.GetValue(obj, null); if (obj2 == null) { return false; } object obj3 = obj2.GetType().GetProperty("Members")?.GetValue(obj2, null); object obj4 = obj2.GetType().GetProperty("MaxPlayers")?.GetValue(obj2, null); PropertyInfo propertyInfo = obj3?.GetType().GetProperty("Count"); if (propertyInfo != null) { memberCount = Convert.ToInt32(propertyInfo.GetValue(obj3, null)); } if (obj4 != null) { maxPlayers = Convert.ToInt32(obj4); } return memberCount.HasValue || maxPlayers.HasValue; } catch (Exception ex) { ManualLogSource log = Log; if (log != null) { log.LogWarning((object)("Reconnect Guard could not read visible lobby counts: " + ex.GetType().Name + ": " + ex.Message)); } return false; } } } } namespace BlueSage.QoLTweaks.Patches { [HarmonyPatch(typeof(TextChannelManager))] internal static class BlueSageCommandPatch { [HarmonyPatch("SendMessageAsync", new Type[] { typeof(byte[]), typeof(byte[]), typeof(bool), typeof(Vector3), typeof(string), typeof(RPCInfo) })] [HarmonyPrefix] private static bool SendMessageAsyncPrefix(byte[] __0, RPCInfo __5) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) if (!IsLocalCommandInvocation(__5)) { return true; } return Plugin.HandleOutgoingChatCommand(__0); } private static bool IsLocalCommandInvocation(RPCInfo info) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)info.manager == (Object)null) { return ((PlayerID)(ref info.sender)).Equals(default(PlayerID)); } return false; } } [HarmonyPatch(typeof(TextChannelManager))] internal static class ChatSendLimitPatch { [HarmonyPatch("OnEnterPressed")] [HarmonyTranspiler] private static IEnumerable<CodeInstruction> OnEnterPressedTranspiler(IEnumerable<CodeInstruction> instructions) { bool replacedVanillaSendLimit = false; foreach (CodeInstruction instruction in instructions) { if (!replacedVanillaSendLimit && IsVanillaSendLimit(instruction)) { replacedVanillaSendLimit = true; yield return new CodeInstruction(OpCodes.Call, (object)AccessTools.Method(typeof(ChatSendLimitPatch), "GetMaxChatSendCharacters", (Type[])null, (Type[])null)); } else { yield return instruction; } } if (!replacedVanillaSendLimit) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)"ChatLimitRemover could not find the vanilla 250-character send cap in TextChannelManager.OnEnterPressed."); } } } private static int GetMaxChatSendCharacters() { return 750; } private static bool IsVanillaSendLimit(CodeInstruction instruction) { if (instruction.opcode == OpCodes.Ldc_I4 && instruction.operand is int num) { return num == 250; } return false; } } [HarmonyPatch(typeof(TextChannelManager))] internal static class ChatTimestampPatch { [HarmonyPatch("AddMessageUI")] [HarmonyPrefix] private static void AddMessageUiPrefix(ref string userName) { if (Plugin.ShouldApplyChatTimestamps && !string.IsNullOrEmpty(userName)) { userName = Plugin.BuildTimestampPrefix() + userName; } } [HarmonyPatch("AddNotification")] [HarmonyPrefix] private static void AddNotificationPrefix(ref string text) { if (Plugin.ShouldApplyNotificationTimestamps && !string.IsNullOrEmpty(text)) { text = Plugin.BuildTimestampPrefix() + text; } } } [HarmonyPatch(typeof(PlayerItemController), "Init")] internal static class EnhancedPlayerPanelPatch { [HarmonyPostfix] private static void Postfix(PlayerItemController __instance, string playerSteamId, string playerName, bool isSelf) { //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) if (!Plugin.ShouldApplyEnhancedPlayerPanel || (Object)(object)__instance == (Object)null || isSelf) { return; } try { Transform val = ((Component)__instance).transform.Find("Group_Buttons"); if (!((Object)(object)val == (Object)null) && !((Object)(object)val.Find("Button_BlueSagePing") != (Object)null) && !((Object)(object)val.Find("Button_BlueSageIdCard") != (Object)null) && !((Object)(object)val.Find("Button_Ping") != (Object)null) && !((Object)(object)val.Find("Button_IdCard") != (Object)null)) { RectTransform component = ((Component)val).GetComponent<RectTransform>(); if ((Object)(object)component != (Object)null) { component.anchoredPosition += new Vector2(-135f, 0f); } Transform referenceButton = val.Find("Button_Report"); CreateTextButton(val, "Button_BlueSagePing", "@", referenceButton, 0, delegate { ClosePlayerPanel(); MentionPlayer(playerName); }); CreateTextButton(val, "Button_BlueSageIdCard", "ID", referenceButton, 1, delegate { ClosePlayerPanel(); OpenIdCard(playerSteamId); }); } } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("Enhanced Player Panel patch skipped a row: " + ex.GetType().Name + ": " + ex.Message)); } } } private static void CreateTextButton(Transform parent, string name, string label, Transform referenceButton, int siblingIndex, Action onClick) { //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Expected O, but got Unknown //IL_0143: Unknown result type (might be due to invalid IL or missing references) //IL_0162: Unknown result type (might be due to invalid IL or missing references) //IL_018a: Unknown result type (might be due to invalid IL or missing references) //IL_0194: Expected O, but got Unknown //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00c4: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject(name, new Type[3] { typeof(RectTransform), typeof(Image), typeof(Button) }); val.transform.SetParent(parent, false); val.transform.SetSiblingIndex(siblingIndex); RectTransform component = val.GetComponent<RectTransform>(); Image component2 = val.GetComponent<Image>(); if ((Object)(object)referenceButton != (Object)null) { RectTransform component3 = ((Component)referenceButton).GetComponent<RectTransform>(); if ((Object)(object)component3 != (Object)null) { component.sizeDelta = component3.sizeDelta; } Image component4 = ((Component)referenceButton).GetComponent<Image>(); if ((Object)(object)component4 != (Object)null) { component2.sprite = component4.sprite; component2.type = component4.type; ((Graphic)component2).color = ((Graphic)component4).color; } LayoutElement component5 = ((Component)referenceButton).GetComponent<LayoutElement>(); if ((Object)(object)component5 != (Object)null) { LayoutElement obj = val.AddComponent<LayoutElement>(); obj.preferredWidth = component5.preferredWidth; obj.preferredHeight = component5.preferredHeight; obj.minWidth = component5.minWidth; obj.minHeight = component5.minHeight; obj.flexibleWidth = component5.flexibleWidth; obj.flexibleHeight = component5.flexibleHeight; } } else { component.sizeDelta = new Vector2(48f, 48f); ((Graphic)component2).color = new Color(0.38f, 0.73f, 0.95f, 1f); } AddButtonLabel(val.transform, label); ((UnityEvent)val.GetComponent<Button>().onClick).AddListener((UnityAction)delegate { onClick(); SFXManager i = MonoSingleton<SFXManager>.I; if ((Object)(object)i != (Object)null) { i.PlayUIClick(); } }); } private static void AddButtonLabel(Transform parent, string label) { //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject("Label", new Type[2] { typeof(RectTransform), typeof(TextMeshProUGUI) }); val.transform.SetParent(parent, false); RectTransform component = val.GetComponent<RectTransform>(); component.anchorMin = Vector2.zero; component.anchorMax = Vector2.one; component.offsetMin = Vector2.zero; component.offsetMax = Vector2.zero; TextMeshProUGUI component2 = val.GetComponent<TextMeshProUGUI>(); ((TMP_Text)component2).text = label; ((TMP_Text)component2).fontSize = ((label.Length > 1) ? 16f : 24f); ((TMP_Text)component2).alignment = (TextAlignmentOptions)514; ((Graphic)component2).color = Color.white; ((Graphic)component2).raycastTarget = false; } private static void ClosePlayerPanel() { PlayerPanelController i = NetworkSingleton<PlayerPanelController>.I; if ((Object)(object)i != (Object)null) { i.ButtonClosePanel(); } } private static void OpenIdCard(string steamId) { TemporaryPhoto i = MonoSingleton<TemporaryPhoto>.I; PlayerPanelController i2 = NetworkSingleton<PlayerPanelController>.I; if ((Object)(object)i == (Object)null || (Object)(object)i2 == (Object)null) { return; } int num = i2.PlayerSteamIDs.IndexOf(steamId); if (num >= 0 && num < i2.PlayerTransforms.Count) { NetworkTransform val = i2.PlayerTransforms[num]; if ((Object)(object)val != (Object)null) { i.ButtonOpenIdCheck(val); } } } private static void MentionPlayer(string playerName) { UIManager i = MonoSingleton<UIManager>.I; if (!((Object)(object)i == (Object)null) && !((Object)(object)i.MessageInput == (Object)null)) { if (i.IsMessagePanelHidden) { i.ButtonHide(); } if (!i.IsMessagePanelActivated) { i.OsSelectMessageInput(); } string text = "@" + playerName + " "; if (string.IsNullOrWhiteSpace(i.MessageInput.text)) { i.MessageInput.text = text; } else { string text2 = (i.MessageInput.text.EndsWith(" ", StringComparison.Ordinal) ? string.Empty : " "); TMP_InputField messageInput = i.MessageInput; messageInput.text = messageInput.text + text2 + text; } ((Selectable)i.MessageInput).Select(); i.MessageInput.ActivateInputField(); i.MessageInput.caretPosition = i.MessageInput.text.Length; } } } [HarmonyPatch(typeof(TaskManager), "IsReadyForFreeFocus")] internal static class FocusAnywhereFreeFocusPatch { [HarmonyPostfix] private static void Postfix(ref bool __result) { if (Plugin.ShouldApplyFocusAnywhere) { __result = true; } } } [HarmonyPatch(typeof(TaskManager), "IsReadyForAreaFocus")] internal static class FocusAnywhereAreaFocusPatch { [HarmonyPostfix] private static void Postfix(ref bool __result) { if (Plugin.ShouldApplyFocusAnywhere) { __result = true; } } } [HarmonyPatch] internal static class FocusAnywhereIsFocusablePatch { [HarmonyTargetMethod] private static MethodBase TargetMethod() { return AccessTools.Method(typeof(PlayerFocusController), "IsFocusable", (Type[])null, (Type[])null); } [HarmonyPostfix] private static void Postfix(ref bool __result) { if (Plugin.ShouldApplyFocusAnywhere) { __result = true; } } } [HarmonyPatch(typeof(PlayerMovementController), "get_IsLake")] internal static class FocusAnywhereLakePatch { [HarmonyPostfix] private static void Postfix(ref bool __result) { if (Plugin.ShouldApplyFocusAnywhere) { __result = false; } } } [HarmonyPatch(typeof(TMP_InputField))] internal static class InputFieldLimitPatch { [HarmonyPatch("OnEnable")] [HarmonyPostfix] private static void OnEnablePostfix(TMP_InputField __instance) { if (!((Object)(object)__instance == (Object)null)) { int? limit = InputFieldLimitPolicy.GetLimit(((Object)__instance).name, 750, 750, 750); if (limit.HasValue) { __instance.characterLimit = limit.Value; } } } } [HarmonyPatch(typeof(PlayerPanelController))] internal static class PlayerLeftPatch { [HarmonyPatch("DespawnHandle")] [HarmonyPrefix] private static void DespawnHandlePrefix(NetworkTransform netTransform, PlayerPanelController __instance) { //IL_0056: Unknown result type (might be due to invalid IL or missing references) if (!Plugin.ShouldApplyLeaveNotifications || (Object)(object)__instance == (Object)null || (Object)(object)netTransform == (Object)null) { return; } try { int num = Math.Min(__instance.PlayerTransforms.Count, __instance.IDInfos.Count); for (int i = 0; i < num; i++) { if (!((Object)(object)__instance.PlayerTransforms[i] != (Object)(object)netTransform)) { byte[] name = __instance.IDInfos[i].Name; string text = ((name == null) ? "Someone" : Encoding.Unicode.GetString(name)); if (string.IsNullOrWhiteSpace(text)) { text = "Someone"; } Plugin.AddLocalNotification(text + " has left the server."); break; } } } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("Leave notification skipped: " + ex.GetType().Name + ": " + ex.Message)); } } } } [HarmonyPatch(typeof(MainSceneManager))] internal static class ReconnectGuardPatch { [HarmonyPatch("ConnectionLost")] [HarmonyPrefix] [HarmonyPriority(800)] [HarmonyBefore(new string[] { "com.andrewlin.ontogether.reconnect" })] private static bool ConnectionLostPrefix(NotificationStatus notificationStatus) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) return Plugin.HandleConnectionLostForReconnectGuard(notificationStatus); } } } namespace BlueSage.QoLTweaks.Core { internal sealed class BlueSageCommand { public string Name { get; } public string Description { get; } public IReadOnlyList<string> Aliases { get; } public Func<BlueSageCommandContext, BlueSageCommandResult> Handler { get; } public bool IsHidden { get; } public BlueSageCommand(string name, string description, IEnumerable<string> aliases, Func<BlueSageCommandContext, BlueSageCommandResult> handler, bool isHidden = false) { Name = NormalizeName(name); Description = description ?? string.Empty; Aliases = (from alias in (aliases ?? Enumerable.Empty<string>()).Select(NormalizeName) where alias.Length > 0 select alias).Distinct<string>(StringComparer.OrdinalIgnoreCase).ToArray(); Handler = handler ?? throw new ArgumentNullException("handler"); IsHidden = isHidden; } public bool Matches(string commandName) { string normalized = NormalizeName(commandName); if (!string.Equals(Name, normalized, StringComparison.OrdinalIgnoreCase)) { return Aliases.Any((string alias) => string.Equals(alias, normalized, StringComparison.OrdinalIgnoreCase)); } return true; } public bool CouldMatch(string commandPrefix) { string normalized = NormalizeName(commandPrefix); if (!Name.StartsWith(normalized, StringComparison.OrdinalIgnoreCase)) { return Aliases.Any((string alias) => alias.StartsWith(normalized, StringComparison.OrdinalIgnoreCase)); } return true; } private static string NormalizeName(string value) { return (value ?? string.Empty).Trim().TrimStart(new char[1] { '/' }).ToLowerInvariant(); } } internal sealed class BlueSageCommandContext { public string RawInput { get; } public string CommandName { get; } public string Arguments { get; } public IReadOnlyList<string> Tokens { get; } public BlueSageCommandContext(string rawInput, string commandName, string arguments) { RawInput = rawInput ?? string.Empty; CommandName = commandName ?? string.Empty; Arguments = arguments ?? string.Empty; Tokens = Arguments.Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries).ToArray(); } } internal sealed class BlueSageCommandResult { public bool Handled { get; } public IReadOnlyList<string> Messages { get; } private BlueSageCommandResult(bool handled, IEnumerable<string> messages) { Handled = handled; Messages = (messages ?? Enumerable.Empty<string>()).Where((string message) => !string.IsNullOrWhiteSpace(message)).ToArray(); } public static BlueSageCommandResult Unhandled() { return new BlueSageCommandResult(handled: false, Array.Empty<string>()); } public static BlueSageCommandResult HandledWithMessages(params string[] messages) { return new BlueSageCommandResult(handled: true, messages); } } internal sealed class BlueSageCommandRegistry { private readonly List<BlueSageCommand> _commands = new List<BlueSageCommand>(); public IReadOnlyList<BlueSageCommand> Commands => _commands; public void Register(BlueSageCommand command) { if (command == null) { throw new ArgumentNullException("command"); } if (_commands.Any((BlueSageCommand existing) => existing.Matches(command.Name))) { throw new InvalidOperationException("BlueSage command already registered: /" + command.Name); } _commands.Add(command); } public BlueSageCommandResult TryExecute(string input) { if (!TryParse(input, out var commandName, out var arguments)) { return BlueSageCommandResult.Unhandled(); } BlueSageCommand blueSageCommand = _commands.FirstOrDefault((BlueSageCommand candidate) => candidate.Matches(commandName)); if (blueSageCommand == null) { return BlueSageCommandResult.Unhandled(); } return blueSageCommand.Handler(new BlueSageCommandContext(input, commandName, arguments)); } public IReadOnlyList<string> Suggest(string input, int maxResults) { if (maxResults < 1) { return Array.Empty<string>(); } string text = input?.TrimStart(Array.Empty<char>()) ?? string.Empty; if (!text.StartsWith("/", StringComparison.Ordinal) && !text.StartsWith("./", StringComparison.Ordinal)) { return Array.Empty<string>(); } string prefix = TrimCommandPrefix(text).Split(new char[1] { ' ' }, 2)[0]; return (from command in _commands.Where((BlueSageCommand command) => !command.IsHidden && command.CouldMatch(prefix)).OrderBy<BlueSageCommand, string>((BlueSageCommand command) => command.Name, StringComparer.OrdinalIgnoreCase).Take(maxResults) select "/" + command.Name + " - " + command.Description).ToArray(); } private static bool TryParse(string input, out string commandName, out string arguments) { commandName = string.Empty; arguments = string.Empty; string text = (input ?? string.Empty).Trim(); if ((!text.StartsWith("/", StringComparison.Ordinal) && !text.StartsWith("./", StringComparison.Ordinal)) || text == "/" || text == "./") { return false; } string text2 = TrimCommandPrefix(text); int num = text2.IndexOf(' '); if (num < 0) { commandName = text2; return true; } commandName = text2.Substring(0, num); arguments = text2.Substring(num + 1).Trim(); return commandName.Length > 0; } private static string TrimCommandPrefix(string input) { if (!input.StartsWith("./", StringComparison.Ordinal)) { return input.Substring(1); } return input.Substring(2); } } internal enum HostHealthIssue { LobbyOwnerInvalid, HostIdentityLost, HostMissingFromPlayerList, HostPersonaEmpty, LongRunningLobbyMayBeDelisted } internal enum HostHealthAction { RefreshLobbyMetadata, RequestPersonaRefresh, WarnOnly } internal sealed class HostHealthInput { public bool IsHost { get; } public bool LobbyValid { get; } public string LocalSteamId { get; } public string LobbyOwnerSteamId { get; } public IReadOnlyList<string> PlayerSteamIds { get; } public int ServerPlayerCount { get; } public string HostPersonaName { get; } public double LobbyAgeHours { get; } public double WarnAfterHours { get; } public bool IsHostMachine { get { if (!IsHost) { if (!IsInvalidSteamId(LocalSteamId) && !IsInvalidSteamId(LobbyOwnerSteamId)) { return string.Equals(LocalSteamId, LobbyOwnerSteamId, StringComparison.Ordinal); } return false; } return true; } } public HostHealthInput(bool isHost, bool lobbyValid, string localSteamId, string lobbyOwnerSteamId, IEnumerable<string> playerSteamIds, int serverPlayerCount, string hostPersonaName, double lobbyAgeHours, double warnAfterHours) { IsHost = isHost; LobbyValid = lobbyValid; LocalSteamId = localSteamId ?? string.Empty; LobbyOwnerSteamId = lobbyOwnerSteamId ?? string.Empty; PlayerSteamIds = (playerSteamIds ?? Array.Empty<string>()).Where((string value) => !string.IsNullOrWhiteSpace(value)).ToArray(); ServerPlayerCount = serverPlayerCount; HostPersonaName = hostPersonaName ?? string.Empty; LobbyAgeHours = lobbyAgeHours; WarnAfterHours = warnAfterHours; } public static HostHealthInput DefaultForTests(bool isHost = true, bool lobbyValid = true, string localSteamId = "111", string lobbyOwnerSteamId = "111", IEnumerable<string>? playerSteamIds = null, int serverPlayerCount = 1, string hostPersonaName = "Blue", double lobbyAgeHours = 1.0, double warnAfterHours = 12.0) { return new HostHealthInput(isHost, lobbyValid, localSteamId, lobbyOwnerSteamId, playerSteamIds ?? new string[1] { lobbyOwnerSteamId }, serverPlayerCount, hostPersonaName, lobbyAgeHours, warnAfterHours); } private static bool IsInvalidSteamId(string steamId) { if (!string.IsNullOrWhiteSpace(steamId)) { return steamId == "0"; } return true; } } internal sealed class HostHealthDecision { public bool ShouldRunDiagnostics { get; } public IReadOnlyList<HostHealthIssue> Issues { get; } public IReadOnlyList<HostHealthAction> Actions { get; } public bool IsHealthy { get { if (ShouldRunDiagnostics) { return Issues.Count == 0; } return false; } } public HostHealthDecision(bool shouldRunDiagnostics, IReadOnlyList<HostHealthIssue> issues, IReadOnlyList<HostHealthAction> actions) { ShouldRunDiagnostics = shouldRunDiagnostics; Issues = issues; Actions = actions; } } internal static class HostHealthPolicy { public static HostHealthDecision Evaluate(HostHealthInput input) { if (input == null || !input.IsHostMachine) { return new HostHealthDecision(shouldRunDiagnostics: false, Array.Empty<HostHealthIssue>(), Array.Empty<HostHealthAction>()); } List<HostHealthIssue> list = new List<HostHealthIssue>(); HashSet<HostHealthAction> hashSet = new HashSet<HostHealthAction>(); hashSet.Add(HostHealthAction.RefreshLobbyMetadata); int num; if (input.LobbyValid && !string.IsNullOrWhiteSpace(input.LobbyOwnerSteamId)) { num = ((input.LobbyOwnerSteamId == "0") ? 1 : 0); if (num == 0) { goto IL_0062; } } else { num = 1; } list.Add(HostHealthIssue.LobbyOwnerInvalid); goto IL_0062; IL_0062: if (num == 0 && !input.PlayerSteamIds.Contains(input.LobbyOwnerSteamId)) { list.Add(HostHealthIssue.HostMissingFromPlayerList); } if (num == 0 && !list.Contains(HostHealthIssue.HostMissingFromPlayerList) && string.IsNullOrWhiteSpace(input.HostPersonaName)) { list.Add(HostHealthIssue.HostPersonaEmpty); hashSet.Add(HostHealthAction.RequestPersonaRefresh); } if (input.LobbyAgeHours >= Math.Max(1.0, input.WarnAfterHours) && !list.Contains(HostHealthIssue.LobbyOwnerInvalid) && !list.Contains(HostHealthIssue.HostIdentityLost) && !list.Contains(HostHealthIssue.HostMissingFromPlayerList) && !list.Contains(HostHealthIssue.HostPersonaEmpty)) { list.Add(HostHealthIssue.LongRunningLobbyMayBeDelisted); hashSet.Add(HostHealthAction.WarnOnly); } return new HostHealthDecision(shouldRunDiagnostics: true, list, hashSet.ToArray()); } public static int ClampIntervalSeconds(int value) { if (value < 60) { return 60; } if (value <= 3600) { return value; } return 3600; } } internal static class InputFieldLimitPolicy { public const int DefaultMaxChatCharacters = 750; public const int MinChatCharacters = 1; public const int MaxChatCharacters = 750; public const int MaxProfileCharacters = 750; public const int MaxSessionNameCharacters = 750; private static readonly HashSet<string> SessionNameFields = new HashSet<string>(StringComparer.Ordinal) { "InputField_SesionName", "InputField_SessionName" }; private static readonly HashSet<string> IdCardFields = new HashSet<string>(StringComparer.Ordinal) { "InputField_Name", "InputField_DateOfBirth", "InputField_HoroscopeSign", "InputField_Location", "InputField_AreaOfStudy", "InputField_FavoriteQuote" }; private static readonly HashSet<string> ChatInputFields = new HashSet<string>(StringComparer.Ordinal) { "InputField_Chat", "InputField_Message", "InputField_ChatMessage", "ChatInputField", "MessageInputField" }; public static int? GetLimit(string fieldName, int maxIdCardCharacters, int maxSessionNameCharacters, int maxChatCharacters) { if (string.IsNullOrEmpty(fieldName)) { return null; } if (IdCardFields.Contains(fieldName)) { return Clamp(maxIdCardCharacters, 1, 750); } if (SessionNameFields.Contains(fieldName)) { return Clamp(maxSessionNameCharacters, 1, 750); } if (LooksLikeChatInput(fieldName)) { return ClampChatCharacters(maxChatCharacters); } return null; } public static int ClampChatCharacters(int value) { return Clamp(value, 1, 750); } private static bool LooksLikeChatInput(string fieldName) { if (ChatInputFields.Contains(fieldName)) { return true; } if (fieldName.IndexOf("chat", StringComparison.OrdinalIgnoreCase) < 0) { return fieldName.IndexOf("message", StringComparison.OrdinalIgnoreCase) >= 0; } return true; } private static int Clamp(int value, int min, int max) { if (value < min) { return min; } if (value > max) { return max; } return value; } } internal sealed class PlayerLimitCompanionStatus { public bool IsLoaded { get; } public int? EffectiveMaxLobbySize { get; } public int? EffectiveDefaultLobbySize { get; } public int? EffectiveShiftSkipRate { get; } public bool? ChatRelayPatchEnabled { get; } private PlayerLimitCompanionStatus(bool isLoaded, int? effectiveMaxLobbySize, int? effectiveDefaultLobbySize, int? effectiveShiftSkipRate, bool? chatRelayPatchEnabled) { IsLoaded = isLoaded; EffectiveMaxLobbySize = effectiveMaxLobbySize; EffectiveDefaultLobbySize = effectiveDefaultLobbySize; EffectiveShiftSkipRate = effectiveShiftSkipRate; ChatRelayPatchEnabled = chatRelayPatchEnabled; } public static PlayerLimitCompanionStatus NotLoaded() { return new PlayerLimitCompanionStatus(isLoaded: false, null, null, null, null); } public static PlayerLimitCompanionStatus FromLoadedValues(int? effectiveMaxLobbySize, int? effectiveDefaultLobbySize, int? effectiveShiftSkipRate, bool? chatRelayPatchEnabled) { return new PlayerLimitCompanionStatus(isLoaded: true, effectiveMaxLobbySize, effectiveDefaultLobbySize, effectiveShiftSkipRate, chatRelayPatchEnabled); } public string ToDiagnosticText() { if (!IsLoaded) { return "PlayerLimit=not loaded"; } return "PlayerLimit=loaded cap=" + FormatInt(EffectiveMaxLobbySize) + " default=" + FormatInt(EffectiveDefaultLobbySize) + " shift=" + FormatInt(EffectiveShiftSkipRate) + " chatRelay=" + FormatBool(ChatRelayPatchEnabled); } private static string FormatInt(int? value) { if (!value.HasValue) { return "?"; } return value.Value.ToString(); } private static string FormatBool(bool? value) { if (!value.HasValue) { return "?"; } if (!value.Value) { return "off"; } return "on"; } } internal sealed class PluginCompatibilityResult { public bool AutoSweepAllowed { get; } public bool ManualSweepAllowed { get; } public bool ChatTweaksAllowed { get; } public IReadOnlyList<string> Reasons { get; } public PluginCompatibilityResult(bool autoSweepAllowed, bool manualSweepAllowed, bool chatTweaksAllowed, IReadOnlyList<string> reasons) { AutoSweepAllowed = autoSweepAllowed; ManualSweepAllowed = manualSweepAllowed; ChatTweaksAllowed = chatTweaksAllowed; Reasons = reasons; } } internal static class PluginCompatibility { public const string AndrewSweepGuid = "com.andrewlin.ontogether.sweep"; public const string AndrewReconnectGuid = "com.andrewlin.ontogether.reconnect"; public const string SimpleQolGuid = "tinyplume.SimpleQOL"; public const string BlueSagePlayerLimitLiftGuid = "com.bluesage.ontogether.playerlimitlift"; public static PluginCompatibilityResult Evaluate(IEnumerable<string> loadedPluginGuids, bool autoDisableSweepWhenAndrewSweepInstalled, bool allowManualSweepWhenAndrewSweepInstalled, bool autoDisableChatTweaksWhenSimpleQoLInstalled) { HashSet<string> loaded = new HashSet<string>(loadedPluginGuids ?? Enumerable.Empty<string>(), StringComparer.OrdinalIgnoreCase); List<string> list = new List<string>(); bool autoSweepAllowed = true; bool manualSweepAllowed = true; bool chatTweaksAllowed = true; bool flag = HasGuidOrFuzzyMatch(loaded, "com.andrewlin.ontogether.sweep", "sweep"); if (autoDisableSweepWhenAndrewSweepInstalled && flag) { autoSweepAllowed = false; list.Add("AndrewLin Sweep is loaded; BlueSage auto sweep is disabled to avoid double sweeping."); if (!allowManualSweepWhenAndrewSweepInstalled) { manualSweepAllowed = false; list.Add("AndrewLin Sweep is loaded; BlueSage manual sweep hotkey is disabled unless explicitly allowed in config."); } } if (autoDisableChatTweaksWhenSimpleQoLInstalled && (HasGuidOrFuzzyMatch(loaded, "tinyplume.SimpleQOL", "simpleqol", "simple_qol") || HasGuidOrFuzzyMatch(loaded, "officerballs.ChatTweaks", "chattweaks", "chat_tweaks"))) { chatTweaksAllowed = false; list.Add("Tiny_Plume Simple_QOL is loaded; BlueSage timestamps and leave notices are disabled to avoid duplicate chat formatting."); } return new PluginCompatibilityResult(autoSweepAllowed, manualSweepAllowed, chatTweaksAllowed, list); } private static bool HasGuidOrFuzzyMatch(HashSet<string> loaded, string exactGuid, params string[] fuzzyNeedles) { if (loaded.Contains(exactGuid)) { return true; } foreach (string item in loaded) { if (!LooksLikeLoadedPluginGuid(item)) { continue; } string text = item.Replace("-", string.Empty).Replace("_", string.Empty).Replace(".", string.Empty); for (int i = 0; i < fuzzyNeedles.Length; i++) { string value = fuzzyNeedles[i].Replace("-", string.Empty).Replace("_", string.Empty).Replace(".", string.Empty); if (text.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0) { return true; } } } return false; } private static bool LooksLikeLoadedPluginGuid(string value) { if (string.IsNullOrWhiteSpace(value)) { return false; } if (value.IndexOfAny(new char[3] { '\\', '/', ':' }) >= 0) { return false; } string text = value.ToLowerInvariant(); if (!text.EndsWith(".dll") && !text.Contains(".dll.") && !text.EndsWith(".old") && !text.EndsWith(".bak")) { return !text.EndsWith(".disabled"); } return false; } } internal enum ReconnectGuardAction { Noop, AllowReconnectMod, ScheduleManagedRetry, WaitForManagedRetryCadence, RunManagedRetryNow, StopManagedRetry } internal sealed class ReconnectGuardDecision { public ReconnectGuardAction Action { get; } public string Reason { get; } public int RecommendedAttemptIntervalSeconds { get; } public int RecommendedMaxAttempts { get; } public ReconnectGuardDecision(ReconnectGuardAction action, string reason) : this(action, reason, 60, 300) { } public ReconnectGuardDecision(ReconnectGuardAction action, string reason, int recommendedAttemptIntervalSeconds, int recommendedMaxAttempts) { Action = action; Reason = reason; RecommendedAttemptIntervalSeconds = recommendedAttemptIntervalSeconds; RecommendedMaxAttempts = recommendedMaxAttempts; } } internal sealed class ReconnectGuardInput { public bool GuardEnabled { get; } public bool ReconnectLoaded { get; } public bool SuppressHostKickReconnect { get; } public bool BlockDuringInitialLobbyGrace { get; } public bool RequireVisibleLobby { get; } public int ReconnectAttemptIntervalSeconds { get; } public int InitialLobbyGraceSeconds { get; } public int MaxGuardedAttempts { get; } public bool IsHost { get; } public bool IsIntentionalLeave { get; } public bool IsAlreadyReconnecting { get; } public string NotificationStatusName { get; } public float? SecondsSinceLobbyCreated { get; } public float? SecondsSinceLastManagedAttempt { get; } public int ReconnectAttempt { get; } public int? VisibleLobbyMemberCount { get; } public int? VisibleLobbyMaxPlayers { get; } public ReconnectGuardInput(bool guardEnabled, bool reconnectLoaded, bool suppressHostKickReconnect, bool blockDuringInitialLobbyGrace, bool requireVisibleLobby, int reconnectAttemptIntervalSeconds, int initialLobbyGraceSeconds, int maxGuardedAttempts, bool isHost, bool isIntentionalLeave, bool isAlreadyReconnecting, string notificationStatusName, float? secondsSinceLobbyCreated, float? secondsSinceLastManagedAttempt, int reconnectAttempt, int? visibleLobbyMemberCount, int? visibleLobbyMaxPlayers) { GuardEnabled = guardEnabled; ReconnectLoaded = reconnectLoaded; SuppressHostKickReconnect = suppressHostKickReconnect; BlockDuringInitialLobbyGrace = blockDuringInitialLobbyGrace; RequireVisibleLobby = requireVisibleLobby; ReconnectAttemptIntervalSeconds = reconnectAttemptIntervalSeconds; InitialLobbyGraceSeconds = initialLobbyGraceSeconds; MaxGuardedAttempts = maxGuardedAttempts; IsHost = isHost; IsIntentionalLeave = isIntentionalLeave; IsAlreadyReconnecting = isAlreadyReconnecting; NotificationStatusName = notificationStatusName ?? string.Empty; SecondsSinceLobbyCreated = secondsSinceLobbyCreated; SecondsSinceLastManagedAttempt = secondsSinceLastManagedAttempt; ReconnectAttempt = reconnectAttempt; VisibleLobbyMemberCount = visibleLobbyMemberCount; VisibleLobbyMaxPlayers = visibleLobbyMaxPlayers; } public static ReconnectGuardInput DefaultForTests(bool guardEnabled = true, bool reconnectLoaded = true, bool suppressHostKickReconnect = true, bool blockDuringInitialLobbyGrace = true, bool requireVisibleLobby = true, int reconnectAttemptIntervalSeconds = 60, int initialLobbyGraceSeconds = 20, int maxGuardedAttempts = 300, bool isHost = false, bool isIntentionalLeave = false, bool isAlreadyReconnecting = false, string notificationStatusName = "HostLost", float? secondsSinceLobbyCreated = 30f, float? secondsSinceLastManagedAttempt = null, int reconnectAttempt = 1, int? visibleLobbyMemberCount = 1, int? visibleLobbyMaxPlayers = 128) { return new ReconnectGuardInput(guardEnabled, reconnectLoaded, suppressHostKickReconnect, blockDuringInitialLobbyGrace, requireVisibleLobby, reconnectAttemptIntervalSeconds, initialLobbyGraceSeconds, maxGuardedAttempts, isHost, isIntentionalLeave, isAlreadyReconnecting, notificationStatusName, secondsSinceLobbyCreated, secondsSinceLastManagedAttempt, reconnectAttempt, visibleLobbyMemberCount, visibleLobbyMaxPlayers); } } internal static class ReconnectGuardPolicy { public static ReconnectGuardDecision Evaluate(ReconnectGuardInput input) { if (!input.GuardEnabled) { return Allow("Reconnect guard disabled."); } if (input.IsHost) { return Allow("Host flow is not guarded."); } if (input.IsIntentionalLeave) { return Allow("Reconnect already sees this as an intentional leave."); } int num = ClampSeconds(input.ReconnectAttemptIntervalSeconds, 60, 300); int num2 = ClampSeconds(input.MaxGuardedAttempts, 1, 300); if (input.IsAlreadyReconnecting) { return Noop("BlueSage managed reconnect is already active; suppressing overlapping reconnect pressure.", num, num2); } if (input.ReconnectAttempt > num2) { return Stop($"Stopping managed reconnect after guarded attempt limit ({input.ReconnectAttempt}/{num2}).", num, num2); } if (input.SuppressHostKickReconnect && LooksLikeHostKick(input.NotificationStatusName)) { return Noop("Host kick style disconnect (" + input.NotificationStatusName + ") detected; not auto reconnecting because the host may have removed this client intentionally.", num, num2); } if (input.BlockDuringInitialLobbyGrace && input.SecondsSinceLobbyCreated.HasValue && input.SecondsSinceLobbyCreated.Value >= 0f && input.SecondsSinceLobbyCreated.Value < (float)ClampSeconds(input.InitialLobbyGraceSeconds, 0, 300)) { return Schedule($"Scheduling managed reconnect during lobby grace window ({input.SecondsSinceLobbyCreated.Value:0.0}s since lobby seen).", num, num2); } if (input.RequireVisibleLobby && (!input.VisibleLobbyMemberCount.HasValue || !input.VisibleLobbyMaxPlayers.HasValue)) { return Schedule("Scheduling managed reconnect because visible lobby player count could not be confirmed.", num, num2); } int valueOrDefault = input.VisibleLobbyMemberCount.GetValueOrDefault(); int valueOrDefault2 = input.VisibleLobbyMaxPlayers.GetValueOrDefault(); if (input.RequireVisibleLobby && valueOrDefault < 1) { return Schedule($"Scheduling managed reconnect because visible lobby has {valueOrDefault}/{valueOrDefault2} players.", num, num2); } if (input.SecondsSinceLastManagedAttempt.HasValue && input.SecondsSinceLastManagedAttempt.Value >= 0f && input.SecondsSinceLastManagedAttempt.Value < (float)num) { return Wait($"Waiting for managed reconnect {num}s cadence ({input.SecondsSinceLastManagedAttempt.Value:0.0}s elapsed).", num, num2); } return Run($"Visible lobby confirmed at {valueOrDefault}/{valueOrDefault2} players; managed reconnect retry may run now.", num, num2); } private static ReconnectGuardDecision Allow(string reason) { return new ReconnectGuardDecision(ReconnectGuardAction.AllowReconnectMod, reason); } private static ReconnectGuardDecision Noop(string reason, int intervalSeconds, int maxAttempts) { return new ReconnectGuardDecision(ReconnectGuardAction.Noop, reason, intervalSeconds, maxAttempts); } private static ReconnectGuardDecision Schedule(string reason, int intervalSeconds, int maxAttempts) { return new ReconnectGuardDecision(ReconnectGuardAction.ScheduleManagedRetry, reason, intervalSeconds, maxAttempts); } private static ReconnectGuardDecision Wait(string reason, int intervalSeconds, int maxAttempts) { return new ReconnectGuardDecision(ReconnectGuardAction.WaitForManagedRetryCadence, reason, intervalSeconds, maxAttempts); } private static ReconnectGuardDecision Run(string reason, int intervalSeconds, int maxAttempts) { return new ReconnectGuardDecision(ReconnectGuardAction.RunManagedRetryNow, reason, intervalSeconds, maxAttempts); } private static ReconnectGuardDecision Stop(string reason, int intervalSeconds, int maxAttempts) { return new ReconnectGuardDecision(ReconnectGuardAction.StopManagedRetry, reason, intervalSeconds, maxAttempts); } private static bool LooksLikeHostKick(string notificationStatusName) { string text = (notificationStatusName ?? string.Empty).Replace("_", string.Empty).Replace("-", string.Empty); if (text.IndexOf("kick", StringComparison.OrdinalIgnoreCase) < 0) { return text.IndexOf("removedbyhost", StringComparison.OrdinalIgnoreCase) >= 0; } return true; } private static int ClampSeconds(int value, int min, int max) { if (value < min) { return min; } if (value <= max) { return value; } return max; } } internal sealed class RichTextStyleResult { public bool Success { get; } public string GeneratedText { get; } public string Message { get; } public int RawLength { get; } private RichTextStyleResult(bool success, string generatedText, string message, int rawLength) { Success = success; GeneratedText = generatedText ?? string.Empty; Message = message ?? string.Empty; RawLength = rawLength; } public static RichTextStyleResult Ok(string generatedText) { return new RichTextStyleResult(success: true, generatedText, "Style copied.", generatedText?.Length ?? 0); } public static RichTextStyleResult Fail(string message, int rawLength = 0) { return new RichTextStyleResult(success: false, string.Empty, message, rawLength); } } internal static class RichTextStyleBuilder { private sealed class ColorRgb { private int Red { get; } private int Green { get; } private int Blue { get; } private ColorRgb(int red, int green, int blue) { Red = red; Green = green; Blue = blue; } public static ColorRgb Parse(string hex) { return new ColorRgb(Convert.ToInt32(hex.Substring(0, 2), 16), Convert.ToInt32(hex.Substring(2, 2), 16), Convert.ToInt32(hex.Substring(4, 2), 16)); } public static ColorRgb Lerp(ColorRgb start, ColorRgb end, double ratio) { return new ColorRgb(LerpChannel(start.Red, end.Red, ratio), LerpChannel(start.Green, end.Green, ratio), LerpChannel(start.Blue, end.Blue, ratio)); } public string ToHex() { return Red.ToString("X2") + Green.ToString("X2") + Blue.ToString("X2"); } private static int LerpChannel(int start, int end, double ratio) { int num = (int)Math.Round((double)start + (double)(end - start) * ratio, MidpointRounding.AwayFromZero); if (num < 0) { return 0; } if (num <= 255) { return num; } return 255; } } public const int DefaultMaxGeneratedCharacters = 750; public static RichTextStyleResult TryBuild(string arguments, int maxGeneratedCharacters = 750) { string text = (arguments ?? string.Empty).Trim(); if (string.IsNullOrWhiteSpace(text)) { return RichTextStyleResult.Fail(Usage()); } switch (text.Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries)[0].ToLowerInvariant()) { case "color": return BuildColorMode(text, maxGeneratedCharacters, bold: false, italic: false); case "bold": return BuildColorMode(text, maxGeneratedCharacters, bold: true, italic: false); case "italic": return BuildColorMode(text, maxGeneratedCharacters, bold: false, italic: true); case "bolditalic": case "bi": return BuildColorMode(text, maxGeneratedCharacters, bold: true, italic: true); case "grad": case "gradient": return BuildGradientMode(text, maxGeneratedCharacters, 2); case "grad3": case "gradient3": return BuildGradientMode(text, maxGeneratedCharacters, 3); case "status": return BuildStatusMode(text, maxGeneratedCharacters); default: return RichTextStyleResult.Fail(Usage()); } } private static RichTextStyleResult BuildColorMode(string input, int maxGeneratedCharacters, bool bold, bool italic) { string[] array = input.Sp