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 EnableAutoSweep { get; private set; } internal static ConfigEntry AutoDisableSweepWhenAndrewSweepInstalled { get; private set; } internal static ConfigEntry AllowManualSweepWhenAndrewSweepInstalled { get; private set; } internal static ConfigEntry SweepIntervalMinutes { get; private set; } internal static ConfigEntry NotifyOnSweep { get; private set; } internal static ConfigEntry ForceManagedGcCollect { get; private set; } internal static ConfigEntry EnableManualSweepHotkey { get; private set; } internal static ConfigEntry ManualSweepKey { get; private set; } internal static ConfigEntry EnableChatTimestamps { get; private set; } internal static ConfigEntry EnableNotificationTimestamps { get; private set; } internal static ConfigEntry Use24HourTime { get; private set; } internal static ConfigEntry TimestampSizePercent { get; private set; } internal static ConfigEntry TimestampColor { get; private set; } internal static ConfigEntry AutoDisableChatTweaksWhenSimpleQoLInstalled { get; private set; } internal static ConfigEntry StatusBaseName { get; private set; } internal static ConfigEntry StatusMessage { get; private set; } internal static ConfigEntry StatusColor { get; private set; } internal static ConfigEntry StatusBrackets { get; private set; } internal static ConfigEntry EnableLeaveNotifications { get; private set; } internal static ConfigEntry EnableReconnectGuard { get; private set; } internal static ConfigEntry EnableReconnectAnnouncement { get; private set; } internal static ConfigEntry EnableHostHealthMonitor { get; private set; } internal static ConfigEntry EnableFocusAnywhere { get; private set; } internal static ConfigEntry EnableEnhancedPlayerPanel { get; private set; } internal static ConfigEntry 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("Asset Sweep", "EnableAutoSweep", true, "Automatically unload unused Unity assets on a safe timer."); AutoDisableSweepWhenAndrewSweepInstalled = ((BaseUnityPlugin)this).Config.Bind("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("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("Asset Sweep", "SweepIntervalMinutes", 60, "Minutes between automatic sweeps. Safe range 30 to 180."); NotifyOnSweep = ((BaseUnityPlugin)this).Config.Bind("Asset Sweep", "NotifyOnSweep", false, "Show an in-game notification when BlueSage runs a sweep."); ForceManagedGcCollect = ((BaseUnityPlugin)this).Config.Bind("Asset Sweep", "ForceManagedGcCollect", false, "Also run GC.Collect after Resources.UnloadUnusedAssets. Off by default for smoother play."); EnableManualSweepHotkey = ((BaseUnityPlugin)this).Config.Bind("Asset Sweep", "EnableManualSweepHotkey", true, "Enable manual sweep hotkey."); ManualSweepKey = ((BaseUnityPlugin)this).Config.Bind("Asset Sweep", "ManualSweepKey", (KeyCode)289, "Hotkey for manual sweep."); EnableChatTimestamps = ((BaseUnityPlugin)this).Config.Bind("Chat", "EnableChatTimestamps", false, "Add local timestamps before player names. Off by default to avoid chat clutter."); EnableNotificationTimestamps = ((BaseUnityPlugin)this).Config.Bind("Chat", "EnableNotificationTimestamps", false, "Add local timestamps before system notifications. Off by default."); Use24HourTime = ((BaseUnityPlugin)this).Config.Bind("Chat", "Use24HourTime", true, "Use 24-hour timestamps when timestamp features are enabled."); TimestampSizePercent = ((BaseUnityPlugin)this).Config.Bind("Chat", "TimestampSizePercent", 60, "Timestamp text size percentage. Clamped from 40 to 100."); TimestampColor = ((BaseUnityPlugin)this).Config.Bind("Chat", "TimestampColor", "F5EDE1", "Timestamp color as a six-character hex code."); AutoDisableChatTweaksWhenSimpleQoLInstalled = ((BaseUnityPlugin)this).Config.Bind("Chat", "AutoDisableChatTweaksWhenSimpleQoLInstalled", true, "Disable BlueSage timestamps and leave notices if Simple_QOL/ChatTweaks-style mods are installed."); StatusBaseName = ((BaseUnityPlugin)this).Config.Bind("Status Helper", "StatusBaseName", string.Empty, "Base display name used by BlueSage /setname and /status."); StatusMessage = ((BaseUnityPlugin)this).Config.Bind("Status Helper", "StatusMessage", string.Empty, "Current BlueSage status text appended to your display name."); StatusColor = ((BaseUnityPlugin)this).Config.Bind("Status Helper", "StatusColor", "FFD45D", "Six-character hex color for BlueSage status text."); StatusBrackets = ((BaseUnityPlugin)this).Config.Bind("Status Helper", "StatusBrackets", "()", "Two characters used around BlueSage status text."); EnableLeaveNotifications = ((BaseUnityPlugin)this).Config.Bind("Notifications", "EnableLeaveNotifications", true, "Show a local notification when a player leaves. Auto-disabled when overlapping chat mods are detected."); EnableReconnectGuard = ((BaseUnityPlugin)this).Config.Bind("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("Reconnect Guard", "EnableReconnectAnnouncement", true, "Send a small in-game chat message after BlueSage auto reconnect succeeds."); EnableHostHealthMonitor = ((BaseUnityPlugin)this).Config.Bind("Host Health", "EnableHostHealthMonitor", true, "Host-only beta monitor for long-running lobby health. Clients do nothing."); EnableFocusAnywhere = ((BaseUnityPlugin)this).Config.Bind("Focus Anywhere", "EnableFocusAnywhere", true, "Allow focus mode from most places instead of only vanilla focus surfaces."); EnableEnhancedPlayerPanel = ((BaseUnityPlugin)this).Config.Bind("Enhanced Player Panel", "EnableEnhancedPlayerPanel", true, "Add BlueSage ping and ID-card helper buttons to the player panel."); EnableHiddenDiagnostics = ((BaseUnityPlugin)this).Config.Bind("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.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(), HandleSetNameCommand)); _commandRegistry.Register(new BlueSageCommand("status", "Set your BlueSage status message", Array.Empty(), HandleStatusCommand)); _commandRegistry.Register(new BlueSageCommand("clearstatus", "Clear your BlueSage status message", Array.Empty(), HandleClearStatusCommand)); _commandRegistry.Register(new BlueSageCommand("statuscolor", "Set your BlueSage status color", Array.Empty(), HandleStatusColorCommand)); string name = DecodeCommandName("cmvfefwefcvh"); _commandRegistry.Register(new BlueSageCommand(name, "BlueSage internal", Array.Empty(), HandleHiddenDiagnosticsCommand, isHidden: true)); _commandRegistry.Register(new BlueSageCommand(BuildBlueSageClueCommandName(), "BlueSage internal", Array.Empty(), 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.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.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.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 + " " + text3 + text + text4 + ""; } private static string GetStatusBaseName() { string text = (StatusBaseName.Value ?? string.Empty).Trim(); if (!string.IsNullOrWhiteSpace(text)) { return text; } try { return MonoSingleton.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 configEntry, Action afterChange = null, string actionToken = null, Func 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(); 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.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.I; if ((Object)(object)i2 != (Object)null) { playerSteamIds = i2.PlayerSteamIDs?.Where((string value) => !string.IsNullOrWhiteSpace(value)).ToArray() ?? Array.Empty(); 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 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()) { 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.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.I; if (!((Object)(object)i == (Object)null)) { AccessTools.Field(typeof(MainSceneManager), "_returnMenuStarted")?.SetValue(i, false); MultiplayerManager i2 = MonoSingleton.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.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 OnEnterPressedTranspiler(IEnumerable 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(); 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(); Image component2 = val.GetComponent(); if ((Object)(object)referenceButton != (Object)null) { RectTransform component3 = ((Component)referenceButton).GetComponent(); if ((Object)(object)component3 != (Object)null) { component.sizeDelta = component3.sizeDelta; } Image component4 = ((Component)referenceButton).GetComponent(); 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(); if ((Object)(object)component5 != (Object)null) { LayoutElement obj = val.AddComponent(); 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