using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Pipes; using System.Linq; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security.AccessControl; using System.Security.Cryptography; using System.Security.Principal; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using BlueSage.QoLTweaks.Contracts; using BlueSage.QoLTweaks.Core; using BlueSage.QoLTweaks.Data; using BlueSage.QoLTweaks.Patches; using HarmonyLib; using Microsoft.CodeAnalysis; using PurrNet; using PurrNet.Modules; using PurrNet.Packing; using Steamworks; using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.Events; using UnityEngine.Networking; using UnityEngine.Profiling; using UnityEngine.SceneManagement; 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.2.4.0")] [assembly: AssemblyInformationalVersion("0.2.4+3726c5820cf95325339f9836749fe3c035da0d13")] [assembly: AssemblyProduct("BlueSage_QoL_Tweaks_Beta")] [assembly: AssemblyTitle("BlueSage_QoL_Tweaks_Beta")] [assembly: AssemblyVersion("0.2.4.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] internal sealed class IsReadOnlyAttribute : Attribute { } [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 { public static class BlueSageExtensionRegistry { private sealed class ChainloaderExtensionCatalog : IBlueSageLoadedExtensionCatalogV1 { public bool TryGetLoaded(string pluginGuid, out BlueSageLoadedExtensionV1 extension) { extension = default(BlueSageLoadedExtensionV1); if (!string.IsNullOrWhiteSpace(pluginGuid) && Chainloader.PluginInfos.TryGetValue(pluginGuid, out var value) && !((Object)(object)((value != null) ? value.Instance : null) == (Object)null)) { BepInPlugin metadata = value.Metadata; if (!(((metadata != null) ? metadata.Version : null) == null)) { Assembly assembly = ((object)value.Instance).GetType().Assembly; extension = new BlueSageLoadedExtensionV1(assembly, value.Metadata.Version, loaded: true); return true; } } return false; } } private sealed class DynamicDiagnosticsProvider : IBlueSageDiagnosticsProviderV1, IBlueSageDiagnosticsControlV2 { private const int PayloadLength = 5; private readonly Func _provider; private readonly Func _overlayVisibilitySetter; internal DynamicDiagnosticsProvider(Func provider, Func overlayVisibilitySetter = null) { _provider = provider ?? throw new ArgumentNullException("provider"); _overlayVisibilitySetter = overlayVisibilitySetter; } public BlueSageDiagnosticsSnapshotV1 GetSnapshot(long nowUtcTicks) { long[] array = _provider(nowUtcTicks); if (array == null || array.Length != 5 || array[0] < 0 || array[0] > 2 || array[1] < 0 || array[1] > 2 || array[2] < 100 || array[2] > 5000) { throw new InvalidOperationException("Diagnostics payload failed the public ABI bounds."); } return new BlueSageDiagnosticsSnapshotV1((BlueSageDiagnosticsOverlayStateV1)array[0], (BlueSageDiagnosticsFreshnessV1)array[1], (int)array[2], array[3], array[4]); } public bool TrySetOverlayVisible(bool visible) { if (_overlayVisibilitySetter != null) { return _overlayVisibilitySetter(visible); } return false; } } private static readonly object Sync = new object(); private static readonly BlueSageExtensionRegistryCore Core = new BlueSageExtensionRegistryCore(new ChainloaderExtensionCatalog()); public static BlueSageExtensionRegistrationTokenV1 RegisterBadgeContributionV1(IBlueSageBadgeContributionV1 provider, string ownerGuid, Version ownerVersion, int abiVersion) { lock (Sync) { return Core.RegisterBadgeContribution(provider, ownerGuid, ownerVersion, abiVersion); } } public static bool UnregisterBadgeContributionV1(BlueSageExtensionRegistrationTokenV1 token) { lock (Sync) { return Core.UnregisterBadgeContribution(token); } } public static BlueSageExtensionRegistrationTokenV1 RegisterDiagnosticsProviderV1(IBlueSageDiagnosticsProviderV1 provider, string ownerGuid, Version ownerVersion, int abiVersion) { lock (Sync) { return Core.RegisterDiagnostics(provider, ownerGuid, ownerVersion, abiVersion); } } public static bool UnregisterDiagnosticsProviderV1(BlueSageExtensionRegistrationTokenV1 token) { lock (Sync) { return Core.UnregisterDiagnostics(token); } } public static BlueSageExtensionRegistrationTokenV1 RegisterHostCapabilityProviderV1(IBlueSageHostCapabilityProviderV1 provider, string ownerGuid, Version ownerVersion, int abiVersion) { lock (Sync) { return Core.RegisterHostCapability(provider, ownerGuid, ownerVersion, abiVersion); } } public static bool UnregisterHostCapabilityProviderV1(BlueSageExtensionRegistrationTokenV1 token) { lock (Sync) { return Core.UnregisterHostCapability(token); } } public static long RegisterDiagnosticsProviderDynamicV1(Func provider, string ownerGuid, Version ownerVersion, int abiVersion) { if (provider == null || provider.Target != null || !provider.Method.IsStatic || provider.Method.DeclaringType == null) { return 0L; } Assembly assembly = provider.Method.DeclaringType.Assembly; lock (Sync) { return Core.RegisterDiagnostics(new DynamicDiagnosticsProvider(provider), ownerGuid, ownerVersion, abiVersion, assembly).Generation; } } public static bool UnregisterDiagnosticsProviderDynamicV1(long generation) { lock (Sync) { return Core.UnregisterDiagnostics(new BlueSageExtensionRegistrationTokenV1(generation)); } } public static long RegisterDiagnosticsProviderDynamicV2(Func provider, Func overlayVisibilitySetter, string ownerGuid, Version ownerVersion, int abiVersion) { if (provider == null || overlayVisibilitySetter == null || provider.Target != null || overlayVisibilitySetter.Target != null || !provider.Method.IsStatic || !overlayVisibilitySetter.Method.IsStatic || provider.Method.DeclaringType == null || overlayVisibilitySetter.Method.DeclaringType == null || provider.Method.DeclaringType.Assembly != overlayVisibilitySetter.Method.DeclaringType.Assembly) { return 0L; } Assembly assembly = provider.Method.DeclaringType.Assembly; lock (Sync) { return Core.RegisterDiagnostics(new DynamicDiagnosticsProvider(provider, overlayVisibilitySetter), ownerGuid, ownerVersion, abiVersion, assembly).Generation; } } internal static void BeginLobby(long lobbyEpoch, long evidenceGeneration) { lock (Sync) { Core.SetEvidenceContext(lobbyEpoch, evidenceGeneration); } } internal static void BeginBadgePresentation(long lobbyEpoch, long evidenceGeneration) { lock (Sync) { Core.SetBadgeEvidenceContext(lobbyEpoch, evidenceGeneration); } } internal static bool TryReadBadgeContribution(BlueSageBadgeContributionRequestV1 request, long nowUtcTicks, out BlueSageBadgeContributionResponseV1 response) { lock (Sync) { return Core.TryReadBadgeContribution(request, nowUtcTicks, out response); } } internal static bool TryReadDiagnostics(long nowUtcTicks, out BlueSageDiagnosticsSnapshotV1 snapshot) { lock (Sync) { return Core.TryReadDiagnostics(nowUtcTicks, out snapshot); } } internal static bool TrySetDiagnosticsOverlayVisible(bool visible) { lock (Sync) { return Core.TrySetDiagnosticsOverlayVisible(visible); } } internal static bool TryReadHostCapability(bool isHost, long nonce, long nowUtcTicks, out BlueSageHostCapabilitySnapshotV1 snapshot) { lock (Sync) { snapshot = default(BlueSageHostCapabilitySnapshotV1); BlueSageHostCapabilityRequestV1 request; return Core.TryCreateHostCapabilityRequest(isHost, nonce, nowUtcTicks, out request) && Core.TryReadHostCapability(request, nowUtcTicks, out snapshot); } } internal static bool TryExecuteHostCapability(BlueSageHostCapabilityActionV1 action, bool isHost, long nonce, long nowUtcTicks, out BlueSageHostCapabilityReceiptV1 receipt) { lock (Sync) { receipt = default(BlueSageHostCapabilityReceiptV1); BlueSageHostCapabilityRequestV1 request; return Core.TryCreateHostCapabilityRequest(isHost, nonce, nowUtcTicks, out request) && Core.TryExecuteHostCapability(action, request, nowUtcTicks, out receipt); } } internal static bool HasLiveHostCapabilityProvider() { lock (Sync) { return Core.HasLiveHostCapabilityProvider(); } } internal static void ClearLobbyState() { lock (Sync) { Core.SetEvidenceContext(0L, 0L); Core.SetBadgeEvidenceContext(0L, 0L); } } internal static void ClearAll() { lock (Sync) { Core.Clear(); } } } internal static class BlueSageHelpFrame { internal static HelpFrameLayout Resolve(BlueSageHelpFrameKind kind, Rect window) { return HelpFramePolicy.Resolve(kind, ((Rect)(ref window)).width, ((Rect)(ref window)).height); } internal static void BeginFooter(GUIStyle footerStyle, HelpFrameLayout layout) { GUILayout.BeginVertical(footerStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Height(layout.FooterHeight), GUILayout.MaxHeight(layout.FooterHeight) }); } internal static void DrawStateAndHelp(GUIStyle textStyle, HelpFrameLayout layout, string stateLine, string helpLine) { GUILayout.Label(stateLine ?? string.Empty, textStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Height(layout.StateLineHeight), GUILayout.MaxHeight(layout.StateLineHeight) }); GUILayout.Label(helpLine ?? string.Empty, textStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Height(layout.HelpLineHeight), GUILayout.MaxHeight(layout.HelpLineHeight) }); } internal static void EndFooter() { GUILayout.EndVertical(); } } internal sealed class BlueSageUiThemePalette { public string Key { get; } public string DisplayName { get; } public Color Window { get; } public Color Panel { get; } public Color Header { get; } public Color HeaderText { get; } public Color LabelText { get; } public Color SmallText { get; } public Color ButtonText { get; } public Color ActiveButtonText { get; } public Color FieldText { get; } public Color PreviewBackground { get; } public bool IsMidnightDock => string.Equals(Key, "Midnight Dock", StringComparison.OrdinalIgnoreCase); public bool UsesWarmContent { get { if (!string.Equals(Key, "BlueSage Harbor", StringComparison.OrdinalIgnoreCase) && !string.Equals(Key, "Vanilla Cream OT", StringComparison.OrdinalIgnoreCase)) { return string.Equals(Key, "OG", StringComparison.OrdinalIgnoreCase); } return true; } } public Color SectionPanel { get { //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) if (!string.Equals(Key, "BlueSage Harbor", StringComparison.OrdinalIgnoreCase)) { return Color.Lerp(Panel, Window, 0.16f); } return new Color(0.975f, 0.945f, 0.885f, 1f); } } public Color SectionBorder { get { //IL_0068: 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_0044: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_002d: 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) if (!string.Equals(Key, "BlueSage Harbor", StringComparison.OrdinalIgnoreCase)) { if (!string.Equals(Key, "Vanilla Cream OT", StringComparison.OrdinalIgnoreCase)) { return Color.Lerp(Header, LabelText, 0.3f); } return Color.Lerp(Header, FieldText, 0.4f); } return new Color(0.65f, 0.52f, 0.39f, 1f); } } public Color FieldBackground { get { //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) if (!IsMidnightDock) { return Color.Lerp(SectionPanel, Color.white, UsesWarmContent ? 0.44f : 0.1f); } return new Color(0.045f, 0.065f, 0.105f, 1f); } } public Color FieldHoverBackground { get { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) if (!IsMidnightDock) { return Color.Lerp(FieldBackground, Header, 0.1f); } return new Color(0.075f, 0.115f, 0.165f, 1f); } } public Color FieldFocusedBackground { get { //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) if (!IsMidnightDock) { return Color.Lerp(FieldBackground, Color.white, 0.16f); } return new Color(0.055f, 0.135f, 0.165f, 1f); } } public Color FieldBorder { get { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) if (!IsMidnightDock) { return SectionBorder; } return new Color(0.28f, 0.42f, 0.56f, 1f); } } public Color FieldFocusedBorder { get { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) if (!IsMidnightDock) { return Color.Lerp(ActiveAccent, DarkStateText, 0.18f); } return new Color(0.24f, 0.72f, 0.78f, 1f); } } public Color ActiveAccent { get { //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) if (!string.Equals(Key, "BlueSage Harbor", StringComparison.OrdinalIgnoreCase)) { return Header; } return new Color(0.91f, 0.49f, 0.39f, 1f); } } public Color EnabledAccent { get { //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_002d: 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) if (!string.Equals(Key, "BlueSage Harbor", StringComparison.OrdinalIgnoreCase)) { return Color.Lerp(Header, new Color(0.45f, 0.8f, 0.36f, 1f), 0.58f); } return new Color(0.39f, 0.55f, 0.24f, 1f); } } public string NavigationKeywordHex { get { if (!IsMidnightDock) { return "005A85"; } return "70D6FF"; } } public Color DisabledAccent { get { //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) if (!string.Equals(Key, "BlueSage Harbor", StringComparison.OrdinalIgnoreCase)) { if (!IsMidnightDock) { return Color.Lerp(ButtonText, Window, 0.58f); } return new Color(0.26f, 0.31f, 0.39f, 1f); } return new Color(0.68f, 0.65f, 0.61f, 1f); } } public Color DarkStateText { get { //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) if (!IsMidnightDock) { if (!string.Equals(Key, "BlueSage Harbor", StringComparison.OrdinalIgnoreCase)) { return FieldText; } return Header; } return Window; } } public Color ActiveStateText { get { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) if (!IsMidnightDock) { return DarkStateText; } return ActiveButtonText; } } public Color EnabledButtonText => DarkStateText; public Color DisabledButtonText { get { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) if (!IsMidnightDock) { return DarkStateText; } return new Color(0.84f, 0.89f, 0.94f, 1f); } } public Color ExperimentalButtonText => DarkStateText; public Color NeutralButtonAccent { get { //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) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) if (!IsMidnightDock) { return Color.Lerp(Panel, ButtonText, 0.3f); } return Color.Lerp(Panel, Color.white, 0.12f); } } public Color NeutralButtonText { get { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) if (!IsMidnightDock) { return DarkStateText; } return ButtonText; } } public Color PreviewText { get { //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) if (!string.Equals(Key, "OG", StringComparison.OrdinalIgnoreCase) && !string.Equals(Key, "Vanilla Cream OT", StringComparison.OrdinalIgnoreCase)) { return LabelText; } return Color.white; } } public Color PopupHeaderText { get { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) if (!IsMidnightDock) { return DarkStateText; } return ActiveButtonText; } } public Color PopupBodyText { get { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) if (!IsMidnightDock) { return DarkStateText; } return LabelText; } } public Color PopupHintText { get { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0016: 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_0009: Unknown result type (might be due to invalid IL or missing references) if (!IsMidnightDock) { return DarkStateText; } return Color.Lerp(SmallText, LabelText, 0.35f); } } public Color ComfortAccent { get { //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) if (!IsMidnightDock) { return new Color(0.39f, 0.55f, 0.24f, 1f); } return new Color(0.42f, 0.76f, 0.49f, 1f); } } public Color ChatAccent { get { //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) if (!IsMidnightDock) { return new Color(0.1f, 0.56f, 0.62f, 1f); } return new Color(0.32f, 0.72f, 0.82f, 1f); } } public Color IdentityAccent { get { //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) if (!IsMidnightDock) { return new Color(0.47f, 0.4f, 0.68f, 1f); } return new Color(0.55f, 0.63f, 0.91f, 1f); } } public Color PersonalizationAccent { get { //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) if (!IsMidnightDock) { return new Color(0.72f, 0.39f, 0.5f, 1f); } return new Color(0.82f, 0.55f, 0.73f, 1f); } } public Color ExperimentalAccent => new Color(1f, 0.42f, 0.08f, 1f); public Color ExperimentalBorder { get { //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) if (!IsMidnightDock) { return Color.Lerp(ExperimentalAccent, DarkStateText, 0.35f); } return ExperimentalAccent; } } public Color FooterBackground { get { //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) if (!string.Equals(Key, "BlueSage Harbor", StringComparison.OrdinalIgnoreCase)) { return Color.Lerp(Header, Window, 0.3f); } return new Color(0.055f, 0.075f, 0.115f, 1f); } } public Color FooterText { get { //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) if (!string.Equals(Key, "BlueSage Harbor", StringComparison.OrdinalIgnoreCase)) { return HeaderText; } return new Color(0.84f, 0.93f, 0.98f, 1f); } } public Color FooterBorder { get { //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) if (!IsMidnightDock) { if (!string.Equals(Key, "BlueSage Harbor", StringComparison.OrdinalIgnoreCase)) { return DarkStateText; } return SectionBorder; } return FieldFocusedBorder; } } public BlueSageUiThemePalette(string key, string displayName, Color window, Color panel, Color header, Color headerText, Color labelText, Color smallText, Color buttonText, Color activeButtonText, Color fieldText, Color previewBackground) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_003c: 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_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_005c: 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) Key = key; DisplayName = displayName; Window = window; Panel = panel; Header = header; HeaderText = headerText; LabelText = labelText; SmallText = smallText; ButtonText = buttonText; ActiveButtonText = activeButtonText; FieldText = fieldText; PreviewBackground = previewBackground; } } internal static class BlueSageUiTheme { public const string DefaultThemeKey = "BlueSage Harbor"; private static readonly Dictionary SolidTextures = new Dictionary(StringComparer.Ordinal); private static readonly Dictionary PanelTextures = new Dictionary(StringComparer.Ordinal); private static readonly BlueSageUiThemePalette[] Palettes = new BlueSageUiThemePalette[4] { new BlueSageUiThemePalette("BlueSage Harbor", "BlueSage Harbor", new Color(0.93f, 0.875f, 0.79f, 0.995f), new Color(0.985f, 0.95f, 0.88f, 0.995f), new Color(0.055f, 0.075f, 0.115f, 1f), new Color(0.84f, 0.95f, 0.98f, 1f), new Color(0.2f, 0.16f, 0.15f, 1f), new Color(0.35f, 0.29f, 0.27f, 1f), new Color(0.22f, 0.18f, 0.17f, 1f), new Color(1f, 0.96f, 0.88f, 1f), new Color(0.12f, 0.1f, 0.1f, 1f), new Color(0.985f, 0.95f, 0.88f, 0.98f)), new BlueSageUiThemePalette("OG", "OG", new Color(0.93f, 0.9f, 0.83f, 0.99f), new Color(0.84f, 0.8f, 0.72f, 0.98f), new Color(0.55f, 0.52f, 0.47f, 1f), new Color(0.12f, 0.09f, 0.1f, 1f), new Color(0.22f, 0.14f, 0.17f, 1f), new Color(0.34f, 0.26f, 0.29f, 1f), new Color(0.24f, 0.17f, 0.2f, 1f), Color.white, new Color(0.14f, 0.11f, 0.12f, 1f), new Color(0.28f, 0.27f, 0.25f, 0.96f)), new BlueSageUiThemePalette("Vanilla Cream OT", "Vanilla Cream OT", new Color(0.98f, 0.93f, 0.84f, 0.99f), new Color(0.91f, 0.82f, 0.69f, 0.98f), new Color(0.91f, 0.49f, 0.42f, 1f), new Color(0.23f, 0.14f, 0.16f, 1f), new Color(0.32f, 0.22f, 0.25f, 1f), new Color(0.43f, 0.31f, 0.33f, 1f), new Color(0.31f, 0.22f, 0.25f, 1f), Color.white, new Color(0.15f, 0.11f, 0.12f, 1f), new Color(0.39f, 0.31f, 0.27f, 0.96f)), new BlueSageUiThemePalette("Midnight Dock", "Midnight Dock", new Color(0.08f, 0.1f, 0.16f, 0.99f), new Color(0.13f, 0.16f, 0.24f, 0.98f), new Color(0.08f, 0.36f, 0.42f, 1f), new Color(0.88f, 0.98f, 1f, 1f), new Color(0.91f, 0.96f, 1f, 1f), new Color(0.72f, 0.82f, 0.9f, 1f), new Color(0.89f, 0.94f, 1f, 1f), new Color(1f, 0.91f, 0.42f, 1f), new Color(0.96f, 0.98f, 1f, 1f), new Color(0.04f, 0.05f, 0.08f, 0.97f)) }; public static int RuntimeTextureGeneration { get; private set; } = 1; public static BlueSageUiThemePalette Current => Get(Plugin.UiThemePreset?.Value); public static string[] Names => Array.ConvertAll(Palettes, (BlueSageUiThemePalette palette) => palette.DisplayName); public static BlueSageUiThemePalette Get(string requested) { string b = Normalize(requested); BlueSageUiThemePalette[] palettes = Palettes; foreach (BlueSageUiThemePalette blueSageUiThemePalette in palettes) { if (string.Equals(blueSageUiThemePalette.Key, b, StringComparison.OrdinalIgnoreCase) || string.Equals(blueSageUiThemePalette.DisplayName, b, StringComparison.OrdinalIgnoreCase)) { return blueSageUiThemePalette; } } return GetDefaultPalette(); } public static string Normalize(string requested) { string text = (requested ?? string.Empty).Trim(); if (text.Length == 0) { return "BlueSage Harbor"; } BlueSageUiThemePalette[] palettes = Palettes; foreach (BlueSageUiThemePalette blueSageUiThemePalette in palettes) { if (string.Equals(blueSageUiThemePalette.Key, text, StringComparison.OrdinalIgnoreCase) || string.Equals(blueSageUiThemePalette.DisplayName, text, StringComparison.OrdinalIgnoreCase)) { return blueSageUiThemePalette.Key; } } return "BlueSage Harbor"; } public static string Next(string requested) { string b = Normalize(requested); for (int i = 0; i < Palettes.Length; i++) { if (string.Equals(Palettes[i].Key, b, StringComparison.OrdinalIgnoreCase)) { return Palettes[(i + 1) % Palettes.Length].Key; } } return GetDefaultPalette().Key; } private static BlueSageUiThemePalette GetDefaultPalette() { BlueSageUiThemePalette[] palettes = Palettes; foreach (BlueSageUiThemePalette blueSageUiThemePalette in palettes) { if (string.Equals(blueSageUiThemePalette.Key, "BlueSage Harbor", StringComparison.OrdinalIgnoreCase)) { return blueSageUiThemePalette; } } return Palettes[0]; } public static GUIStyle CreateTextInputStyle(GUIStyle source, BlueSageUiThemePalette palette, int fontSize, bool wordWrap) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001b: 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_002a: Expected O, but got Unknown //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Expected O, but got Unknown //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_0086: 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_0097: Unknown result type (might be due to invalid IL or missing references) //IL_009d: 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) //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Unknown result type (might be due to invalid IL or missing references) //IL_00ca: Unknown result type (might be due to invalid IL or missing references) //IL_00d1: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00dd: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Unknown result type (might be due to invalid IL or missing references) //IL_00ee: Unknown result type (might be due to invalid IL or missing references) //IL_00f4: Unknown result type (might be due to invalid IL or missing references) //IL_00fa: Unknown result type (might be due to invalid IL or missing references) //IL_0104: Unknown result type (might be due to invalid IL or missing references) //IL_010b: Unknown result type (might be due to invalid IL or missing references) //IL_0111: Unknown result type (might be due to invalid IL or missing references) //IL_0117: Unknown result type (might be due to invalid IL or missing references) //IL_0122: Expected O, but got Unknown GUIStyle val = new GUIStyle(source) { richText = false, fontSize = fontSize, wordWrap = wordWrap, border = new RectOffset(1, 1, 1, 1), padding = new RectOffset(8, 8, 5, 5) }; ApplyState(val.normal, palette.FieldBackground, palette.FieldBorder, palette.FieldText); ApplyState(val.hover, palette.FieldHoverBackground, palette.FieldBorder, palette.FieldText); ApplyState(val.active, palette.FieldFocusedBackground, palette.FieldFocusedBorder, palette.FieldText); ApplyState(val.focused, palette.FieldFocusedBackground, palette.FieldFocusedBorder, palette.FieldText); ApplyState(val.onNormal, palette.FieldBackground, palette.FieldBorder, palette.FieldText); ApplyState(val.onHover, palette.FieldHoverBackground, palette.FieldBorder, palette.FieldText); ApplyState(val.onActive, palette.FieldFocusedBackground, palette.FieldFocusedBorder, palette.FieldText); ApplyState(val.onFocused, palette.FieldFocusedBackground, palette.FieldFocusedBorder, palette.FieldText); return val; } public static GUIStyle CreateStateButtonStyle(GUIStyle source, Color fill, Color border, Color text, int fontSize) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Expected O, but got Unknown //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0039: 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_0059: Unknown result type (might be due to invalid IL or missing references) //IL_005a: 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_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_0093: 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_0095: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: Unknown result type (might be due to invalid IL or missing references) //IL_00b0: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00b8: Unknown result type (might be due to invalid IL or missing references) //IL_00be: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Unknown result type (might be due to invalid IL or missing references) //IL_00ca: Unknown result type (might be due to invalid IL or missing references) //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00d5: Unknown result type (might be due to invalid IL or missing references) //IL_00db: Unknown result type (might be due to invalid IL or missing references) //IL_00dc: Unknown result type (might be due to invalid IL or missing references) //IL_00dd: Unknown result type (might be due to invalid IL or missing references) //IL_00e3: Unknown result type (might be due to invalid IL or missing references) //IL_00e9: Unknown result type (might be due to invalid IL or missing references) //IL_00ea: Unknown result type (might be due to invalid IL or missing references) //IL_00eb: 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_00fa: Unknown result type (might be due to invalid IL or missing references) //IL_0100: Unknown result type (might be due to invalid IL or missing references) //IL_0106: Unknown result type (might be due to invalid IL or missing references) //IL_0107: Unknown result type (might be due to invalid IL or missing references) //IL_0108: Unknown result type (might be due to invalid IL or missing references) //IL_010e: Unknown result type (might be due to invalid IL or missing references) //IL_0114: Unknown result type (might be due to invalid IL or missing references) //IL_0115: Unknown result type (might be due to invalid IL or missing references) //IL_0116: Unknown result type (might be due to invalid IL or missing references) //IL_0120: Unknown result type (might be due to invalid IL or missing references) //IL_0125: Unknown result type (might be due to invalid IL or missing references) //IL_012c: Expected O, but got Unknown GUIStyle val = new GUIStyle(source) { fontSize = fontSize, fontStyle = (FontStyle)1, border = new RectOffset(1, 1, 1, 1) }; bool flag = RelativeLuminance(text) >= 0.5f; Color fill2 = Color.Lerp(fill, flag ? Color.black : Color.white, flag ? 0.08f : 0.11f); Color fill3 = Color.Lerp(fill, flag ? Color.black : Color.white, flag ? 0.18f : 0.2f); ApplyState(val.normal, fill, border, text); ApplyState(val.hover, fill2, Color.Lerp(border, Color.white, 0.16f), text); ApplyState(val.active, fill3, border, text); ApplyState(val.focused, fill2, Color.Lerp(border, Color.white, 0.16f), text); ApplyState(val.onNormal, fill, border, text); ApplyState(val.onHover, fill2, Color.Lerp(border, Color.white, 0.16f), text); ApplyState(val.onActive, fill3, border, text); ApplyState(val.onFocused, fill2, Color.Lerp(border, Color.white, 0.16f), text); return val; } public static GUIStyle CreateNeutralButtonStyle(GUIStyle source, BlueSageUiThemePalette palette, int fontSize) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) return CreateStateButtonStyle(source, palette.NeutralButtonAccent, palette.FieldBorder, palette.NeutralButtonText, fontSize); } public static GUIStyle CreateSwatchButtonStyle(GUIStyle source, int fontSize) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Expected O, but got Unknown //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_005f: 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_0066: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_00a7: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: 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_00bd: Unknown result type (might be due to invalid IL or missing references) //IL_00be: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_00d3: Unknown result type (might be due to invalid IL or missing references) //IL_00d4: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Unknown result type (might be due to invalid IL or missing references) //IL_00e3: Unknown result type (might be due to invalid IL or missing references) //IL_00e9: Unknown result type (might be due to invalid IL or missing references) //IL_00ea: Unknown result type (might be due to invalid IL or missing references) //IL_00ef: Unknown result type (might be due to invalid IL or missing references) //IL_00f9: Unknown result type (might be due to invalid IL or missing references) //IL_00ff: Unknown result type (might be due to invalid IL or missing references) //IL_0100: Unknown result type (might be due to invalid IL or missing references) //IL_0105: Unknown result type (might be due to invalid IL or missing references) //IL_0110: Expected O, but got Unknown GUIStyle val = new GUIStyle(source) { fontSize = fontSize, fontStyle = (FontStyle)1, border = new RectOffset(1, 1, 1, 1) }; Color white = Color.white; Color fill = default(Color); ((Color)(ref fill))..ctor(0.92f, 0.92f, 0.92f, 1f); Color fill2 = default(Color); ((Color)(ref fill2))..ctor(0.82f, 0.82f, 0.82f, 1f); ApplyState(val.normal, white, Color.black, Color.white); ApplyState(val.hover, fill, Color.black, Color.white); ApplyState(val.active, fill2, Color.black, Color.white); ApplyState(val.focused, fill, Color.black, Color.white); ApplyState(val.onNormal, white, Color.black, Color.white); ApplyState(val.onHover, fill, Color.black, Color.white); ApplyState(val.onActive, fill2, Color.black, Color.white); ApplyState(val.onFocused, fill, Color.black, Color.white); return val; } public static Color BestTextColor(Color background, BlueSageUiThemePalette palette) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0018: 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) //IL_002b: 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) Color val = palette?.DarkStateText ?? Color.black; if (!(ContrastRatio(val, background) >= ContrastRatio(Color.white, background))) { return Color.white; } return val; } private static float ContrastRatio(Color first, Color second) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) float val = RelativeLuminance(first); float val2 = RelativeLuminance(second); float num = Math.Max(val, val2); float num2 = Math.Min(val, val2); return (num + 0.05f) / (num2 + 0.05f); } private static float RelativeLuminance(Color color) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) return 0.2126f * Linearize(color.r) + 0.7152f * Linearize(color.g) + 0.0722f * Linearize(color.b); } private static float Linearize(float channel) { if (!(channel <= 0.03928f)) { return (float)Math.Pow((channel + 0.055f) / 1.055f, 2.4000000953674316); } return channel / 12.92f; } public static Texture2D GetSolidTexture(Color color) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Expected O, but got Unknown //IL_006b: Unknown result type (might be due to invalid IL or missing references) string text = ColorKey(color); if (SolidTextures.TryGetValue(text, out var value)) { if ((Object)(object)value != (Object)null) { return value; } SolidTextures.Remove(text); RuntimeTextureGeneration++; } Texture2D val = new Texture2D(1, 1) { name = "BlueSageUi-Solid-" + text, hideFlags = (HideFlags)61, wrapMode = (TextureWrapMode)1, filterMode = (FilterMode)0 }; val.SetPixel(0, 0, color); val.Apply(); SolidTextures[text] = val; return val; } public static Texture2D GetPanelTexture(Color fill, Color border) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_0069: 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) //IL_0078: Expected O, but got Unknown //IL_00a1: 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) string text = ColorKey(fill) + "-" + ColorKey(border); if (PanelTextures.TryGetValue(text, out var value)) { if ((Object)(object)value != (Object)null) { return value; } PanelTextures.Remove(text); RuntimeTextureGeneration++; } Texture2D val = new Texture2D(3, 3) { name = "BlueSageUi-Panel-" + text, hideFlags = (HideFlags)61, wrapMode = (TextureWrapMode)1, filterMode = (FilterMode)0 }; for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { bool flag = j == 0 || i == 0 || j == 2 || i == 2; val.SetPixel(j, i, flag ? border : fill); } } val.Apply(); PanelTextures[text] = val; return val; } public static bool AreRuntimeTexturesAlive() { foreach (Texture2D value in SolidTextures.Values) { if ((Object)(object)value == (Object)null) { return false; } } foreach (Texture2D value2 in PanelTextures.Values) { if ((Object)(object)value2 == (Object)null) { return false; } } return true; } public static void ReleaseRuntimeTextures() { foreach (Texture2D value in SolidTextures.Values) { if ((Object)(object)value != (Object)null) { Object.Destroy((Object)(object)value); } } foreach (Texture2D value2 in PanelTextures.Values) { if ((Object)(object)value2 != (Object)null) { Object.Destroy((Object)(object)value2); } } SolidTextures.Clear(); PanelTextures.Clear(); RuntimeTextureGeneration++; } private static string ColorKey(Color color) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) Color32 val = Color32.op_Implicit(color); return val.r.ToString("X2") + val.g.ToString("X2") + val.b.ToString("X2") + val.a.ToString("X2"); } private static void ApplyState(GUIStyleState state, Color fill, Color border, Color text) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) state.background = GetPanelTexture(fill, border); state.textColor = text; } } internal enum BlueSagePublicWindow { QolMenu, StyleHelper } internal static class BlueSageWindowCoordinator { private static bool _qolMenuVisible; private static bool _styleHelperVisible; internal static void SetVisible(BlueSagePublicWindow window, bool visible) { if (window == BlueSagePublicWindow.QolMenu) { _qolMenuVisible = visible; } else { _styleHelperVisible = visible; } } internal static WindowFitResult ResolveForOpen(BlueSagePublicWindow window, bool hasOpened, Rect current, float preferredMinWidth, float preferredMinHeight, float screenWidth, float screenHeight) { SetVisible(window, visible: true); return WindowFitPolicy.FitForOpen(hasOpened, ((Rect)(ref current)).x, ((Rect)(ref current)).y, ((Rect)(ref current)).width, ((Rect)(ref current)).height, preferredMinWidth, preferredMinHeight, screenWidth, screenHeight); } internal static WindowFitResult ResolveForFrame(BlueSagePublicWindow window, Rect current, float preferredMinWidth, float preferredMinHeight, float screenWidth, float screenHeight) { SetVisible(window, visible: true); return WindowFitPolicy.FitToScreen(((Rect)(ref current)).x, ((Rect)(ref current)).y, ((Rect)(ref current)).width, ((Rect)(ref current)).height, preferredMinWidth, preferredMinHeight, screenWidth, screenHeight, expandToMaximum: false); } } internal static class BlueSageWindowHoverScope { private static int _hoveredWindowId; private static int _hoveredPriority; private static int _hoveredFrame = -1; internal static void RegisterWindow(int windowId, Rect screenRect, int priority) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) Event current = Event.current; if (current != null && ((Rect)(ref screenRect)).Contains(current.mousePosition)) { int frameCount = Time.frameCount; if (_hoveredFrame != frameCount || priority >= _hoveredPriority) { _hoveredWindowId = windowId; _hoveredPriority = priority; _hoveredFrame = frameCount; } } } internal static bool IsWindowHovered(int windowId) { if (_hoveredFrame == Time.frameCount) { return _hoveredWindowId == windowId; } return false; } internal static void UnregisterWindow(int windowId) { if (_hoveredFrame == Time.frameCount && _hoveredWindowId == windowId) { _hoveredWindowId = 0; _hoveredPriority = 0; _hoveredFrame = -1; } } } internal static class ChalkboardModerationController { internal static string DescribeAuthority() { if (!SteamIdModerationController.CanLocalIssue(out var isHost, out var _)) { return "local-only"; } if (!isHost) { return "authenticated Helper-to-Host"; } return "Host-direct"; } internal static int CountCompatibleRecipients() { return (PlayerIdentityEvidenceController.GetVerifiedRoster() ?? Array.Empty()).Count(IsCompatibleRecipient); } internal static bool IsCompatibleRecipient(PlayerIdentityEvidence evidence) { //IL_0029: 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_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) if (evidence == null || evidence.IsLocal || !ulong.TryParse(evidence.SteamId, NumberStyles.None, CultureInfo.InvariantCulture, out var result) || !Plugin.TryGetRescueLobby(out var lobbyId) || lobbyId == CSteamID.Nil) { return false; } try { return CompatibleClientPresencePolicy.IsFresh(SteamMatchmaking.GetLobbyMemberData(lobbyId, new CSteamID(result), "bluesage_qol_client_v1"), DateTimeOffset.UtcNow.ToUnixTimeSeconds()); } catch { return false; } } internal static string BuildTruth(string action, int boardIndex, string scene, string snapshot, string rollback, string vanillaObserver = "not-observed") { return ChalkboardCommandPolicy.BuildActionTruth(action, boardIndex, scene, DescribeAuthority(), CountCompatibleRecipients(), snapshot, rollback, vanillaObserver); } } internal static class ChalkboardPersistenceController { private const string ChalkyGuid = "com.andrewlin.ontogether.chalky"; private const string Magic = "BSCH1"; private const int MaxDimension = 512; private const int MaxColorIndex = 255; private const long MemberDataPollIntervalMilliseconds = 500L; private const int ClearCooldownSeconds = 5; private static readonly Regex SafeName = new Regex("^[A-Za-z0-9][A-Za-z0-9_-]{0,39}$", RegexOptions.Compiled); private static readonly FieldInfo BoardIndexField = AccessTools.Field(typeof(DrawingManager), "_boardIndex"); private static readonly HashSet SeenClearNonces = new HashSet(StringComparer.Ordinal); private static readonly Queue ClearNonceOrder = new Queue(); private static readonly Dictionary LastClearRequestByIssuer = new Dictionary(StringComparer.Ordinal); private static readonly Dictionary LastClearByBoard = new Dictionary(StringComparer.Ordinal); private static DateTime _lastSharedRestoreUtc = DateTime.MinValue; private static long _nextMemberDataPollUnixMilliseconds; private static string _memberDataLobbyKey = string.Empty; private static string _boardSetSignature = string.Empty; private static string _lastListedBoardSetSignature = string.Empty; private static long _lastListedBoardSetUnixSeconds; private static bool _rollbackUnresolved; internal static bool ChalkyInstalled => Chainloader.PluginInfos.ContainsKey("com.andrewlin.ontogether.chalky"); internal static bool Available { get { ConfigEntry enableChalkboardPersistence = Plugin.EnableChalkboardPersistence; if (enableChalkboardPersistence != null && enableChalkboardPersistence.Value) { return !ChalkyInstalled; } return false; } } internal static string SaveDirectory => Path.Combine(Paths.ConfigPath, "BlueSageChalkboards"); internal static string Status() { if (ChalkyInstalled) { return "Chalkboard Persistence: Andrew's Chalky is installed, so BlueSage is safely standing down."; } return "Chalkboard Persistence: " + (Available ? "on." : "off.") + " Catalog=0 River/Stage, 1 Classroom board A (face A), 2 Classroom board B (face B); index 3 is unsupported. Local view/save/delete/map refresh are available to every QoL user. Shared load/clear require Host-direct or authenticated Helper-to-Host authority, a fresh /chalk boards map, and exact current identity. Compatible clients receive full-state replacement; vanilla observer state remains not-observed until live UAT. There is no authoritative drawing-owner identity. rollback=" + ((_rollbackUnresolved || ChalkboardSnapshotTransportController.DestructiveActionsBlocked) ? ChalkboardSnapshotTransportController.RollbackState : "ready") + "."; } internal static string Save(string name, string boardToken) { if (!Available) { return Status(); } if (!TryResolve(name, boardToken, out var normalized, out var board, out var index, out var error)) { return error; } TrySaveResolved(normalized, board, index, out var message); return message; } private static bool TrySaveResolved(string normalized, QuadPainterGPU board, int index, out string message) { try { ValidateGrid(board.PaintColors); Directory.CreateDirectory(SaveDirectory); string text = Path.Combine(SaveDirectory, normalized + ".bschalk"); string text2 = text + ".tmp"; using (FileStream fileStream = new FileStream(text2, FileMode.Create, FileAccess.Write, FileShare.None)) { using BinaryWriter binaryWriter = new BinaryWriter(fileStream); binaryWriter.Write("BSCH1"); binaryWriter.Write(board.PaintColors.Length); binaryWriter.Write(board.PaintColors[0].Ints.Count); for (int i = 0; i < board.PaintColors.Length; i++) { for (int j = 0; j < board.PaintColors[i].Ints.Count; j++) { binaryWriter.Write(board.PaintColors[i].Ints[j]); } } fileStream.Flush(flushToDisk: true); } if (File.Exists(text)) { string destinationBackupFileName = text + ".bak"; File.Replace(text2, text, destinationBackupFileName, ignoreMetadataErrors: true); } else { File.Move(text2, text); } message = $"Chalkboard '{normalized}' saved locally from board {index}. " + ChalkboardModerationController.BuildTruth("save", index, DescribeBoardScene(board, index), normalized, "not-needed"); return true; } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("Chalkboard save failed safely: " + ex.GetType().Name + ": " + ex.Message)); } message = "Chalkboard save failed safely; the existing save was left intact."; return false; } } internal static string Load(string name, string boardToken) { //IL_00f9: 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) //IL_00ca: Unknown result type (might be due to invalid IL or missing references) //IL_0146: Unknown result type (might be due to invalid IL or missing references) if (!Available) { return Status(); } if (!TryNormalizeName(name, out var normalized, out var error)) { return error; } if (!TryResolveBoard(boardToken, out var board, out var index, out var fingerprint, out var error2)) { return error2; } DrawingManager i = MonoSingleton.I; if (!ChalkboardCommandPolicy.TryAuthorizeDestructiveAction(index, (i?.QuadPainterGPUS?.Count).GetValueOrDefault(), _lastListedBoardSetSignature, _lastListedBoardSetUnixSeconds, BuildBoardSetSignature(i), DateTimeOffset.UtcNow.ToUnixTimeSeconds(), _rollbackUnresolved || ChalkboardSnapshotTransportController.DestructiveActionsBlocked, out var error3)) { return error3; } if (!SteamIdModerationController.CanLocalIssue(out var isHost, out var reason)) { return reason; } if (isHost) { string issuerSteamId = (SteamManager.Initialized ? ((ulong)SteamUser.GetSteamID()).ToString() : string.Empty); ExecuteLoadAsHost(normalized, index, fingerprint, issuerSteamId, out var message); return message; } if (!Plugin.TryGetRescueLobby(out var lobbyId) || lobbyId == CSteamID.Nil) { return "The Helper chalkboard restore request could not reach the current host; no board changed."; } try { string nonce = Guid.NewGuid().ToString("N").Substring(0, 16); string text = ChalkboardModerationPolicy.BuildRestoreMemberData(index, fingerprint, DateTimeOffset.UtcNow.ToUnixTimeSeconds(), nonce, normalized); SteamMatchmaking.SetLobbyMemberData(lobbyId, "bluesage_chalk_request_v2", text); return $"Helper restore request sent privately for host-local snapshot '{normalized}' and board {index}. " + "The host will revalidate exact Helper identity, current scene/index/fingerprint, freshness, request size/version, nonce/replay, and its own saved snapshot before changing the board; no chat line was sent. " + ChalkboardModerationController.BuildTruth("load-request", index, DescribeBoardScene(board, index), normalized, "pending-host"); } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("Helper chalkboard restore request failed safely: " + ex.GetType().Name + ": " + ex.Message)); } return "The Helper chalkboard restore request failed safely; no board changed."; } } private static ChalkboardClearResult ExecuteLoadAsHost(string normalized, int index, string expectedFingerprint, string issuerSteamId, out string message) { message = "No chalkboard changed."; if (!SafeName.IsMatch(normalized ?? string.Empty) || !SteamIdModerationController.TryGetOwnedHostLobby(out var _, out var _) || !SteamIdModerationController.IsAuthorizedCurrentIssuer(issuerSteamId)) { message = "Only the active host or a currently authorized Helper can request a shared chalkboard restore."; return ChalkboardClearResult.Rejected; } if (!TryResolveBoard(index.ToString(CultureInfo.InvariantCulture), out var board, out var index2, out var fingerprint, out var error)) { message = error; return ChalkboardClearResult.Rejected; } if (index2 != index || !string.Equals(fingerprint, expectedFingerprint, StringComparison.Ordinal)) { message = "The board identity changed after selection. No board changed; run /chalk boards again."; return ChalkboardClearResult.Rejected; } if (!((NetworkIdentity)board).isServer) { message = "The current client is not the authoritative board server. No board changed."; return ChalkboardClearResult.Rejected; } if (_rollbackUnresolved || ChalkboardSnapshotTransportController.DestructiveActionsBlocked) { message = "A prior chalkboard rollback remains unresolved. No destructive board action is allowed."; return ChalkboardClearResult.Rejected; } if ((DateTime.UtcNow - _lastSharedRestoreUtc).TotalSeconds < 8.0) { message = "Chalkboard restore is cooling down; wait a few seconds before another shared restore."; return ChalkboardClearResult.Rejected; } string path = Path.Combine(SaveDirectory, normalized + ".bschalk"); if (!File.Exists(path)) { message = "Host-local chalkboard snapshot '" + normalized + "' was not found. Use /chalk list."; return ChalkboardClearResult.Rejected; } int[][] array = null; string text = "preload-b" + index + "-" + DateTime.UtcNow.ToString("yyyyMMddTHHmmssZ", CultureInfo.InvariantCulture); try { int[][] grid = ReadGrid(path, board); if (!TrySaveResolved(text, board, index, out var message2)) { message = "Pre-restore recovery snapshot failed, so BlueSage left the board unchanged. " + message2; return ChalkboardClearResult.Rejected; } array = CopyGrid(board.PaintColors); ChalkboardDeliveryResult chalkboardDeliveryResult = ApplyGridAndSync(board, grid); _lastSharedRestoreUtc = DateTime.UtcNow; string text2 = ChalkboardModerationController.BuildTruth("load", index, DescribeBoardScene(board, index), text, "ready"); if (!chalkboardDeliveryResult.IsComplete) { message = $"Chalkboard '{normalized}' restored locally to board {index}, but compatible-recipient delivery was partial " + $"(attempted {chalkboardDeliveryResult.Attempted}, sent {chalkboardDeliveryResult.Sent}, skipped {chalkboardDeliveryResult.Skipped}). " + "After the restore cooldown, repeating the exact action is retry-safe. " + text2; return ChalkboardClearResult.Partial; } message = $"Chalkboard '{normalized}' restored to board {index}. Compatible QoL clients replace stale pixels before the native full-board repaint. " + text2; return ChalkboardClearResult.Complete; } catch (Exception ex) { bool flag = array == null; if (array != null) { try { ApplyGridAndSync(board, array); flag = true; } catch (Exception ex2) { RestoreGridValues(board, array); _rollbackUnresolved = true; ChalkboardSnapshotTransportController.MarkRollbackUnresolved("restore " + ex2.GetType().Name); ManualLogSource log = Plugin.Log; if (log != null) { log.LogError((object)("Chalkboard restore rollback repaint failed after restoring grid values: " + ex2.GetType().Name + ": " + ex2.Message)); } } } ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)("Chalkboard restore rejected safely: " + ex.GetType().Name + ": " + ex.Message)); } message = "Chalkboard restore failed; rollback=" + (flag ? "restored" : "unresolved-blocking") + ", recovery snapshot='" + text + "', vanillaObserver=not-observed."; return ChalkboardClearResult.Rejected; } } internal static string List() { if (ChalkyInstalled) { return Status(); } if (!Directory.Exists(SaveDirectory)) { return "Chalkboard saves: none yet."; } string[] array = (from value in Directory.GetFiles(SaveDirectory, "*.bschalk").Select(Path.GetFileNameWithoutExtension) orderby value select value).Take(30).ToArray(); return ((array.Length == 0) ? "Chalkboard saves: none yet." : ("Chalkboard saves: " + string.Join(", ", array) + ".")) + " authority=local-only, snapshot=local-library, rollback=" + (_rollbackUnresolved ? "unresolved" : "ready") + ", vanillaObserver=not-applicable."; } internal static string Delete(string name) { if (ChalkyInstalled) { return Status(); } if (!TryNormalizeName(name, out var normalized, out var error)) { return error; } string text = Path.Combine(SaveDirectory, normalized + ".bschalk"); if (!File.Exists(text)) { return "Chalkboard '" + normalized + "' was not found in the local save library."; } try { string text2 = text + ".bak"; File.Copy(text, text2, overwrite: true); File.Delete(text); return "Deleted local chalkboard save '" + normalized + "' after preserving '" + Path.GetFileName(text2) + "'. " + ChalkboardCommandPolicy.BuildActionTruth("delete", -1, "local-save-library", "local-only", 0, Path.GetFileName(text2), "reversible-from-backup", "not-applicable"); } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("Chalkboard local delete failed safely: " + ex.GetType().Name + ": " + ex.Message)); } return "Chalkboard local delete failed safely; the saved snapshot was not intentionally removed."; } } internal static string RefreshMap() { return "Chalkboard map refreshed locally. " + Boards(); } internal static string View(string boardToken) { if (!Available) { return Status(); } if (!TryResolveBoard(boardToken, out var board, out var index, out var fingerprint, out var error)) { return error; } return "Viewed exact board fingerprint " + fingerprint + ". " + ChalkboardModerationController.BuildTruth("view", index, DescribeBoardScene(board, index), "none", _rollbackUnresolved ? "unresolved" : "ready"); } internal static string Boards() { //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Unknown result type (might be due to invalid IL or missing references) //IL_00fa: Unknown result type (might be due to invalid IL or missing references) //IL_01d9: Unknown result type (might be due to invalid IL or missing references) //IL_01de: Unknown result type (might be due to invalid IL or missing references) //IL_01f9: Unknown result type (might be due to invalid IL or missing references) //IL_01fe: Unknown result type (might be due to invalid IL or missing references) //IL_0202: Unknown result type (might be due to invalid IL or missing references) //IL_020a: Unknown result type (might be due to invalid IL or missing references) //IL_0212: Unknown result type (might be due to invalid IL or missing references) //IL_0276: Unknown result type (might be due to invalid IL or missing references) //IL_0277: Unknown result type (might be due to invalid IL or missing references) //IL_02fb: Unknown result type (might be due to invalid IL or missing references) //IL_030a: Unknown result type (might be due to invalid IL or missing references) //IL_0319: Unknown result type (might be due to invalid IL or missing references) if (!Available) { return Status(); } DrawingManager i = MonoSingleton.I; if ((Object)(object)i == (Object)null || i.QuadPainterGPUS == null || i.QuadPainterGPUS.Count == 0) { return "Chalkboards are not ready in this scene."; } RefreshBoardSetState(i); _lastListedBoardSetSignature = _boardSetSignature; _lastListedBoardSetUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); int num = (int)(BoardIndexField?.GetValue(i) ?? ((object)(-1))); PlayerController val = NetworkSingleton.I?.MainPlayerController; Vector3 val2 = (((Object)(object)val != (Object)null) ? ((Component)val).transform.position : Vector3.zero); bool flag = (Object)(object)val != (Object)null; int num2 = -1; float num3 = float.MaxValue; if (flag) { int num4 = Math.Min(i.QuadPainterGPUS.Count, 3); for (int j = 0; j < num4; j++) { QuadPainterGPU val3 = i.QuadPainterGPUS[j]; if (!((Object)(object)val3 == (Object)null)) { float num5 = Vector3.Distance(val2, ((Component)val3).transform.position); if (num5 < num3) { num3 = num5; num2 = j; } } } } List list = new List(); foreach (ChalkboardCatalogEntry item in ChalkboardCatalogPolicy.Build24259292) { int index = item.Index; if (index >= i.QuadPainterGPUS.Count) { list.Add(index + " " + item.Label + " unavailable in current scene"); continue; } QuadPainterGPU val4 = i.QuadPainterGPUS[index]; if ((Object)(object)val4 == (Object)null) { list.Add(index + " " + item.Label + " unavailable"); continue; } string text = BuildHierarchyPath(((Component)val4).transform); Scene scene = ((Component)val4).gameObject.scene; string value = ((Scene)(ref scene)).name ?? string.Empty; Vector3 position = ((Component)val4).transform.position; string text2 = (string.Equals(ChalkboardModerationPolicy.ClassifyBuild24259292Location(index, position.x, position.y, position.z), item.Label, StringComparison.Ordinal) ? "mapped" : "AMBIGUOUS-MAPPING"); string text3 = ((index == num) ? " selected" : string.Empty) + ((index == num2) ? " nearest" : string.Empty); string text4 = (flag ? (", " + Vector3.Distance(val2, position).ToString("0.0", CultureInfo.InvariantCulture) + "m away") : string.Empty); string text5 = ((text.Length > 96) ? ("…" + text.Substring(text.Length - 95)) : text); list.Add($"{index} {item.Label} scene={SafeDisplayName(value)} state={text2} at ({position.x:0.0},{position.y:0.0},{position.z:0.0}){text4}" + ((text3.Length > 0) ? (" [" + text3.Trim() + "]") : string.Empty) + " — " + text5); } string text6 = ((i.QuadPainterGPUS.Count > 3) ? " Extra live surfaces are ignored and cannot become index 3." : string.Empty); return "Chalkboards (fresh for " + 30 + "s; exact build 24259292 catalog): " + string.Join("; ", list) + "." + text6 + " Local actions: view/save/delete/refresh. Shared actions: Host-direct or authenticated Helper-to-Host load/clear. compatibleRecipients=" + ChalkboardModerationController.CountCompatibleRecipients() + ", snapshot=required-before-destructive-action, rollback=" + (_rollbackUnresolved ? "unresolved" : "ready") + ", vanillaObserver=not-observed. Clear/load only after rechecking this list."; } internal static string TryClear(string boardToken) { //IL_00ec: Unknown result type (might be due to invalid IL or missing references) //IL_00ee: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Unknown result type (might be due to invalid IL or missing references) //IL_0137: Unknown result type (might be due to invalid IL or missing references) if (!Available) { return Status(); } if (!TryResolveBoard(boardToken, out var board, out var index, out var fingerprint, out var error)) { return error; } DrawingManager i = MonoSingleton.I; string currentBoardSetSignature = BuildBoardSetSignature(i); if (!ChalkboardCommandPolicy.TryAuthorizeDestructiveAction(index, (i?.QuadPainterGPUS?.Count).GetValueOrDefault(), _lastListedBoardSetSignature, _lastListedBoardSetUnixSeconds, currentBoardSetSignature, DateTimeOffset.UtcNow.ToUnixTimeSeconds(), _rollbackUnresolved || ChalkboardSnapshotTransportController.DestructiveActionsBlocked, out var error2)) { return error2; } if (!SteamIdModerationController.CanLocalIssue(out var isHost, out var reason)) { return reason; } if (isHost) { string issuerSteamId = (SteamManager.Initialized ? ((ulong)SteamUser.GetSteamID()).ToString() : string.Empty); ExecuteClearAsHost(index, fingerprint, issuerSteamId, out var message); return message; } if (!Plugin.TryGetRescueLobby(out var lobbyId) || lobbyId == CSteamID.Nil) { return "The Helper chalkboard clear request could not reach the current host; no board changed."; } try { string nonce = Guid.NewGuid().ToString("N").Substring(0, 16); string text = ChalkboardModerationPolicy.BuildMemberData(index, fingerprint, DateTimeOffset.UtcNow.ToUnixTimeSeconds(), nonce); SteamMatchmaking.SetLobbyMemberData(lobbyId, "bluesage_chalk_request_v2", text); return $"Helper clear request sent privately for board {index}. The host will recheck your Helper role, the live board fingerprint, freshness, nonce/replay guard, scene mapping, and exact index before clearing; no chat line was sent. " + ChalkboardModerationController.BuildTruth("clear-request", index, DescribeBoardScene(board, index), "host-pre-action-required", "pending-host"); } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("Helper chalkboard clear request failed safely: " + ex.GetType().Name + ": " + ex.Message)); } return "The Helper chalkboard clear request failed safely; no board changed."; } } internal static void PollHostMemberDataRequests() { //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00b5: Unknown result type (might be due to invalid IL or missing references) long num = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); if (num < _nextMemberDataPollUnixMilliseconds) { return; } _nextMemberDataPollUnixMilliseconds = num + 500; if (!Available || !SteamIdModerationController.TryGetOwnedHostLobby(out var lobby, out var ownerSteamId)) { return; } string text = ((ulong)lobby).ToString(); if (!string.Equals(_memberDataLobbyKey, text, StringComparison.Ordinal)) { _memberDataLobbyKey = text; ResetRelayAndCooldownState(); } RefreshBoardSetState(MonoSingleton.I); int numLobbyMembers = SteamMatchmaking.GetNumLobbyMembers(lobby); for (int i = 0; i < numLobbyMembers; i++) { CSteamID lobbyMemberByIndex = SteamMatchmaking.GetLobbyMemberByIndex(lobby, i); string text2 = ((ulong)lobbyMemberByIndex).ToString(); if (!string.Equals(text2, ownerSteamId, StringComparison.Ordinal) && ChalkboardModerationPolicy.TryParseMemberData(SteamMatchmaking.GetLobbyMemberData(lobby, lobbyMemberByIndex, "bluesage_chalk_request_v2"), out var request)) { ProcessHelperClearRequest(lobby, text2, request); } } } internal static void NotifyModerationBoardReview(string targetSteamId) { if (Available) { Plugin.AddLocalNotification("Chalkboard moderation follow-up for banned Steam …" + Suffix(targetSteamId) + ": build 24259292 exposes no authoritative drawing-owner identity, so BlueSage did not guess or auto-clear a board. If abusive chalk remains, run /chalk boards, verify the location/index, then /chalk clear . A local recovery snapshot is saved before every clear."); } } internal static bool PrepareForFullSnapshot(QuadPainterGPU board) { return ChalkboardSnapshotTransportController.PrepareForFullSnapshot(board); } private static void ProcessHelperClearRequest(CSteamID lobby, string senderSteamId, ChalkboardClearRequest request) { //IL_002a: Unknown result type (might be due to invalid IL or missing references) long num = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (!ChalkboardModerationPolicy.IsFresh(request.IssuedUnixSeconds, num) || !PlayerIdentityEvidenceController.TryResolveExact(senderSteamId, out var _, out var _) || CountLobbyMembers(lobby, senderSteamId) != 1 || !SteamIdModerationController.IsAuthorizedCurrentIssuer(senderSteamId) || !RememberClearNonce(request.Nonce) || (LastClearRequestByIssuer.TryGetValue(senderSteamId, out var value) && num - value < 2)) { return; } LastClearRequestByIssuer[senderSteamId] = num; ChalkboardClearResult chalkboardClearResult; string message; if (string.Equals(request.Action, "load", StringComparison.Ordinal)) { chalkboardClearResult = ExecuteLoadAsHost(request.SnapshotName, request.BoardIndex, request.BoardFingerprint, senderSteamId, out message); } else { if (!string.Equals(request.Action, "clear", StringComparison.Ordinal)) { return; } chalkboardClearResult = ExecuteClearAsHost(request.BoardIndex, request.BoardFingerprint, senderSteamId, out message); } ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)("Host non-chat chalkboard " + request.Action + " request from Steam …" + Suffix(senderSteamId) + ": " + message)); } Plugin.AddLocalNotification(chalkboardClearResult switch { ChalkboardClearResult.Partial => "Authorized Helper chalkboard action needs retry: ", ChalkboardClearResult.Complete => "Authorized Helper chalkboard action: ", _ => "Helper chalkboard action rejected: ", } + message); } private static ChalkboardClearResult ExecuteClearAsHost(int index, string expectedFingerprint, string issuerSteamId, out string message) { //IL_0083: Unknown result type (might be due to invalid IL or missing references) message = "No chalkboard changed."; if (!SteamIdModerationController.TryGetOwnedHostLobby(out var lobby, out var _) || !SteamIdModerationController.IsAuthorizedCurrentIssuer(issuerSteamId)) { message = "Only the active host or a currently authorized Helper can request a shared chalkboard clear."; return ChalkboardClearResult.Rejected; } if (!TryResolveBoard(index.ToString(CultureInfo.InvariantCulture), out var board, out var index2, out var fingerprint, out var error)) { message = error; return ChalkboardClearResult.Rejected; } if (index2 != index || !string.Equals(fingerprint, expectedFingerprint, StringComparison.Ordinal)) { message = "The board identity changed after selection. No board changed; run /chalk boards again."; return ChalkboardClearResult.Rejected; } if (!((NetworkIdentity)board).isServer) { message = "The current client is not the authoritative board server. No board changed."; return ChalkboardClearResult.Rejected; } if (_rollbackUnresolved || ChalkboardSnapshotTransportController.DestructiveActionsBlocked) { message = "A prior chalkboard rollback remains unresolved. No destructive board action is allowed."; return ChalkboardClearResult.Rejected; } string key = (ulong)lobby + "|" + fingerprint; if (LastClearByBoard.TryGetValue(key, out var value) && (DateTime.UtcNow - value).TotalSeconds < 5.0) { message = "That board clear is cooling down; wait a few seconds, then recheck /chalk boards."; return ChalkboardClearResult.Rejected; } string text = "modclear-b" + index + "-" + DateTime.UtcNow.ToString("yyyyMMddTHHmmssZ", CultureInfo.InvariantCulture); if (!TrySaveResolved(text, board, index, out var message2)) { message = "Pre-clear recovery snapshot failed, so BlueSage left the board unchanged. " + message2; return ChalkboardClearResult.Rejected; } int[][] grid = CopyGrid(board.PaintColors); try { int[][] grid2 = CreateBlankGrid(board.PaintColors); ChalkboardDeliveryResult chalkboardDeliveryResult = ApplyGridAndSync(board, grid2); LastClearByBoard[key] = DateTime.UtcNow; string text2 = ChalkboardModerationController.BuildTruth("clear", index, DescribeBoardScene(board, index), text, "ready"); if (!chalkboardDeliveryResult.IsComplete) { message = $"Cleared board {index} locally after saving recovery snapshot '{text}', but verified-recipient delivery was partial " + $"(attempted {chalkboardDeliveryResult.Attempted}, sent {chalkboardDeliveryResult.Sent}, skipped {chalkboardDeliveryResult.Skipped}). " + $"After the {5}s cooldown, /chalk clear {index} is retry-safe because the authoritative board is already blank. " + text2; return ChalkboardClearResult.Partial; } message = $"Cleared board {index} after saving recovery snapshot '{text}'. " + $"Verified-recipient delivery completed (attempted {chalkboardDeliveryResult.Attempted}, sent {chalkboardDeliveryResult.Sent}, skipped {chalkboardDeliveryResult.Skipped}). " + "Compatible QoL clients replace stale pixels before the native full-board repaint; validate any vanilla observer separately. " + text2; return ChalkboardClearResult.Complete; } catch (Exception ex) { bool flag = false; try { ApplyGridAndSync(board, grid); flag = true; } catch (Exception ex2) { RestoreGridValues(board, grid); _rollbackUnresolved = true; ChalkboardSnapshotTransportController.MarkRollbackUnresolved("clear " + ex2.GetType().Name); ManualLogSource log = Plugin.Log; if (log != null) { log.LogError((object)("Chalkboard clear rollback repaint failed: " + ex2.GetType().Name + ": " + ex2.Message)); } } ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)("Chalkboard clear failed safely: " + ex.GetType().Name + ": " + ex.Message)); } message = "Chalkboard clear failed; rollback=" + (flag ? "restored" : "unresolved-blocking") + ", recovery snapshot='" + text + "', vanillaObserver=not-observed."; return ChalkboardClearResult.Rejected; } } private unsafe static ChalkboardDeliveryResult ApplyGridAndSync(QuadPainterGPU board, int[][] grid) { //IL_00d0: Unknown result type (might be due to invalid IL or missing references) //IL_00d6: Unknown result type (might be due to invalid IL or missing references) //IL_00e0: Unknown result type (might be due to invalid IL or missing references) //IL_00e6: Unknown result type (might be due to invalid IL or missing references) //IL_0102: Unknown result type (might be due to invalid IL or missing references) //IL_0107: Unknown result type (might be due to invalid IL or missing references) //IL_01af: Unknown result type (might be due to invalid IL or missing references) //IL_01b4: Unknown result type (might be due to invalid IL or missing references) //IL_01d2: Unknown result type (might be due to invalid IL or missing references) //IL_01d5: 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_01ff: Unknown result type (might be due to invalid IL or missing references) //IL_0205: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)board == (Object)null || board.PaintColors == null || grid == null || grid.Length != board.PaintColors.Length) { throw new InvalidDataException("board grid dimensions changed"); } for (int i = 0; i < grid.Length; i++) { if (board.PaintColors[i].Ints == null || grid[i] == null || grid[i].Length != board.PaintColors[i].Ints.Count) { throw new InvalidDataException("board grid dimensions changed"); } for (int j = 0; j < grid[i].Length; j++) { board.PaintColors[i].Ints[j] = grid[i][j]; } } if (!ChalkboardSnapshotTransportController.PrepareForFullSnapshot(board)) { throw new InvalidOperationException("full-board render replacement could not clear the pending-pixel queue and render texture"); } board.GetQuadImage_Original_1(default(PlayerID), board.PaintColors, default(RPCInfo)); IReadOnlyList obj = PlayerIdentityEvidenceController.GetVerifiedRoster() ?? Array.Empty(); PlayerPanelController i2 = NetworkSingleton.I; string b = ((object)((NetworkIdentity)board).localPlayerForced/*cast due to .constrained prefix*/).ToString(); int num = 0; int num2 = 0; int num3 = 0; foreach (PlayerIdentityEvidence item in obj) { if (item == null || item.IsLocal || string.Equals(item.NetworkPlayerId, b, StringComparison.Ordinal)) { continue; } bool flag = ChalkboardModerationController.IsCompatibleRecipient(item); if (flag) { num++; } int rosterIndex = item.RosterIndex; if (i2?.PlayerIDs == null || rosterIndex < 0 || rosterIndex >= i2.PlayerIDs.Count) { if (flag) { num3++; } continue; } PlayerID val = i2.PlayerIDs[rosterIndex]; if (!string.Equals(((object)(*(PlayerID*)(&val))/*cast due to .constrained prefix*/).ToString(), item.NetworkPlayerId, StringComparison.Ordinal) || val == ((NetworkIdentity)board).localPlayerForced) { if (flag) { num3++; } continue; } try { board.GetQuadImage(val, board.PaintColors, default(RPCInfo)); if (flag) { num2++; } } catch (Exception ex) { if (flag) { num3++; } ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("Chalkboard verified-recipient snapshot delivery skipped safely for roster " + rosterIndex + ": " + ex.GetType().Name + ": " + ex.Message)); } } } return new ChalkboardDeliveryResult(num, num2, num3); } private static int[][] CopyGrid(IntList[] grid) { ValidateGrid(grid); int[][] array = new int[grid.Length][]; for (int i = 0; i < grid.Length; i++) { array[i] = grid[i].Ints.ToArray(); } return array; } private static int[][] CreateBlankGrid(IntList[] grid) { ValidateGrid(grid); int[][] array = new int[grid.Length][]; for (int i = 0; i < grid.Length; i++) { array[i] = new int[grid[i].Ints.Count]; } return array; } private static void RestoreGridValues(QuadPainterGPU board, int[][] grid) { if (board?.PaintColors == null || grid == null || grid.Length != board.PaintColors.Length) { return; } for (int i = 0; i < grid.Length; i++) { if (grid[i] != null && board.PaintColors[i].Ints != null && grid[i].Length == board.PaintColors[i].Ints.Count) { for (int j = 0; j < grid[i].Length; j++) { board.PaintColors[i].Ints[j] = grid[i][j]; } } } } private static bool TryResolveBoard(string boardToken, out QuadPainterGPU board, out int index, out string fingerprint, out string error) { //IL_00bf: 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) //IL_00ec: Unknown result type (might be due to invalid IL or missing references) //IL_00f1: 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_00fd: Unknown result type (might be due to invalid IL or missing references) //IL_0105: Unknown result type (might be due to invalid IL or missing references) //IL_013f: Unknown result type (might be due to invalid IL or missing references) //IL_0147: Unknown result type (might be due to invalid IL or missing references) //IL_014f: Unknown result type (might be due to invalid IL or missing references) board = null; index = -1; fingerprint = string.Empty; DrawingManager i = MonoSingleton.I; RefreshBoardSetState(i); int valueOrDefault = (i?.QuadPainterGPUS?.Count).GetValueOrDefault(); if (!ChalkboardModerationPolicy.TryParseExplicitBoardIndex(boardToken, valueOrDefault, out index, out error)) { return false; } board = i.QuadPainterGPUS[index]; if ((Object)(object)board == (Object)null) { error = "Board " + index + " is unavailable. Run /chalk boards again."; return false; } if (!ChalkboardCatalogPolicy.TryGet(index, out var entry)) { error = "Board " + index + " is outside build 24259292's exact catalog. Index 3 is never accepted."; return false; } Scene scene = ((Component)board).gameObject.scene; string sceneName = ((Scene)(ref scene)).name ?? string.Empty; string hierarchyPath = BuildHierarchyPath(((Component)board).transform); Vector3 position = ((Component)board).transform.position; if (!string.Equals(ChalkboardModerationPolicy.ClassifyBuild24259292Location(index, position.x, position.y, position.z), entry.Label, StringComparison.Ordinal)) { error = "Board " + index + " has an ambiguous scene/position mapping. No action is allowed; refresh /chalk boards."; return false; } fingerprint = ChalkboardModerationPolicy.BuildBoardFingerprint(sceneName, hierarchyPath, position.x, position.y, position.z); return true; } private static void RefreshBoardSetState(DrawingManager manager) { string text = BuildBoardSetSignature(manager); if (!string.Equals(_boardSetSignature, text, StringComparison.Ordinal)) { _boardSetSignature = text; ResetRelayAndCooldownState(); } } private static string BuildBoardSetSignature(DrawingManager manager) { //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Unknown result type (might be due to invalid IL or missing references) if (manager?.QuadPainterGPUS == null || manager.QuadPainterGPUS.Count == 0) { return ""; } List list = new List(manager.QuadPainterGPUS.Count); for (int i = 0; i < manager.QuadPainterGPUS.Count; i++) { QuadPainterGPU val = manager.QuadPainterGPUS[i]; if ((Object)(object)val == (Object)null) { list.Add(i.ToString(CultureInfo.InvariantCulture) + ":unavailable"); continue; } Vector3 position = ((Component)val).transform.position; string text = i.ToString(CultureInfo.InvariantCulture); Scene scene = ((Component)val).gameObject.scene; list.Add(text + ":" + ChalkboardModerationPolicy.BuildBoardFingerprint(((Scene)(ref scene)).name ?? string.Empty, BuildHierarchyPath(((Component)val).transform), position.x, position.y, position.z)); } return string.Join("|", list); } private static void ResetRelayAndCooldownState() { SeenClearNonces.Clear(); ClearNonceOrder.Clear(); LastClearRequestByIssuer.Clear(); LastClearByBoard.Clear(); _lastSharedRestoreUtc = DateTime.MinValue; _lastListedBoardSetSignature = string.Empty; _lastListedBoardSetUnixSeconds = 0L; _rollbackUnresolved = false; ChalkboardSnapshotTransportController.ResetForBoardSetChange(); } private static string BuildHierarchyPath(Transform transform) { Stack stack = new Stack(); Transform val = transform; while ((Object)(object)val != (Object)null) { stack.Push(SafeDisplayName(((Object)val).name) + "[" + val.GetSiblingIndex() + "]"); val = val.parent; } return string.Join("/", stack.ToArray()); } private static string SafeDisplayName(string value) { string text = (value ?? "Board").Replace("\r", " ").Replace("\n", " ").Replace("|", "/") .Trim(); if (text.Length != 0) { return text; } return "Board"; } private static int CountLobbyMembers(CSteamID lobby, string steamId) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) int num = 0; int numLobbyMembers = SteamMatchmaking.GetNumLobbyMembers(lobby); for (int i = 0; i < numLobbyMembers; i++) { if (string.Equals(((ulong)SteamMatchmaking.GetLobbyMemberByIndex(lobby, i)).ToString(), steamId, StringComparison.Ordinal)) { num++; } } return num; } private static bool RememberClearNonce(string nonce) { if (!SeenClearNonces.Add(nonce)) { return false; } ClearNonceOrder.Enqueue(nonce); while (ClearNonceOrder.Count > 128) { SeenClearNonces.Remove(ClearNonceOrder.Dequeue()); } return true; } private static string Suffix(string steamId) { string text; if (steamId == null || steamId.Length <= 6) { text = steamId; if (text == null) { return "unknown"; } } else { text = steamId.Substring(steamId.Length - 6); } return text; } private static int[][] ReadGrid(string path, QuadPainterGPU board) { using FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); using BinaryReader binaryReader = new BinaryReader(fileStream); if (binaryReader.ReadString() != "BSCH1") { throw new InvalidDataException("unsupported save version"); } int num = binaryReader.ReadInt32(); int num2 = binaryReader.ReadInt32(); if (num != board.PaintColors.Length || num < 1 || num > 512 || num2 != board.PaintColors[0].Ints.Count || num2 < 1 || num2 > 512) { throw new InvalidDataException("board dimensions do not match this board"); } int[][] array = new int[num][]; for (int i = 0; i < num; i++) { array[i] = new int[num2]; for (int j = 0; j < num2; j++) { int num3 = binaryReader.ReadInt32(); if (num3 < 0 || num3 > 255) { throw new InvalidDataException("invalid chalk color index"); } array[i][j] = num3; } } if (fileStream.Position != fileStream.Length) { throw new InvalidDataException("unexpected trailing board data"); } return array; } private static bool TryNormalizeName(string name, out string normalized, out string error) { normalized = (name ?? string.Empty).Trim(); error = string.Empty; if (SafeName.IsMatch(normalized)) { return true; } error = "Chalkboard name must be 1-40 letters, numbers, underscores, or hyphens."; return false; } private static string DescribeBoardScene(QuadPainterGPU board, int index) { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) ChalkboardCatalogEntry entry; string obj = (ChalkboardCatalogPolicy.TryGet(index, out entry) ? entry.Label : "unmapped"); object obj2; if (board == null) { obj2 = null; } else { Scene scene = ((Component)board).gameObject.scene; obj2 = ((Scene)(ref scene)).name; } if (obj2 == null) { obj2 = "unavailable"; } string value = (string)obj2; return obj + "@" + SafeDisplayName(value); } private static bool TryResolve(string name, string boardToken, out string normalized, out QuadPainterGPU board, out int index, out string error) { //IL_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_00fc: Unknown result type (might be due to invalid IL or missing references) //IL_0100: Unknown result type (might be due to invalid IL or missing references) //IL_0107: Unknown result type (might be due to invalid IL or missing references) //IL_010e: Unknown result type (might be due to invalid IL or missing references) normalized = string.Empty; board = null; index = -1; error = string.Empty; if (!TryNormalizeName(name, out normalized, out error)) { return false; } DrawingManager i = MonoSingleton.I; if ((Object)(object)i == (Object)null || i.QuadPainterGPUS == null) { error = "Chalkboards are not ready in this scene."; return false; } RefreshBoardSetState(i); index = ((!string.IsNullOrWhiteSpace(boardToken) && int.TryParse(boardToken, out var result)) ? result : ((int)(BoardIndexField?.GetValue(i) ?? ((object)(-1))))); if (index < 0 || index >= i.QuadPainterGPUS.Count) { error = $"Choose a board first or provide an index from 0 to {Math.Max(0, i.QuadPainterGPUS.Count - 1)}."; return false; } board = i.QuadPainterGPUS[index]; if ((Object)(object)board == (Object)null || !ChalkboardCatalogPolicy.TryGet(index, out var entry)) { error = "The selected board is outside build 24259292's verified catalog 0-2."; return false; } Vector3 position = ((Component)board).transform.position; if (!string.Equals(ChalkboardModerationPolicy.ClassifyBuild24259292Location(index, position.x, position.y, position.z), entry.Label, StringComparison.Ordinal)) { error = "The selected board mapping is ambiguous; use /chalk boards and choose exact index 0, 1, or 2."; return false; } return true; } private static void ValidateGrid(IntList[] grid) { //IL_0072: 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_0078: Unknown result type (might be due to invalid IL or missing references) //IL_0080: 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) if (grid == null || grid.Length < 1 || grid.Length > 512 || grid[0].Ints == null || grid[0].Ints.Count < 1 || grid[0].Ints.Count > 512) { throw new InvalidDataException("unsupported board dimensions"); } int count = grid[0].Ints.Count; foreach (IntList val in grid) { if (val.Ints == null || val.Ints.Count != count || val.Ints.Any((int value) => value < 0 || value > 255)) { throw new InvalidDataException("invalid board grid"); } } } } internal static class ChalkboardSnapshotTransportController { private static readonly FieldInfo RenderTextureField = AccessTools.Field(typeof(QuadPainterGPU), "_rt"); private static readonly FieldInfo PendingPixelsField = AccessTools.Field(typeof(QuadPainterGPU), "_pixelsToUpdate"); private static bool _destructiveActionsBlocked; private static string _rollbackFailure = string.Empty; internal static bool DestructiveActionsBlocked => _destructiveActionsBlocked; internal static string RollbackState { get { if (!_destructiveActionsBlocked) { return "ready"; } return "unresolved (" + _rollbackFailure + ")"; } } internal static void MarkRollbackUnresolved(string reason) { _destructiveActionsBlocked = true; _rollbackFailure = Bound(reason); } internal static void ResetForBoardSetChange() { _destructiveActionsBlocked = false; _rollbackFailure = string.Empty; } internal static bool PrepareForFullSnapshot(QuadPainterGPU board) { //IL_0067: Unknown result type (might be due to invalid IL or missing references) if (!ChalkboardPersistenceController.Available || (Object)(object)board == (Object)null) { return false; } try { object? obj = RenderTextureField?.GetValue(board); RenderTexture val = (RenderTexture)((obj is RenderTexture) ? obj : null); Dictionary dictionary = PendingPixelsField?.GetValue(board) as Dictionary; if ((Object)(object)val == (Object)null || dictionary == null) { return false; } dictionary.Clear(); RenderTexture active = RenderTexture.active; try { RenderTexture.active = val; GL.Clear(true, true, Color.clear); } finally { RenderTexture.active = active; } return true; } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogDebug((object)("Chalkboard full-snapshot render clear skipped safely: " + ex.GetType().Name + ": " + ex.Message)); } return false; } } private static string Bound(string value) { string text = (value ?? "unknown").Replace("\r", " ").Replace("\n", " ").Trim(); if (text.Length > 96) { return text.Substring(0, 96); } return text; } } internal sealed class ChatReadabilityController : MonoBehaviour { private const float DestroyedStatePruneIntervalSeconds = 5f; private bool _lastReadability; private bool _lastBackdrop; private int _lastChatScalePercent; private int _lastChatFontPercent; private int _lastChatHeightPercent; private bool _lastMessagePanelHidden; private bool _lastMessagePanelActivated; private int _lastScreenWidth; private int _lastScreenHeight; private int _lastRefreshRequestVersion; private float _nextDestroyedStatePruneAt; private void Update() { bool shouldApplyChatReadability = Plugin.ShouldApplyChatReadability; bool shouldApplyPersistentChatBackdrop = Plugin.ShouldApplyPersistentChatBackdrop; int lockedChatUiScalePercent = Plugin.LockedChatUiScalePercent; int lockedChatFontSizePercent = Plugin.LockedChatFontSizePercent; int lockedChatWindowHeightPercent = Plugin.LockedChatWindowHeightPercent; if (!(shouldApplyChatReadability || shouldApplyPersistentChatBackdrop) && lockedChatUiScalePercent == 100 && lockedChatFontSizePercent == 100 && lockedChatWindowHeightPercent == 100 && !_lastReadability && !_lastBackdrop && _lastChatScalePercent == 100 && _lastChatFontPercent == 100 && _lastChatHeightPercent == 100) { if (Time.unscaledTime >= _nextDestroyedStatePruneAt) { _nextDestroyedStatePruneAt = Time.unscaledTime + 5f; ChatReadabilityPatch.PruneDestroyedState(); } return; } if (Time.unscaledTime >= _nextDestroyedStatePruneAt) { _nextDestroyedStatePruneAt = Time.unscaledTime + 5f; ChatReadabilityPatch.PruneDestroyedState(); } UIManager i = MonoSingleton.I; bool flag = (Object)(object)i != (Object)null && i.IsMessagePanelHidden; bool flag2 = (Object)(object)i != (Object)null && i.IsMessagePanelActivated; int width = Screen.width; int height = Screen.height; int refreshRequestVersion = ChatReadabilityPatch.RefreshRequestVersion; if (shouldApplyChatReadability != _lastReadability || shouldApplyPersistentChatBackdrop != _lastBackdrop || lockedChatUiScalePercent != _lastChatScalePercent || lockedChatFontSizePercent != _lastChatFontPercent || lockedChatWindowHeightPercent != _lastChatHeightPercent || flag != _lastMessagePanelHidden || flag2 != _lastMessagePanelActivated || width != _lastScreenWidth || height != _lastScreenHeight || refreshRequestVersion != _lastRefreshRequestVersion) { _lastReadability = shouldApplyChatReadability; _lastBackdrop = shouldApplyPersistentChatBackdrop; _lastChatScalePercent = lockedChatUiScalePercent; _lastChatFontPercent = lockedChatFontSizePercent; _lastChatHeightPercent = lockedChatWindowHeightPercent; _lastMessagePanelHidden = flag; _lastMessagePanelActivated = flag2; _lastScreenWidth = width; _lastScreenHeight = height; _lastRefreshRequestVersion = refreshRequestVersion; ChatReadabilityPatch.RefreshVisibleChat(); } } } internal static class CloneIncidentActionController { private const int MemberDataPollIntervalMilliseconds = 1000; private static readonly HashSet SeenResolutionNonces = new HashSet(StringComparer.Ordinal); private static readonly Queue ResolutionNonceOrder = new Queue(); private static readonly Dictionary LastResolutionRequestByIssuer = new Dictionary(StringComparer.Ordinal); private static string _lastLobbyResolutionNonce = string.Empty; private static long _nextMemberDataPollUnixMilliseconds; internal static void ResetLobbyState() { SeenResolutionNonces.Clear(); ResolutionNonceOrder.Clear(); LastResolutionRequestByIssuer.Clear(); _lastLobbyResolutionNonce = string.Empty; _nextMemberDataPollUnixMilliseconds = 0L; } internal static IReadOnlyList GetOpenIncidents() { if (!Plugin.CanUseLobbySafety(out var _)) { return Array.Empty(); } CloneIncidentLedger.Prune(DateTimeOffset.UtcNow.ToUnixTimeSeconds()); return CloneIncidentLedger.Snapshot.OrderByDescending((CloneIncident item) => item.LastObservedUnixSeconds).ToArray(); } internal static IReadOnlyList GetIncidentHistory() { if (!Plugin.CanUseLobbySafety(out var _)) { return Array.Empty(); } CloneIncidentLedger.Prune(DateTimeOffset.UtcNow.ToUnixTimeSeconds()); return CloneIncidentLedger.HistorySnapshot.OrderByDescending((CloneIncident item) => item.LastObservedUnixSeconds).ToArray(); } internal static bool TryCopySteamId(string incidentId, bool suspect, out string message) { if (!Plugin.CanUseLobbySafety(out var _)) { message = "Lobby Safety is available only to the current host or a Helper assigned by that host. No player or incident details were shown."; return false; } if (!CloneIncidentLedger.TryResolveIncident(incidentId, out CloneIncident incident)) { message = "That Clone Shield incident is no longer open."; return false; } message = string.Concat(str2: PlayerIdentityEvidenceController.Suffix(GUIUtility.systemCopyBuffer = (suspect ? incident.SuspectSteamId : incident.VictimSteamId)), str0: suspect ? "Suspect" : "Protected victim", str1: " SteamID64 copied exactly: …", str3: "."); return true; } internal static bool TryCopyBanPreviewCommand(string incidentId, out string message) { if (!Plugin.CanUseLobbySafety(out var _)) { message = "Lobby Safety is available only to the current host or a Helper assigned by that host. No player or incident details were shown."; return false; } if (!CloneIncidentLedger.TryResolveIncident(incidentId, out CloneIncident incident)) { message = "That Clone Shield incident is no longer open."; return false; } GUIUtility.systemCopyBuffer = "/sidban " + incident.Id; message = "Exact suspect preview command copied for " + incident.Id + " (Steam …" + PlayerIdentityEvidenceController.Suffix(incident.SuspectSteamId) + "). Paste to preview; confirmation remains separate."; return true; } internal static bool TryResolveForLobby(string incidentId, out string message) { //IL_0104: Unknown result type (might be due to invalid IL or missing references) //IL_0106: Unknown result type (might be due to invalid IL or missing references) //IL_011c: Unknown result type (might be due to invalid IL or missing references) if (!Plugin.CanUseLobbySafety(out var _)) { message = "Lobby Safety is available only to the current host or a Helper assigned by that host. No player or incident details were shown."; return false; } if (!CloneIncidentLedger.TryResolveIncident(incidentId, out CloneIncident incident)) { message = "That Clone Shield incident is no longer open."; return false; } if (!SteamIdModerationController.CanLocalIssue(out var isHost2, out var reason)) { message = reason; return false; } long num = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); CloneIncidentResolutionRequest request = new CloneIncidentResolutionRequest(incident.VictimSteamId, incident.SuspectSteamId, incident.StableId, num, Guid.NewGuid().ToString("N").Substring(0, 16)); if (isHost2) { if (!TryPublishHostResolution(request, out message)) { return false; } CloneIncidentLedger.MarkResolved(incident.Id, TryGetLocalSteamId(), num); PlayerIdentityRoleHighlightController.RefreshAll(); RecordLifecycle("resolved", incident, "host-published", "lobby-safety-host"); message = incident.Id + " resolved. The host-published QoL lobby state clears this incident without adding a chat line."; return true; } string text = CloneIncidentResolutionProtocolPolicy.BuildMemberDataRequest(request.VictimSteamId, request.SuspectSteamId, request.StableId, request.IssuedUnixSeconds, request.Nonce); if (string.IsNullOrWhiteSpace(text) || !Plugin.TryGetRescueLobby(out var lobbyId) || lobbyId == CSteamID.Nil) { message = "The non-chat resolution request could not reach the QoL host, so the incident stayed open."; return false; } try { SteamMatchmaking.SetLobbyMemberData(lobbyId, "bluesage_incident_request_v2", text); } catch (Exception ex) { message = "The non-chat resolution request failed safely (" + ex.GetType().Name + "), so the incident stayed open."; return false; } message = incident.Id + " resolution published through non-chat Steam member data. It stays open until host-authenticated lobby state confirms completion; no chat line was sent."; return true; } internal static bool TryDismissLocal(string incidentId, out string message) { if (!Plugin.CanUseLobbySafety(out var _)) { message = "Lobby Safety is available only to the current host or a Helper assigned by that host. No player or incident details were shown."; return false; } if (!CloneIncidentLedger.TryResolveIncident(incidentId, out CloneIncident incident) || !CloneIncidentLedger.DismissLocal(incidentId, DateTimeOffset.UtcNow.ToUnixTimeSeconds())) { message = "That Clone Shield incident is no longer open."; return false; } PlayerIdentityRoleHighlightController.RefreshAll(); message = incidentId.ToUpperInvariant() + " dismissed on this client only. No lobby moderation state changed."; RecordLifecycle("dismissed-local", incident, "closed", "lobby-safety-local"); return true; } internal static bool TryDismissAllOpenLocal(out string message) { if (!Plugin.CanUseLobbySafety(out var _)) { message = "Lobby Safety is available only to the current host or a Helper assigned by that host. No player or incident details were shown."; return false; } CloneIncident[] array = CloneIncidentLedger.Snapshot.ToArray(); int num = CloneIncidentLedger.DismissAllOpenLocal(DateTimeOffset.UtcNow.ToUnixTimeSeconds()); CloneIncident[] array2 = array; foreach (CloneIncident incident in array2) { RecordLifecycle("dismissed-local", incident, "bulk-closed", "lobby-safety-local"); } if (num > 0) { PlayerIdentityRoleHighlightController.RefreshAll(); } message = ((num == 0) ? "No open Clone Shield incidents needed local dismissal. No lobby moderation state changed." : ("Dismissed " + num + " open Clone Shield " + ((num == 1) ? "incident" : "incidents") + " on this client only. No bans, Helper state, or lobby moderation state changed.")); return true; } internal static bool MarkStaleAbsent(string incidentId, out CloneIncident incident) { if (!CloneIncidentLedger.TryResolveIncident(incidentId, out incident)) { return false; } if (!CloneIncidentLedger.MarkStaleAbsent(incidentId, DateTimeOffset.UtcNow.ToUnixTimeSeconds())) { return false; } RecordLifecycle("stale-absent", incident, "closed", "verified-roster-reconciliation"); return true; } internal static bool MarkNoLongerMatching(string incidentId, out CloneIncident incident) { if (!CloneIncidentLedger.TryResolveIncident(incidentId, out incident)) { return false; } if (!CloneIncidentLedger.MarkNoLongerMatching(incidentId, DateTimeOffset.UtcNow.ToUnixTimeSeconds())) { return false; } RecordLifecycle("no-longer-matching", incident, "closed", "verified-roster-reconciliation"); return true; } internal static bool TryDismissOpenForVerifiedPlayer(string steamId, out string message) { if (!Plugin.CanUseLobbySafety(out var _)) { message = "Lobby Safety is available only to the current host or a Helper assigned by that host. No player or incident details were shown."; return false; } if (!PlayerIdentityEvidenceController.TryResolveExact(steamId, out var evidence, out var error)) { message = "Player incident cleanup refused: " + error + "."; return false; } IReadOnlyList readOnlyList = CloneIncidentLedger.DismissOpenForSteamId(evidence.SteamId, DateTimeOffset.UtcNow.ToUnixTimeSeconds()); foreach (CloneIncident item in readOnlyList) { RecordLifecycle("dismissed-local", item, "player-flags-cleared", "lobby-safety-local"); } if (readOnlyList.Count > 0) { PlayerIdentityRoleHighlightController.RefreshAll(); } message = ((readOnlyList.Count == 0) ? "No open Clone Shield flags were attached to that verified row." : ("Cleared " + readOnlyList.Count + " open Clone Shield " + ((readOnlyList.Count == 1) ? "incident" : "incidents") + " involving " + evidence.DisplayName + " on this client only. Audit lifecycle records remain; no ban or lobby state changed.")); return true; } internal static int ClearClosedHistory() { if (!Plugin.CanUseLobbySafety(out var _)) { return 0; } return CloneIncidentLedger.ClearClosed(); } internal static void RecordLifecycle(string action, CloneIncident incident, string outcome, string source) { if (incident != null) { string targetRole = "SUSPECT;victim=" + incident.VictimSteamId + ";stable=" + incident.StableId + ";match=" + incident.MatchType; SessionAuditController.RecordModeration("clone-incident-" + (action ?? "observed"), source ?? "clone-shield", outcome ?? string.Empty, incident.SuspectSteamId, incident.SuspectLabel, string.Empty, "", incident.Id, targetRole); ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)("Clone Shield incident " + incident.Id + " " + action + ": victim=…" + PlayerIdentityEvidenceController.Suffix(incident.VictimSteamId) + ", suspect=…" + PlayerIdentityEvidenceController.Suffix(incident.SuspectSteamId) + ", stable=" + incident.StableId + ", match=" + incident.MatchType + ".")); } } } internal static void PollHostMemberDataRequests() { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_006b: 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) //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) long num = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); if (num < _nextMemberDataPollUnixMilliseconds) { return; } _nextMemberDataPollUnixMilliseconds = num + 1000; try { if (!SteamIdModerationController.IsLocalHost() || !Plugin.TryGetRescueLobby(out var lobbyId) || lobbyId == CSteamID.Nil) { return; } string b = ((ulong)SteamMatchmaking.GetLobbyOwner(lobbyId)).ToString(); int numLobbyMembers = SteamMatchmaking.GetNumLobbyMembers(lobbyId); for (int i = 0; i < numLobbyMembers; i++) { CSteamID lobbyMemberByIndex = SteamMatchmaking.GetLobbyMemberByIndex(lobbyId, i); string text = ((ulong)lobbyMemberByIndex).ToString(); if (!string.Equals(text, b, StringComparison.Ordinal) && CloneIncidentResolutionProtocolPolicy.TryParseMemberDataRequest(SteamMatchmaking.GetLobbyMemberData(lobbyId, lobbyMemberByIndex, "bluesage_incident_request_v2"), out var request) && PlayerIdentityEvidenceController.TryResolveExact(text, out var _, out var _)) { ProcessResolutionRequest(request, text); } } } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogDebug((object)("Clone Shield non-chat helper request poll failed closed: " + ex.GetType().Name + ": " + ex.Message)); } } } private static void ProcessResolutionRequest(CloneIncidentResolutionRequest request, string senderSteamId) { long num = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (!CloneIncidentResolutionProtocolPolicy.IsFresh(request, num) || !RememberResolutionNonce(request.Nonce)) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)"Rejected expired or replayed Clone Shield resolution request."); } return; } if (!PlayerIdentityEvidenceController.TryResolveExact(senderSteamId, out var _, out var _) || !SteamIdModerationController.IsAuthorizedCurrentIssuer(senderSteamId)) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)"Rejected Clone Shield resolution request from a non-helper sender."); } return; } if (LastResolutionRequestByIssuer.TryGetValue(senderSteamId, out var value) && num - value < 2) { ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogWarning((object)("Rate-limited repeated Clone Shield resolution request from Steam …" + PlayerIdentityEvidenceController.Suffix(senderSteamId) + ".")); } return; } if (!string.Equals(CloneIncidentLedger.BuildStableId(request.VictimSteamId, request.SuspectSteamId), request.StableId, StringComparison.Ordinal) || !CloneIncidentLedger.Snapshot.Any((CloneIncident item) => string.Equals(item.StableId, request.StableId, StringComparison.Ordinal) && string.Equals(item.VictimSteamId, request.VictimSteamId, StringComparison.Ordinal) && string.Equals(item.SuspectSteamId, request.SuspectSteamId, StringComparison.Ordinal))) { ManualLogSource log4 = Plugin.Log; if (log4 != null) { log4.LogWarning((object)"Rejected Clone Shield resolution request that did not match an open host incident."); } return; } LastResolutionRequestByIssuer[senderSteamId] = num; if (!TryPublishHostResolution(request, out string message)) { ManualLogSource log5 = Plugin.Log; if (log5 != null) { log5.LogWarning((object)("Clone Shield host resolution publish failed safely: " + message)); } return; } CloneIncidentLedger.MarkResolvedPair(request.VictimSteamId, request.SuspectSteamId, senderSteamId, num); PlayerIdentityRoleHighlightController.RefreshAll(); RecordLifecycle("resolved", CloneIncidentLedger.HistorySnapshot.LastOrDefault((CloneIncident item) => string.Equals(item.StableId, request.StableId, StringComparison.Ordinal)), "helper-approved", "lobby-safety-host"); Plugin.AddLocalNotification("Authorized helper resolved Clone Shield " + request.StableId + ". No chat message was sent."); } internal static void PollHostResolutionMetadata() { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) try { if (!Plugin.TryGetRescueLobby(out var lobbyId) || lobbyId == CSteamID.Nil || !SteamManager.Initialized) { return; } string text = ((ulong)SteamMatchmaking.GetLobbyOwner(lobbyId)).ToString(); if (!CloneIncidentResolutionProtocolPolicy.TryParseLobbyValue(SteamMatchmaking.GetLobbyData(lobbyId, "bluesage_clone_resolution") ?? string.Empty, text, DateTimeOffset.UtcNow.ToUnixTimeSeconds(), out var request) || string.Equals(_lastLobbyResolutionNonce, request.Nonce, StringComparison.Ordinal)) { return; } _lastLobbyResolutionNonce = request.Nonce; if (string.Equals(CloneIncidentLedger.BuildStableId(request.VictimSteamId, request.SuspectSteamId), request.StableId, StringComparison.Ordinal) && CloneIncidentLedger.MarkResolvedPair(request.VictimSteamId, request.SuspectSteamId, text, request.IssuedUnixSeconds)) { PlayerIdentityRoleHighlightController.RefreshAll(); RecordLifecycle("resolved", CloneIncidentLedger.HistorySnapshot.LastOrDefault((CloneIncident item) => string.Equals(item.StableId, request.StableId, StringComparison.Ordinal)), "host-confirmed", "lobby-safety-peer"); if (Plugin.CanUseLobbySafety(out var _)) { Plugin.AddLocalNotification("Clone Shield " + request.StableId + " was resolved by the verified host/helper team."); } } } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogDebug((object)("Clone Shield resolution metadata poll skipped safely: " + ex.GetType().Name + ": " + ex.Message)); } } } private static bool TryPublishHostResolution(CloneIncidentResolutionRequest request, out string message) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Unknown result type (might be due to invalid IL or missing references) message = "Only the active QoL lobby host can publish incident completion."; try { if (!Plugin.TryGetRescueLobby(out var lobbyId) || lobbyId == CSteamID.Nil || !SteamManager.Initialized || (Object)(object)NetworkManager.main == (Object)null || !NetworkManager.main.isHost) { return false; } string text = ((ulong)SteamMatchmaking.GetLobbyOwner(lobbyId)).ToString(); string b = ((ulong)SteamUser.GetSteamID()).ToString(); if (!string.Equals(text, b, StringComparison.Ordinal)) { return false; } string text2 = CloneIncidentResolutionProtocolPolicy.BuildLobbyValue(text, request); if (!SteamMatchmaking.SetLobbyData(lobbyId, "bluesage_clone_resolution", text2)) { message = "Steam rejected the host incident-completion update."; return false; } message = "Host incident completion published."; return true; } catch (Exception ex) { message = "Host incident completion failed safely (" + ex.GetType().Name + ")."; return false; } } private static bool RememberResolutionNonce(string nonce) { if (!SeenResolutionNonces.Add(nonce)) { return false; } ResolutionNonceOrder.Enqueue(nonce); while (ResolutionNonceOrder.Count > 128) { SeenResolutionNonces.Remove(ResolutionNonceOrder.Dequeue()); } return true; } private static string TryGetLocalSteamId() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) try { return ((ulong)SteamUser.GetSteamID()).ToString(); } catch { return string.Empty; } } } internal sealed class CloneShieldController : MonoBehaviour { private sealed class RosterCloneState { public string SteamId = string.Empty; public string RichName = string.Empty; public string PlainName = string.Empty; public string AvatarFingerprint = string.Empty; } private const float ScanIntervalSeconds = 6f; private const float WarningCooldownSeconds = 300f; private static readonly FieldInfo CustomizationDataField = AccessTools.Field(typeof(PlayerCustomizationController), "_customizationData"); private readonly Dictionary _lastWarningTimes = new Dictionary(StringComparer.Ordinal); private readonly Dictionary _previousRoster = new Dictionary(StringComparer.Ordinal); private readonly Dictionary _missingIncidentScans = new Dictionary(StringComparer.Ordinal); private readonly Dictionary _nonMatchingIncidentScans = new Dictionary(StringComparer.Ordinal); private float _nextScanAt; private float _nextAlignmentWaitLogAt; private bool _alignmentWaitWasLogged; private string _observedLobbyCode = string.Empty; private bool _wasEnabled; private void Update() { if (!Plugin.ShouldApplyCloneShield) { if (_wasEnabled || _observedLobbyCode.Length > 0 || _previousRoster.Count > 0) { ResetObservationState(); PlayerIdentityRoleHighlightController.RefreshAll(); } _wasEnabled = false; } else { _wasEnabled = true; if (!(Time.unscaledTime < _nextScanAt)) { _nextScanAt = Time.unscaledTime + 6f; ScanVisibleRosterForClone(); } } } private void ScanVisibleRosterForClone() { //IL_01a1: Unknown result type (might be due to invalid IL or missing references) try { if (!TryGetActiveLobbyCode(out var lobbyCode)) { ResetObservationState(); return; } if (!string.Equals(_observedLobbyCode, lobbyCode, StringComparison.Ordinal)) { ResetObservationState(); _observedLobbyCode = lobbyCode; CloneIncidentLedger.Reset(lobbyCode); } CloneIncidentActionController.PollHostResolutionMetadata(); PlayerPanelController i = NetworkSingleton.I; if ((Object)(object)i == (Object)null) { return; } int val = i.IDInfos?.Count ?? 0; int val2 = i.PlayerTransforms?.Count ?? 0; int val3 = i.PlayerSteamIDs?.Count ?? 0; _ = i.PlayerIDs?.Count; int num = Math.Max(val3, Math.Max(val, val2)); if (num == 0) { return; } string error; bool coverageComplete; IReadOnlyList verifiedCurrentSteamIds = PlayerIdentityEvidenceController.GetVerifiedCurrentSteamIds(out error, out coverageComplete); if (!(verifiedCurrentSteamIds.Count > 0 && coverageComplete)) { LogAlignmentWait(error); return; } if (_alignmentWaitWasLogged) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogDebug((object)"Clone Shield roster alignment recovered; moderation-grade incident observation resumed."); } } _alignmentWaitWasLogged = false; _nextAlignmentWaitLogAt = 0f; string localSteamId = GetLocalSteamId(); HashSet hashSet = new HashSet(verifiedCurrentSteamIds, StringComparer.Ordinal); Dictionary dictionary = new Dictionary(StringComparer.Ordinal); for (int j = 0; j < num; j++) { string text = ((i.PlayerSteamIDs != null && j < i.PlayerSteamIDs.Count) ? i.PlayerSteamIDs[j] : string.Empty); if (hashSet.Contains(text) && !dictionary.ContainsKey(text)) { string text2 = ((i.IDInfos != null && j < i.IDInfos.Count) ? ReadIdInfoName(i.IDInfos[j]) : ReadTransformName(i, j)); CustomizationData customizationData = (string.Equals(text, localSteamId, StringComparison.Ordinal) ? GetLocalCustomizationData() : GetCandidateCustomizationData(i, j)); dictionary[text] = new RosterCloneState { SteamId = text, RichName = (text2 ?? string.Empty), PlainName = SanitizeDisplayName(PlayerMentionProvider.StripRichText(text2)), AvatarFingerprint = BuildCustomizationFingerprint(customizationData) }; } } ReconcileAbsentIncidents(hashSet); if (_previousRoster.Count > 0) { DetectNewCloneTransitions(dictionary, localSteamId); } ReconcileNoLongerMatchingIncidents(dictionary); _previousRoster.Clear(); foreach (KeyValuePair item in dictionary) { _previousRoster[item.Key] = item.Value; } } catch (Exception ex) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)("Clone Shield scan skipped safely: " + ex.GetType().Name + ": " + ex.Message)); } } } private void DetectNewCloneTransitions(Dictionary currentRoster, string localSteamId) { foreach (RosterCloneState culprit in currentRoster.Values) { RosterCloneState value; bool flag = _previousRoster.TryGetValue(culprit.SteamId, out value); bool flag2 = !flag || !string.Equals(value.AvatarFingerprint, culprit.AvatarFingerprint, StringComparison.Ordinal); bool flag3 = !flag || !string.Equals(value.RichName, culprit.RichName, StringComparison.Ordinal); if (!flag2 && !flag3) { continue; } foreach (RosterCloneState victim in currentRoster.Values) { if (string.Equals(victim.SteamId, culprit.SteamId, StringComparison.Ordinal) || !_previousRoster.TryGetValue(victim.SteamId, out var value2)) { continue; } CloneMatchKind num = CloneTransitionPolicy.Evaluate(flag, value?.AvatarFingerprint, culprit.AvatarFingerprint, value?.RichName, culprit.RichName, value2.AvatarFingerprint, victim.AvatarFingerprint, value2.RichName, victim.RichName, IsUsefulCustomizationFingerprint(culprit.AvatarFingerprint), IsUsefulExactName(culprit.RichName)); bool flag4 = (num & CloneMatchKind.AvatarOutfit) != 0; bool flag5 = (num & CloneMatchKind.ExactStyledName) != 0; if (!flag4 && !flag5) { continue; } string matchType = ((flag4 && flag5) ? "avatar/outfit and exact styled name" : (flag4 ? "avatar/outfit" : "exact styled name")); bool flag6 = CloneIncidentLedger.Snapshot.Any((CloneIncident item) => string.Equals(item.VictimSteamId, victim.SteamId, StringComparison.Ordinal) && string.Equals(item.SuspectSteamId, culprit.SteamId, StringComparison.Ordinal)); CloneIncident cloneIncident = CloneIncidentLedger.Record(victim.SteamId, culprit.SteamId, matchType, victim.PlainName, culprit.PlainName, DateTimeOffset.UtcNow.ToUnixTimeSeconds()); if (cloneIncident != null) { PlayerIdentityRoleHighlightController.RefreshAll(); if (!flag6) { CloneIncidentActionController.RecordLifecycle("opened", cloneIncident, "verified", "clone-shield-local"); } } if (cloneIncident != null && !string.IsNullOrWhiteSpace(localSteamId) && string.Equals(victim.SteamId, localSteamId, StringComparison.Ordinal)) { PublishPotentialClone(victim, culprit, matchType, cloneIncident.Id); } } } } private static string GetLocalSteamId() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) try { return ((ulong)SteamUser.GetSteamID()).ToString(); } catch { return string.Empty; } } private void PublishPotentialClone(RosterCloneState victim, RosterCloneState culprit, string matchType, string incidentId) { string key = victim.SteamId + "|" + culprit.SteamId + "|" + matchType; if (!_lastWarningTimes.TryGetValue(key, out var value) || !(Time.unscaledTime - value < 300f)) { _lastWarningTimes[key] = Time.unscaledTime; string text = BuildPublicIdentityLabel(victim.PlainName, victim.SteamId); string text2 = BuildPublicIdentityLabel(culprit.PlainName, culprit.SteamId); string text3 = "[Clone Shield] " + text + " was potentially cloned by " + text2 + " (" + matchType + "). Evidence IDs: victim ..." + PlayerIdentityEvidenceController.Suffix(victim.SteamId) + "; suspect ..." + PlayerIdentityEvidenceController.Suffix(culprit.SteamId) + "; match " + matchType + ". Be aware of the player list, Steam ID, and profile; verify identity in case this alert is incorrect."; if (!Plugin.TrySendLobbyChatMessage(text3)) { Plugin.AddLocalNotification(text3 + " Public send was unavailable."); } ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)("Clone Shield public alert: victimSteam=…" + PlayerIdentityEvidenceController.Suffix(victim.SteamId) + ", culpritSteam=…" + PlayerIdentityEvidenceController.Suffix(culprit.SteamId) + ", match=" + matchType + ".")); } } } private static string BuildPublicIdentityLabel(string displayName, string steamId) { string obj = (string.IsNullOrWhiteSpace(displayName) ? "Unknown player" : displayName); string text = (steamId ?? string.Empty).Trim(); string text2 = ((text.Length > 6) ? text.Substring(text.Length - 6) : text); return obj + " (ID ..." + (string.IsNullOrWhiteSpace(text2) ? "unknown" : text2) + ")"; } private static bool IsUsefulExactName(string richName) { if (string.IsNullOrWhiteSpace(richName)) { return false; } return PlayerMentionProvider.StripRichText(richName).Trim().Length >= 2; } private static string SanitizeDisplayName(string displayName) { string text = (displayName ?? string.Empty).Replace("\r", " ").Replace("\n", " ").Trim(); if (text.Length <= 48) { return text; } return text.Substring(0, 48); } private static bool TryGetActiveLobbyCode(out string lobbyCode) { lobbyCode = string.Empty; try { MultiplayerManager i = MonoSingleton.I; if ((Object)(object)i == (Object)null || !i.LobbyStatus) { return false; } lobbyCode = i.LobbyCode?.Trim() ?? string.Empty; if (!ulong.TryParse(lobbyCode, out var result) || result == 0L || !SteamManager.Initialized) { return false; } return true; } catch { lobbyCode = string.Empty; return false; } } private void ResetObservationState() { _observedLobbyCode = string.Empty; _previousRoster.Clear(); _lastWarningTimes.Clear(); _missingIncidentScans.Clear(); _nonMatchingIncidentScans.Clear(); _alignmentWaitWasLogged = false; _nextAlignmentWaitLogAt = 0f; CloneIncidentLedger.Reset(string.Empty); CloneIncidentActionController.ResetLobbyState(); } private void LogAlignmentWait(string detail) { if (!_alignmentWaitWasLogged || !(Time.unscaledTime < _nextAlignmentWaitLogAt)) { _alignmentWaitWasLogged = true; _nextAlignmentWaitLogAt = Time.unscaledTime + 60f; string text = (string.IsNullOrWhiteSpace(detail) ? "native/Steam roster verification is not ready" : detail); ManualLogSource log = Plugin.Log; if (log != null) { log.LogDebug((object)("Clone Shield waiting for aligned unique Steam/PurrNet/Steam-lobby roster before recording evidence: " + text + ". Further identical waits are suppressed for 60 seconds.")); } } } private void ReconcileAbsentIncidents(HashSet verifiedSteamIds) { CloneIncident[] array = CloneIncidentLedger.Snapshot.ToArray(); foreach (CloneIncident cloneIncident in array) { if (verifiedSteamIds.Contains(cloneIncident.VictimSteamId) && verifiedSteamIds.Contains(cloneIncident.SuspectSteamId)) { _missingIncidentScans.Remove(cloneIncident.StableId); continue; } int value; int num = ((!_missingIncidentScans.TryGetValue(cloneIncident.StableId, out value)) ? 1 : (value + 1)); _missingIncidentScans[cloneIncident.StableId] = num; if (num >= 2 && CloneIncidentActionController.MarkStaleAbsent(cloneIncident.Id, out CloneIncident _)) { _missingIncidentScans.Remove(cloneIncident.StableId); PlayerIdentityRoleHighlightController.RefreshAll(); } } } private void ReconcileNoLongerMatchingIncidents(Dictionary currentRoster) { CloneIncident[] array = CloneIncidentLedger.Snapshot.ToArray(); foreach (CloneIncident cloneIncident in array) { if (!currentRoster.TryGetValue(cloneIncident.VictimSteamId, out var value) || !currentRoster.TryGetValue(cloneIncident.SuspectSteamId, out var value2)) { _nonMatchingIncidentScans.Remove(cloneIncident.StableId); continue; } bool num = cloneIncident.MatchType.IndexOf("avatar", StringComparison.OrdinalIgnoreCase) >= 0; bool flag = cloneIncident.MatchType.IndexOf("name", StringComparison.OrdinalIgnoreCase) >= 0; bool num2 = num && IsUsefulCustomizationFingerprint(value.AvatarFingerprint) && string.Equals(value.AvatarFingerprint, value2.AvatarFingerprint, StringComparison.Ordinal); bool flag2 = flag && IsUsefulExactName(value.RichName) && string.Equals(value.RichName, value2.RichName, StringComparison.Ordinal); if (num2 || flag2) { _nonMatchingIncidentScans.Remove(cloneIncident.StableId); continue; } int value3; int num3 = ((!_nonMatchingIncidentScans.TryGetValue(cloneIncident.StableId, out value3)) ? 1 : (value3 + 1)); _nonMatchingIncidentScans[cloneIncident.StableId] = num3; if (num3 >= 2 && CloneIncidentActionController.MarkNoLongerMatching(cloneIncident.Id, out CloneIncident _)) { _nonMatchingIncidentScans.Remove(cloneIncident.StableId); PlayerIdentityRoleHighlightController.RefreshAll(); } } } private static string ReadIdInfoName(object idInfo) { if (idInfo == null) { return string.Empty; } object obj = AccessTools.Field(idInfo.GetType(), "Name")?.GetValue(idInfo); if (obj is string result) { return result; } if (obj is byte[] array && array.Length != 0) { return Encoding.Unicode.GetString(array).TrimEnd(new char[1]); } return string.Empty; } private static string ReadTransformName(PlayerPanelController panel, int index) { try { if (panel?.PlayerTransforms == null || index < 0 || index >= panel.PlayerTransforms.Count) { return string.Empty; } NetworkTransform val = panel.PlayerTransforms[index]; PlayerController val2 = (((Object)(object)val != (Object)null) ? ((Component)val).GetComponent() : null); if ((Object)(object)((val2 != null) ? val2.PlayerNameText : null) != (Object)null) { return ((TMP_Text)val2.PlayerNameText).text ?? string.Empty; } return ((Object)(object)val != (Object)null) ? ((Object)val).name : string.Empty; } catch { return string.Empty; } } private static CustomizationData GetLocalCustomizationData() { try { return MonoSingleton.I?.CustomizationData; } catch { return null; } } private static CustomizationData GetCandidateCustomizationData(PlayerPanelController panel, int index) { try { if (panel?.PlayerTransforms == null || index < 0 || index >= panel.PlayerTransforms.Count) { return null; } NetworkTransform val = panel.PlayerTransforms[index]; PlayerCustomizationController val2 = (((Object)(object)val != (Object)null) ? ((Component)val).GetComponent() : null); return (CustomizationData)(((Object)(object)val2 != (Object)null) ? /*isinst with value type is only supported in some contexts*/: null); } catch { return null; } } private static string BuildCustomizationFingerprint(CustomizationData customizationData) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Expected O, but got Unknown if (customizationData == null) { return string.Empty; } try { CustomizationDataIDs3 val = new CustomizationDataIDs3(customizationData); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("full=").Append(val.IsFullBody ? "1" : "0"); AppendShapeMap(stringBuilder, "A", val.AppearanceToShapeAndColors); AppendShapeMap(stringBuilder, "O", val.OutfitToShapeAndColors); return stringBuilder.ToString(); } catch { return string.Empty; } } private static void AppendShapeMap(StringBuilder builder, string prefix, IDictionary shapes) { //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_008d: 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) //IL_00dd: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Unknown result type (might be due to invalid IL or missing references) if (builder == null || shapes == null) { return; } foreach (KeyValuePair item in shapes.OrderBy((KeyValuePair pair) => Convert.ToInt32(pair.Key))) { builder.Append('|').Append(prefix).Append(':') .Append(Convert.ToInt32(item.Key)) .Append('='); builder.Append(item.Value.ShapeIndex).Append(':'); if (item.Value.ColorIndexes == null || item.Value.ColorIndexes.Count == 0) { continue; } for (int num = 0; num < item.Value.ColorIndexes.Count; num++) { if (num > 0) { builder.Append(','); } builder.Append(item.Value.ColorIndexes[num]); } } } private static bool IsUsefulCustomizationFingerprint(string fingerprint) { if (!string.IsNullOrWhiteSpace(fingerprint) && fingerprint.Contains("|A:") && fingerprint.Contains("|O:")) { return fingerprint.Length > 32; } return false; } } internal sealed class CommandTypeaheadController : MonoBehaviour { private readonly struct CommandSuggestion { public string Label { get; } public string Completion { get; } public string ArgumentToken { get; } public string Hint { get; } public bool IsArgument { get; } public CommandSuggestion(string label, string completion, string argumentToken, string hint, bool isArgument) { Label = label ?? string.Empty; Completion = completion ?? string.Empty; ArgumentToken = argumentToken ?? string.Empty; Hint = hint ?? string.Empty; IsArgument = isArgument; } public static CommandSuggestion FromRegistryLine(string line) { string text = line ?? string.Empty; string completion = text.Split(new char[1] { ' ' }, 2)[0]; string hint = (text.Contains(" - ") ? text.Substring(text.IndexOf(" - ", StringComparison.Ordinal) + 3) : "complete command"); return new CommandSuggestion(text, completion, string.Empty, hint, isArgument: false); } } private const float AutocompleteCooldownSeconds = 0.15f; private const int MaxSuggestions = 7; private const int MaxBrowserSuggestions = 64; private const int MaxVisibleSuggestions = 8; private float _lastAutocompleteAt = -1f; private string _lastObservedInputText = string.Empty; private int _lastObservedCaret = -1; private IReadOnlyList _suggestions = Array.Empty(); private Vector2 _scrollPosition; private GUIStyle _popupStyle; private GUIStyle _popupHeaderStyle; private GUIStyle _popupButtonStyle; private GUIStyle _popupHintStyle; private Texture2D _popupBackgroundTexture; private string _lastThemeKey = string.Empty; private int _renderedTextureGeneration; private static readonly Dictionary> ArgumentSuggestions = new Dictionary>(StringComparer.OrdinalIgnoreCase) { ["assetsweep"] = CleanupChoices("assetsweep"), ["sweep"] = CleanupChoices("sweep"), ["as"] = CleanupChoices("as"), ["hosthealth"] = ToggleWithRun("hosthealth"), ["hh"] = ToggleWithRun("hh"), ["leavenotices"] = Toggle("leavenotices"), ["ln"] = Toggle("ln"), ["timestamps"] = new CommandSuggestion[6] { Choice("timestamps", "on", "enable chat and notice timestamps"), Choice("timestamps", "off", "disable chat and notice timestamps"), Choice("timestamps", "12", "use 12-hour AM/PM"), Choice("timestamps", "24", "use 24-hour time"), Choice("timestamps", "reset", "restore enabled 12-hour defaults"), Choice("timestamps", "status", "show timestamp state and format") }, ["tt"] = new CommandSuggestion[6] { Choice("tt", "on", "enable chat and notice timestamps"), Choice("tt", "off", "disable chat and notice timestamps"), Choice("tt", "12", "use 12-hour AM/PM"), Choice("tt", "24", "use 24-hour time"), Choice("tt", "reset", "restore enabled 12-hour defaults"), Choice("tt", "status", "show timestamp state and format") }, ["chatreadability"] = Toggle("chatreadability"), ["chatread"] = Toggle("chatread"), ["chatbackdrop"] = Toggle("chatbackdrop"), ["chatbg"] = Toggle("chatbg"), ["chatoutline"] = new CommandSuggestion[4] { Choice("chatoutline", "75", "default outline"), Choice("chatoutline", "50", "lighter outline"), Choice("chatoutline", "100", "crisp outline"), Choice("chatoutline", "status", "show current intensity") }, ["outline"] = new CommandSuggestion[4] { Choice("outline", "75", "default outline"), Choice("outline", "50", "lighter outline"), Choice("outline", "100", "crisp outline"), Choice("outline", "status", "show current intensity") }, ["chatscale"] = new CommandSuggestion[5] { Choice("chatscale", "100", "vanilla whole chat UI"), Choice("chatscale", "125", "larger whole chat UI"), Choice("chatscale", "150", "extra large whole chat UI"), Choice("chatscale", "reset", "return whole UI to vanilla"), Choice("chatscale", "status", "show current whole UI scale") }, ["chatresize"] = new CommandSuggestion[5] { Choice("chatresize", "100", "vanilla whole chat UI"), Choice("chatresize", "125", "larger whole chat UI"), Choice("chatresize", "150", "extra large whole chat UI"), Choice("chatresize", "reset", "return whole UI to vanilla"), Choice("chatresize", "status", "show current whole UI scale") }, ["cr"] = new CommandSuggestion[5] { Choice("cr", "100", "vanilla whole chat UI"), Choice("cr", "125", "larger whole chat UI"), Choice("cr", "150", "extra large whole chat UI"), Choice("cr", "reset", "return whole UI to vanilla"), Choice("cr", "status", "show current whole UI scale") }, ["chatfont"] = ChatFontChoices("chatfont"), ["chatfontsize"] = ChatFontChoices("chatfontsize"), ["cf"] = ChatFontChoices("cf"), ["chatheight"] = ChatHeightChoices("chatheight"), ["chatstretch"] = ChatHeightChoices("chatstretch"), ["chath"] = ChatHeightChoices("chath"), ["chatrows"] = ChatHistoryChoices("chatrows"), ["chathistory"] = ChatHistoryChoices("chathistory"), ["chatr"] = ChatHistoryChoices("chatr"), ["chatvanilla"] = Array.Empty(), ["chatreset"] = Array.Empty(), ["chatdefault"] = Array.Empty(), ["chatoutlinecolor"] = HexColor("chatoutlinecolor", "normal outline"), ["outlinecolor"] = HexColor("outlinecolor", "normal outline"), ["blackoutlinecolor"] = HexColor("blackoutlinecolor", "black text outline"), ["blackchatoutline"] = HexColor("blackchatoutline", "black text outline"), ["blackoutline"] = HexColor("blackoutline", "black text outline"), ["bettermove"] = Toggle("bettermove"), ["bm"] = Toggle("bm"), ["reconnectguard"] = Toggle("reconnectguard"), ["rg"] = Toggle("rg"), ["reconnectmessage"] = Toggle("reconnectmessage"), ["rm"] = Toggle("rm"), ["focusanywhere"] = Toggle("focusanywhere"), ["fa"] = Toggle("fa"), ["minimaplabels"] = Toggle("minimaplabels"), ["mml"] = Toggle("mml"), ["playersaver"] = PlayerRenderSaverChoices("playersaver"), ["rendercull"] = PlayerRenderSaverChoices("rendercull"), ["playercull"] = PlayerRenderSaverChoices("playercull"), ["chalk"] = ChalkboardChoices("chalk"), ["chalkboard"] = ChalkboardChoices("chalkboard"), ["communitybans"] = CommunityBanChoices("communitybans"), ["communityban"] = CommunityBanChoices("communityban"), ["bansync"] = CommunityBanChoices("bansync"), ["cloneshield"] = Toggle("cloneshield"), ["cloneguard"] = Toggle("cloneguard"), ["identityshield"] = Toggle("identityshield"), ["idsync"] = IdentityResyncChoices("idsync"), ["identitysync"] = IdentityResyncChoices("identitysync"), ["rostersync"] = IdentityResyncChoices("rostersync"), ["copychat"] = Toggle("copychat"), ["ccopy"] = Toggle("ccopy"), ["chatlinks"] = Toggle("chatlinks"), ["clinks"] = Toggle("clinks"), ["spoons"] = new CommandSuggestion[10] { Choice("spoons", "0", "empty spoon drawer"), Choice("spoons", "1", "show [1/5sp]"), Choice("spoons", "2", "show [2/5sp]"), Choice("spoons", "3", "show [3/5sp]"), Choice("spoons", "4", "show [4/5sp]"), Choice("spoons", "5", "show [5/5sp]"), Choice("spoons", "off", "hide spoon tag"), Choice("spoons", "on", "show spoon tag"), Choice("spoons", "status", "show spoon state"), Choice("spoons", "reset", "restore hidden [5/5sp] default") }, ["setmood"] = new CommandSuggestion[5] { Choice("setmood", "energy", "show [x/5energy]"), Choice("setmood", "spoons", "show [x/5spoons]"), Choice("setmood", "default", "restore [x/5sp]"), Choice("setmood", "reset", "restore [x/5sp]"), Choice("setmood", "status", "show current custom label") }, ["spoonlabel"] = new CommandSuggestion[5] { Choice("spoonlabel", "energy", "show [x/5energy]"), Choice("spoonlabel", "spoons", "show [x/5spoons]"), Choice("spoonlabel", "default", "restore [x/5sp]"), Choice("spoonlabel", "reset", "restore [x/5sp]"), Choice("spoonlabel", "status", "show current custom label") }, ["helper"] = new CommandSuggestion[4] { Choice("helper", "add", "host: authorize a current lobby player"), Choice("helper", "remove", "host: revoke a Helper"), Choice("helper", "list", "show the host-scoped Helper list"), Choice("helper", "reset", "host: remove every Helper") }, ["lobbyhelper"] = HelperAlias("lobbyhelper"), ["lobbymod"] = HelperAlias("lobbymod"), ["modhelper"] = HelperAlias("modhelper"), ["moderationhelper"] = HelperAlias("moderationhelper"), ["brb"] = Array.Empty(), ["afk"] = Array.Empty(), ["back"] = Array.Empty(), ["pingsound"] = new CommandSuggestion[8] { Choice("pingsound", "on", "enable ping sound"), Choice("pingsound", "off", "disable ping sound"), Choice("pingsound", "status", "show sound status"), Choice("pingsound", "click", "soft UI click"), Choice("pingsound", "change", "UI change chirp"), Choice("pingsound", "error", "stronger alert"), Choice("pingsound", "task", "task-complete sound"), Choice("pingsound", "ticket", "ticket reward sound") }, ["setpingcolor"] = new CommandSuggestion[4] { Choice("setpingcolor", "FFD700", "classic ping yellow highlight"), Choice("setpingcolor", "9B59B6", "purple message highlight"), Choice("setpingcolor", "00C7BE", "cyan message highlight"), Choice("setpingcolor", "FF6B8A", "pink message highlight") }, ["statuscolor"] = HexColor("statuscolor", "status").Concat(new CommandSuggestion[1] { Choice("statuscolor", "reset", "restore warm gold status color") }).ToArray(), ["setname"] = new CommandSuggestion[1] { Choice("setname", "reset", "restore your current Steam persona name") }, ["bsqol"] = QolChoices("bsqol"), ["qol"] = QolChoices("qol"), ["style"] = StyleChoices("style"), ["sty"] = StyleChoices("sty"), ["styleui"] = Array.Empty(), ["sui"] = Array.Empty() }; private void Update() { TMP_InputField chatInput = GetChatInput(); if ((Object)(object)chatInput == (Object)null || !chatInput.isFocused) { ClearSuggestions(); _lastObservedInputText = string.Empty; _lastObservedCaret = -1; return; } string text = chatInput.text ?? string.Empty; int caretPosition = chatInput.caretPosition; if (!string.Equals(text, _lastObservedInputText, StringComparison.Ordinal) || caretPosition != _lastObservedCaret) { _lastObservedInputText = text; _lastObservedCaret = caretPosition; RefreshSuggestions(chatInput); } if (Input.GetKeyDown((KeyCode)9) && !(Time.unscaledTime - _lastAutocompleteAt < 0.15f) && _suggestions.Count > 0 && TryAutocomplete(chatInput, _suggestions[0])) { _lastAutocompleteAt = Time.unscaledTime; } } private void OnGUI() { //IL_00be: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_010c: Unknown result type (might be due to invalid IL or missing references) //IL_0144: Unknown result type (might be due to invalid IL or missing references) //IL_0149: Unknown result type (might be due to invalid IL or missing references) //IL_0170: Unknown result type (might be due to invalid IL or missing references) //IL_01a2: Expected O, but got Unknown if (_suggestions == null || _suggestions.Count == 0) { return; } EnsurePopupStyles(); float num = Mathf.Clamp((float)Screen.width - 24f, 300f, 560f); float num2 = (float)Mathf.Min(_suggestions.Count, 8) * 40f; float num3 = 58f + num2; float num4 = Mathf.Clamp(36f, 8f, Mathf.Max(8f, (float)Screen.width - num - 8f)); Rect val = default(Rect); ((Rect)(ref val))..ctor(num4, Mathf.Max(36f, (float)Screen.height - num3 - 118f), num, num3); if ((Object)(object)_popupBackgroundTexture != (Object)null) { GUI.DrawTexture(val, (Texture)(object)_popupBackgroundTexture); } GUILayout.BeginArea(val, _popupStyle); GUILayout.Label("BlueSage commands • Tab completes the first match", _popupHeaderStyle, Array.Empty()); GUILayout.Label((_suggestions.Count > 8) ? "Scroll for more. Nothing sends until you press Enter." : "Nothing sends until you press Enter.", _popupHintStyle, Array.Empty()); _scrollPosition = GUILayout.BeginScrollView(_scrollPosition, false, _suggestions.Count > 8, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(num - 24f), GUILayout.Height(num2 + 4f) }); for (int i = 0; i < _suggestions.Count; i++) { CommandSuggestion suggestion = _suggestions[i]; if (GUILayout.Button(new GUIContent(suggestion.Label, suggestion.Hint), _popupButtonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(num - 48f), GUILayout.Height(36f) })) { TMP_InputField chatInput = GetChatInput(); if ((Object)(object)chatInput != (Object)null) { TryAutocomplete(chatInput, suggestion); } } } GUILayout.EndScrollView(); GUILayout.EndArea(); } private void RefreshSuggestions(TMP_InputField input) { string text = input.text ?? string.Empty; if (!IsSlashCommandContext(text, input.caretPosition)) { ClearSuggestions(); } else { _suggestions = BuildSuggestions(text, input.caretPosition); } } private static IReadOnlyList BuildSuggestions(string text, int caretPosition) { string text2 = text.Substring(0, Math.Max(0, Math.Min(caretPosition, text.Length))); if (TryBuildArgumentSuggestions(text2, out var suggestions)) { return suggestions; } int maxResults = (IsCommandBrowserQuery(text2) ? 64 : 7); return (from suggestion in Plugin.GetCommandSuggestions(text2, maxResults).Select(CommandSuggestion.FromRegistryLine) where !string.IsNullOrWhiteSpace(suggestion.Completion) select suggestion).ToArray(); } private static bool TryBuildArgumentSuggestions(string current, out IReadOnlyList suggestions) { suggestions = Array.Empty(); string text = current.TrimStart(Array.Empty()); if (!text.StartsWith("/", StringComparison.Ordinal) && !text.StartsWith("./", StringComparison.Ordinal)) { return false; } string text2 = (text.StartsWith("./", StringComparison.Ordinal) ? text.Substring(2) : text.Substring(1)); int num = text2.IndexOf(' '); if (num < 0) { return false; } string command = text2.Substring(0, num); string text3 = text2.Substring(num + 1).TrimStart(Array.Empty()); int num2 = text3.LastIndexOf(' '); string currentToken = ((num2 >= 0) ? text3.Substring(num2 + 1) : text3); if (Plugin.IsHelperManagementCommandName(command) && !Plugin.CanManageHelpers()) { suggestions = Array.Empty(); return true; } if (Plugin.IsLobbySafetyCommandName(command) && !Plugin.CanUseLobbySafety(out var _)) { suggestions = Array.Empty(); return true; } if (string.Equals(command, "sidincident", StringComparison.OrdinalIgnoreCase) || string.Equals(command, "cloneincident", StringComparison.OrdinalIgnoreCase)) { suggestions = BuildIncidentSuggestions(command, text3, currentToken, num2); return true; } if (string.Equals(command, "sidban", StringComparison.OrdinalIgnoreCase) || string.Equals(command, "sidunban", StringComparison.OrdinalIgnoreCase) || string.Equals(command, "unbansid", StringComparison.OrdinalIgnoreCase)) { if (num2 >= 0) { suggestions = new CommandSuggestion[1] { Choice(command, "confirm", "confirm the exact previewed target within 30 seconds") }.Where((CommandSuggestion choice) => choice.ArgumentToken.StartsWith(currentToken, StringComparison.OrdinalIgnoreCase)).ToArray(); return suggestions.Count > 0; } suggestions = (from token in (from text4 in Plugin.GetModerationTargetSuggestions(!string.Equals(command, "sidban", StringComparison.OrdinalIgnoreCase)) where text4.StartsWith(currentToken, StringComparison.OrdinalIgnoreCase) select text4).Take(64) select Choice(command, token, string.Equals(command, "sidban", StringComparison.OrdinalIgnoreCase) ? "Clone Shield suspect; preview before confirming" : "native banned Steam ID; preview before confirming")).ToArray(); return suggestions.Count > 0; } if (string.Equals(command, "sidwho", StringComparison.OrdinalIgnoreCase) || string.Equals(command, "sidlookup", StringComparison.OrdinalIgnoreCase)) { if (num2 >= 0 && text3.StartsWith("copy ", StringComparison.OrdinalIgnoreCase)) { suggestions = (from token in (from text4 in Plugin.GetIdentityLookupSuggestions() where !string.Equals(text4, "list", StringComparison.OrdinalIgnoreCase) && text4.StartsWith(currentToken, StringComparison.OrdinalIgnoreCase) select text4).Take(64) select Choice(command, token, "copy this uniquely verified live identity record locally")).ToArray(); return suggestions.Count > 0; } suggestions = (from text4 in (from text4 in Plugin.GetIdentityLookupSuggestions().Concat(new string[1] { "copy" }) where text4.StartsWith(currentToken, StringComparison.OrdinalIgnoreCase) select text4).Take(64) select Choice(command, text4, (text4 == "list") ? "open the authorized verified roster in Lobby Safety" : "verify this current row before moderation")).ToArray(); return suggestions.Count > 0; } if (!ArgumentSuggestions.TryGetValue(command, out var value)) { return false; } suggestions = value.Where((CommandSuggestion choice) => choice.ArgumentToken.StartsWith(currentToken, StringComparison.OrdinalIgnoreCase)).Take(64).ToArray(); return suggestions.Count > 0; } private static IReadOnlyList BuildIncidentSuggestions(string command, string argumentPrefix, string currentToken, int lastSpace) { if (lastSpace >= 0) { string text = argumentPrefix.Substring(0, lastSpace).Trim(); if (string.Equals(text, "clear-player", StringComparison.OrdinalIgnoreCase) || string.Equals(text, "clear-row", StringComparison.OrdinalIgnoreCase)) { return (from value in (from value in Plugin.GetIdentityLookupSuggestions() where !string.Equals(value, "list", StringComparison.OrdinalIgnoreCase) && value.StartsWith(currentToken, StringComparison.OrdinalIgnoreCase) select value).Take(64) select Choice(command, value, "clear local incident flags only after this verified row resolves uniquely")).ToArray(); } if (text.Length > 1 && text.StartsWith("C", StringComparison.OrdinalIgnoreCase) && text.IndexOf(' ') < 0) { return new CommandSuggestion[2] { Choice(command, "resolve", "host-validated resolution for " + text.ToUpperInvariant()), Choice(command, "dismiss", "clear " + text.ToUpperInvariant() + " on this client only") }.Where((CommandSuggestion choice) => choice.ArgumentToken.StartsWith(currentToken, StringComparison.OrdinalIgnoreCase)).ToArray(); } return Array.Empty(); } CommandSuggestion[] first = new CommandSuggestion[5] { Choice(command, "status", "list open and closed Clone Shield incidents"), Choice(command, "dismiss-all", "clear every open incident on this client only"), Choice(command, "clear-player", "choose one verified player and clear their local incident flags"), Choice(command, "clear-history", "remove closed incident history only"), Choice(command, "clear-all", "dismiss open incidents locally and clear closed history") }; IEnumerable second = from incident in CloneIncidentActionController.GetOpenIncidents() select Choice(command, incident.Id, "choose this incident, then resolve or dismiss"); return (from choice in first.Concat(second) where choice.ArgumentToken.StartsWith(currentToken, StringComparison.OrdinalIgnoreCase) select choice).Take(64).ToArray(); } private static bool IsCommandBrowserQuery(string current) { string a = (current ?? string.Empty).Trim(); if (!string.Equals(a, "/", StringComparison.Ordinal)) { return string.Equals(a, "./", StringComparison.Ordinal); } return true; } private static bool TryAutocomplete(TMP_InputField input, CommandSuggestion suggestion) { if ((Object)(object)input == (Object)null || string.IsNullOrWhiteSpace(suggestion.Completion)) { return false; } string text = input.text ?? string.Empty; int num = Math.Max(0, Math.Min(input.caretPosition, text.Length)); string beforeCaret = text.Substring(0, num); string text2 = text.Substring(num); string text3 = (suggestion.IsArgument ? ReplaceCurrentArgument(beforeCaret, suggestion.ArgumentToken) : (suggestion.Completion + " ")); input.text = text3 + text2; input.caretPosition = text3.Length; ((Selectable)input).Select(); input.ActivateInputField(); return true; } private static string ReplaceCurrentArgument(string beforeCaret, string argumentToken) { int num = beforeCaret.LastIndexOf(' '); if (num < 0) { return beforeCaret + argumentToken + " "; } return beforeCaret.Substring(0, num + 1) + argumentToken + " "; } private static bool IsSlashCommandContext(string text, int caretPosition) { if (string.IsNullOrWhiteSpace(text)) { return false; } string text2 = text[..Math.Max(0, Math.Min(caretPosition, text.Length))].TrimStart(Array.Empty()); if (!text2.StartsWith("/", StringComparison.Ordinal)) { return text2.StartsWith("./", StringComparison.Ordinal); } return true; } private static TMP_InputField GetChatInput() { try { UIManager i = MonoSingleton.I; return (i != null) ? i.MessageInput : null; } catch { return null; } } private void ClearSuggestions() { _suggestions = Array.Empty(); } private void EnsurePopupStyles() { //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Expected O, but got Unknown //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Expected O, but got Unknown //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00b5: 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) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00d2: Expected O, but got Unknown //IL_00dd: Unknown result type (might be due to invalid IL or missing references) //IL_00e2: Unknown result type (might be due to invalid IL or missing references) //IL_00ea: Unknown result type (might be due to invalid IL or missing references) //IL_00f1: Unknown result type (might be due to invalid IL or missing references) //IL_0100: Expected O, but got Unknown BlueSageUiThemePalette current = BlueSageUiTheme.Current; if (_popupStyle == null || !string.Equals(_lastThemeKey, current.Key, StringComparison.Ordinal) || _renderedTextureGeneration != BlueSageUiTheme.RuntimeTextureGeneration || !BlueSageUiTheme.AreRuntimeTexturesAlive()) { _lastThemeKey = current.Key; _popupBackgroundTexture = BlueSageUiTheme.GetSolidTexture(PopupBackground(current)); GUIStyle val = new GUIStyle(GUI.skin.box) { padding = new RectOffset(10, 10, 8, 10) }; val.normal.background = _popupBackgroundTexture; val.normal.textColor = current.PopupBodyText; _popupStyle = val; GUIStyle val2 = new GUIStyle(GUI.skin.label) { fontSize = 12, fontStyle = (FontStyle)1 }; val2.normal.textColor = current.PopupHeaderText; _popupHeaderStyle = val2; GUIStyle val3 = new GUIStyle(GUI.skin.label) { fontSize = 11 }; val3.normal.textColor = current.PopupHintText; _popupHintStyle = val3; _popupButtonStyle = BlueSageUiTheme.CreateNeutralButtonStyle(GUI.skin.button, current, 13); _popupButtonStyle.alignment = (TextAnchor)3; _popupButtonStyle.wordWrap = true; _popupButtonStyle.clipping = (TextClipping)0; _renderedTextureGeneration = BlueSageUiTheme.RuntimeTextureGeneration; } } private static Color PopupBackground(BlueSageUiThemePalette theme) { //IL_001f: 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_0024: 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) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) Color val = (Color)(((??)theme?.Panel) ?? new Color(0.06f, 0.07f, 0.11f, 1f)); return new Color(val.r * 0.72f, val.g * 0.72f, val.b * 0.72f, 1f); } private static IReadOnlyList Toggle(string command) { return new CommandSuggestion[3] { Choice(command, "on", "turn this feature on"), Choice(command, "off", "turn this feature off"), Choice(command, "status", "show current state") }; } private static IReadOnlyList IdentityResyncChoices(string command) { List list = new List { Choice(command, "status", "read-only exact-correlation coverage and last transaction"), Choice(command, "refresh", "local BlueSage surfaces from already-available native state") }; bool isHost; string reason; switch (SteamIdModerationController.GetLocalAccessState(out isHost, out reason)) { case LobbySafetyAccessState.Host: list.Add(Choice(command, "repair", "verified Host one-shot repair for this stable generation")); break; case LobbySafetyAccessState.HelperReady: list.Add(Choice(command, "request", "authenticated Helper request relayed for Host revalidation")); break; } return list; } private static IReadOnlyList CleanupChoices(string command) { return new CommandSuggestion[4] { Choice(command, "now", "run one manual cleanup now"), Choice(command, "on", "enable BlueSage Automatic Cleanup"), Choice(command, "off", "disable BlueSage Automatic Cleanup"), Choice(command, "status", "show Automatic Cleanup status") }; } private static IReadOnlyList ToggleWithRun(string command) { return Toggle(command).Concat(new CommandSuggestion[1] { Choice(command, "run", "run a manual check now") }).ToArray(); } private static IReadOnlyList HexColor(string command, string label) { return new CommandSuggestion[5] { Choice(command, "000000", label + " black"), Choice(command, "FFFFFF", label + " white"), Choice(command, "FFD45D", label + " warm"), Choice(command, "70D6FF", label + " cool"), Choice(command, "status", "show current color") }; } private static IReadOnlyList ChatHeightChoices(string command) { return new CommandSuggestion[8] { Choice(command, "on", "moderate taller chat window"), Choice(command, "off", "vanilla chat window"), Choice(command, "toggle", "switch taller window on or off"), Choice(command, "100", "vanilla chat window"), Choice(command, "140", "taller chat window"), Choice(command, "180", "very tall chat window"), Choice(command, "reset", "return to vanilla window"), Choice(command, "status", "show current window size") }; } private static IReadOnlyList ChatFontChoices(string command) { return new CommandSuggestion[8] { Choice(command, "on", "larger message text"), Choice(command, "off", "vanilla chat text"), Choice(command, "toggle", "switch larger message text on or off"), Choice(command, "100", "vanilla chat text"), Choice(command, "125", "larger message text"), Choice(command, "150", "extra large message text"), Choice(command, "reset", "return to vanilla text"), Choice(command, "status", "show current font size") }; } private static IReadOnlyList ChatHistoryChoices(string command) { return new CommandSuggestion[6] { Choice(command, "50", "lighter busy-lobby scrollback"), Choice(command, "100", "lighter busy-lobby scrollback"), Choice(command, "150", "balanced default"), Choice(command, "250", "maximum scrollback; highest UI cost"), Choice(command, "reset", "restore the 150-row default"), Choice(command, "status", "show configured retained rows") }; } private static IReadOnlyList PlayerRenderSaverChoices(string command) { return new CommandSuggestion[9] { Choice(command, "on", "hide far/overflow remote renderers locally"), Choice(command, "off", "restore all remote renderers"), Choice(command, "toggle", "switch local render saver on or off"), Choice(command, "status", "show visible and hidden counts"), Choice(command, "reset", "restore default radius and visible count"), Choice(command, "radius", "set hide radius in meters"), Choice(command, "max", "set closest visible player count"), Choice(command, "45", "default radius hint"), Choice(command, "32", "default closest visible hint") }; } private static IReadOnlyList ChalkboardChoices(string command) { return new CommandSuggestion[9] { Choice(command, "boards", "map the current scene-local board indices and locations"), Choice(command, "view", "inspect one exact board locally without changing it"), Choice(command, "clear", "Host/Helper: clear an exact index after a recovery snapshot"), Choice(command, "status", "show chalkboard feature and compatibility state"), Choice(command, "list", "list local recovery and named board saves"), Choice(command, "save", "save a named local board snapshot"), Choice(command, "load", "Host/Helper: request an exact named snapshot restore"), Choice(command, "delete", "delete a local named save after preserving a backup"), Choice(command, "refresh", "refresh the exact local 0-2 board map") }; } private static IReadOnlyList CommunityBanChoices(string command) { return new CommandSuggestion[5] { Choice(command, "status", "show bounded local upstream validation state"), Choice(command, "refresh", "validate now and append only missing local pairs"), Choice(command, "on", "enable local additive validation"), Choice(command, "off", "disable local additive validation"), Choice(command, "attestation", "show bounded attestation location and state") }; } private static IReadOnlyList StyleChoices(string command) { return new CommandSuggestion[6] { Choice(command, "color", "wrap text with Color A"), Choice(command, "bold", "toggle bold styling"), Choice(command, "italic", "toggle italic styling"), Choice(command, "gradient", "use Color A to B"), Choice(command, "gradient3", "use Color A to B to C"), Choice(command, "status", "build colored status text: status #HEX text") }; } private static IReadOnlyList QolChoices(string command) { return new CommandSuggestion[2] { Choice(command, "status", "show the current feature summary"), Choice(command, "hosthealth", "run one host-health check") }; } private static IReadOnlyList HelperAlias(string command) { return new CommandSuggestion[4] { Choice(command, "add", "host: authorize a current lobby player"), Choice(command, "remove", "host: revoke a Helper"), Choice(command, "list", "show the host-scoped Helper list"), Choice(command, "reset", "host: remove every Helper") }; } private static CommandSuggestion Choice(string command, string token, string hint) { return new CommandSuggestion("/" + command + " " + token + " - " + hint, "/" + command, token, hint, isArgument: true); } } internal static class CommunityBanListController { [Serializable] private sealed class CommunityBanPayload { public string[] BanServerPlayers = Array.Empty(); public string[] BanServerPlayerNicks = Array.Empty(); } internal const float SuccessIntervalSeconds = 21600f; internal const float RetryIntervalSeconds = 900f; internal const int MaxPayloadBytes = 262144; private static readonly CommunityBanSyncStatus CurrentStatus = new CommunityBanSyncStatus(); internal static bool RefreshInFlight { get; private set; } internal static string StatusText() { CurrentStatus.Enabled = CanApply(); return CommunityBanSyncPolicy.BuildStatusText(CurrentStatus); } internal static string FriendlyStatusText() { CurrentStatus.Enabled = CanApply(); return CommunityBanSyncPolicy.BuildFriendlyStatusText(CurrentStatus); } internal static void SetNextAttempt(DateTime nextAttemptUtc) { CurrentStatus.Enabled = CanApply(); CurrentStatus.NextAttemptUtc = nextAttemptUtc.ToUniversalTime(); WriteCurrentAttestation(); } internal static void MarkDisabled() { CurrentStatus.Enabled = false; CurrentStatus.NextAttemptUtc = null; WriteCurrentAttestation(); } internal static IEnumerator ReconcileFromSource(Action completed) { bool succeeded = false; if (RefreshInFlight) { completed?.Invoke(obj: false); yield break; } RefreshInFlight = true; DateTime attemptUtc = DateTime.UtcNow; CommunityBanSyncPolicy.BeginAttempt(CurrentStatus, CanApply(), attemptUtc); try { if (!CanApply()) { Fail("sync is disabled", attemptUtc); yield break; } CommunityBanSourceResult source = null; yield return CommunityBanSourceClient.Fetch(delegate(CommunityBanSourceResult result) { source = result; }); if (source == null) { Fail("source client returned no result", attemptUtc); yield break; } CurrentStatus.HttpResult = CommunityBanSyncPolicy.BoundStatusField(source.HttpResult, "not attempted"); CurrentStatus.SourceRevision = CommunityBanSyncPolicy.BoundStatusField(source.SourceRevision); CurrentStatus.SourceHash = source.SourceHash; if (!source.Succeeded) { Fail(source.Error, attemptUtc); yield break; } if (!TryParsePayload(source.Payload, out var payload, out var error)) { Fail("JSON validation failed: " + error, attemptUtc); yield break; } BanData val = MonoSingleton.I?.BanData; if (val?.BanServerPlayers == null || val.BanServerPlayerNicks == null) { Fail("native paired storage is unavailable", attemptUtc); yield break; } if (!CommunityBanListMergePolicy.TryBuildAdditions(val.BanServerPlayers, val.BanServerPlayerNicks, payload?.BanServerPlayers, payload?.BanServerPlayerNicks, out var additions, out var error2)) { Fail(error2, attemptUtc); yield break; } if (!NativeBanStoreAdapter.TryApplyAdditions(additions, out var existingCount, out var error3)) { Fail("native persistence failed: " + error3, attemptUtc); yield break; } int valueOrDefault = (payload?.BanServerPlayers?.Length).GetValueOrDefault(); CurrentStatus.AcceptedCount = valueOrDefault; CurrentStatus.LastSuccessUtc = DateTime.UtcNow; CurrentStatus.NextAttemptUtc = CommunityBanSyncPolicy.ComputeNextAttemptUtc(CurrentStatus.LastSuccessUtc.Value, succeeded: true); CurrentStatus.BoundedError = string.Empty; WriteCurrentAttestation(); ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)("[BlueSageCommunityBanList] current upstream validated; accepted=" + valueOrDefault + "; existingPreserved=" + existingCount + "; addedMissing=" + additions.Length + "; liveActions=0; authorityGrants=0.")); } succeeded = true; } finally { RefreshInFlight = false; completed?.Invoke(succeeded); } } internal static void WriteCurrentAttestation() { try { string text = Path.Combine(Paths.ConfigPath, "BlueSageAudit"); Directory.CreateDirectory(text); string text2 = Path.Combine(text, "community-ban-source-attestation.json"); string text3 = text2 + ".tmp"; string destinationBackupFileName = text2 + ".bak"; File.WriteAllText(text3, CommunityBanSyncPolicy.BuildAttestationJson(CurrentStatus), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); if (File.Exists(text2)) { File.Replace(text3, text2, destinationBackupFileName, ignoreMetadataErrors: true); } else { File.Move(text3, text2); } } catch (Exception ex) { CurrentStatus.BoundedError = CommunityBanSyncPolicy.BoundError(string.IsNullOrWhiteSpace(CurrentStatus.BoundedError) ? ("attestation write failed: " + ex.GetType().Name) : (CurrentStatus.BoundedError + "; attestation write failed: " + ex.GetType().Name)); ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[BlueSageCommunityBanList] bounded attestation write failed: " + ex.GetType().Name + ". No identity fields were written.")); } } } private static void Fail(string error, DateTime attemptUtc) { CurrentStatus.BoundedError = CommunityBanSyncPolicy.BoundError(error); CurrentStatus.NextAttemptUtc = CommunityBanSyncPolicy.ComputeNextAttemptUtc(attemptUtc, succeeded: false); WriteCurrentAttestation(); ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[BlueSageCommunityBanList] refresh rejected safely: " + ((CurrentStatus.BoundedError.Length == 0) ? "unavailable" : CurrentStatus.BoundedError) + ". Local native ban pairs were not intentionally changed.")); } } private static bool CanApply() { if (Plugin.EnableCommunityBanListSync != null) { return Plugin.EnableCommunityBanListSync.Value; } return false; } private static bool TryParsePayload(string json, out CommunityBanPayload payload, out string error) { payload = null; error = string.Empty; try { payload = JsonUtility.FromJson(json ?? string.Empty); if (payload != null) { return true; } error = "empty document"; return false; } catch (Exception ex) { error = ex.GetType().Name; return false; } } } internal sealed class CommunityBanSourceResult { internal bool Succeeded { get; set; } internal string Payload { get; set; } = string.Empty; internal string HttpResult { get; set; } = "not attempted"; internal string SourceRevision { get; set; } = "unavailable"; internal string SourceHash { get; set; } = "unavailable"; internal string Error { get; set; } = string.Empty; } internal static class CommunityBanSourceClient { internal const int RequestTimeoutSeconds = 10; internal const string SourceUrl = "https://raw.githubusercontent.com/andrewlimforfun/ot-community-banlist/master/BanData.txt"; internal static IEnumerator Fetch(Action completed) { CommunityBanSourceResult result = new CommunityBanSourceResult(); UnityWebRequest request = UnityWebRequest.Get("https://raw.githubusercontent.com/andrewlimforfun/ot-community-banlist/master/BanData.txt"); try { request.timeout = 10; yield return request.SendWebRequest(); long responseCode = request.responseCode; result.HttpResult = ((responseCode > 0) ? (responseCode + " " + ((object)request.result/*cast due to .constrained prefix*/).ToString()) : ((object)request.result/*cast due to .constrained prefix*/).ToString()); if ((int)request.result != 1 || responseCode < 200 || responseCode >= 300) { result.Error = CommunityBanSyncPolicy.BoundError("fetch failed: " + ((object)request.result/*cast due to .constrained prefix*/).ToString() + " (HTTP " + responseCode + ")"); completed?.Invoke(result); yield break; } DownloadHandler downloadHandler = request.downloadHandler; string text = ((downloadHandler != null) ? downloadHandler.text : null) ?? string.Empty; int byteCount = Encoding.UTF8.GetByteCount(text); if (byteCount <= 0 || byteCount > 262144) { result.Error = "payload size rejected"; completed?.Invoke(result); yield break; } result.Payload = text; result.SourceHash = ComputeSha256(text); string responseHeader = request.GetResponseHeader("ETag"); if (string.IsNullOrWhiteSpace(responseHeader)) { responseHeader = request.GetResponseHeader("Last-Modified"); } result.SourceRevision = CommunityBanSyncPolicy.BoundStatusField(responseHeader); result.Succeeded = true; completed?.Invoke(result); } finally { ((IDisposable)request)?.Dispose(); } } private static string ComputeSha256(string value) { using SHA256 sHA = SHA256.Create(); byte[] array = sHA.ComputeHash(Encoding.UTF8.GetBytes(value ?? string.Empty)); StringBuilder stringBuilder = new StringBuilder(array.Length * 2); for (int i = 0; i < array.Length; i++) { stringBuilder.Append(array[i].ToString("X2")); } return stringBuilder.ToString(); } } internal static class StatisticsPulsePresentationPolicy { public static string BuildStreakValue(int currentStreak, int longestStreak) { return "\ud83d\udd25 " + Math.Max(0, currentStreak) + " • Best " + Math.Max(0, longestStreak); } } internal static class HostConsoleController { private static readonly long RefreshIntervalTicks = TimeSpan.FromSeconds(1.0).Ticks; private static HostConsoleSnapshot _lastSnapshot; private static long _lastCaptureUtcTicks; private static long _capabilityNonce; internal static BlueSageHostCapabilitySnapshotV1 LastCapabilitySnapshot { get; private set; } internal static bool HasCapabilitySnapshot { get; private set; } internal static int LastVerifiedRosterCount { get; private set; } internal static string LastRosterSummary { get; private set; } = "Roster has not been checked yet."; internal static string LastPlayerLimitSummary { get; private set; } = "PlayerLimit not checked."; internal static HostConsoleSnapshot Capture() { long ticks = DateTime.UtcNow.Ticks; long num = ticks - _lastCaptureUtcTicks; if (_lastSnapshot != null && num >= 0 && num < RefreshIntervalTicks) { return _lastSnapshot; } bool isHost; string reason; LobbySafetyAccessState localAccessState = SteamIdModerationController.GetLocalAccessState(out isHost, out reason); PlayerIdentityEvidenceController.GetVerifiedRoster(out var status); IdentityResyncCoverage cachedIdentityResyncCoverage = PlayerIdentityEvidenceController.GetCachedIdentityResyncCoverage(); PlayerLimitCompanionStatus playerLimitCompanionStatus = Plugin.GetPlayerLimitCompanionStatus(); LastVerifiedRosterCount = cachedIdentityResyncCoverage.VerifiedRows; LastRosterSummary = status; LastPlayerLimitSummary = playerLimitCompanionStatus.ToDiagnosticText(); bool playerLimitHealthy = !playerLimitCompanionStatus.IsLoaded || (playerLimitCompanionStatus.EffectiveMaxLobbySize.GetValueOrDefault() > 0 && playerLimitCompanionStatus.EffectiveDefaultLobbySize.GetValueOrDefault() > 0 && playerLimitCompanionStatus.EffectiveShiftSkipRate.GetValueOrDefault() > 0 && playerLimitCompanionStatus.ChatRelayPatchEnabled.HasValue); bool flag = BlueSageExtensionRegistry.HasLiveHostCapabilityProvider(); long num2 = Interlocked.Increment(ref _capabilityNonce); if (num2 <= 0) { Interlocked.Exchange(ref _capabilityNonce, 1L); num2 = 1L; } BlueSageHostCapabilitySnapshotV1 snapshot = default(BlueSageHostCapabilitySnapshotV1); HasCapabilitySnapshot = flag && BlueSageExtensionRegistry.TryReadHostCapability(isHost, num2, ticks, out snapshot); LastCapabilitySnapshot = (HasCapabilitySnapshot ? snapshot : default(BlueSageHostCapabilitySnapshotV1)); _lastSnapshot = HostConsoleReadinessPolicy.Resolve(localAccessState == LobbySafetyAccessState.Host && isHost, cachedIdentityResyncCoverage.Ready, Plugin.HostCleanupLaneHealthy, Plugin.HostReconnectLaneHealthy, playerLimitHealthy, flag, HasCapabilitySnapshot && snapshot.State == BlueSageHostCapabilityStateV1.Ready, relayRequired: false, relayHealthy: false); _lastCaptureUtcTicks = ticks; return _lastSnapshot; } internal static bool TryExecuteCapability(BlueSageHostCapabilityActionV1 action, out string message) { bool isHost; string reason; LobbySafetyAccessState localAccessState = SteamIdModerationController.GetLocalAccessState(out isHost, out reason); long ticks = DateTime.UtcNow.Ticks; long num = Interlocked.Increment(ref _capabilityNonce); if (localAccessState != LobbySafetyAccessState.Host || !isHost || num <= 0 || !BlueSageExtensionRegistry.TryExecuteHostCapability(action, isHost: true, num, ticks, out var receipt)) { message = "Private host action unavailable; live host and provider checks failed closed."; return false; } message = receipt.Message; _lastCaptureUtcTicks = 0L; return receipt.Succeeded; } internal static string AuditNativeModerationUi() { Type? typeFromHandle = typeof(PlayerItemController); bool value = typeFromHandle.GetMethod("ButtonMutePlayer", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) != null; bool value2 = typeFromHandle.GetMethod("ButtonIgnorePlayer", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) != null; bool value3 = typeFromHandle.GetMethod("ButtonReport", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) != null; bool value4 = typeFromHandle.GetMethod("ButtonBan", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) != null; return "Native moderation UI: mute=" + OnOff(value) + ", ignore=" + OnOff(value2) + ", report=" + OnOff(value3) + ", host remove=" + OnOff(value4) + ". Audit only; no moderation action fired."; } private static string OnOff(bool value) { if (!value) { return "unavailable"; } return "ready"; } } internal static class IdentityResyncController { private const long RuntimePollIntervalMilliseconds = 500L; private const long RequestPollIntervalMilliseconds = 500L; private const int MaximumRememberedNonces = 128; private static readonly IdentityResyncCoordinator Coordinator = new IdentityResyncCoordinator(); private static readonly HashSet SeenNonces = new HashSet(StringComparer.Ordinal); private static readonly Queue NonceOrder = new Queue(); private static IdentityResyncRuntimeShape _lastShape; private static bool _hasLastShape; private static long _nextRuntimePollMilliseconds; private static long _nextRequestPollMilliseconds; private static string _nonceScope = string.Empty; private static string _lastTransaction = "none"; internal static void Tick() { long num = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); if (num >= _nextRuntimePollMilliseconds) { _nextRuntimePollMilliseconds = num + 500; ObserveRuntimeShape(num); } if (num >= _nextRequestPollMilliseconds) { _nextRequestPollMilliseconds = num + 500; PollHostRequests(num); } } internal static BlueSageCommandResult HandleCommand(BlueSageCommandContext context) { long nowMilliseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); ObserveRuntimeShape(nowMilliseconds); bool isHost; string reason; LobbySafetyAccessState localAccessState = SteamIdModerationController.GetLocalAccessState(out isHost, out reason); IdentityResyncPlan identityResyncPlan = IdentityResyncPlanPolicy.Plan(context?.CommandName ?? "idsync", context?.Arguments ?? string.Empty, MapAccess(localAccessState)); if (!identityResyncPlan.Allowed) { return BlueSageCommandResult.HandledWithMessages(identityResyncPlan.Reason, BuildStatus()); } return identityResyncPlan.Action switch { IdentityResyncAction.Status => BlueSageCommandResult.HandledWithMessages(BuildStatus(), "Use /idsync refresh for local BlueSage presentation, verified Host /idsync repair for one exact pass, or authenticated Helper /idsync request for Host relay."), IdentityResyncAction.RefreshLocalSurfaces => RefreshLocalSurfaces(), IdentityResyncAction.RepairIncompleteRows => ExecuteRepairFromCommand(nowMilliseconds), IdentityResyncAction.PublishHostRequest => PublishHelperRequest(nowMilliseconds), _ => BlueSageCommandResult.HandledWithMessages(identityResyncPlan.Reason), }; } internal static string StatusText() { ObserveRuntimeShape(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); return BuildStatus(); } internal static string ExecuteMenuAction(string action) { string text = (action ?? string.Empty).Trim().ToLowerInvariant(); BlueSageCommandResult blueSageCommandResult = HandleCommand(new BlueSageCommandContext("/idsync " + text, "idsync", text)); return string.Join(" ", blueSageCommandResult.Messages.ToArray()); } internal static void Reset() { Coordinator.Reset(); SeenNonces.Clear(); NonceOrder.Clear(); _lastShape = default(IdentityResyncRuntimeShape); _hasLastShape = false; _nextRuntimePollMilliseconds = 0L; _nextRequestPollMilliseconds = 0L; _nonceScope = string.Empty; _lastTransaction = "none"; } private static BlueSageCommandResult RefreshLocalSurfaces() { IReadOnlyList cachedIdentityResyncSurfaceTargets = PlayerIdentityEvidenceController.GetCachedIdentityResyncSurfaceTargets(); if (cachedIdentityResyncSurfaceTargets.Count == 0) { return BlueSageCommandResult.HandledWithMessages("Identity refresh made no change because no already-available native row state exists. It did not force a Steam/PurrNet snapshot.", BuildStatus()); } PlayerIdentityRoleHighlightController.RefreshRowsFromAlreadyAvailableSnapshot(cachedIdentityResyncSurfaceTargets); _lastTransaction = "local-refresh: rows=" + cachedIdentityResyncSurfaceTargets.Count; return BlueSageCommandResult.HandledWithMessages("Identity refresh rebuilt " + cachedIdentityResyncSurfaceTargets.Count + " BlueSage-owned local row surface(s) from already-available native state. No authoritative capture, name write, list clear, moderation action, or host request ran.", BuildStatus()); } private static BlueSageCommandResult ExecuteRepairFromCommand(long nowMilliseconds) { if (!TryGetCurrentHostScope(out var _, out var _, out var scope)) { return BlueSageCommandResult.HandledWithMessages("Identity repair stopped because this client is not the exact current Steam lobby owner and active PurrNet Host."); } long currentGeneration = Coordinator.CurrentGeneration; TryExecuteRepair("host-command", scope, currentGeneration, nowMilliseconds, string.Empty, string.Empty, string.Empty, out var result); return BlueSageCommandResult.HandledWithMessages(result, BuildStatus()); } private static BlueSageCommandResult PublishHelperRequest(long nowMilliseconds) { //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_00cf: Unknown result type (might be due to invalid IL or missing references) if (SteamIdModerationController.GetLocalAccessState(out var _, out var _) != LobbySafetyAccessState.HelperReady || !SteamIdModerationController.TryGetFreshCurrentCapability(out var lobby, out var ownerSteamId, out var capability)) { return BlueSageCommandResult.HandledWithMessages("Identity repair request was not published because current Helper authentication is not ready."); } string text = IdentityResyncRequestProtocol.FingerprintScope(((ulong)lobby).ToString(), ownerSteamId); long currentGeneration = Coordinator.CurrentGeneration; if (!string.Equals(text, Coordinator.CurrentScope, StringComparison.Ordinal) || currentGeneration <= 0) { return BlueSageCommandResult.HandledWithMessages("Identity repair request was not published because the current lobby generation is not stable yet."); } string nonce = Guid.NewGuid().ToString("N").Substring(0, 16); if (!IdentityResyncRequestProtocol.TryBuild(text, currentGeneration, IdentityResyncRequestProtocol.FingerprintCapability(capability), DateTimeOffset.UtcNow.ToUnixTimeSeconds(), nonce, out var payload, out var error)) { return BlueSageCommandResult.HandledWithMessages(error); } try { SteamMatchmaking.SetLobbyMemberData(lobby, "bluesage_idsync_request_v1", payload); _lastTransaction = "helper-request-published: generation=" + currentGeneration; return BlueSageCommandResult.HandledWithMessages("Identity repair request published through authenticated non-chat member data. The current Host will revalidate this Helper, lobby, owner, capability, generation, nonce, and exact row before one bounded repair."); } catch (Exception ex) { return BlueSageCommandResult.HandledWithMessages("Identity repair request failed safely (" + ex.GetType().Name + "); no repair or identity mutation ran."); } } private static void PollHostRequests(long nowMilliseconds) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_0075: 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_0097: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_00fe: Unknown result type (might be due to invalid IL or missing references) if (!TryGetCurrentHostScope(out var lobby, out var ownerSteamId, out var scope) || !SteamIdModerationController.TryGetFreshCurrentCapability(out var lobby2, out var ownerSteamId2, out var capability) || lobby2 != lobby || !string.Equals(ownerSteamId2, ownerSteamId, StringComparison.Ordinal) || !string.Equals(scope, Coordinator.CurrentScope, StringComparison.Ordinal)) { return; } ReconcileNonceScope(scope); long currentGeneration = Coordinator.CurrentGeneration; string text = IdentityResyncRequestProtocol.FingerprintCapability(capability); int numLobbyMembers = SteamMatchmaking.GetNumLobbyMembers(lobby); for (int i = 0; i < numLobbyMembers; i++) { CSteamID lobbyMemberByIndex = SteamMatchmaking.GetLobbyMemberByIndex(lobby, i); string text2 = ((ulong)lobbyMemberByIndex).ToString(); if (!string.Equals(text2, ownerSteamId, StringComparison.Ordinal) && IdentityResyncRequestProtocol.TryParse(SteamMatchmaking.GetLobbyMemberData(lobby, lobbyMemberByIndex, "bluesage_idsync_request_v1") ?? string.Empty, out var request, out var _) && IdentityResyncRequestProtocol.IsFresh(request, DateTimeOffset.UtcNow.ToUnixTimeSeconds()) && string.Equals(request.LobbyFingerprint, scope, StringComparison.Ordinal) && request.Generation == currentGeneration && string.Equals(request.CapabilityFingerprint, text, StringComparison.Ordinal) && SteamIdModerationController.IsUniqueCurrentLobbyMember(lobby, text2) && SteamIdModerationController.IsAuthorizedCurrentIssuer(text2) && !IsNonceRemembered(request.Nonce)) { TryExecuteRepair("helper-request", scope, currentGeneration, nowMilliseconds, text2, text, request.Nonce, out var result); Plugin.AddLocalNotification(result); } } } private static bool TryExecuteRepair(string source, string expectedScope, long expectedGeneration, long nowMilliseconds, string helperSenderSteamId, string expectedCapabilityFingerprint, string requestNonce, out string result) { //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00ca: Unknown result type (might be due to invalid IL or missing references) //IL_011b: Unknown result type (might be due to invalid IL or missing references) //IL_011d: Unknown result type (might be due to invalid IL or missing references) //IL_0141: Unknown result type (might be due to invalid IL or missing references) result = "Identity repair did not run."; if (!TryGetCurrentHostScope(out var lobby, out var ownerSteamId, out var scope) || !string.Equals(expectedScope, scope, StringComparison.Ordinal) || expectedGeneration != Coordinator.CurrentGeneration) { result = "Identity repair stopped because the current Host, lobby, owner, or generation changed."; return false; } if (!Coordinator.TryBegin(expectedScope, expectedGeneration, nowMilliseconds, out var lease, out var decision)) { result = FormatBeginRefusal(decision); return false; } if (!string.IsNullOrWhiteSpace(requestNonce) && !RememberNonce(requestNonce)) { Coordinator.Abandon(lease); result = "Identity repair request was already consumed. No capture or row refresh ran."; return false; } Stopwatch stopwatch = Stopwatch.StartNew(); int targetedRows = 0; int healedRowCount = 0; bool capturePerformed = false; string text = "refused"; IdentityResyncCoverage cachedIdentityResyncCoverage = PlayerIdentityEvidenceController.GetCachedIdentityResyncCoverage(); try { PlayerIdentityEvidenceController.ForceRefreshIncompleteRosterRowsOnce(out var _, out var after, out var _, out var targetedSteamIds, out healedRowCount, out capturePerformed); targetedRows = targetedSteamIds.Count; cachedIdentityResyncCoverage = PlayerIdentityEvidenceController.GetCachedIdentityResyncCoverage(); if (!TryGetCurrentHostScope(out var lobby2, out var ownerSteamId2, out var scope2) || lobby2 != lobby || !string.Equals(ownerSteamId2, ownerSteamId, StringComparison.Ordinal) || !string.Equals(scope2, expectedScope, StringComparison.Ordinal) || expectedGeneration != Coordinator.CurrentGeneration) { result = "Identity repair stopped after capture because current Host/lobby ownership or generation migrated. No row surface was refreshed."; return false; } if (!string.IsNullOrWhiteSpace(helperSenderSteamId) && (!SteamIdModerationController.TryGetFreshCurrentCapability(out var lobby3, out var ownerSteamId3, out var capability) || lobby3 != lobby || !string.Equals(ownerSteamId3, ownerSteamId, StringComparison.Ordinal) || !string.Equals(IdentityResyncRequestProtocol.FingerprintCapability(capability), expectedCapabilityFingerprint, StringComparison.Ordinal) || !SteamIdModerationController.IsUniqueCurrentLobbyMember(lobby, helperSenderSteamId) || !SteamIdModerationController.IsAuthorizedCurrentIssuer(helperSenderSteamId) || !PlayerIdentityEvidenceController.TryResolveExactCached(helperSenderSteamId, out var _, out after))) { result = "Identity repair request failed final exact Helper/lobby revalidation. No row surface was refreshed."; return false; } PlayerIdentityRoleHighlightController.RefreshRows(targetedSteamIds); text = (cachedIdentityResyncCoverage.Ready ? "ready" : "locked"); result = "Identity repair completed one bounded exact-correlation transaction: targetedRows=" + targetedRows + ", healedRows=" + healedRowCount + ", lockedRows=" + cachedIdentityResyncCoverage.LockedRows + ", capturePerformed=" + capturePerformed + ". " + (cachedIdentityResyncCoverage.Ready ? "Coverage is READY." : ("Unresolved or conflicting rows remain fail-closed: " + cachedIdentityResyncCoverage.Reason + ".")); return true; } catch (Exception ex) { text = "failed-" + ex.GetType().Name; result = "Identity repair failed safely (" + ex.GetType().Name + "); exact-row locks remain in force."; return false; } finally { stopwatch.Stop(); Coordinator.Complete(lease, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); _lastTransaction = source + ": outcome=" + text + ", targetedRows=" + targetedRows + ", healedRows=" + healedRowCount + ", lockedRows=" + cachedIdentityResyncCoverage.LockedRows + ", durationMs=" + stopwatch.ElapsedMilliseconds; string text2 = SafeReason(cachedIdentityResyncCoverage.Reason); ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)("[BlueSageIdentityResync] source=" + source + "; outcome=" + text + "; targetedRows=" + targetedRows + "; healedRows=" + healedRowCount + "; lockedRows=" + cachedIdentityResyncCoverage.LockedRows + "; capturePerformed=" + capturePerformed + "; durationMs=" + stopwatch.ElapsedMilliseconds + "; reason=" + text2 + ".")); } SessionAuditController.RecordIdentityResync(source, text, targetedRows, healedRowCount, cachedIdentityResyncCoverage.LockedRows, stopwatch.ElapsedMilliseconds, text2); } } private static void ObserveRuntimeShape(long nowMilliseconds) { if (!TryCaptureRuntimeShape(out var shape)) { Coordinator.ObserveScope(string.Empty, string.Empty, nowMilliseconds); _lastShape = default(IdentityResyncRuntimeShape); _hasLastShape = false; return; } IdentityResyncTrigger identityResyncTrigger = (_hasLastShape ? IdentityResyncTriggerPolicy.Classify(_lastShape, shape) : IdentityResyncTrigger.LobbyOrOwnerIdentityChanged); if (identityResyncTrigger == IdentityResyncTrigger.None) { Coordinator.ObserveScope(shape.LobbyFingerprint, shape.OwnerFingerprint, nowMilliseconds); } else { Coordinator.ObserveTrigger(shape.LobbyFingerprint, shape.OwnerFingerprint, identityResyncTrigger, nowMilliseconds); } _lastShape = shape; _hasLastShape = true; } private static bool TryCaptureRuntimeShape(out IdentityResyncRuntimeShape shape) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_0058: 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_018f: Unknown result type (might be due to invalid IL or missing references) shape = default(IdentityResyncRuntimeShape); try { if (!SteamManager.Initialized || !Plugin.TryGetRescueLobby(out var lobbyId) || lobbyId == CSteamID.Nil) { return false; } CSteamID lobbyOwner = SteamMatchmaking.GetLobbyOwner(lobbyId); if (lobbyOwner == CSteamID.Nil) { return false; } string lobbyIdentity = ((ulong)lobbyId).ToString(); string text = ((ulong)lobbyOwner).ToString(); PlayerPanelController i = NetworkSingleton.I; int panelToken = (((Object)(object)i != (Object)null) ? ((Object)i).GetInstanceID() : 0); int num = 0; long num2 = 1469598103934665603L; if (((i != null) ? i.PlayerItemControllers : null) != null) { foreach (PlayerItemController playerItemController in i.PlayerItemControllers) { if (!((Object)(object)playerItemController == (Object)null) && !((Object)(object)((Component)playerItemController).gameObject == (Object)null) && ((Component)playerItemController).gameObject.activeSelf) { num++; num2 = Mix(num2, ((Object)playerItemController).GetInstanceID()); } } } if (i?.PlayerSteamIDs != null) { foreach (string playerSteamID in i.PlayerSteamIDs) { num2 = Mix(num2, playerSteamID ?? string.Empty); } } if (i?.PlayerIDs != null) { foreach (PlayerID playerID in i.PlayerIDs) { num2 = Mix(num2, ((object)playerID/*cast due to .constrained prefix*/).GetHashCode()); } } if (i?.PlayerTransforms != null) { foreach (NetworkTransform playerTransform in i.PlayerTransforms) { num2 = Mix(num2, ((Object)(object)playerTransform != (Object)null) ? ((Object)playerTransform).GetInstanceID() : 0); } } shape = new IdentityResyncRuntimeShape(IdentityResyncRequestProtocol.FingerprintScope(lobbyIdentity, text), IdentityResyncRequestProtocol.FingerprintCapability("owner|" + text), panelToken, num, num2); return true; } catch { return false; } } private static bool TryGetCurrentHostScope(out CSteamID lobby, out string ownerSteamId, out string scope) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) scope = string.Empty; if (!SteamIdModerationController.TryGetOwnedHostLobby(out lobby, out ownerSteamId)) { return false; } scope = IdentityResyncRequestProtocol.FingerprintScope(((ulong)lobby).ToString(), ownerSteamId); return true; } private static void ReconcileNonceScope(string scope) { if (!string.Equals(_nonceScope, scope, StringComparison.Ordinal)) { _nonceScope = scope; SeenNonces.Clear(); NonceOrder.Clear(); } } private static bool RememberNonce(string nonce) { if (!SeenNonces.Add(nonce)) { return false; } NonceOrder.Enqueue(nonce); while (NonceOrder.Count > 128) { SeenNonces.Remove(NonceOrder.Dequeue()); } return true; } private static bool IsNonceRemembered(string nonce) { return SeenNonces.Contains(nonce ?? string.Empty); } private static string BuildStatus() { IdentityResyncCoverage cachedIdentityResyncCoverage = PlayerIdentityEvidenceController.GetCachedIdentityResyncCoverage(); return "Identity resync status: coverage=" + (cachedIdentityResyncCoverage.Ready ? "READY" : "LOCKED") + "; verifiedRows=" + cachedIdentityResyncCoverage.VerifiedRows + "; nativeRows=" + cachedIdentityResyncCoverage.NativeRows + "; purrNetRows=" + cachedIdentityResyncCoverage.PurrNetRows + "; steamMembers=" + cachedIdentityResyncCoverage.SteamMembers + "; lockedRows=" + cachedIdentityResyncCoverage.LockedRows + "; generation=" + Coordinator.CurrentGeneration + "; inFlight=" + Coordinator.IsInFlight + "; why=" + SafeReason(cachedIdentityResyncCoverage.Reason) + "; lastTransaction=" + _lastTransaction + "."; } private static string FormatBeginRefusal(IdentityResyncBeginDecision decision) { if (decision.Reason == IdentityResyncBeginReason.QuietWindow) { return "Identity repair is waiting for the 500 ms generation quiet window (" + decision.RetryAfterMilliseconds + " ms remain). No repair ran."; } if (decision.Reason == IdentityResyncBeginReason.GlobalCooldown) { return "Identity repair is globally throttled for " + Math.Max(1L, (decision.RetryAfterMilliseconds + 999) / 1000) + " more second(s). No repair ran."; } return "Identity repair was refused before capture: " + decision.Reason.ToString() + "."; } private static IdentityResyncAccess MapAccess(LobbySafetyAccessState state) { return state switch { LobbySafetyAccessState.Host => IdentityResyncAccess.Host, LobbySafetyAccessState.HelperReady => IdentityResyncAccess.HelperReady, LobbySafetyAccessState.HelperWaitingForHost => IdentityResyncAccess.HelperWaitingForHost, _ => IdentityResyncAccess.Ordinary, }; } private static string SafeReason(string value) { string text = (string.IsNullOrWhiteSpace(value) ? "none" : value.Replace("\r", " ").Replace("\n", " ").Replace(";", ",") .Trim()); if (text.Length > 160) { return text.Substring(0, 160); } return text; } private static long Mix(long hash, int value) { return (hash ^ value) * 1099511628211L; } private static long Mix(long hash, string value) { string text = value ?? string.Empty; for (int i = 0; i < text.Length; i++) { hash = (hash ^ text[i]) * 1099511628211L; } return hash; } } internal sealed class MentionPingController : MonoBehaviour { private const float AutocompleteCooldownSeconds = 0.15f; private const int MaxSuggestions = 5; private float _lastAutocompleteAt = -1f; private IReadOnlyList _suggestions = Array.Empty(); private int _mentionStart = -1; private int _mentionCaret = -1; private string _mentionQuery = string.Empty; private string _lastObservedInputText = string.Empty; private int _lastObservedCaret = -1; private GUIStyle _popupStyle; private GUIStyle _popupHeaderStyle; private GUIStyle _popupButtonStyle; private string _lastThemeKey = string.Empty; private int _renderedTextureGeneration; private bool _lastLobbyPingAccess; private void Update() { if (Plugin.EnablePingMentions == null || !Plugin.EnablePingMentions.Value) { ClearSuggestions(); _lastObservedInputText = string.Empty; _lastObservedCaret = -1; return; } TMP_InputField chatInput = GetChatInput(); if ((Object)(object)chatInput == (Object)null || !chatInput.isFocused) { ClearSuggestions(); _lastObservedInputText = string.Empty; _lastObservedCaret = -1; return; } string text = chatInput.text ?? string.Empty; int caretPosition = chatInput.caretPosition; bool isHost; bool flag = Plugin.CanUseLobbySafety(out isHost); if (!string.Equals(text, _lastObservedInputText, StringComparison.Ordinal) || caretPosition != _lastObservedCaret || flag != _lastLobbyPingAccess) { _lastObservedInputText = text; _lastObservedCaret = caretPosition; _lastLobbyPingAccess = flag; RefreshSuggestions(chatInput); } if (Input.GetKeyDown((KeyCode)9) && !(Time.unscaledTime - _lastAutocompleteAt < 0.15f) && TryAutocomplete(chatInput, (_suggestions.Count > 0) ? _suggestions[0] : string.Empty)) { _lastAutocompleteAt = Time.unscaledTime; } } private void OnGUI() { //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00d8: Expected O, but got Unknown if (_suggestions == null || _suggestions.Count == 0) { return; } EnsurePopupStyles(); float num = 340f; float num2 = 44f + (float)_suggestions.Count * 30f; GUILayout.BeginArea(new Rect(36f, Mathf.Max(36f, (float)Screen.height - num2 - 118f), num, num2), _popupStyle); GUILayout.Label("BlueSage @ mention suggestions - Tab completes the first match", _popupHeaderStyle, Array.Empty()); for (int i = 0; i < _suggestions.Count; i++) { string text = _suggestions[i]; if (GUILayout.Button(new GUIContent(text.Equals("lobby", StringComparison.OrdinalIgnoreCase) ? "@lobby - ping everyone using compatible ping mods" : ("@" + text), "Click or press Tab to complete the mention."), _popupButtonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(26f) })) { TMP_InputField chatInput = GetChatInput(); if ((Object)(object)chatInput != (Object)null) { TryAutocomplete(chatInput, text); } } } GUILayout.EndArea(); } public static void PlayConfiguredPing() { if (Plugin.EnablePingSound == null || !Plugin.EnablePingSound.Value) { return; } try { SFXManager i = MonoSingleton.I; if ((Object)(object)i != (Object)null) { switch (Plugin.GetPingSoundMode()) { case "change": i.PlayUIChange(); break; case "error": i.PlayUIError(); break; case "task": i.PlayTaskComplete(); break; case "ticket": i.PlayEarnTicket(); break; default: i.PlayUIClick(); break; } } } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("Ping Mentions sound skipped safely: " + ex.GetType().Name + ": " + ex.Message)); } } } private static TMP_InputField GetChatInput() { try { UIManager i = MonoSingleton.I; return (i != null) ? i.MessageInput : null; } catch { return null; } } private void RefreshSuggestions(TMP_InputField input) { if (!TryFindMentionContext(input, out var atIndex, out var caret, out var query)) { ClearSuggestions(); return; } _mentionStart = atIndex; _mentionCaret = caret; _mentionQuery = query; _suggestions = PlayerMentionProvider.FindMentionMatches(query, 5); } private static bool TryAutocomplete(TMP_InputField input, string preferredMatch) { if (!TryFindMentionContext(input, out var atIndex, out var caret, out var query)) { return false; } string text = (string.IsNullOrWhiteSpace(preferredMatch) ? PlayerMentionProvider.FindBestMentionMatch(query) : preferredMatch); if (string.IsNullOrWhiteSpace(text)) { return false; } if (string.Equals(text, "lobby", StringComparison.OrdinalIgnoreCase) && !Plugin.CanUseLobbySafety(out var _)) { return false; } string text2 = MentionNamePolicy.ToMentionInsertionToken(text); if (string.IsNullOrWhiteSpace(text2)) { return false; } string obj = input.text ?? string.Empty; string text3 = obj.Substring(0, atIndex); string text4 = obj.Substring(caret); string text5 = "@" + text2 + " "; input.text = text3 + text5 + text4; input.caretPosition = text3.Length + text5.Length; ((Selectable)input).Select(); input.ActivateInputField(); return true; } private static bool TryFindMentionContext(TMP_InputField input, out int atIndex, out int caret, out string query) { atIndex = -1; caret = -1; query = string.Empty; string text = input.text ?? string.Empty; caret = Math.Max(0, Math.Min(input.caretPosition, text.Length)); if (caret <= 0) { return false; } atIndex = text.LastIndexOf('@', caret - 1); if (atIndex < 0) { return false; } if (atIndex > 0 && !char.IsWhiteSpace(text[atIndex - 1])) { return false; } query = text.Substring(atIndex + 1, caret - atIndex - 1); if (!string.IsNullOrWhiteSpace(query)) { return query.IndexOfAny(new char[4] { ' ', '\t', '\r', '\n' }) < 0; } return false; } private void ClearSuggestions() { _suggestions = Array.Empty(); _mentionStart = -1; _mentionCaret = -1; _mentionQuery = string.Empty; } private void EnsurePopupStyles() { //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Expected O, but got Unknown //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Expected O, but got Unknown //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00b0: 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_00c6: Expected O, but got Unknown BlueSageUiThemePalette current = BlueSageUiTheme.Current; if (_popupStyle == null || !string.Equals(_lastThemeKey, current.Key, StringComparison.Ordinal) || _renderedTextureGeneration != BlueSageUiTheme.RuntimeTextureGeneration || !BlueSageUiTheme.AreRuntimeTexturesAlive()) { _lastThemeKey = current.Key; GUIStyle val = new GUIStyle(GUI.skin.box) { padding = new RectOffset(10, 10, 8, 10) }; val.normal.background = BlueSageUiTheme.GetSolidTexture(current.PreviewBackground); val.normal.textColor = current.PreviewText; _popupStyle = val; GUIStyle val2 = new GUIStyle(GUI.skin.label) { fontSize = 12, fontStyle = (FontStyle)1 }; val2.normal.textColor = current.PopupHeaderText; _popupHeaderStyle = val2; _popupButtonStyle = BlueSageUiTheme.CreateNeutralButtonStyle(GUI.skin.button, current, 13); _popupButtonStyle.alignment = (TextAnchor)3; _renderedTextureGeneration = BlueSageUiTheme.RuntimeTextureGeneration; } } } internal static class NativeBanStoreAdapter { internal static bool TryApplyAdditions(CommunityBanAddition[] additions, out int existingCount, out string error) { existingCount = 0; error = string.Empty; DataManager i = MonoSingleton.I; BanData val = i?.BanData; if (val?.BanServerPlayers == null || val.BanServerPlayerNicks == null) { error = "native paired storage is unavailable"; return false; } if (val.BanServerPlayers.Count != val.BanServerPlayerNicks.Count) { error = "native paired storage is inconsistent"; return false; } existingCount = val.BanServerPlayers.Count; int num = 0; try { CommunityBanAddition[] array = additions ?? Array.Empty(); foreach (CommunityBanAddition communityBanAddition in array) { if (communityBanAddition == null) { throw new InvalidOperationException("validated addition was unavailable"); } val.BanServerPlayers.Add(communityBanAddition.SteamId); val.BanServerPlayerNicks.Add(communityBanAddition.Nickname); num++; } if (num > 0) { i.SaveBanData(); } return true; } catch (Exception ex) { string text; try { Rollback(val, existingCount); i.SaveBanData(); text = "persisted"; } catch (Exception ex2) { Rollback(val, existingCount); text = "memory-only-unresolved-" + ex2.GetType().Name; } error = ex.GetType().Name + "; rolledBack=" + num + "; rollback=" + text; return false; } } private static void Rollback(BanData bans, int existingCount) { while (bans.BanServerPlayers.Count > existingCount) { bans.BanServerPlayers.RemoveAt(bans.BanServerPlayers.Count - 1); } while (bans.BanServerPlayerNicks.Count > existingCount) { bans.BanServerPlayerNicks.RemoveAt(bans.BanServerPlayerNicks.Count - 1); } } } internal sealed class NativeIdCardTarget { internal PlayerPanelController Panel; internal PlayerItemController Row; internal NetworkTransform NetworkTransform; internal PlayerController PlayerController; internal PlayerIDInfo PlayerInfo; internal string SteamId = string.Empty; internal string PlayerId = string.Empty; internal string DisplayNameRaw = string.Empty; internal int PlayerIndex = -1; } internal static class NativeIdCardController { private static readonly FieldInfo PlayerSteamIdField = AccessTools.Field(typeof(PlayerItemController), "_playerSteamId"); internal static bool TryOpen(string source, string requestedSteamId, int observedIndex, string expectedPlayerId, PlayerItemController expectedRow, NetworkTransform expectedTransform, bool requireObservedIndex, NativeIdCardCallerKind callerKind = NativeIdCardCallerKind.OrdinaryUser) { if (!TryResolveCurrentRow(source, requestedSteamId, observedIndex, expectedPlayerId, expectedRow, expectedTransform, requireObservedIndex, callerKind, out var target, out var _)) { return false; } TemporaryPhoto temporaryPhoto = MonoSingleton.I; if ((Object)(object)temporaryPhoto == (Object)null) { LogOutcome(source, NativeIdCardResolutionState.FreshEvidenceUnavailable, observedIndex, requestedSteamId, "the native ID-card controller is unavailable"); return false; } PlayerPanelController currentPanel = NetworkSingleton.I; int playerIndex = target.PlayerIndex; NativeIdCardInvocationState nativeIdCardInvocationState = NativeIdCardInvocationGuard.Invoke(() => IsFinalTupleStable(target), delegate { PlayerPanelController obj = currentPanel; if (obj != null) { obj.ButtonClosePanel(); } }, delegate { TryOpenExactPlayerCard(temporaryPhoto, target); }); if (nativeIdCardInvocationState != NativeIdCardInvocationState.Opened) { LogOutcome(source, NativeIdCardResolutionState.FinalTupleChanged, observedIndex, requestedSteamId, (nativeIdCardInvocationState == NativeIdCardInvocationState.TupleChangedAfterPanelClose) ? "the exact native identity tuple changed during panel close" : "the exact native identity tuple changed before panel close"); return false; } LogOutcome(source, NativeIdCardResolutionState.Ready, playerIndex, requestedSteamId, "native exact-controller ID card opened"); return true; } internal static bool TryResolveCurrentRow(string source, string requestedSteamId, int observedIndex, string expectedPlayerId, PlayerItemController expectedRow, NetworkTransform expectedTransform, bool requireObservedIndex, NativeIdCardCallerKind callerKind, out NativeIdCardTarget target, out NativeIdCardResolutionDecision decision) { //IL_00ea: Unknown result type (might be due to invalid IL or missing references) //IL_00ef: Unknown result type (might be due to invalid IL or missing references) //IL_00f3: Unknown result type (might be due to invalid IL or missing references) //IL_01aa: Unknown result type (might be due to invalid IL or missing references) //IL_01b0: Unknown result type (might be due to invalid IL or missing references) //IL_01c1: Unknown result type (might be due to invalid IL or missing references) //IL_01bc: Unknown result type (might be due to invalid IL or missing references) //IL_02ba: Unknown result type (might be due to invalid IL or missing references) //IL_0348: Unknown result type (might be due to invalid IL or missing references) //IL_034a: Unknown result type (might be due to invalid IL or missing references) //IL_034f: Unknown result type (might be due to invalid IL or missing references) target = null; PlayerPanelController i = NetworkSingleton.I; PlayerIdentityEvidence evidence; string error; bool flag = PlayerIdentityEvidenceController.TryResolveExact(requestedSteamId, out evidence, out error); bool flag2 = AreNativeListsAligned(i); int num = ((i == null) ? ((int?)null) : i.PlayerItemControllers?.IndexOf(expectedRow)) ?? (-1); int expectedIndex = (requireObservedIndex ? observedIndex : num); PlayerItemController val = ((flag2 && num >= 0 && num < i.PlayerItemControllers.Count) ? i.PlayerItemControllers[num] : null); string currentRowSteamId = (((Object)(object)val != (Object)null) ? ((PlayerSteamIdField?.GetValue(val) as string) ?? string.Empty) : string.Empty); string text; if (!flag2 || num < 0 || num >= i.PlayerIDs.Count) { text = string.Empty; } else { PlayerID val2 = i.PlayerIDs[num]; text = PurrNetPlayerIdPolicy.Format(PackedULong.op_Implicit(((PlayerID)(ref val2)).id)); } string text2 = text; string expectedPlayerId2 = (requireObservedIndex ? (expectedPlayerId ?? string.Empty) : text2); int num2 = (flag ? evidence.RosterIndex : (-1)); NetworkTransform val3 = ((flag2 && num2 >= 0 && num2 < i.PlayerTransforms.Count) ? i.PlayerTransforms[num2] : null); PlayerController val4 = ((flag2 && num2 >= 0 && num2 < i.PlayerControllers.Count) ? i.PlayerControllers[num2] : null); NetworkTransform val5 = (((Object)(object)val4 != (Object)null) ? ((Component)val4).GetComponent() : null); PlayerIDInfo val6 = (PlayerIDInfo)((flag2 && num2 >= 0 && num2 < i.IDInfos.Count) ? i.IDInfos[num2] : default(PlayerIDInfo)); NativeIdCardResolutionContext context = new NativeIdCardResolutionContext { CallerKind = callerKind, RequestedSteamId = (requestedSteamId ?? string.Empty), FreshResolutionSucceeded = flag, FreshResolvedSteamId = (flag ? evidence.SteamId : string.Empty), FreshResolvedPlayerId = (flag ? evidence.NetworkPlayerId : string.Empty), ExpectedPlayerId = expectedPlayerId2, FreshResolvedIndex = num2, ExpectedIndex = expectedIndex, CurrentRowIndex = num, CurrentRowReferenceMatches = ((Object)(object)val != (Object)null && (Object)(object)expectedRow != (Object)null && val == expectedRow), CurrentRowSteamId = currentRowSteamId, CurrentRowPlayerId = text2, HasLiveNetworkTransform = ((Object)(object)val3 != (Object)null), NativeListsAligned = flag2, ExpectedNetworkTransformMatches = (!requireObservedIndex || ((Object)(object)expectedTransform != (Object)null && val3 == expectedTransform)), FinalTupleStable = (flag && (Object)(object)val4 != (Object)null && IsCurrentTupleStable(i, num2, requestedSteamId, evidence.NetworkPlayerId, expectedRow, val3, val4, val6) && (Object)(object)val5 != (Object)null && val5 == val3) }; decision = NativeIdCardResolutionPolicy.Evaluate(context); if (!decision.MayOpenNativeCard) { string detail = (string.IsNullOrWhiteSpace(decision.Reason) ? error : decision.Reason); LogOutcome(source, decision.State, observedIndex, requestedSteamId, detail); return false; } target = new NativeIdCardTarget { Panel = i, Row = val, NetworkTransform = val3, PlayerController = val4, PlayerInfo = SnapshotPlayerIdInfo(val6), SteamId = evidence.SteamId, PlayerId = evidence.NetworkPlayerId, DisplayNameRaw = evidence.DisplayNameRaw, PlayerIndex = decision.PlayerIndex }; return true; } private static bool AreNativeListsAligned(PlayerPanelController panel) { if ((Object)(object)panel == (Object)null || panel.PlayerSteamIDs == null || panel.PlayerIDs == null || panel.PlayerTransforms == null || panel.PlayerControllers == null || panel.IDInfos == null || panel.PlayerItemControllers == null) { return false; } if (NativeRosterShapePolicy.HasAlignedActivePrefix(panel.PlayerSteamIDs.Count, panel.PlayerIDs.Count, panel.PlayerTransforms.Count, panel.IDInfos.Count, panel.PlayerItemControllers.Count)) { return panel.PlayerControllers.Count >= panel.PlayerSteamIDs.Count; } return false; } private static bool IsCurrentTupleStable(PlayerPanelController panel, int index, string steamId, string playerId, PlayerItemController row, NetworkTransform networkTransform, PlayerController playerController, PlayerIDInfo playerInfo) { //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_0078: 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) //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Unknown result type (might be due to invalid IL or missing references) if (!AreNativeListsAligned(panel) || index < 0 || index >= panel.PlayerSteamIDs.Count) { return false; } PlayerController val = panel.PlayerControllers[index]; NetworkTransform val2 = (((Object)(object)val != (Object)null) ? ((Component)val).GetComponent() : null); if ((Object)(object)playerController != (Object)null && panel.PlayerControllers[index] == playerController && (Object)(object)val2 != (Object)null && val2 == networkTransform && PlayerIdInfoMatches(panel.IDInfos[index], playerInfo) && string.Equals(panel.PlayerSteamIDs[index], steamId, StringComparison.Ordinal)) { PlayerID val3 = panel.PlayerIDs[index]; if (string.Equals(PurrNetPlayerIdPolicy.Format(PackedULong.op_Implicit(((PlayerID)(ref val3)).id)), playerId, StringComparison.Ordinal) && (Object)(object)panel.PlayerTransforms[index] != (Object)null && panel.PlayerTransforms[index] == networkTransform && (Object)(object)panel.PlayerItemControllers[index] != (Object)null && panel.PlayerItemControllers[index] == row) { return string.Equals(PlayerSteamIdField?.GetValue(panel.PlayerItemControllers[index]) as string, steamId, StringComparison.Ordinal); } } return false; } private static bool IsFinalTupleStable(NativeIdCardTarget target) { //IL_0043: Unknown result type (might be due to invalid IL or missing references) if (target == null) { return false; } PlayerPanelController i = NetworkSingleton.I; if ((Object)(object)i != (Object)null && i == target.Panel) { return IsCurrentTupleStable(i, target.PlayerIndex, target.SteamId, target.PlayerId, target.Row, target.NetworkTransform, target.PlayerController, target.PlayerInfo); } return false; } private static void TryOpenExactPlayerCard(TemporaryPhoto temporaryPhoto, NativeIdCardTarget target) { temporaryPhoto.CapturePhoto(target.PlayerController); } private static PlayerIDInfo SnapshotPlayerIdInfo(PlayerIDInfo source) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_0083: 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) return new PlayerIDInfo { Name = CloneBytes(source.Name), DateOfBirth = CloneBytes(source.DateOfBirth), HoroscopeSign = CloneBytes(source.HoroscopeSign), Location = CloneBytes(source.Location), AreaOfStudy = CloneBytes(source.AreaOfStudy), FavoriteQuote = CloneBytes(source.FavoriteQuote), IDCardColorIndex = source.IDCardColorIndex, IsGold = source.IsGold }; } private static bool PlayerIdInfoMatches(PlayerIDInfo current, PlayerIDInfo expected) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_005f: 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_0072: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) if (ByteArraysEqual(current.Name, expected.Name) && ByteArraysEqual(current.DateOfBirth, expected.DateOfBirth) && ByteArraysEqual(current.HoroscopeSign, expected.HoroscopeSign) && ByteArraysEqual(current.Location, expected.Location) && ByteArraysEqual(current.AreaOfStudy, expected.AreaOfStudy) && ByteArraysEqual(current.FavoriteQuote, expected.FavoriteQuote) && current.IDCardColorIndex == expected.IDCardColorIndex) { return current.IsGold == expected.IsGold; } return false; } private static byte[] CloneBytes(byte[] source) { if (source != null) { return (byte[])source.Clone(); } return null; } private static bool ByteArraysEqual(byte[] left, byte[] right) { if (left == right) { return true; } if (left == null || right == null || left.Length != right.Length) { return false; } for (int i = 0; i < left.Length; i++) { if (left[i] != right[i]) { return false; } } return true; } private static void LogOutcome(string source, NativeIdCardResolutionState state, int index, string steamId, string detail) { string text = (string.IsNullOrWhiteSpace(source) ? "unknown" : source.Replace("\r", string.Empty).Replace("\n", string.Empty)); if (text.Length > 32) { text = text.Substring(0, 32); } string text2 = (string.IsNullOrWhiteSpace(detail) ? "none" : detail.Replace("\r", " ").Replace("\n", " ")); if (text2.Length > 160) { text2 = text2.Substring(0, 159) + "…"; } string text3 = "[BlueSageNativeId] source=" + text + "; outcome=" + state.ToString() + "; index=" + index + "; steamIdPresent=" + !string.IsNullOrWhiteSpace(steamId) + "; detail=" + text2 + "."; if (state == NativeIdCardResolutionState.Ready) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)text3); } } else { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)text3); } } } } internal sealed class PerformanceOverlayController : MonoBehaviour { private const float OverlayHeight = 28f; private readonly PerformanceOverlayState _state = new PerformanceOverlayState(); private string _label = "FPS -- PING -- RAM --"; private float _nextRefreshAt; private Rect _rect; private bool _dragging; private Vector2 _dragOffset; private GUIStyle _style; private Texture2D _background; private void Update() { _state.SampleFrame(Time.unscaledDeltaTime); ConfigEntry enablePerformanceOverlay = Plugin.EnablePerformanceOverlay; if (enablePerformanceOverlay != null && enablePerformanceOverlay.Value && !(Time.unscaledTime < _nextRefreshAt)) { PerformanceOverlaySettings performanceOverlaySettings = ReadSettings(); _label = _state.BuildLabel(ReadActiveSessionPing(), ProcessRuntimeMetricsCapture.ReadWorkingSetBytes()); _nextRefreshAt = Time.unscaledTime + (float)performanceOverlaySettings.RefreshSeconds; } } private void OnGUI() { //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_00cc: Unknown result type (might be due to invalid IL or missing references) //IL_00d2: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) ConfigEntry enablePerformanceOverlay = Plugin.EnablePerformanceOverlay; if (enablePerformanceOverlay == null || !enablePerformanceOverlay.Value) { return; } PerformanceOverlaySettings performanceOverlaySettings = ReadSettings(); EnsureStyle(performanceOverlaySettings.FontSize); if (!_dragging) { _rect = new Rect((float)performanceOverlaySettings.OffsetX, (float)performanceOverlaySettings.OffsetY, (float)performanceOverlaySettings.Width, 28f); } Rect val = default(Rect); ((Rect)(ref val))..ctor(((Rect)(ref _rect)).x, ((Rect)(ref _rect)).y, Mathf.Max(1f, ((Rect)(ref _rect)).width - 24f), ((Rect)(ref _rect)).height); Rect val2 = new Rect(((Rect)(ref _rect)).xMax - 24f, ((Rect)(ref _rect)).y, 24f, ((Rect)(ref _rect)).height); HandleDrag(performanceOverlaySettings, val); GUI.Label(val, _label, _style); if (GUI.Button(val2, "×")) { Plugin.EnablePerformanceOverlay.Value = false; Plugin.Instance?.SaveConfigFromQolMenu(); _dragging = false; ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)"Public QoL performance overlay hidden from its in-bar shortcut; reopen it from QoL Menu > Toggles > Personalization."); } } } private static int? ReadActiveSessionPing() { try { NetworkManager main = NetworkManager.main; TickManager val = default(TickManager); if ((Object)(object)main == (Object)null || !main.TryGetModule(false, ref val) || val == null) { return null; } return TickManagerPingPolicy.FromRttSeconds(val.rtt); } catch { return null; } } private static PerformanceOverlaySettings ReadSettings() { return PerformanceOverlaySettingsPolicy.Normalize(Plugin.PerformanceOverlayRefreshSeconds?.Value ?? 0.25f, Plugin.PerformanceOverlayFontSize?.Value ?? 13, Plugin.PerformanceOverlayOffsetX?.Value ?? 12f, Plugin.PerformanceOverlayOffsetY?.Value ?? 174f, Plugin.PerformanceOverlayWidth?.Value ?? 320f); } private void EnsureStyle(int fontSize) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Expected O, but got Unknown //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_008d: 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_00a5: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Unknown result type (might be due to invalid IL or missing references) //IL_00ce: Expected O, but got Unknown //IL_00d8: Unknown result type (might be due to invalid IL or missing references) //IL_00e2: Expected O, but got Unknown if (_style == null) { _background = new Texture2D(1, 1); ((Object)_background).name = "BlueSagePublicPerformanceOverlayBackground"; ((Object)_background).hideFlags = (HideFlags)61; _background.SetPixel(0, 0, new Color(0.025f, 0.055f, 0.075f, 0.82f)); _background.Apply(false, true); GUIStyle val = new GUIStyle(GUI.skin.label) { alignment = (TextAnchor)4, fontStyle = (FontStyle)1, wordWrap = false, clipping = (TextClipping)1 }; val.normal.background = _background; val.normal.textColor = new Color(0.72f, 0.92f, 0.94f, 1f); _style = val; _style.padding = new RectOffset(6, 6, 2, 2); } _style.fontSize = fontSize; } private void HandleDrag(PerformanceOverlaySettings settings, Rect dragRect) { //IL_000b: 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_005a: Invalid comparison between Unknown and I4 //IL_008c: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Invalid comparison between Unknown and I4 //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Unknown result type (might be due to invalid IL or missing references) Event current = Event.current; if (current != null) { if ((int)current.type == 0 && current.button == 0 && ((Rect)(ref dragRect)).Contains(current.mousePosition)) { _dragging = true; _dragOffset = current.mousePosition - ((Rect)(ref _rect)).position; current.Use(); } else if ((int)current.type == 3 && current.button == 0 && _dragging) { MoveTo(current.mousePosition - _dragOffset, settings); current.Use(); } else if ((int)current.type == 1 && current.button == 0 && _dragging) { MoveTo(((Rect)(ref _rect)).position, settings); Plugin.PerformanceOverlayOffsetX.Value = ((Rect)(ref _rect)).x; Plugin.PerformanceOverlayOffsetY.Value = ((Rect)(ref _rect)).y; Plugin.Instance?.SaveConfigFromQolMenu(); _dragging = false; current.Use(); } } } private void MoveTo(Vector2 requested, PerformanceOverlaySettings settings) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0007: 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) PerformanceOverlayPosition performanceOverlayPosition = PerformanceOverlayPositionPolicy.ClampToScreen(requested.x, requested.y, settings.Width, 28.0, Screen.width, Screen.height); ((Rect)(ref _rect)).position = new Vector2((float)performanceOverlayPosition.OffsetX, (float)performanceOverlayPosition.OffsetY); } private void OnDestroy() { if ((Object)(object)_background != (Object)null) { Object.Destroy((Object)(object)_background); _background = null; } } } internal sealed class PlayerIdentityEvidence { internal string SteamId = string.Empty; internal string DisplayName = string.Empty; internal string DisplayNameRaw = string.Empty; internal string SteamPersona = string.Empty; internal string Role = "PLAYER"; internal int RosterIndex = -1; internal string NetworkPlayerId = string.Empty; internal ulong PurrNetId; internal int TransformInstanceId; internal bool IsLocal; } internal static class PlayerIdentityEvidenceController { private sealed class SnapshotRoleContext { internal string LocalSteamId = string.Empty; internal string OwnerSteamId = string.Empty; internal string[] HelperSteamIds = Array.Empty(); internal readonly Dictionary RoleBySteamId = new Dictionary(StringComparer.Ordinal); } private sealed class RosterSnapshot { internal PlayerIdentityEvidence[] Verified = Array.Empty(); internal string Error = string.Empty; internal string CoverageIssue = string.Empty; internal bool CoverageComplete; internal int NativeRowCount; internal int PurrNetRowCount; internal int LobbyMemberCount; internal int LiveSteamCheckCount; internal int LeftStaleCount; internal int AmbiguousCount; internal ulong LobbyId; internal ulong LocalSteamId; internal ulong LobbyOwnerSteamId; internal string[] NativeSteamIds = Array.Empty(); internal string[] NativePlayerIds = Array.Empty(); internal string[] NativePurrNetIds = Array.Empty(); internal bool[] HasLiveTransform = Array.Empty(); internal int[] TransformInstanceIds = Array.Empty(); internal string[] LobbyMemberSteamIds = Array.Empty(); internal string[] PublishedHelperSteamIds = Array.Empty(); internal long CapturedUtcTicks; internal long CaptureStartedTimestamp; internal int CapturedFrame; } private static readonly FieldInfo PlayerSteamIdField = AccessTools.Field(typeof(PlayerItemController), "_playerSteamId"); private static readonly long SnapshotLifetimeTicks = TimeSpan.FromSeconds(1.0).Ticks; private static RosterSnapshot _cachedSnapshot; private static bool _snapshotDirty = true; private static long _snapshotCaptureCount; private static double _lastSnapshotCaptureMilliseconds; private static double _maxSnapshotCaptureMilliseconds; private static string _lastCoverageTransitionKey = string.Empty; internal static void InvalidateRosterCache() { _snapshotDirty = true; } internal static bool TryResolveExact(string steamId, out PlayerIdentityEvidence evidence, out string error) { RosterSnapshot rosterSnapshot = CaptureRosterSnapshot(cacheAllowed: false); error = rosterSnapshot.Error; evidence = rosterSnapshot.Verified.SingleOrDefault((PlayerIdentityEvidence candidate) => string.Equals(candidate.SteamId, steamId, StringComparison.Ordinal)); if (evidence != null) { return true; } error = DescribeUnresolvedExactRow(steamId, rosterSnapshot); return false; } internal static bool TryResolveExactWithAuthorityFingerprint(string steamId, ulong expectedLobbyId, out PlayerIdentityEvidence evidence, out string fingerprint, out bool contextValid, out string error) { fingerprint = string.Empty; RosterSnapshot rosterSnapshot = CaptureRosterSnapshot(cacheAllowed: true); contextValid = TryBuildAuthorityFingerprint(rosterSnapshot, expectedLobbyId, out fingerprint); error = rosterSnapshot.Error; if (!contextValid && string.IsNullOrWhiteSpace(error)) { error = ((!string.IsNullOrWhiteSpace(rosterSnapshot.CoverageIssue)) ? rosterSnapshot.CoverageIssue : "the bounded current roster snapshot is incomplete"); } evidence = (contextValid ? rosterSnapshot.Verified.SingleOrDefault((PlayerIdentityEvidence candidate) => string.Equals(candidate.SteamId, steamId, StringComparison.Ordinal)) : null); if (evidence != null) { return true; } if (contextValid) { error = DescribeUnresolvedExactRow(steamId, rosterSnapshot); } return false; } internal static bool TryResolveExactWithCompanionFingerprint(string steamId, ulong expectedLobbyId, out PlayerIdentityEvidence evidence, out string fingerprint, out bool contextValid, out string error) { fingerprint = string.Empty; string reason = string.Empty; RosterSnapshot rosterSnapshot = CaptureRosterSnapshot(cacheAllowed: true); contextValid = rosterSnapshot != null && string.IsNullOrWhiteSpace(rosterSnapshot.Error) && rosterSnapshot.LobbyId == expectedLobbyId && CompanionExactIdentityFingerprintPolicy.TryBuildFingerprint(rosterSnapshot.LobbyId, rosterSnapshot.LobbyOwnerSteamId, rosterSnapshot.LocalSteamId, rosterSnapshot.LobbyMemberSteamIds, rosterSnapshot.PublishedHelperSteamIds, rosterSnapshot.Verified.Select((PlayerIdentityEvidence candidate) => candidate.SteamId).ToArray(), rosterSnapshot.Verified.Select((PlayerIdentityEvidence candidate) => candidate.PurrNetId).ToArray(), rosterSnapshot.Verified.Select((PlayerIdentityEvidence candidate) => candidate.TransformInstanceId).ToArray(), out fingerprint, out reason); error = rosterSnapshot?.Error ?? "the bounded current roster snapshot is unavailable"; evidence = (contextValid ? rosterSnapshot.Verified.SingleOrDefault((PlayerIdentityEvidence candidate) => string.Equals(candidate.SteamId, steamId, StringComparison.Ordinal)) : null); if (evidence != null) { return true; } if (contextValid) { error = DescribeUnresolvedExactRow(steamId, rosterSnapshot); } else if (string.IsNullOrWhiteSpace(error)) { error = (string.IsNullOrWhiteSpace(reason) ? "the bounded exact-row companion identity context is invalid" : reason); } return false; } internal static bool TryCaptureAuthorityEvidenceFingerprint(ulong expectedLobbyId, out string fingerprint) { return TryBuildAuthorityFingerprint(CaptureRosterSnapshot(cacheAllowed: false), expectedLobbyId, out fingerprint); } internal static bool TryCaptureAuthorityEvidenceFingerprintCached(ulong expectedLobbyId, out string fingerprint) { return TryBuildAuthorityFingerprint(CaptureRosterSnapshot(cacheAllowed: true), expectedLobbyId, out fingerprint); } internal static bool TryCaptureCompleteAuthorityEvidenceAndRowsCached(ulong expectedLobbyId, out string fingerprint, out HashSet currentRows) { fingerprint = string.Empty; currentRows = new HashSet(); RosterSnapshot rosterSnapshot = CaptureRosterSnapshot(cacheAllowed: true); if (rosterSnapshot == null || !rosterSnapshot.CoverageComplete || !TryBuildAuthorityFingerprint(rosterSnapshot, expectedLobbyId, out fingerprint)) { return false; } return TryCaptureAlignedCurrentRows(rosterSnapshot, out currentRows); } private static bool TryCaptureAlignedCurrentRows(RosterSnapshot snapshot, out HashSet currentRows) { currentRows = new HashSet(); PlayerPanelController i = NetworkSingleton.I; if (snapshot == null || (Object)(object)i == (Object)null || i.PlayerSteamIDs == null || i.PlayerIDs == null || i.PlayerTransforms == null || i.IDInfos == null || i.PlayerItemControllers == null || !NativeRosterShapePolicy.HasAlignedActivePrefix(i.PlayerSteamIDs.Count, i.PlayerIDs.Count, i.PlayerTransforms.Count, i.IDInfos.Count, i.PlayerItemControllers.Count) || snapshot.Verified == null || snapshot.Verified.Length == 0 || snapshot.Verified.Length != snapshot.LobbyMemberCount || !PanelAuthorityTupleMatchesSnapshot(i, snapshot)) { return false; } Dictionary dictionary = new Dictionary(); PlayerIdentityEvidence[] verified = snapshot.Verified; foreach (PlayerIdentityEvidence playerIdentityEvidence in verified) { int num = playerIdentityEvidence?.RosterIndex ?? (-1); if (num < 0 || num >= i.PlayerItemControllers.Count) { return false; } PlayerItemController val = i.PlayerItemControllers[num]; if ((Object)(object)val == (Object)null || (Object)(object)((Component)val).gameObject == (Object)null || !string.Equals(PlayerSteamIdField?.GetValue(val) as string, playerIdentityEvidence.SteamId, StringComparison.Ordinal) || !currentRows.Add(val)) { return false; } dictionary[num] = val; } if (currentRows.Count != snapshot.Verified.Length || NetworkSingleton.I != i || !PanelAuthorityTupleMatchesSnapshot(i, snapshot)) { currentRows.Clear(); return false; } foreach (KeyValuePair item in dictionary) { if (item.Key < 0 || item.Key >= i.PlayerItemControllers.Count || i.PlayerItemControllers[item.Key] != item.Value || !string.Equals(PlayerSteamIdField?.GetValue(item.Value) as string, snapshot.NativeSteamIds[item.Key], StringComparison.Ordinal)) { currentRows.Clear(); return false; } } return true; } private static bool PanelAuthorityTupleMatchesSnapshot(PlayerPanelController panel, RosterSnapshot snapshot) { if ((Object)(object)panel != (Object)null && snapshot != null && panel.PlayerSteamIDs != null && panel.PlayerIDs != null && panel.PlayerTransforms != null && panel.IDInfos != null && panel.PlayerItemControllers != null && NativeRosterShapePolicy.HasAlignedActivePrefix(panel.PlayerSteamIDs.Count, panel.PlayerIDs.Count, panel.PlayerTransforms.Count, panel.IDInfos.Count, panel.PlayerItemControllers.Count) && panel.PlayerSteamIDs.SequenceEqual(snapshot.NativeSteamIds) && panel.PlayerIDs.Select((PlayerID id) => PackedULong.op_Implicit(((PlayerID)(ref id)).id).ToString(CultureInfo.InvariantCulture)).SequenceEqual(snapshot.NativePurrNetIds)) { return panel.PlayerTransforms.Select((NetworkTransform transform) => (!((Object)(object)transform == (Object)null)) ? ((Object)transform).GetInstanceID() : 0).SequenceEqual(snapshot.TransformInstanceIds); } return false; } internal static bool TryResolveExactCached(string steamId, out PlayerIdentityEvidence evidence, out string error) { RosterSnapshot rosterSnapshot = CaptureRosterSnapshot(cacheAllowed: true); error = rosterSnapshot.Error; evidence = rosterSnapshot.Verified.SingleOrDefault((PlayerIdentityEvidence candidate) => string.Equals(candidate.SteamId, steamId, StringComparison.Ordinal)); if (evidence != null) { return true; } error = DescribeUnresolvedExactRow(steamId, rosterSnapshot); return false; } internal static bool TryResolveRosterIndex(int senderIndex, out PlayerIdentityEvidence evidence, out string error) { return TryResolveRosterIndex(CaptureRosterSnapshot(cacheAllowed: true), senderIndex, out evidence, out error); } internal static bool TryResolveRosterIndexFresh(int senderIndex, out PlayerIdentityEvidence evidence, out string error) { return TryResolveRosterIndex(CaptureRosterSnapshot(cacheAllowed: false), senderIndex, out evidence, out error); } private static bool TryResolveRosterIndex(RosterSnapshot snapshot, int senderIndex, out PlayerIdentityEvidence evidence, out string error) { error = snapshot.Error; evidence = snapshot.Verified.SingleOrDefault((PlayerIdentityEvidence candidate) => candidate.RosterIndex == senderIndex); if (evidence != null) { return true; } if (string.IsNullOrWhiteSpace(error)) { error = "the sender index is not a verified current player row"; } return false; } internal static bool TryResolveLookup(string query, out PlayerIdentityEvidence evidence, out string error) { evidence = null; IReadOnlyList readOnlyList = ResolveLookupMatches(query, out error); if (readOnlyList.Count == 1) { evidence = readOnlyList[0]; error = string.Empty; return true; } if (readOnlyList.Count > 1) { error = "That query matches " + readOnlyList.Count + " verified current players. Select the exact row or use more name/SteamID detail."; } return false; } internal static IReadOnlyList ResolveLookupMatches(string query, out string error) { if (!Plugin.CanUseLobbySafety(out var _)) { error = "Lobby Safety is available only to the current host or a Helper assigned by that host. No player or incident details were shown."; return Array.Empty(); } string clean = (query ?? string.Empty).Trim(); RosterSnapshot rosterSnapshot = CaptureRosterSnapshot(cacheAllowed: false); IReadOnlyList verified = rosterSnapshot.Verified; string error2 = rosterSnapshot.Error; if (!string.IsNullOrWhiteSpace(error2) && verified.Count == 0) { error = error2; return Array.Empty(); } if (CloneIncidentLedger.TryResolveIncident(clean, out CloneIncident incident)) { PlayerIdentityEvidence playerIdentityEvidence = verified.SingleOrDefault((PlayerIdentityEvidence item) => string.Equals(item.SteamId, incident.SuspectSteamId, StringComparison.Ordinal)); error = ((playerIdentityEvidence != null) ? string.Empty : ((!string.IsNullOrWhiteSpace(error2)) ? error2 : "The incident suspect is no longer a verified current player.")); if (playerIdentityEvidence != null) { return new PlayerIdentityEvidence[1] { playerIdentityEvidence }; } return Array.Empty(); } if (ModerationProtocolPolicy.IsSteamId64(clean)) { PlayerIdentityEvidence playerIdentityEvidence2 = verified.SingleOrDefault((PlayerIdentityEvidence item) => string.Equals(item.SteamId, clean, StringComparison.Ordinal)); error = ((playerIdentityEvidence2 != null) ? string.Empty : ((!string.IsNullOrWhiteSpace(error2)) ? error2 : "That exact SteamID64 is not a verified current player.")); if (playerIdentityEvidence2 != null) { return new PlayerIdentityEvidence[1] { playerIdentityEvidence2 }; } return Array.Empty(); } PlayerLookupCandidate[] candidates = verified.Select((PlayerIdentityEvidence item) => new PlayerLookupCandidate { SteamId = item.SteamId, DisplayNameRaw = item.DisplayNameRaw, DisplayNamePlain = item.DisplayName, SteamPersona = item.SteamPersona, Role = item.Role, IsLocal = item.IsLocal }).ToArray(); IReadOnlyList source = PlayerLookupPolicy.FindMatches(clean, candidates, out error); HashSet ids = new HashSet(source.Select((PlayerLookupCandidate item) => item.SteamId), StringComparer.Ordinal); PlayerIdentityEvidence[] array = verified.Where((PlayerIdentityEvidence item) => ids.Contains(item.SteamId)).ToArray(); if (array.Length == 0 && !string.IsNullOrWhiteSpace(error2)) { error = error2 + " Verified rows remain usable, but Clone Shield and absence-sensitive actions wait for complete coverage."; } return array; } internal static IReadOnlyList FilterVerifiedRoster(IReadOnlyList roster, string query, out string error) { if (string.IsNullOrWhiteSpace(query)) { error = string.Empty; return roster ?? Array.Empty(); } PlayerLookupCandidate[] candidates = (roster ?? Array.Empty()).Select((PlayerIdentityEvidence item) => new PlayerLookupCandidate { SteamId = item.SteamId, DisplayNameRaw = item.DisplayNameRaw, DisplayNamePlain = item.DisplayName, SteamPersona = item.SteamPersona, Role = item.Role, IsLocal = item.IsLocal }).ToArray(); IReadOnlyList source = PlayerLookupPolicy.FindMatches(query, candidates, out error); HashSet ids = new HashSet(source.Select((PlayerLookupCandidate item) => item.SteamId), StringComparer.Ordinal); return (roster ?? Array.Empty()).Where((PlayerIdentityEvidence item) => ids.Contains(item.SteamId)).ToArray(); } internal static IReadOnlyList GetVerifiedRoster() { if (!Plugin.CanUseLobbySafety(out var _)) { return Array.Empty(); } return CaptureRosterSnapshot(cacheAllowed: true).Verified; } internal static IReadOnlyList GetVerifiedRoster(out string status) { if (!Plugin.CanUseLobbySafety(out var _)) { status = "Lobby Safety is available only to the current host or a Helper assigned by that host. No player or incident details were shown."; return Array.Empty(); } RosterSnapshot rosterSnapshot = CaptureRosterSnapshot(cacheAllowed: true); IReadOnlyList verified = rosterSnapshot.Verified; string error = rosterSnapshot.Error; string coverageIssue = rosterSnapshot.CoverageIssue; int nativeRowCount = rosterSnapshot.NativeRowCount; int lobbyMemberCount = rosterSnapshot.LobbyMemberCount; int num = Math.Max(0, nativeRowCount - verified.Count); if (!string.IsNullOrWhiteSpace(error) && verified.Count == 0) { status = "Verification paused: " + error + ". No hidden or guessed row is actionable."; return verified; } status = verified.Count + " active verified • " + nativeRowCount + " player-list rows • " + lobbyMemberCount + " Steam lobby members • " + num + " non-actionable."; if (rosterSnapshot.LeftStaleCount > 0) { status = status + " " + rosterSnapshot.LeftStaleCount + " left/stale history " + ((rosterSnapshot.LeftStaleCount == 1) ? "row is" : "rows are") + " locked."; } if (rosterSnapshot.AmbiguousCount > 0) { status = status + " " + rosterSnapshot.AmbiguousCount + " ambiguous/syncing " + ((rosterSnapshot.AmbiguousCount == 1) ? "row is" : "rows are") + " locked."; } if (!string.IsNullOrWhiteSpace(coverageIssue)) { status = status + " " + coverageIssue + " Exact verified rows remain usable; Clone Shield and absence-sensitive actions wait for complete coverage."; } else if (!string.IsNullOrWhiteSpace(error)) { status = status + " " + error; } status += " Mod installation is not part of identity verification."; return verified; } internal static string GetDiagnosticRosterSummary() { return FormatDiagnosticRosterSummary(CaptureRosterSnapshot(cacheAllowed: true)); } internal static IdentityResyncCoverage GetCachedIdentityResyncCoverage() { RosterSnapshot cachedSnapshot = _cachedSnapshot; if (cachedSnapshot == null) { return IdentityResyncCoverage.Unavailable("no authoritative identity snapshot is available yet"); } long num = DateTime.UtcNow.Ticks - cachedSnapshot.CapturedUtcTicks; string text = cachedSnapshot.Error; if ((_snapshotDirty || num < 0 || num > SnapshotLifetimeTicks) && string.IsNullOrWhiteSpace(text)) { text = "the cached identity snapshot is stale; status remains fail-closed"; } return new IdentityResyncCoverage(cachedSnapshot.Verified.Length, cachedSnapshot.NativeRowCount, cachedSnapshot.PurrNetRowCount, cachedSnapshot.LobbyMemberCount, cachedSnapshot.LiveSteamCheckCount, cachedSnapshot.LeftStaleCount, cachedSnapshot.AmbiguousCount, captureAvailable: true, text); } internal static IReadOnlyList GetCachedIdentityResyncSurfaceTargets() { RosterSnapshot cachedSnapshot = _cachedSnapshot; if (cachedSnapshot == null) { return Array.Empty(); } return cachedSnapshot.NativeSteamIds.Where(ModerationProtocolPolicy.IsSteamId64).Distinct(StringComparer.Ordinal).ToArray(); } internal static HashSet GetVerifiedCurrentSteamIdSetAlreadyCached() { return new HashSet(_cachedSnapshot?.Verified.Select((PlayerIdentityEvidence item) => item.SteamId) ?? Enumerable.Empty(), StringComparer.Ordinal); } internal static void ForceRefreshIncompleteRosterRowsOnce(out string before, out string after, out bool coverageComplete, out IReadOnlyList targetedSteamIds, out int healedRowCount, out bool capturePerformed) { RosterSnapshot cachedSnapshot = _cachedSnapshot; if (cachedSnapshot == null) { before = "not-yet-captured"; RosterSnapshot rosterSnapshot = CaptureRosterSnapshot(cacheAllowed: false); targetedSteamIds = PlayerIdentityEvidencePolicy.SelectManualResyncTargets(rosterSnapshot.NativeSteamIds, rosterSnapshot.LobbyMemberSteamIds, rosterSnapshot.Verified.Select((PlayerIdentityEvidence item) => item.SteamId).ToArray()); after = FormatDiagnosticRosterSummary(rosterSnapshot); coverageComplete = rosterSnapshot.CoverageComplete; healedRowCount = 0; capturePerformed = true; return; } before = FormatDiagnosticRosterSummary(cachedSnapshot); IReadOnlyList readOnlyList = PlayerIdentityEvidencePolicy.SelectManualResyncTargets(cachedSnapshot.NativeSteamIds, cachedSnapshot.LobbyMemberSteamIds, cachedSnapshot.Verified.Select((PlayerIdentityEvidence item) => item.SteamId).ToArray()); long ticks = DateTime.UtcNow.Ticks; bool flag = ((ticks >= cachedSnapshot.CapturedUtcTicks) ? (ticks - cachedSnapshot.CapturedUtcTicks) : (SnapshotLifetimeTicks + 1)) > SnapshotLifetimeTicks; bool flag2 = _snapshotDirty || flag || !cachedSnapshot.CoverageComplete || !string.IsNullOrWhiteSpace(cachedSnapshot.Error); if (readOnlyList.Count == 0 && !flag2) { targetedSteamIds = readOnlyList; after = before; coverageComplete = cachedSnapshot.CoverageComplete; healedRowCount = 0; capturePerformed = false; return; } InvalidateRosterCache(); RosterSnapshot rosterSnapshot2 = CaptureRosterSnapshot(cacheAllowed: false); after = FormatDiagnosticRosterSummary(rosterSnapshot2); coverageComplete = rosterSnapshot2.CoverageComplete; HashSet hashSet = new HashSet(rosterSnapshot2.Verified.Select((PlayerIdentityEvidence item) => item.SteamId), StringComparer.Ordinal); IReadOnlyList second = PlayerIdentityEvidencePolicy.SelectManualResyncTargets(rosterSnapshot2.NativeSteamIds, rosterSnapshot2.LobbyMemberSteamIds, rosterSnapshot2.Verified.Select((PlayerIdentityEvidence item) => item.SteamId).ToArray()); targetedSteamIds = readOnlyList.Concat(second).Distinct(StringComparer.Ordinal).ToArray(); healedRowCount = readOnlyList.Count(hashSet.Contains); capturePerformed = true; } private static string FormatDiagnosticRosterSummary(RosterSnapshot snapshot) { IReadOnlyList verified = snapshot.Verified; int nativeRowCount = snapshot.NativeRowCount; int lobbyMemberCount = snapshot.LobbyMemberCount; int num = Math.Max(0, nativeRowCount - verified.Count); return "verified=" + verified.Count + ",native=" + nativeRowCount + ",steam=" + lobbyMemberCount + ",excluded=" + num + ",liveSteamCheck=" + snapshot.LiveSteamCheckCount + ",leftStale=" + snapshot.LeftStaleCount + ",ambiguous=" + snapshot.AmbiguousCount + ",complete=" + snapshot.CoverageComplete + ",snapshotCaptures=" + _snapshotCaptureCount + ",snapshotLastMs=" + _lastSnapshotCaptureMilliseconds.ToString("0.0") + ",snapshotMaxMs=" + _maxSnapshotCaptureMilliseconds.ToString("0.0") + ",error=" + DiagnosticValue(snapshot.Error) + ",coverageIssue=" + DiagnosticValue(snapshot.CoverageIssue); } private static string DiagnosticValue(string value) { if (!string.IsNullOrWhiteSpace(value)) { return value.Replace(",", ";").Replace("\r", " ").Replace("\n", " ") .Trim(); } return "none"; } internal static IReadOnlyList GetVerifiedCurrentSteamIds(out string error) { bool coverageComplete; return GetVerifiedCurrentSteamIds(out error, out coverageComplete); } internal static IReadOnlyList GetVerifiedCurrentSteamIds(out string error, out bool coverageComplete) { RosterSnapshot rosterSnapshot = CaptureRosterSnapshot(cacheAllowed: true); coverageComplete = rosterSnapshot.CoverageComplete; error = ((!string.IsNullOrWhiteSpace(rosterSnapshot.Error)) ? rosterSnapshot.Error : rosterSnapshot.CoverageIssue); return rosterSnapshot.Verified.Select((PlayerIdentityEvidence item) => item.SteamId).Distinct(StringComparer.Ordinal).ToArray(); } internal static HashSet GetVerifiedCurrentSteamIdSetCached() { return new HashSet(CaptureRosterSnapshot(cacheAllowed: true).Verified.Select((PlayerIdentityEvidence item) => item.SteamId), StringComparer.Ordinal); } internal static IReadOnlyList GetLookupSuggestions() { if (!Plugin.CanUseLobbySafety(out var _)) { return Array.Empty(); } List list = new List { "list" }; list.AddRange(from item in CloneIncidentLedger.Snapshot.Reverse().Take(12) select item.Id); string error; string[] source = GetVerifiedCurrentSteamIds(out error).Distinct(StringComparer.Ordinal).ToArray(); foreach (string item in source.Take(64)) { string suffix = Suffix(item); list.Add((source.Count((string other) => other.EndsWith(suffix, StringComparison.Ordinal)) == 1) ? suffix : item); } return list.Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } internal static string Suffix(string steamId) { string text; if (steamId == null || steamId.Length <= 6) { text = steamId; if (text == null) { return "unknown"; } } else { text = steamId.Substring(steamId.Length - 6); } return text; } internal static string BuildCopyableEvidence(PlayerIdentityEvidence evidence) { if (evidence == null) { return string.Empty; } return "BlueSage verified live identity\nDisplayed name: " + RecordValue(evidence.DisplayName) + "\nRaw rendered name/TMP: " + RecordValue(evidence.DisplayNameRaw) + "\nSteam persona: " + RecordValue(string.IsNullOrWhiteSpace(evidence.SteamPersona) ? "not cached" : evidence.SteamPersona) + "\nSteamID64: " + evidence.SteamId + "\nVerified live row: #" + (evidence.RosterIndex + 1) + "\nPlayerID: " + RecordValue(evidence.NetworkPlayerId) + "\nCurrent role: " + RecordValue(evidence.Role); } private static string RecordValue(string value) { return (value ?? string.Empty).Replace("\r", "\\r").Replace("\n", "\\n").Trim(); } private static bool TryBuildAuthorityFingerprint(RosterSnapshot snapshot, ulong expectedLobbyId, out string fingerprint) { fingerprint = string.Empty; if (snapshot != null && string.IsNullOrWhiteSpace(snapshot.Error) && snapshot.LobbyId == expectedLobbyId) { return IdentityEvidenceGenerationPolicy.TryBuildFingerprint(snapshot.LobbyId, snapshot.LobbyOwnerSteamId, snapshot.NativeSteamIds, snapshot.NativePurrNetIds, snapshot.TransformInstanceIds, snapshot.LobbyMemberSteamIds, snapshot.LocalSteamId, snapshot.PublishedHelperSteamIds, snapshot.CoverageComplete, snapshot.LiveSteamCheckCount, snapshot.LeftStaleCount, snapshot.AmbiguousCount, out fingerprint); } return false; } private static bool TryReadCurrentLobbyMembers(out CSteamID lobby, out List members, out string error) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) lobby = CSteamID.Nil; members = new List(); error = "the current Steam lobby is unavailable"; try { if (!SteamManager.Initialized || !Plugin.TryGetRescueLobby(out lobby) || lobby == CSteamID.Nil) { return false; } int numLobbyMembers = SteamMatchmaking.GetNumLobbyMembers(lobby); for (int i = 0; i < numLobbyMembers; i++) { members.Add(((ulong)SteamMatchmaking.GetLobbyMemberByIndex(lobby, i)).ToString()); } error = string.Empty; return true; } catch (Exception ex) { error = "Steam lobby verification failed safely (" + ex.GetType().Name + ")"; return false; } } private static SnapshotRoleContext BuildRoleContext(CSteamID lobby, IReadOnlyCollection lobbyMembers) { //IL_0007: 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_002b: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) SnapshotRoleContext snapshotRoleContext = new SnapshotRoleContext(); try { snapshotRoleContext.LocalSteamId = ((ulong)SteamUser.GetSteamID()).ToString(CultureInfo.InvariantCulture); } catch { } snapshotRoleContext.OwnerSteamId = ((ulong)SteamMatchmaking.GetLobbyOwner(lobby)).ToString(CultureInfo.InvariantCulture); HashSet hashSet = new HashSet((IEnumerable)(((object)lobbyMembers) ?? ((object)Array.Empty())), StringComparer.Ordinal); HashSet publishedHelpers = SteamIdModerationController.GetPublishedHelpers(lobby, snapshotRoleContext.OwnerSteamId); snapshotRoleContext.HelperSteamIds = CurrentLobbyHelperPolicy.FilterCurrent(publishedHelpers, hashSet); HashSet hashSet2 = new HashSet(snapshotRoleContext.HelperSteamIds, StringComparer.Ordinal); if (!Plugin.CanUseLobbySafety(out var _)) { return snapshotRoleContext; } Dictionary> dictionary = new Dictionary>(StringComparer.Ordinal); foreach (string item in hashSet) { if (string.Equals(snapshotRoleContext.LocalSteamId, item, StringComparison.Ordinal)) { AddRole(dictionary, item, "SELF"); } if (string.Equals(snapshotRoleContext.OwnerSteamId, item, StringComparison.Ordinal)) { AddRole(dictionary, item, "HOST"); } if (hashSet2.Contains(item)) { AddRole(dictionary, item, "HELPER"); } } foreach (CloneIncident item2 in CloneIncidentLedger.Snapshot) { if (hashSet.Contains(item2.VictimSteamId)) { AddRole(dictionary, item2.VictimSteamId, "VICTIM " + item2.Id + " (PROTECTED)"); } if (hashSet.Contains(item2.SuspectSteamId)) { AddRole(dictionary, item2.SuspectSteamId, "SUSPECT " + item2.Id); } } foreach (KeyValuePair> item3 in dictionary) { snapshotRoleContext.RoleBySteamId[item3.Key] = string.Join(" • ", item3.Value.Distinct(StringComparer.Ordinal)); } return snapshotRoleContext; } private static void AddRole(Dictionary> roles, string steamId, string role) { if (!roles.TryGetValue(steamId, out var value)) { value = (roles[steamId] = new List()); } value.Add(role); } private static RosterSnapshot CaptureRosterSnapshot(bool cacheAllowed) { //IL_0220: Unknown result type (might be due to invalid IL or missing references) //IL_02ca: Unknown result type (might be due to invalid IL or missing references) //IL_061c: Unknown result type (might be due to invalid IL or missing references) //IL_061e: Unknown result type (might be due to invalid IL or missing references) //IL_06a1: Unknown result type (might be due to invalid IL or missing references) long ticks = DateTime.UtcNow.Ticks; RosterSnapshot cachedSnapshot = _cachedSnapshot; if (cacheAllowed && cachedSnapshot != null && (cachedSnapshot.CapturedFrame == Time.frameCount || (!_snapshotDirty && ticks - cachedSnapshot.CapturedUtcTicks <= SnapshotLifetimeTicks))) { return cachedSnapshot; } RosterSnapshot rosterSnapshot = new RosterSnapshot { CapturedUtcTicks = ticks, CaptureStartedTimestamp = Stopwatch.GetTimestamp(), CapturedFrame = Time.frameCount }; PlayerPanelController i = NetworkSingleton.I; if ((Object)(object)i == (Object)null || i.PlayerSteamIDs == null || i.PlayerIDs == null || i.PlayerTransforms == null || i.IDInfos == null) { rosterSnapshot.Error = "the native player roster is not ready"; return Cache(rosterSnapshot); } int num = (rosterSnapshot.NativeRowCount = i.PlayerSteamIDs.Count); rosterSnapshot.PurrNetRowCount = i.PlayerIDs.Count; if (i.PlayerIDs.Count != num || i.PlayerTransforms.Count != num || i.IDInfos.Count != num) { rosterSnapshot.Error = "the native Steam, network-player, transform, and identity lists are not aligned"; return Cache(rosterSnapshot); } rosterSnapshot.NativeSteamIds = i.PlayerSteamIDs.ToArray(); rosterSnapshot.NativePlayerIds = i.PlayerIDs.Select((PlayerID id) => PurrNetPlayerIdPolicy.Format(PackedULong.op_Implicit(((PlayerID)(ref id)).id))).ToArray(); rosterSnapshot.NativePurrNetIds = i.PlayerIDs.Select((PlayerID id) => PackedULong.op_Implicit(((PlayerID)(ref id)).id).ToString(CultureInfo.InvariantCulture)).ToArray(); rosterSnapshot.TransformInstanceIds = i.PlayerTransforms.Select((NetworkTransform transform) => (!((Object)(object)transform == (Object)null)) ? ((Object)transform).GetInstanceID() : 0).ToArray(); rosterSnapshot.HasLiveTransform = rosterSnapshot.TransformInstanceIds.Select((int instanceId) => instanceId != 0).ToArray(); if (!TryReadCurrentLobbyMembers(out var lobby, out var members, out var error)) { rosterSnapshot.Error = error; return Cache(rosterSnapshot); } rosterSnapshot.LobbyId = (ulong)lobby; rosterSnapshot.LobbyMemberSteamIds = members.ToArray(); rosterSnapshot.LobbyMemberCount = members.Count; if (members.Any((string id) => !ModerationProtocolPolicy.IsSteamId64(id)) || members.Distinct(StringComparer.Ordinal).Count() != members.Count) { rosterSnapshot.Error = "the current Steam lobby member list is invalid or duplicated"; return Cache(rosterSnapshot); } string[] nativeSteamIds = rosterSnapshot.NativeSteamIds; string[] nativePurrNetIds = rosterSnapshot.NativePurrNetIds; Dictionary dictionary = CountValues(nativeSteamIds); Dictionary dictionary2 = CountValues(nativePurrNetIds); HashSet hashSet = new HashSet(members, StringComparer.Ordinal); SnapshotRoleContext snapshotRoleContext = BuildRoleContext(lobby, members); if (!ulong.TryParse(snapshotRoleContext.LocalSteamId, NumberStyles.None, CultureInfo.InvariantCulture, out rosterSnapshot.LocalSteamId) || rosterSnapshot.LocalSteamId == 0L || !ulong.TryParse(snapshotRoleContext.OwnerSteamId, NumberStyles.None, CultureInfo.InvariantCulture, out rosterSnapshot.LobbyOwnerSteamId) || rosterSnapshot.LobbyOwnerSteamId == 0L || !hashSet.Contains(snapshotRoleContext.LocalSteamId) || !hashSet.Contains(snapshotRoleContext.OwnerSteamId)) { rosterSnapshot.Error = "the current local or lobby-owner Steam identity is unavailable"; return Cache(rosterSnapshot); } rosterSnapshot.PublishedHelperSteamIds = snapshotRoleContext.HelperSteamIds; List list = new List(); for (int num2 = 0; num2 < nativeSteamIds.Length; num2++) { string text = nativeSteamIds[num2]; string text2 = nativePurrNetIds[num2]; bool num3 = ModerationProtocolPolicy.IsSteamId64(text); int value; int num4 = (dictionary.TryGetValue(text ?? string.Empty, out value) ? value : 0); int value2; int num5 = (dictionary2.TryGetValue(text2 ?? string.Empty, out value2) ? value2 : 0); bool flag = num3 && num4 == 1; ulong result; bool flag2 = ulong.TryParse(text2, NumberStyles.None, CultureInfo.InvariantCulture, out result) && result != 0L && num5 == 1; bool flag3 = rosterSnapshot.HasLiveTransform[num2]; bool flag4 = num3 && hashSet.Contains(text); if (!flag || !flag2 || !flag3 || !flag4) { switch (PlayerIdentityEvidencePolicy.ClassifyUnresolvedRow(text, rosterSnapshot.LobbyMemberSteamIds, flag ? 1 : num4, flag2, flag3)) { case PlayerIdentityEvidencePolicy.UnresolvedRowState.LiveSteamCheck: rosterSnapshot.LiveSteamCheckCount++; break; case PlayerIdentityEvidencePolicy.UnresolvedRowState.LeftStale: rosterSnapshot.LeftStaleCount++; break; default: rosterSnapshot.AmbiguousCount++; break; } } else { string text3 = ReadRosterNameRaw(i, num2); bool isLocal = string.Equals(snapshotRoleContext.LocalSteamId, text, StringComparison.Ordinal); string text4 = NormalizeName(text3); list.Add(new PlayerIdentityEvidence { SteamId = text, DisplayNameRaw = text3, DisplayName = (string.IsNullOrWhiteSpace(text4) ? "Player" : text4), SteamPersona = ReadSteamPersona(text), Role = (snapshotRoleContext.RoleBySteamId.TryGetValue(text, out var value3) ? value3 : "PLAYER"), RosterIndex = num2, NetworkPlayerId = text2, PurrNetId = result, TransformInstanceId = rosterSnapshot.TransformInstanceIds[num2], IsLocal = isLocal }); } } if (i.PlayerSteamIDs.Count != nativeSteamIds.Length || !i.PlayerSteamIDs.SequenceEqual(nativeSteamIds) || i.PlayerIDs.Count != nativePurrNetIds.Length || !i.PlayerIDs.Select((PlayerID id) => PackedULong.op_Implicit(((PlayerID)(ref id)).id).ToString(CultureInfo.InvariantCulture)).SequenceEqual(nativePurrNetIds) || i.PlayerTransforms.Count != rosterSnapshot.TransformInstanceIds.Length || !i.PlayerTransforms.Select((NetworkTransform transform) => (!((Object)(object)transform == (Object)null)) ? ((Object)transform).GetInstanceID() : 0).SequenceEqual(rosterSnapshot.TransformInstanceIds)) { rosterSnapshot.Error = "the native Steam/PurrNet/transform roster changed while BlueSage was reading it; refresh and try again"; rosterSnapshot.Verified = Array.Empty(); return Cache(rosterSnapshot); } if (!TryReadCurrentLobbyMembers(out var lobby2, out var members2, out var _) || lobby2 != lobby || !members2.OrderBy((string result2) => result2, StringComparer.Ordinal).SequenceEqual(members.OrderBy((string result2) => result2, StringComparer.Ordinal))) { rosterSnapshot.Error = "the Steam lobby membership changed while BlueSage was reading it; refresh and try again"; rosterSnapshot.Verified = Array.Empty(); return Cache(rosterSnapshot); } SnapshotRoleContext snapshotRoleContext2 = BuildRoleContext(lobby2, members2); if (!string.Equals(snapshotRoleContext2.LocalSteamId, snapshotRoleContext.LocalSteamId, StringComparison.Ordinal) || !string.Equals(snapshotRoleContext2.OwnerSteamId, snapshotRoleContext.OwnerSteamId, StringComparison.Ordinal) || !snapshotRoleContext2.HelperSteamIds.SequenceEqual(snapshotRoleContext.HelperSteamIds)) { rosterSnapshot.Error = "the local/owner/helper authority context changed while BlueSage was reading it; refresh and try again"; rosterSnapshot.Verified = Array.Empty(); return Cache(rosterSnapshot); } HashSet verifiedIds = new HashSet(list.Select((PlayerIdentityEvidence item) => item.SteamId), StringComparer.Ordinal); string[] array = members.Where((string member) => !verifiedIds.Contains(member)).ToArray(); rosterSnapshot.CoverageComplete = PlayerIdentityEvidencePolicy.IsCoverageComplete(string.IsNullOrWhiteSpace(rosterSnapshot.Error), array.Length, rosterSnapshot.LiveSteamCheckCount, rosterSnapshot.AmbiguousCount); rosterSnapshot.CoverageIssue = BuildCoverageIssue(rosterSnapshot, array.Length); rosterSnapshot.Verified = list.OrderBy((PlayerIdentityEvidence item) => item.RosterIndex).ToArray(); return Cache(rosterSnapshot); } private static RosterSnapshot Cache(RosterSnapshot snapshot) { double num = (double)(Stopwatch.GetTimestamp() - snapshot.CaptureStartedTimestamp) * 1000.0 / (double)Stopwatch.Frequency; _snapshotCaptureCount++; _lastSnapshotCaptureMilliseconds = num; if (num > _maxSnapshotCaptureMilliseconds) { _maxSnapshotCaptureMilliseconds = num; } _snapshotDirty = false; _cachedSnapshot = snapshot; LogCoverageTransition(snapshot); return snapshot; } private static string BuildCoverageIssue(RosterSnapshot snapshot, int missingCurrentMembers) { List list = new List(); if (missingCurrentMembers > 0) { list.Add(missingCurrentMembers + " current Steam " + ((missingCurrentMembers == 1) ? "member is" : "members are") + " still waiting for one unique live player row"); } if (snapshot.LiveSteamCheckCount > 0) { list.Add(snapshot.LiveSteamCheckCount + " live game/PurrNet " + ((snapshot.LiveSteamCheckCount == 1) ? "row is" : "rows are") + " outside current Steam lobby membership. Profile access remains available; identity-sensitive actions stay locked"); } if (snapshot.AmbiguousCount > 0) { list.Add(snapshot.AmbiguousCount + " live native " + ((snapshot.AmbiguousCount == 1) ? "row is" : "rows are") + " ambiguous and locked"); } return string.Join("; ", list); } private static void LogCoverageTransition(RosterSnapshot snapshot) { string text = (string.IsNullOrWhiteSpace(snapshot.Error) ? "none" : snapshot.Error); string text2 = snapshot.CoverageComplete + "|" + snapshot.Verified.Length + "|" + snapshot.NativeRowCount + "|" + snapshot.LobbyMemberCount + "|" + snapshot.LiveSteamCheckCount + "|" + snapshot.LeftStaleCount + "|" + snapshot.AmbiguousCount + "|" + text; if (string.Equals(_lastCoverageTransitionKey, text2, StringComparison.Ordinal)) { return; } _lastCoverageTransitionKey = text2; string text3 = "[BlueSageIdentityCoverage] complete=" + snapshot.CoverageComplete + "; verified=" + snapshot.Verified.Length + "; native=" + snapshot.NativeRowCount + "; steam=" + snapshot.LobbyMemberCount + "; liveNativeOnly=" + snapshot.LiveSteamCheckCount + "; leftStale=" + snapshot.LeftStaleCount + "; ambiguous=" + snapshot.AmbiguousCount + "; identitySensitiveActions=" + (snapshot.CoverageComplete ? "ready" : "fail-closed") + "."; if (snapshot.CoverageComplete) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)text3); } } else { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)text3); } } } private static Dictionary CountValues(IEnumerable values) { Dictionary dictionary = new Dictionary(StringComparer.Ordinal); foreach (string value2 in values) { string key = value2 ?? string.Empty; dictionary[key] = ((!dictionary.TryGetValue(key, out var value)) ? 1 : (value + 1)); } return dictionary; } private static string DescribeUnresolvedExactRow(string steamId, RosterSnapshot snapshot) { int num = snapshot.NativeSteamIds.Count((string candidate) => string.Equals(candidate, steamId, StringComparison.Ordinal)); bool hasUniquePlayerId = false; bool hasLiveTransform = false; if (num == 1) { int num2 = Array.FindIndex(snapshot.NativeSteamIds, (string candidate) => string.Equals(candidate, steamId, StringComparison.Ordinal)); if (num2 >= 0 && num2 < snapshot.NativePurrNetIds.Length) { string playerId = snapshot.NativePurrNetIds[num2]; hasUniquePlayerId = !string.IsNullOrWhiteSpace(playerId) && snapshot.NativePurrNetIds.Count((string candidate) => string.Equals(candidate, playerId, StringComparison.Ordinal)) == 1; hasLiveTransform = num2 < snapshot.HasLiveTransform.Length && snapshot.HasLiveTransform[num2]; } } return PlayerIdentityEvidencePolicy.DescribeUnresolvedExactRow(steamId, snapshot.LobbyMemberSteamIds, snapshot.Error, num, hasUniquePlayerId, hasLiveTransform); } private static string ReadRosterNameRaw(PlayerPanelController panel, int index) { try { IList iDInfos = panel.IDInfos; if (iDInfos == null || index < 0 || index >= iDInfos.Count) { return "Player"; } object obj = AccessTools.Field(iDInfos[index].GetType(), "Name")?.GetValue(iDInfos[index]); string value = ((obj is byte[] bytes) ? Encoding.Unicode.GetString(bytes).TrimEnd(new char[1]) : ((obj as string) ?? string.Empty)); return string.IsNullOrWhiteSpace(value) ? "Player" : Truncate(value, 4096); } catch { return "Player"; } } private static string ReadSteamPersona(string steamId) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) try { if (!ulong.TryParse(steamId, out var result)) { return string.Empty; } return Truncate(NormalizeName(SteamFriends.GetFriendPersonaName(new CSteamID(result)) ?? string.Empty), 96); } catch { return string.Empty; } } private static string NormalizeName(string value) { return Regex.Replace(value ?? string.Empty, "<.*?>", string.Empty).Replace("\r", " ").Replace("\n", " ") .Trim(); } private static string Truncate(string value, int maximum) { if (value.Length > maximum) { return value.Substring(0, maximum - 1) + "…"; } return value; } } internal sealed class PlayerRenderSaverController : MonoBehaviour { private readonly struct RemotePlayerRenderCandidate { public Transform Root { get; } public float DistanceMeters { get; } public RemotePlayerRenderCandidate(Transform root, float distanceMeters) { Root = root; DistanceMeters = distanceMeters; } } private readonly struct SavedRendererState { public bool Enabled { get; } public int OwnerId { get; } public SavedRendererState(bool enabled, int ownerId) { Enabled = enabled; OwnerId = ownerId; } } private readonly struct SavedAnimatorState { public AnimatorCullingMode CullingMode { get; } public int OwnerId { get; } public SavedAnimatorState(AnimatorCullingMode cullingMode, int ownerId) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Unknown result type (might be due to invalid IL or missing references) CullingMode = cullingMode; OwnerId = ownerId; } } private readonly struct SavedParticleState { public bool EmissionEnabled { get; } public int OwnerId { get; } public SavedParticleState(bool emissionEnabled, int ownerId) { EmissionEnabled = emissionEnabled; OwnerId = ownerId; } } private readonly struct SavedAudioState { public bool Muted { get; } public int OwnerId { get; } public SavedAudioState(bool muted, int ownerId) { Muted = muted; OwnerId = ownerId; } } private sealed class CachedVisualSet { public Transform Root { get; } public Renderer[] Renderers { get; } public Animator[] Animators { get; } public ParticleSystem[] Particles { get; } public AudioSource[] AudioSources { get; } public CachedVisualSet(Transform root, Renderer[] renderers, Animator[] animators, ParticleSystem[] particles, AudioSource[] audioSources) { Root = root; Renderers = renderers; Animators = animators; Particles = particles; AudioSources = audioSources; } } private static readonly Dictionary RendererStates = new Dictionary(); private static readonly Dictionary AnimatorStates = new Dictionary(); private static readonly Dictionary ParticleStates = new Dictionary(); private static readonly Dictionary AudioStates = new Dictionary(); private static readonly Dictionary VisualCache = new Dictionary(); private static readonly HashSet HiddenOwners = new HashSet(); private float _nextRefreshAt; internal static int LastHiddenRemotePlayers { get; private set; } internal static int LastVisibleRemotePlayers { get; private set; } internal static int LastCandidateRemotePlayers { get; private set; } internal static int LastSuppressedRenderers { get; private set; } internal static string LastRuntimeState { get; private set; } = "not scanned yet"; private void Update() { if (Time.unscaledTime < _nextRefreshAt) { return; } _nextRefreshAt = Time.unscaledTime + 1f; try { Refresh(); } catch (Exception ex) { LastRuntimeState = "scan failed: " + ex.GetType().Name; ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("Player Render Saver scan failed safely: " + ex.GetType().Name + ": " + ex.Message)); } RestoreAll("scan failed; renderers restored"); } } private void OnDestroy() { RestoreAll(); } private static void Refresh() { //IL_0026: Unknown result type (might be due to invalid IL or missing references) if (!Plugin.ShouldApplyPlayerRenderSaver) { RestoreAll("off"); return; } if (!TryGetLocalPosition(out var position)) { RestoreAll("waiting for local player"); return; } List list = (from candidate in CollectRemoteCandidates(position) orderby candidate.DistanceMeters select candidate).ToList(); LastCandidateRemotePlayers = list.Count; HashSet hashSet = new HashSet(); int num = 0; int num2 = 0; for (int num3 = 0; num3 < list.Count; num3++) { RemotePlayerRenderCandidate remotePlayerRenderCandidate = list[num3]; bool num4 = PlayerRenderSaverPolicy.ShouldHideRemotePlayer(remotePlayerRenderCandidate.DistanceMeters, num3, Plugin.LockedPlayerRenderSaverRadiusMeters, Plugin.LockedPlayerRenderSaverMaxVisiblePlayers, HiddenOwners.Contains(((Object)remotePlayerRenderCandidate.Root).GetInstanceID())); int instanceID = ((Object)remotePlayerRenderCandidate.Root).GetInstanceID(); if (num4) { hashSet.Add(instanceID); HideRoot(remotePlayerRenderCandidate.Root, instanceID); num++; } else { RestoreRoot(instanceID); num2++; } } RestoreStale(hashSet); LastHiddenRemotePlayers = num; LastVisibleRemotePlayers = num2; LastSuppressedRenderers = RendererStates.Count; LastRuntimeState = ((list.Count == 0) ? "no remote player roots found" : ((num == 0) ? "active; every remote player is inside the current budgets" : ((LastSuppressedRenderers == 0) ? "players selected for hiding, but no avatar renderers were found under their panel roots" : "active; avatar renderers suppressed"))); } internal static void RestoreAll() { RestoreAll(Plugin.ShouldApplyPlayerRenderSaver ? "restored" : "off"); } private static void RestoreAll(string runtimeState) { //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_00d0: Unknown result type (might be due to invalid IL or missing references) KeyValuePair[] array = RendererStates.ToArray(); for (int i = 0; i < array.Length; i++) { KeyValuePair keyValuePair = array[i]; if ((Object)(object)keyValuePair.Key != (Object)null) { keyValuePair.Key.enabled = keyValuePair.Value.Enabled; } } KeyValuePair[] array2 = AnimatorStates.ToArray(); for (int i = 0; i < array2.Length; i++) { KeyValuePair keyValuePair2 = array2[i]; if ((Object)(object)keyValuePair2.Key != (Object)null) { keyValuePair2.Key.cullingMode = keyValuePair2.Value.CullingMode; } } KeyValuePair[] array3 = ParticleStates.ToArray(); for (int i = 0; i < array3.Length; i++) { KeyValuePair keyValuePair3 = array3[i]; if ((Object)(object)keyValuePair3.Key != (Object)null) { EmissionModule emission = keyValuePair3.Key.emission; ((EmissionModule)(ref emission)).enabled = keyValuePair3.Value.EmissionEnabled; } } KeyValuePair[] array4 = AudioStates.ToArray(); for (int i = 0; i < array4.Length; i++) { KeyValuePair keyValuePair4 = array4[i]; if ((Object)(object)keyValuePair4.Key != (Object)null) { keyValuePair4.Key.mute = keyValuePair4.Value.Muted; } } RendererStates.Clear(); AnimatorStates.Clear(); ParticleStates.Clear(); AudioStates.Clear(); VisualCache.Clear(); HiddenOwners.Clear(); LastHiddenRemotePlayers = 0; LastVisibleRemotePlayers = 0; LastCandidateRemotePlayers = 0; LastSuppressedRenderers = 0; LastRuntimeState = runtimeState; } private static IEnumerable CollectRemoteCandidates(Vector3 localPosition) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) PlayerPanelController panel = NetworkSingleton.I; if (panel?.PlayerTransforms == null || panel.PlayerTransforms.Count == 0) { yield break; } string localSteamId = GetLocalSteamId(); int count = panel.PlayerTransforms.Count; for (int index = 0; index < count; index++) { NetworkTransform val = panel.PlayerTransforms[index]; Transform val2 = (((Object)(object)val != (Object)null) ? ((Component)val).transform : null); if (!((Object)(object)val2 == (Object)null)) { string a = ((panel.PlayerSteamIDs != null && index < panel.PlayerSteamIDs.Count) ? panel.PlayerSteamIDs[index] : string.Empty); if (string.IsNullOrEmpty(localSteamId) || !string.Equals(a, localSteamId, StringComparison.Ordinal)) { yield return new RemotePlayerRenderCandidate(val2, Vector3.Distance(localPosition, val2.position)); } } } } private static bool TryGetLocalPosition(out Vector3 position) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) position = Vector3.zero; try { TextChannelManager i = NetworkSingleton.I; if ((Object)(object)i?.MainPlayer == (Object)null) { return false; } position = i.MainPlayer.position; return true; } catch { return false; } } private static string GetLocalSteamId() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) try { return ((ulong)SteamUser.GetSteamID()).ToString(); } catch { return string.Empty; } } private static void HideRoot(Transform root, int ownerId) { //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_00f9: Unknown result type (might be due to invalid IL or missing references) //IL_00fe: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Unknown result type (might be due to invalid IL or missing references) //IL_00e3: Unknown result type (might be due to invalid IL or missing references) CachedVisualSet orCreateVisualSet = GetOrCreateVisualSet(root, ownerId); Renderer[] renderers = orCreateVisualSet.Renderers; foreach (Renderer val in renderers) { if (!((Object)(object)val == (Object)null)) { if (!RendererStates.ContainsKey(val)) { RendererStates[val] = new SavedRendererState(val.enabled, ownerId); } val.enabled = false; } } Animator[] animators = orCreateVisualSet.Animators; foreach (Animator val2 in animators) { if (!((Object)(object)val2 == (Object)null)) { if (!AnimatorStates.ContainsKey(val2)) { AnimatorStates[val2] = new SavedAnimatorState(val2.cullingMode, ownerId); } val2.cullingMode = (AnimatorCullingMode)2; } } ParticleSystem[] particles = orCreateVisualSet.Particles; foreach (ParticleSystem val3 in particles) { if (!((Object)(object)val3 == (Object)null)) { if (!ParticleStates.ContainsKey(val3)) { Dictionary particleStates = ParticleStates; EmissionModule emission = val3.emission; particleStates[val3] = new SavedParticleState(((EmissionModule)(ref emission)).enabled, ownerId); } EmissionModule emission2 = val3.emission; ((EmissionModule)(ref emission2)).enabled = false; } } AudioSource[] audioSources = orCreateVisualSet.AudioSources; foreach (AudioSource val4 in audioSources) { if (!((Object)(object)val4 == (Object)null) && !IsEssentialAudio(val4)) { if (!AudioStates.ContainsKey(val4)) { AudioStates[val4] = new SavedAudioState(val4.mute, ownerId); } val4.mute = true; } } HiddenOwners.Add(ownerId); } private static bool IsEssentialAudio(AudioSource audio) { if (audio.spatialBlend <= 0.01f) { return true; } Transform val = ((Component)audio).transform; int num = 0; while ((Object)(object)val != (Object)null && num < 6) { string text = ((Object)val).name ?? string.Empty; if (text.IndexOf("voice", StringComparison.OrdinalIgnoreCase) >= 0 || text.IndexOf("vivox", StringComparison.OrdinalIgnoreCase) >= 0 || text.IndexOf("speech", StringComparison.OrdinalIgnoreCase) >= 0) { return true; } num++; val = val.parent; } return false; } private static void RestoreRoot(int ownerId) { //IL_00bd: Unknown result type (might be due to invalid IL or missing references) KeyValuePair[] array = RendererStates.ToArray(); for (int i = 0; i < array.Length; i++) { KeyValuePair keyValuePair = array[i]; if (keyValuePair.Value.OwnerId == ownerId) { if ((Object)(object)keyValuePair.Key != (Object)null) { keyValuePair.Key.enabled = keyValuePair.Value.Enabled; } RendererStates.Remove(keyValuePair.Key); } } KeyValuePair[] array2 = AnimatorStates.ToArray(); for (int i = 0; i < array2.Length; i++) { KeyValuePair keyValuePair2 = array2[i]; if (keyValuePair2.Value.OwnerId == ownerId) { if ((Object)(object)keyValuePair2.Key != (Object)null) { keyValuePair2.Key.cullingMode = keyValuePair2.Value.CullingMode; } AnimatorStates.Remove(keyValuePair2.Key); } } RestoreOwnedParticles(ownerId); RestoreOwnedAudio(ownerId); HiddenOwners.Remove(ownerId); } private static void RestoreStale(HashSet hiddenOwnerIds) { KeyValuePair[] array = RendererStates.ToArray(); for (int i = 0; i < array.Length; i++) { KeyValuePair keyValuePair = array[i]; if ((Object)(object)keyValuePair.Key == (Object)null || !hiddenOwnerIds.Contains(keyValuePair.Value.OwnerId)) { if ((Object)(object)keyValuePair.Key != (Object)null) { keyValuePair.Key.enabled = keyValuePair.Value.Enabled; } RendererStates.Remove(keyValuePair.Key); } } int[] array2 = HiddenOwners.ToArray(); foreach (int num in array2) { if (!hiddenOwnerIds.Contains(num)) { RestoreRoot(num); } } array2 = VisualCache.Keys.ToArray(); foreach (int key in array2) { if ((Object)(object)VisualCache[key].Root == (Object)null) { VisualCache.Remove(key); } } } private static CachedVisualSet GetOrCreateVisualSet(Transform root, int ownerId) { if (VisualCache.TryGetValue(ownerId, out var value) && (Object)(object)value.Root == (Object)(object)root) { return value; } value = new CachedVisualSet(root, ((Component)root).GetComponentsInChildren(true), ((Component)root).GetComponentsInChildren(true), ((Component)root).GetComponentsInChildren(true), ((Component)root).GetComponentsInChildren(true)); VisualCache[ownerId] = value; return value; } private static void RestoreOwnedParticles(int ownerId) { //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) KeyValuePair[] array = ParticleStates.ToArray(); for (int i = 0; i < array.Length; i++) { KeyValuePair keyValuePair = array[i]; if (keyValuePair.Value.OwnerId == ownerId) { if ((Object)(object)keyValuePair.Key != (Object)null) { EmissionModule emission = keyValuePair.Key.emission; ((EmissionModule)(ref emission)).enabled = keyValuePair.Value.EmissionEnabled; } ParticleStates.Remove(keyValuePair.Key); } } } private static void RestoreOwnedAudio(int ownerId) { KeyValuePair[] array = AudioStates.ToArray(); for (int i = 0; i < array.Length; i++) { KeyValuePair keyValuePair = array[i]; if (keyValuePair.Value.OwnerId == ownerId) { if ((Object)(object)keyValuePair.Key != (Object)null) { keyValuePair.Key.mute = keyValuePair.Value.Muted; } AudioStates.Remove(keyValuePair.Key); } } } } [BepInPlugin("com.bluesage.ontogether.qoltweaks.beta", "BlueSage QoL Tweaks - Beta", "0.2.4")] [BepInProcess("OnTogether.exe")] [BepInProcess("On-Together")] public sealed class Plugin : BaseUnityPlugin { private sealed class DisplayNameDraft { public string BaseName; public string StatusMessage; public string StatusColor; public string StatusBrackets; public bool SpoonsEnabled; public int SpoonCount; public string SpoonLabel; public string SpoonsUpdatedUtc; } private sealed class PendingModerationAction { public string Action; public string TargetSteamId; public string IncidentId; public string LobbyCode; public bool TargetWasOffline; public long ExpiresUnixSeconds; } private const string DefaultSpoonLabel = "sp"; public const string PluginGuid = "com.bluesage.ontogether.qoltweaks.beta"; public const string PluginName = "BlueSage QoL Tweaks - Beta"; public const string PluginVersion = "0.2.4"; public const string PluginDisplayVersion = "0.2.4"; public const string PluginReleaseDate = "2026-07-30"; internal const string PluginBuildIdentity = "0.2.4+20260730.1-public-24414155-release"; private const string StyleStateMigrationTarget = "0.2.4-legacy-style-repair-v1"; private readonly Harmony _harmony = new Harmony("com.bluesage.ontogether.qoltweaks.beta"); private readonly SweepOperationGate _sweepOperationGate = new SweepOperationGate(); private readonly SweepSchedulePolicy _sweepSchedule = new SweepSchedulePolicy(); private Coroutine _sweepCoroutine; private Coroutine _activeSweepOperationCoroutine; private bool _ownsSweepRunInBackgroundOverride; private bool _runInBackgroundBeforeSweepOverride; private Coroutine _compatibilityRefreshCoroutine; private Coroutine _hostHealthCoroutine; private Coroutine _communityBanListCoroutine; private Coroutine _managedReconnectCoroutine; private Coroutine _lastLobbyRejoinFallbackCoroutine; private Coroutine _replacementLobbyDiscoveryCoroutine; private Coroutine _cleanMenuReconnectCoroutine; private Coroutine _reconnectAnnouncementCoroutine; private Coroutine _welcomeMessageCoroutine; private Coroutine _rescueReceiverCapabilityCoroutine; private CSteamID _rescueReceiverCapabilityLobby = CSteamID.Nil; private PluginCompatibilityResult _compatibility; private BlueSageCommandRegistry _commandRegistry; private float _lastLobbyVisibleAt = -1f; private float _lastManagedReconnectAttemptAt = -1f; private int _managedReconnectAttempt; private bool _managedReconnectActive; private bool _managedReconnectSuccessAnnounced; private int _reconnectReadySampleCount; private float _reconnectQuorumReadySince = -1f; private string _lastReconnectLobbyCode = string.Empty; private string _lastReconnectLobbyOwnerSteamId = string.Empty; private string _lastReconnectLobbyNameFingerprint = string.Empty; private bool _replacementLobbyDiscoveryAttempted; private bool _lastLobbyRejoinInProgress; private bool _reconnectMenuFallbackInProgress; private bool _cleanMenuReconnectActive; private int _cleanMenuReconnectGeneration; private string _cleanMenuReconnectPhase = "idle"; private float _hostHealthStartedAt = -1f; private HostHealthDecision _lastHostHealthDecision; private GameObject _mentionPingControllerObject; private GameObject _commandTypeaheadControllerObject; private GameObject _chatReadabilityControllerObject; private GameObject _cloneShieldControllerObject; private GameObject _playerRenderSaverControllerObject; private GameObject _performanceOverlayControllerObject; private GameObject _sessionAuditControllerObject; private GameObject _sessionRunMarkerControllerObject; private GameObject _stewardAnnouncementBridgeControllerObject; private StyleHelperWindow _styleHelperWindow; private QolMenuWindow _qolMenuWindow; private bool _welcomeMessageShownForLobby; private bool _welcomePrimaryShownForLobby; private bool _welcomeContextShownForLobby; private string _welcomeVoiceStateLogKey = string.Empty; private string _welcomeLobbyKey = string.Empty; private string _lastStatusBeforePreset = string.Empty; private PendingModerationAction _pendingModerationAction; private float _nextModerationCapabilityPublishAt; private bool _hadLobbySafetyAccess; internal const int LockedHostHealthIntervalSeconds = 300; internal const bool LockedHostHealthRefreshLobbyMetadata = true; internal const bool LockedHostHealthRequestPersonaRefresh = true; internal const bool LockedHostHealthShowNotifications = false; 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 = 5; internal const int LockedReconnectMaxAttempts = 3; internal const int LockedReconnectPostConnectValidationSeconds = 8; internal const int LockedReconnectReadyStabilitySeconds = 2; internal const int LockedReconnectCleanupTimeoutSeconds = 10; internal const int LockedReconnectJoinValidationTimeoutSeconds = 45; internal const bool LockedReconnectShowNotifications = true; internal const string LockedReconnectAnnouncementMessage = "Auto Reconnected by Blues - BlueSage QoL Tweaks Mod"; internal const int LockedReconnectAnnouncementDelaySeconds = 3; private static NativeAssetSweepOwnershipState? _nativeAssetSweepOwnership; internal static ManualLogSource Log { get; private set; } internal static Plugin Instance { get; private set; } internal static int LockedMaxIdCardCharacters => InputFieldLimitPolicy.ClampProfileCharacters(MaxProfileCharacters?.Value ?? 3000); internal static int LockedMaxSessionNameCharacters => InputFieldLimitPolicy.ClampSessionNameCharacters(MaxSessionNameCharacters?.Value ?? 3000); internal static int LockedMaxChatCharacters => InputFieldLimitPolicy.ClampChatCharacters(MaxChatCharacters?.Value ?? 3000); internal static int LockedChatOutlineIntensityPercent => ChatReadabilityStylePolicy.ClampOutlineIntensityPercent(ChatOutlineIntensity?.Value ?? 75); internal static int LockedChatUiScalePercent => ChatAccessibilityScalePolicy.ClampScalePercent(ChatUiScalePercent?.Value ?? 100); internal static int LockedChatFontSizePercent => ChatFontSizePolicy.ClampFontSizePercent(ChatFontSizePercent?.Value ?? 100); internal static int LockedChatWindowHeightPercent => ChatWindowHeightPolicy.ClampHeightPercent(ChatWindowHeightPercent?.Value ?? 100); internal static int LockedChatHistoryRows => ChatHistoryLimitPolicy.ClampRows(ChatHistoryRows?.Value ?? 150); internal static int LockedPlayerRenderSaverRadiusMeters => PlayerRenderSaverPolicy.ClampRadiusMeters(PlayerRenderSaverRadiusMeters?.Value ?? 45); internal static int LockedPlayerRenderSaverMaxVisiblePlayers => PlayerRenderSaverPolicy.ClampMaxVisiblePlayers(PlayerRenderSaverMaxVisiblePlayers?.Value ?? 32); internal static float LockedSelfSpeedMultiplier => SelfMovementPolicy.ClampSpeed(SelfSpeedMultiplier?.Value ?? 1f); internal static float LockedSelfJumpMultiplier => SelfMovementPolicy.ClampJump(SelfJumpMultiplier?.Value ?? 1f); internal static float LockedSelfGravityMultiplier => SelfMovementPolicy.ClampGravity(SelfGravityMultiplier?.Value ?? 1f); internal static string LockedChatOutlineColorHex => ChatReadabilityStylePolicy.NormalizeHexColor(ChatOutlineColor?.Value, "000000"); internal static string LockedBlackChatOutlineColorHex => ChatReadabilityStylePolicy.NormalizeHexColor(BlackChatOutlineColor?.Value, "C0C0C0"); 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 EnableManualSweepHotkey { get; private set; } internal static ConfigEntry ManualSweepKey { get; private set; } internal static ConfigEntry EnableVivoxPositionRateGuard { 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 EnableChatReadability { get; private set; } internal static ConfigEntry EnablePersistentChatBackdrop { get; private set; } internal static ConfigEntry ChatOutlineIntensity { get; private set; } internal static ConfigEntry ChatUiScalePercent { get; private set; } internal static ConfigEntry ChatFontSizePercent { get; private set; } internal static ConfigEntry ChatWindowHeightPercent { get; private set; } internal static ConfigEntry ChatHistoryRows { get; private set; } internal static ConfigEntry ChatOutlineColor { get; private set; } internal static ConfigEntry BlackChatOutlineColor { get; private set; } internal static ConfigEntry BlackNamesOutline { get; private set; } internal static ConfigEntry EnableBetterMove { get; private set; } internal static ConfigEntry EnableUnlimitedConsumables { get; private set; } internal static ConfigEntry EnableNoclipFly { get; private set; } internal static ConfigEntry SelfSavedPosition { get; private set; } internal static ConfigEntry SelfSpeedMultiplier { get; private set; } internal static ConfigEntry SelfJumpMultiplier { get; private set; } internal static ConfigEntry SelfGravityMultiplier { 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 EnableSpoons { get; private set; } internal static ConfigEntry SpoonCount { get; private set; } internal static ConfigEntry SpoonLabel { get; private set; } internal static ConfigEntry SpoonsUpdatedUtc { get; private set; } internal static ConfigEntry StatusBaseNameLegacyBackup { get; private set; } internal static ConfigEntry StyleStateMigrationVersion { get; private set; } internal static ConfigEntry EnableLeaveNotifications { get; private set; } internal static ConfigEntry EnableWelcomeMessage { 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 EnableCommunityBanListSync { get; private set; } internal static ConfigEntry EnableFocusAnywhere { get; private set; } internal static ConfigEntry EnableEnhancedPlayerPanel { get; private set; } internal static ConfigEntry EnableCloneShield { get; private set; } internal static ConfigEntry EnablePlayerRenderSaver { get; private set; } internal static ConfigEntry EnablePerformanceOverlay { get; private set; } internal static ConfigEntry PerformanceOverlayRefreshSeconds { get; private set; } internal static ConfigEntry PerformanceOverlayFontSize { get; private set; } internal static ConfigEntry PerformanceOverlayOffsetX { get; private set; } internal static ConfigEntry PerformanceOverlayOffsetY { get; private set; } internal static ConfigEntry PerformanceOverlayWidth { get; private set; } internal static ConfigEntry PlayerRenderSaverRadiusMeters { get; private set; } internal static ConfigEntry PlayerRenderSaverMaxVisiblePlayers { get; private set; } internal static ConfigEntry EnableHiddenDiagnostics { get; private set; } internal static ConfigEntry EnableLocalAuditExports { get; private set; } internal static ConfigEntry EnableStewardAnnouncementBridge { get; private set; } internal static ConfigEntry StewardBridgeKeyPath { get; private set; } internal static ConfigEntry StewardBridgeServiceIdentity { get; private set; } internal static ConfigEntry EnablePingMentions { get; private set; } internal static ConfigEntry EnablePingSound { get; private set; } internal static ConfigEntry PingHighlightColor { get; private set; } internal static ConfigEntry PingMentionColor { get; private set; } internal static ConfigEntry PingSoundMode { get; private set; } internal static ConfigEntry LobbyPingDelegates { get; private set; } internal static ConfigEntry LobbyPingDelegateOwnerSteamId { get; private set; } internal static ConfigEntry LobbyModerationDelegates { get; private set; } internal static ConfigEntry LobbyModerationDelegateOwnerSteamId { get; private set; } internal static ConfigEntry LobbyHelpers { get; private set; } internal static ConfigEntry LobbyHelperOwnerSteamId { get; private set; } internal static int LockedLobbyHelperCount => LobbyPingAuthorizationPolicy.ParseDelegates(LobbyHelpers?.Value).Length; internal static int LockedLobbyPingDelegateCount => LockedLobbyHelperCount; internal static int LockedLobbyModerationDelegateCount => LockedLobbyHelperCount; internal static ConfigEntry EnableChatCopy { get; private set; } internal static ConfigEntry EnableChatUrlLinks { get; private set; } internal static ConfigEntry EnableStyleUi { get; private set; } internal static ConfigEntry EnableExtendedAvatarStyles { get; private set; } internal static ConfigEntry EnableChalkboardPersistence { get; private set; } internal static ConfigEntry EnableMiniMapPlayerLabels { get; private set; } internal static ConfigEntry EnableMiniMapFriendColors { get; private set; } internal static ConfigEntry UiThemePreset { get; private set; } internal static ConfigEntry MaxChatCharacters { get; private set; } internal static ConfigEntry MaxProfileCharacters { get; private set; } internal static ConfigEntry MaxSessionNameCharacters { get; private set; } internal static ConfigEntry ConfigMigrationVersion { get; private set; } internal static NativeAssetSweepOwnershipState NativeAssetSweepOwnership { get { NativeAssetSweepOwnershipState? nativeAssetSweepOwnership = _nativeAssetSweepOwnership; if (!nativeAssetSweepOwnership.HasValue) { NativeAssetSweepOwnershipState? nativeAssetSweepOwnershipState = (_nativeAssetSweepOwnership = DetectNativeAssetSweepOwnership()); return nativeAssetSweepOwnershipState.Value; } return nativeAssetSweepOwnership.GetValueOrDefault(); } } internal static bool NativeAssetSweepDetected => NativeAssetSweepOwnership != NativeAssetSweepOwnershipState.BlueSage; internal static bool NativeAssetSweepOwnershipVerified => NativeAssetSweepOwnership == NativeAssetSweepOwnershipState.NativeGame; internal static bool AutoSweepAllowed { get { if ((Object)(object)Instance != (Object)null && Instance._compatibility != null) { return NativeAssetSweepPolicy.AllowBlueSageAutoSweep(NativeAssetSweepDetected, Instance._compatibility.AutoSweepAllowed, EnableAutoSweep?.Value ?? false); } return false; } } internal static bool ManualSweepAllowed { get { if ((Object)(object)Instance != (Object)null && Instance._compatibility != null) { return NativeAssetSweepPolicy.AllowBlueSageManualSweep(NativeAssetSweepDetected, Instance._compatibility.ManualSweepAllowed); } return false; } } internal static bool HostCleanupLaneHealthy { get { if (!NativeAssetSweepOwnershipVerified) { return AutoSweepAllowed; } return true; } } internal static bool HostReconnectLaneHealthy { get { if ((Object)(object)Instance != (Object)null) { ConfigEntry enableReconnectGuard = EnableReconnectGuard; if (enableReconnectGuard != null && enableReconnectGuard.Value && !Instance._managedReconnectActive) { return !Instance._cleanMenuReconnectActive; } } return false; } } internal static bool ChatTweaksAllowed { get { if ((Object)(object)Instance != (Object)null) { return Instance._compatibility.ChatTweaksAllowed; } return false; } } internal static bool HostHealthRepairsAllowed { get { if ((Object)(object)Instance != (Object)null && Instance._compatibility != null) { return Instance._compatibility.HostHealthRepairsAllowed; } 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 ShouldApplyChatReadability { get { if (EnableChatReadability != null) { return EnableChatReadability.Value; } return false; } } internal static bool ShouldApplyPersistentChatBackdrop { get { if (EnablePersistentChatBackdrop != null) { return EnablePersistentChatBackdrop.Value; } return false; } } internal static bool ShouldApplyBlackNamesOutline { get { if (BlackNamesOutline != null) { return BlackNamesOutline.Value; } return false; } } internal static bool ShouldApplyBetterMove { get { if (EnableBetterMove != null) { return EnableBetterMove.Value; } return false; } } internal static bool ShouldApplyUnlimitedConsumables { get { if (EnableUnlimitedConsumables != null) { return EnableUnlimitedConsumables.Value; } return false; } } internal static bool ShouldApplyNoclipFly { get { if (EnableNoclipFly != null) { return EnableNoclipFly.Value; } return false; } } internal static bool ShouldApplySelfMovementTuning { get { if (!(Math.Abs(LockedSelfSpeedMultiplier - 1f) > 0.001f) && !(Math.Abs(LockedSelfJumpMultiplier - 1f) > 0.001f)) { return Math.Abs(LockedSelfGravityMultiplier - 1f) > 0.001f; } return true; } } internal static bool ShouldApplySpoons { get { if (EnableSpoons != null) { return EnableSpoons.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; } } internal static bool ShouldApplyCloneShield { get { if (EnableCloneShield != null) { return EnableCloneShield.Value; } return false; } } internal static bool ShouldApplyPlayerRenderSaver { get { if (EnablePlayerRenderSaver != null) { return EnablePlayerRenderSaver.Value; } return false; } } internal static bool ShouldApplyChatCopy { get { if (EnableChatCopy != null && EnableChatCopy.Value) { return !IsPluginLoaded("com.jai.ontogether.copychat"); } return false; } } internal static bool ShouldApplyChatUrlLinks { get { if (EnableChatUrlLinks != null) { return EnableChatUrlLinks.Value; } return false; } } internal static bool ShouldApplyMiniMapPlayerLabels { get { if (EnableMiniMapPlayerLabels != null) { return EnableMiniMapPlayerLabels.Value; } return false; } } internal static bool ShouldApplyMiniMapFriendColors { get { if (EnableMiniMapFriendColors != null) { return EnableMiniMapFriendColors.Value; } return false; } } internal static bool ShouldEmitHiddenDiagnostics => true; public static bool IsPerformanceOverlayActive => PerformanceOverlayOwnershipPolicy.IsActive((Object)(object)Instance != (Object)null, (Object)(object)Instance != (Object)null && ((Behaviour)Instance).enabled, observationOnly: false, (Object)(object)Instance?._performanceOverlayControllerObject != (Object)null, EnablePerformanceOverlay?.Value ?? false); internal static bool IsStyleHelperVisible { get { Plugin instance = Instance; if (instance == null) { return false; } return instance._styleHelperWindow?.IsVisible == true; } } private static NativeAssetSweepOwnershipState DetectNativeAssetSweepOwnership() { bool mainSceneTimerPresent = false; bool settingsDurationPresent = false; bool unloadUnusedAssetsCallPresent = false; try { Type type = AccessTools.TypeByName("MainSceneManager") ?? AccessTools.TypeByName("AssetLoadingManager"); Type type2 = AccessTools.TypeByName("SettingsData"); if (type == null || type2 == null) { return NativeAssetSweepPolicy.ResolveOwnership(mainSceneTimerPresent, settingsDurationPresent, unloadUnusedAssetsCallPresent, inspectionFailed: true); } mainSceneTimerPresent = AccessTools.Field(type, "_sweepTimer") != null; settingsDurationPresent = AccessTools.Field(type2, "SweepDuration") != null; MethodInfo methodInfo = AccessTools.Method(type, "Update", Type.EmptyTypes, (Type[])null); if (methodInfo == null) { return NativeAssetSweepPolicy.ResolveOwnership(mainSceneTimerPresent, settingsDurationPresent, unloadUnusedAssetsCallPresent, inspectionFailed: true); } foreach (CodeInstruction originalInstruction in PatchProcessor.GetOriginalInstructions((MethodBase)methodInfo, (ILGenerator)null)) { if (originalInstruction.operand is MethodInfo methodInfo2 && string.Equals(methodInfo2.DeclaringType?.FullName, "UnityEngine.Resources", StringComparison.Ordinal) && string.Equals(methodInfo2.Name, "UnloadUnusedAssets", StringComparison.Ordinal) && methodInfo2.GetParameters().Length == 0) { unloadUnusedAssetsCallPresent = true; break; } } return NativeAssetSweepPolicy.ResolveOwnership(mainSceneTimerPresent, settingsDurationPresent, unloadUnusedAssetsCallPresent, inspectionFailed: false); } catch { return NativeAssetSweepPolicy.ResolveOwnership(mainSceneTimerPresent, settingsDurationPresent, unloadUnusedAssetsCallPresent, inspectionFailed: true); } } private void Awake() { Instance = this; Log = ((BaseUnityPlugin)this).Logger; BindConfig(); ApplyOneTimeConfigMigration(); ApplyStyleStateMigration(); SyncNotificationTimestampsWithChatTimestamps("startup config normalization"); RegisterBlueSageCommands(); RefreshCompatibility("initial BepInEx loaded-plugin scan"); if (NativeAssetSweepOwnershipVerified) { Log.LogInfo((object)"The native game cleanup timer is present and remains untouched. BlueSage Automatic Cleanup is a separate opt-in loop; explicit manual cleanup remains available when compatibility allows it."); } else if (NativeAssetSweepDetected) { Log.LogWarning((object)"Native asset-cleanup inspection is incomplete. BlueSage will not alter native behavior; its separate automatic loop remains opt-in and explicit manual cleanup remains available when compatibility allows it."); } ApplyHarmonyPatchesFailOpen(); StartRescueReceiverCapabilityPublisher(); RestartSweepLoop(); RestartHostHealthLoop(); StartCommunityBanListMaintenance(); StartMentionPingController(); StartCommandTypeaheadController(); StartChatReadabilityController(); StartSessionAuditController(); StartSessionRunMarkerController(); StartStewardAnnouncementBridgeController(); StartCloneShieldController(); StartPlayerRenderSaverController(); StartPerformanceOverlayController(); StartStyleHelperWindow(); _compatibilityRefreshCoroutine = ((MonoBehaviour)this).StartCoroutine(RefreshCompatibilityAfterLoadSettles()); StartWelcomeMessageLoop(); Log.LogInfo((object)string.Format("{0} {1} loaded. Build={2}, NativeAssetSweep={3}, AutoSweep={4}, ChatTweaksAllowed={5}.", "BlueSage QoL Tweaks - Beta", "0.2.4", "0.2.4+20260730.1-public-24414155-release", NativeAssetSweepDetected, AutoSweepAllowed, ChatTweaksAllowed)); } private void ApplyHarmonyPatchesFailOpen() { Type[] typesFromAssembly = AccessTools.GetTypesFromAssembly(typeof(Plugin).Assembly); foreach (Type type in typesFromAssembly) { bool flag = type.GetCustomAttributes(typeof(HarmonyPatch), inherit: true).Length != 0; if (!flag) { MethodInfo[] methods = type.GetMethods(AccessTools.all); for (int j = 0; j < methods.Length; j++) { if (methods[j].GetCustomAttributes(typeof(HarmonyPatch), inherit: true).Length != 0) { flag = true; break; } } } if (flag) { ApplyHarmonyPatchTypeFailOpen(type); } } } private void ApplyHarmonyPatchTypeFailOpen(Type patchType) { try { _harmony.CreateClassProcessor(patchType).Patch(); } catch (Exception ex) { Log.LogError((object)("Harmony patch type '" + patchType.FullName + "' skipped after compatibility failure: " + ex.GetType().Name + ": " + ex.Message + ". Continuing with compatible controllers; Home, Insert, sweep, and menu startup remain available.")); } } private void Update() { //IL_0036: Unknown result type (might be due to invalid IL or missing references) if (PluginShutdownController.IsShuttingDown) { return; } SteamRuntimeFacade.CaptureLive(); if (Input.GetKeyDown((KeyCode)278)) { ToggleQolMenuFromHotkey(); } if (Input.GetKeyDown((KeyCode)277)) { ToggleStyleUiFromHotkey(); } if (Input.GetKeyDown(ManualSweepKey.Value)) { RunSweep("manual hotkey", requireManualPermission: true); } if (Time.unscaledTime >= _nextModerationCapabilityPublishAt) { _nextModerationCapabilityPublishAt = Time.unscaledTime + 5f; SteamIdModerationController.PublishHostCapability(); bool isHost; bool flag = CanUseLobbySafety(out isHost); bool num = _hadLobbySafetyAccess != flag; if (_hadLobbySafetyAccess && !flag) { _pendingModerationAction = null; } _hadLobbySafetyAccess = flag; if (num) { PlayerIdentityEvidenceController.InvalidateRosterCache(); PlayerIdentityRoleHighlightController.RefreshAll(); } } SteamIdModerationController.PollHostMemberDataRequests(); CloneIncidentActionController.PollHostMemberDataRequests(); ChalkboardPersistenceController.PollHostMemberDataRequests(); IdentityResyncController.Tick(); } private void ToggleQolMenuFromHotkey() { if ((Object)(object)_qolMenuWindow == (Object)null) { StartQolMenuWindow(); } if ((Object)(object)_qolMenuWindow == (Object)null) { ManualLogSource log = Log; if (log != null) { log.LogWarning((object)"QoL menu hotkey pressed, but QoL menu window could not start."); } return; } _qolMenuWindow.ToggleVisible(); ManualLogSource log2 = Log; if (log2 != null) { log2.LogInfo((object)"QoL menu toggled via Home key."); } } private void ToggleStyleUiFromHotkey() { string text = ToggleStyleUiFromQolMenu(); ManualLogSource log = Log; if (log != null) { log.LogInfo((object)("Style Helper toggled via Insert key. " + text)); } } private void OnApplicationQuit() { BeginPluginShutdown("application-quit"); } private void OnDestroy() { BeginPluginShutdown("destroy"); } private void BeginPluginShutdown(string trigger) { PluginShutdownController.Begin(trigger, RunSteamShutdownCleanup, RunLocalShutdownCleanup); } private void RunSteamShutdownCleanup(SteamRuntimeSnapshot snapshot) { ClearRescueReceiverCapability(snapshot); SteamIdModerationController.ClearHostCapabilityForShutdown(snapshot); PlayerRoleLabelController.ClearPublishedStateForShutdown(snapshot); ClearHostHealthPresenceForShutdown(snapshot); } private static void ClearHostHealthPresenceForShutdown(SteamRuntimeSnapshot snapshot) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0028: 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) if (snapshot == null || !snapshot.SteamWasReady || !snapshot.LocalWasOwner || snapshot.Lobby == CSteamID.Nil) { return; } try { SteamMatchmaking.SetLobbyData(snapshot.Lobby, "bluesage_qol_heartbeat", string.Empty); SteamMatchmaking.SetLobbyData(snapshot.Lobby, "heartbeat", string.Empty); } catch { } } private void RunLocalShutdownCleanup() { SessionRunMarkerController.MarkCleanClose(); if (_rescueReceiverCapabilityCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_rescueReceiverCapabilityCoroutine); _rescueReceiverCapabilityCoroutine = null; } if (_sweepCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_sweepCoroutine); _sweepCoroutine = null; } _sweepSchedule.Reset(); if (_activeSweepOperationCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_activeSweepOperationCoroutine); _activeSweepOperationCoroutine = null; _sweepOperationGate.Complete(_sweepOperationGate.ActiveSequence); } ReleaseSweepBackgroundExecution(); if (_compatibilityRefreshCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_compatibilityRefreshCoroutine); _compatibilityRefreshCoroutine = null; } if (_managedReconnectCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_managedReconnectCoroutine); _managedReconnectCoroutine = null; } if (_lastLobbyRejoinFallbackCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_lastLobbyRejoinFallbackCoroutine); _lastLobbyRejoinFallbackCoroutine = null; } if (_replacementLobbyDiscoveryCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_replacementLobbyDiscoveryCoroutine); _replacementLobbyDiscoveryCoroutine = null; } if (_cleanMenuReconnectCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_cleanMenuReconnectCoroutine); _cleanMenuReconnectCoroutine = null; } _reconnectMenuFallbackInProgress = false; _cleanMenuReconnectActive = false; if (_hostHealthCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_hostHealthCoroutine); _hostHealthCoroutine = null; } if (_communityBanListCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_communityBanListCoroutine); _communityBanListCoroutine = null; } if (_reconnectAnnouncementCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_reconnectAnnouncementCoroutine); _reconnectAnnouncementCoroutine = null; } if (_welcomeMessageCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_welcomeMessageCoroutine); _welcomeMessageCoroutine = null; } if ((Object)(object)_mentionPingControllerObject != (Object)null) { Object.Destroy((Object)(object)_mentionPingControllerObject); _mentionPingControllerObject = null; } if ((Object)(object)_commandTypeaheadControllerObject != (Object)null) { Object.Destroy((Object)(object)_commandTypeaheadControllerObject); _commandTypeaheadControllerObject = null; } if ((Object)(object)_chatReadabilityControllerObject != (Object)null) { Object.Destroy((Object)(object)_chatReadabilityControllerObject); _chatReadabilityControllerObject = null; } if ((Object)(object)_cloneShieldControllerObject != (Object)null) { Object.Destroy((Object)(object)_cloneShieldControllerObject); _cloneShieldControllerObject = null; } if ((Object)(object)_playerRenderSaverControllerObject != (Object)null) { Object.Destroy((Object)(object)_playerRenderSaverControllerObject); _playerRenderSaverControllerObject = null; } if ((Object)(object)_performanceOverlayControllerObject != (Object)null) { Object.Destroy((Object)(object)_performanceOverlayControllerObject); _performanceOverlayControllerObject = null; } if ((Object)(object)_sessionAuditControllerObject != (Object)null) { Object.Destroy((Object)(object)_sessionAuditControllerObject); _sessionAuditControllerObject = null; } if ((Object)(object)_stewardAnnouncementBridgeControllerObject != (Object)null) { Object.Destroy((Object)(object)_stewardAnnouncementBridgeControllerObject); _stewardAnnouncementBridgeControllerObject = null; } if ((Object)(object)_sessionRunMarkerControllerObject != (Object)null) { Object.Destroy((Object)(object)_sessionRunMarkerControllerObject); _sessionRunMarkerControllerObject = null; } PlayerRenderSaverController.RestoreAll(); SelfMovementPatch.CleanupRuntimeState(); if (EnableNoclipFly != null && EnableNoclipFly.Value) { EnableNoclipFly.Value = false; ((BaseUnityPlugin)this).Config.Save(); } HostVisibilityProtocolPatch.RestoreAll(); PlayerRoleLabelController.RestoreAll(); PlayerIdentityRoleHighlightController.ClearAll(); PlayerPanelSurfaceController.ClearAll(); IdentityResyncController.Reset(); MiniMapPlayerLabelPatch.ClearAll(); if ((Object)(object)_styleHelperWindow != (Object)null) { Object.Destroy((Object)(object)((Component)_styleHelperWindow).gameObject); _styleHelperWindow = null; } if ((Object)(object)_qolMenuWindow != (Object)null) { Object.Destroy((Object)(object)((Component)_qolMenuWindow).gameObject); _qolMenuWindow = null; } BlueSageUiTheme.ReleaseRuntimeTextures(); ChatReadabilityPatch.RestoreChatAccessibilityScale(); ChatReadabilityPatch.RestoreChatFontSize(); ChatReadabilityPatch.RestoreChatWindowHeight(); _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, "Run BlueSage Automatic Cleanup on its own coordinated 10-minute default timer. Existing saved off choices remain off; manual cleanup remains available."); 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", true, "Allow manual BlueSage cleanup when AndrewLin Sweep is installed. Compatibility guards still apply; Automatic Cleanup does not authorize manual actions."); SweepIntervalMinutes = ((BaseUnityPlugin)this).Config.Bind("Asset Sweep", "SweepIntervalMinutes", 10, "Minutes between BlueSage automatic cleanups. Safe range 5 to 180. Used only when BlueSage Automatic Cleanup is explicitly enabled; the game native interval is never changed."); EnableManualSweepHotkey = ((BaseUnityPlugin)this).Config.Bind("Asset Sweep", "EnableManualSweepHotkey", true, "Deprecated compatibility key retained so existing configs still load. ManualSweepKey always remains active; menu and slash-command cleanup remain available."); ManualSweepKey = ((BaseUnityPlugin)this).Config.Bind("Asset Sweep", "ManualSweepKey", (KeyCode)289, "Hotkey for manual sweep."); EnableVivoxPositionRateGuard = ((BaseUnityPlugin)this).Config.Bind("Performance Safety", "EnableVivoxPositionRateGuard", true, "Coalesce the game's per-frame Vivox 3D-position requests to the Unity-recommended 4 Hz. Disable only for compatibility testing."); EnableChatTimestamps = ((BaseUnityPlugin)this).Config.Bind("Chat", "EnableChatTimestamps", true, "Add local timestamps before player names."); EnableNotificationTimestamps = ((BaseUnityPlugin)this).Config.Bind("Chat", "EnableNotificationTimestamps", true, "Add local timestamps before system notifications."); Use24HourTime = ((BaseUnityPlugin)this).Config.Bind("Chat", "Use24HourTime", false, "Use 24-hour timestamps when timestamp features are enabled. Off uses 12-hour time with AM/PM."); 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."); EnableChatReadability = ((BaseUnityPlugin)this).Config.Bind("Chat Readability", "EnableChatReadability", true, "Add local outline and shadow to chat text so white or colored messages stay readable on bright backgrounds."); EnablePersistentChatBackdrop = ((BaseUnityPlugin)this).Config.Bind("Chat Readability", "EnablePersistentChatBackdrop", true, "Keep the local chat panel/backdrop easier to read when the game exposes it. Local UI only; does not rewrite messages."); ChatOutlineIntensity = ((BaseUnityPlugin)this).Config.Bind("Chat Readability", "ChatOutlineIntensity", 75, "Chat outline strength from 25 to 100. Default 75 keeps chat readable with a stronger outline; 100 is the crispest outline."); ChatUiScalePercent = ((BaseUnityPlugin)this).Config.Bind("Chat Readability", "ChatUiScalePercent", 100, "Local chat UI accessibility scale from 75 to 200 percent. Default 100 keeps vanilla sizing; larger values enlarge the chat panel and visible message text."); ChatFontSizePercent = ((BaseUnityPlugin)this).Config.Bind("Chat Readability", "ChatFontSizePercent", 100, "Local chat message font size from 75 to 200 percent. Default 100 keeps vanilla text size; larger values enlarge message text without scaling the whole chat shell."); ChatWindowHeightPercent = ((BaseUnityPlugin)this).Config.Bind("Chat Readability", "ChatWindowHeightPercent", 100, "Local chat window height from 100 to 200 percent. Default 100 keeps vanilla size; larger values add vertical chat space without scaling text."); ChatHistoryRows = ((BaseUnityPlugin)this).Config.Bind("Chat Readability", "ChatHistoryRows", 150, "Retained chat rows from 25 to 250. Default 150 balances useful scrollback with large-lobby UI cost; BlueSage never lowers a higher vanilla or other-mod limit."); ChatOutlineColor = ((BaseUnityPlugin)this).Config.Bind("Chat Readability", "ChatOutlineColor", "000000", "Six-character hex color for the normal local chat outline path. Default 000000."); BlackChatOutlineColor = ((BaseUnityPlugin)this).Config.Bind("Chat Readability", "BlackChatOutlineColor", "C0C0C0", "Six-character hex color for true-black styled chat/name outline path. Default C0C0C0 light grey."); BlackNamesOutline = ((BaseUnityPlugin)this).Config.Bind("Chat Readability", "BlackNamesOutline", true, "Use the black-text outline color for true-black chat/name rows. On by default so black styled names and messages stay readable; turn off to force the normal outline color."); EnableBetterMove = ((BaseUnityPlugin)this).Config.Bind("BetterMove", "EnableBetterMove", true, "Hold Shift to run or Ctrl to slow-walk. This changes only your character and is safe to turn off."); EnableUnlimitedConsumables = ((BaseUnityPlugin)this).Config.Bind("Consumables", "EnableUnlimitedConsumables", true, "Once you own a canteen item or chalk color, using it no longer lowers its count. Clothing, bait, rods, and bobbers stay vanilla. Turn this off to restore normal item use immediately."); EnableNoclipFly = ((BaseUnityPlugin)this).Config.Bind("Self Movement", "EnableNoclipFly", false, "Self-only noclip/fly mode. Always reset off when BlueSage loads or unloads. Controls: WASD, Space/E up, Ctrl/Q down, Shift boost, Alt precision."); SelfSavedPosition = ((BaseUnityPlugin)this).Config.Bind("Self Movement", "SavedPosition", string.Empty, "Local saved self-teleport position in x,y,z format."); SelfSpeedMultiplier = ((BaseUnityPlugin)this).Config.Bind("Self Movement", "SpeedMultiplier", 1f, "Self-only movement speed multiplier. 1 is vanilla; range 0.25 to 5."); SelfJumpMultiplier = ((BaseUnityPlugin)this).Config.Bind("Self Movement", "JumpMultiplier", 1f, "Self-only jump-height multiplier. 1 is vanilla; range 0.25 to 5."); SelfGravityMultiplier = ((BaseUnityPlugin)this).Config.Bind("Self Movement", "GravityMultiplier", 1f, "Self-only gravity/fall multiplier. 1 is vanilla; range 0.1 to 5."); if (EnableNoclipFly.Value) { EnableNoclipFly.Value = false; ((BaseUnityPlugin)this).Config.Save(); } 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."); EnableSpoons = ((BaseUnityPlugin)this).Config.Bind("Spoons", "EnableSpoons", false, "Optional compact social-battery tag for your BlueSage display name. Use /spoons on, Save Spoons, or Show Tag if you want it visible."); SpoonCount = ((BaseUnityPlugin)this).Config.Bind("Spoons", "SpoonCount", 5, "Social battery from 0 to 5. Use /spoons 3 or Style Helper > Status."); SpoonLabel = ((BaseUnityPlugin)this).Config.Bind("Spoons", "SpoonLabel", "sp", "Short custom label used after 0/5. Use /setmood label; default is sp."); SpoonsUpdatedUtc = ((BaseUnityPlugin)this).Config.Bind("Spoons", "SpoonsUpdatedUtc", string.Empty, "UTC timestamp for the last spoon update."); EnableStyleUi = ((BaseUnityPlugin)this).Config.Bind("Style Helper", "EnableStyleUi", true, "Enable the BlueSage /styleui preset and clipboard helper window."); EnableExtendedAvatarStyles = ((BaseUnityPlugin)this).Config.Bind("Avatar Styles", "EnableExtendedAvatarStyles", true, "Add reversible avatar preset slots 4-9 while preserving vanilla slots 1-3."); EnableChalkboardPersistence = ((BaseUnityPlugin)this).Config.Bind("Chalkboards", "EnableChalkboardPersistence", true, "Save chalkboards locally. Only the host can restore a saved board for the lobby. Automatically stands down when Chalky is installed."); MaxChatCharacters = ((BaseUnityPlugin)this).Config.Bind("Text Limits", "MaxChatCharacters", 3000, "Default 3000. Advanced users may raise this up to 4000, but longer chat can behave oddly with UI wrapping, rich-text tags, or future game updates."); MaxProfileCharacters = ((BaseUnityPlugin)this).Config.Bind("Text Limits", "MaxProfileCharacters", 3000, "Default 3000 for names, ID cards, and styled profile text. Raising this up to 4000 is optional and may be unstable in some UI fields."); MaxSessionNameCharacters = ((BaseUnityPlugin)this).Config.Bind("Text Limits", "MaxSessionNameCharacters", 3000, "Default 3000 for room/session names. Raising this up to 4000 is optional and may be unstable or visually messy."); EnableLeaveNotifications = ((BaseUnityPlugin)this).Config.Bind("Notifications", "EnableLeaveNotifications", true, "Show a local notification when a player leaves. Auto-disabled when overlapping chat mods are detected."); EnableWelcomeMessage = ((BaseUnityPlugin)this).Config.Bind("Notifications", "EnableWelcomeMessage", true, "Show BlueSage QoL navigation tips once each time you join a lobby."); EnableReconnectGuard = ((BaseUnityPlugin)this).Config.Bind("Reconnect Guard", "EnableReconnectGuard", true, "Try one guarded rejoin after the game reports a real disconnect, then stop safely if the lobby is not playable."); 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, "Quiet background checks for long-running lobbies. Most useful for hosts; use /hh run for a manual check."); EnableCommunityBanListSync = ((BaseUnityPlugin)this).Config.Bind("Lobby Safety", "EnableCommunityBanListSync", false, "Off by default. Any QoL client may opt in to periodic local validation of the AndrewLimForFun On-Together community BanData source. Validation is atomic and appends only missing pairs to this client's native ban store. Existing rows are never changed or removed; refresh never kicks, bans, or grants authority."); 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 local @mention and ID-card buttons, plus a verified Steam-ID tooltip on the native green profile button. Native player-panel actions remain available."); EnableEnhancedPlayerPanel.SettingChanged += delegate { if (!EnableEnhancedPlayerPanel.Value) { PlayerPanelSurfaceController.ClearAll(); } }; EnableCloneShield = ((BaseUnityPlugin)this).Config.Bind("Enhanced Player Panel", "EnableCloneShield", true, "Send one cautious public alert when someone newly matches your full avatar/outfit or exact styled name. The alert is a clue, not an automatic moderation action."); EnablePlayerRenderSaver = ((BaseUnityPlugin)this).Config.Bind("Performance", "EnablePlayerRenderSaver", false, "EXPERIMENTAL opt-in: locally hide far/overflow remote player renderers in very large lobbies while keeping chat and player-list rows visible."); EnablePerformanceOverlay = ((BaseUnityPlugin)this).Config.Bind("Performance Overlay", "EnablePerformanceOverlay", true, "Show the public draggable FPS/PING/RAM bar. Hosts honestly show PING -- because they have no remote round trip; clients show active-session RTT when available."); PerformanceOverlayRefreshSeconds = ((BaseUnityPlugin)this).Config.Bind("Performance Overlay", "RefreshSeconds", 0.25f, "Overlay refresh interval. Runtime clamps 0.25 to 5 seconds."); PerformanceOverlayFontSize = ((BaseUnityPlugin)this).Config.Bind("Performance Overlay", "FontSize", 13, "Overlay font size. Runtime clamps 8 to 32."); PerformanceOverlayOffsetX = ((BaseUnityPlugin)this).Config.Bind("Performance Overlay", "OffsetX", 12f, "Saved draggable overlay X position."); PerformanceOverlayOffsetY = ((BaseUnityPlugin)this).Config.Bind("Performance Overlay", "OffsetY", 174f, "Saved draggable overlay Y position."); PerformanceOverlayWidth = ((BaseUnityPlugin)this).Config.Bind("Performance Overlay", "Width", 320f, "Overlay width. Runtime keeps at least 320 pixels so FPS/PING/RAM remain one straight line."); PlayerRenderSaverRadiusMeters = ((BaseUnityPlugin)this).Config.Bind("Performance", "PlayerRenderSaverRadiusMeters", 45, "Player Render Saver keeps remote players inside this distance visible. Safe range 10 to 250 meters."); PlayerRenderSaverMaxVisiblePlayers = ((BaseUnityPlugin)this).Config.Bind("Performance", "PlayerRenderSaverMaxVisiblePlayers", 32, "Player Render Saver also keeps this many closest remote players visible. Safe range 1 to 128."); EnablePingMentions = ((BaseUnityPlugin)this).Config.Bind("Ping Mentions", "EnablePingMentions", true, "Highlight personal mentions, offer simple autocomplete, and play a local ping sound when you are mentioned. Lobby-wide pings are limited to the current host and assigned Helpers."); EnablePingSound = ((BaseUnityPlugin)this).Config.Bind("Ping Mentions", "EnablePingSound", true, "Play the local UI ping sound when a message mentions you."); PingHighlightColor = ((BaseUnityPlugin)this).Config.Bind("Ping Mentions", "PingHighlightColor", "9B59B6", "Six-character hex color for mentioned-message background highlight."); PingMentionColor = ((BaseUnityPlugin)this).Config.Bind("Ping Mentions", "PingMentionColor", "FFD700", "Six-character hex color for @mention text."); PingSoundMode = ((BaseUnityPlugin)this).Config.Bind("Ping Mentions", "PingSoundMode", "ticket", "Local mention ping sound: click, change, error, task, or ticket. Default ticket. Use /pingsound off to mute."); LobbyHelpers = ((BaseUnityPlugin)this).Config.Bind("Helpers", "LobbyHelpers", string.Empty, "Exact SteamID64 values assigned by the current lobby host. Helpers may use @lobby and request host-validated moderation actions."); LobbyHelperOwnerSteamId = ((BaseUnityPlugin)this).Config.Bind("Helpers", "LobbyHelperOwnerSteamId", string.Empty, "Steam ID of the host who granted the helper list; prevents permissions carrying across unrelated hosts."); LobbyPingDelegates = ((BaseUnityPlugin)this).Config.Bind("Legacy Helpers", "LobbyPingDelegates", string.Empty, "Legacy @lobby helper list. BlueSage imports matching host-scoped entries into Helpers."); LobbyPingDelegateOwnerSteamId = ((BaseUnityPlugin)this).Config.Bind("Legacy Helpers", "LobbyPingDelegateOwnerSteamId", string.Empty, "Legacy @lobby helper host scope retained for safe migration."); LobbyModerationDelegates = ((BaseUnityPlugin)this).Config.Bind("Legacy Helpers", "LobbyModerationDelegates", string.Empty, "Legacy moderation helper list. BlueSage imports matching host-scoped entries into Helpers."); LobbyModerationDelegateOwnerSteamId = ((BaseUnityPlugin)this).Config.Bind("Legacy Helpers", "LobbyModerationDelegateOwnerSteamId", string.Empty, "Legacy moderation helper host scope retained for safe migration."); EnableChatCopy = ((BaseUnityPlugin)this).Config.Bind("Chat Copy", "EnableChatCopy", true, "Ctrl-left-click a chat message to copy only its text while plain left-click stays with the vanilla ID card. Right-click remains supported where the game delivers it. Local-only; no names, timestamps, or status labels."); EnableChatUrlLinks = ((BaseUnityPlugin)this).Config.Bind("Chat Copy", "EnableChatUrlLinks", true, "Highlight safe http/https links. Shift-left-click once copies and warns; repeat within 10 seconds to open. Plain left-click remains the vanilla ID card."); EnableMiniMapPlayerLabels = ((BaseUnityPlugin)this).Config.Bind("MiniMap", "EnableMiniMapPlayerLabels", true, "Local minimap ID-click helper. Dot click can open ID cards when player index data resolves safely. Floating name labels were removed because they can cause FPS/CPU lag in large lobbies."); EnableMiniMapPlayerLabels.SettingChanged += delegate { if (!EnableMiniMapPlayerLabels.Value) { MiniMapPlayerLabelPatch.ClearAll(); } }; EnableMiniMapFriendColors = ((BaseUnityPlugin)this).Config.Bind("MiniMap", "EnableMiniMapFriendColors", true, "Color Steam friends green on the local minimap using cached Steam relationship checks. Automatically stands down when MinimapFriends is installed."); UiThemePreset = ((BaseUnityPlugin)this).Config.Bind("Style Helper", "UiThemePreset", "BlueSage Harbor", "Menu theme: BlueSage Harbor, Midnight Dock, Vanilla Cream OT, or OG. Existing saved choices are preserved."); EnableHiddenDiagnostics = ((BaseUnityPlugin)this).Config.Bind("Developer", "EnableHiddenDiagnostics", true, "Locked on through 0.2.x Early Access so community logs retain consistent privacy-safe troubleshooting breadcrumbs. /bluedevdebug off explains the release lock and keeps this enabled."); EnableHiddenDiagnostics.SettingChanged += delegate { EnforceHiddenDiagnosticsLockedOn(); }; EnableLocalAuditExports = ((BaseUnityPlugin)this).Config.Bind("Local Audit", "EnableLocalAuditExports", true, "Export a bounded local chat/notification JSONL transcript and running BepInEx log mirror. Exact SteamID/persona enrichment is added only while you are the verified host or an assigned Helper. Never uploaded automatically; /auditlog controls it."); EnableStewardAnnouncementBridge = ((BaseUnityPlugin)this).Config.Bind("Local Steward", "EnableStewardAnnouncementBridge", false, "Accept authenticated local-only supporter announcements from the BlueSage Steward pipe. Off by default and Host-owned Global notification only."); StewardBridgeKeyPath = ((BaseUnityPlugin)this).Config.Bind("Local Steward", "StewardBridgeKeyPath", string.Empty, "Absolute path to a DPAPI CurrentUser-protected bridge key bundle outside the mod package."); StewardBridgeServiceIdentity = ((BaseUnityPlugin)this).Config.Bind("Local Steward", "StewardBridgeServiceIdentity", string.Empty, "Optional Windows service account allowed to write to the local steward pipe. Empty restricts access to the current Windows user."); EnableLocalAuditExports.SettingChanged += delegate { SessionAuditController.ReconcileEnabledState(); }; ConfigMigrationVersion = ((BaseUnityPlugin)this).Config.Bind("Internal", "ConfigMigrationVersion", string.Empty, "Tracks one-time BlueSage QoL default migrations. Safe to ignore."); StatusBaseNameLegacyBackup = ((BaseUnityPlugin)this).Config.Bind("Internal", "StatusBaseNameLegacyBackup", string.Empty, "One local rollback copy of a malformed pre-0.2.0 styled base repaired by BlueSage. Safe to leave unchanged."); StyleStateMigrationVersion = ((BaseUnityPlugin)this).Config.Bind("Internal", "StyleStateMigrationVersion", string.Empty, "Tracks the idempotent pre-0.2.0 style-state repair. Safe to ignore."); } private void ApplyOneTimeConfigMigration() { bool alreadyMigrated = ConfigMigrationPolicy.HasCompletedMigration(ConfigMigrationVersion?.Value, "final-defaults-v2"); bool alreadyMigrated2 = ConfigMigrationPolicy.HasCompletedMigration(ConfigMigrationVersion?.Value, "sweep-15-default-v1"); bool alreadyMigrated3 = ConfigMigrationPolicy.HasCompletedMigration(ConfigMigrationVersion?.Value, "sweep-10-safety-v2"); bool alreadyMigrated4 = ConfigMigrationPolicy.HasCompletedMigration(ConfigMigrationVersion?.Value, "sweep-auto-opt-in-v3"); bool alreadyMigrated5 = ConfigMigrationPolicy.HasCompletedMigration(ConfigMigrationVersion?.Value, "community-ban-any-client-opt-in-v1"); bool flag = false; if (SweepIntervalMinutes != null) { int configuredMinutes = SweepSettingsPolicy.MigrateLegacyDefaultIntervalMinutes(SweepIntervalMinutes.Value, alreadyMigrated2); configuredMinutes = SweepSettingsPolicy.MigrateTenMinuteSafetyInterval(configuredMinutes, alreadyMigrated3); if (configuredMinutes != SweepIntervalMinutes.Value) { SweepIntervalMinutes.Value = configuredMinutes; flag = true; } } if (EnableAutoSweep != null) { bool flag2 = SweepSettingsPolicy.MigrateAutomaticCleanupOptIn(EnableAutoSweep.Value, alreadyMigrated4); if (flag2 != EnableAutoSweep.Value) { EnableAutoSweep.Value = flag2; flag = true; } } if (ChatOutlineIntensity != null) { int currentValue = ConfigMigrationPolicy.MigrateOldDefaultInt(ChatOutlineIntensity.Value, 50, 75, alreadyMigrated); currentValue = ConfigMigrationPolicy.MigrateOldDefaultInt(currentValue, 60, 75, alreadyMigrated); if (currentValue != ChatOutlineIntensity.Value) { ChatOutlineIntensity.Value = currentValue; flag = true; } } if (ChatUiScalePercent != null) { int num = ChatAccessibilityScalePolicy.ClampScalePercent(ChatUiScalePercent.Value); if (num != ChatUiScalePercent.Value) { ChatUiScalePercent.Value = num; flag = true; } } if (ChatFontSizePercent != null) { int num2 = ChatFontSizePolicy.ClampFontSizePercent(ChatFontSizePercent.Value); if (num2 != ChatFontSizePercent.Value) { ChatFontSizePercent.Value = num2; flag = true; } } if (ChatWindowHeightPercent != null) { int num3 = ChatWindowHeightPolicy.ClampHeightPercent(ChatWindowHeightPercent.Value); if (num3 != ChatWindowHeightPercent.Value) { ChatWindowHeightPercent.Value = num3; flag = true; } } if (ChatHistoryRows != null) { int num4 = ConfigMigrationPolicy.MigrateOldDefaultInt(ChatHistoryRows.Value, 100, 150, alreadyMigrated); if (num4 != ChatHistoryRows.Value) { ChatHistoryRows.Value = num4; flag = true; } } if (BlackChatOutlineColor != null) { string text = ConfigMigrationPolicy.MigrateOldDefaultString(BlackChatOutlineColor.Value, "FFFFFF", "C0C0C0", alreadyMigrated); if (!string.Equals(text, BlackChatOutlineColor.Value, StringComparison.OrdinalIgnoreCase)) { BlackChatOutlineColor.Value = text; flag = true; } } if (Use24HourTime != null) { bool flag3 = ConfigMigrationPolicy.MigrateOldDefaultBool(Use24HourTime.Value, oldDefault: true, newDefault: false, alreadyMigrated); if (flag3 != Use24HourTime.Value) { Use24HourTime.Value = flag3; flag = true; } } if (EnableExtendedAvatarStyles != null) { bool flag4 = ConfigMigrationPolicy.MigrateOldDefaultBool(EnableExtendedAvatarStyles.Value, oldDefault: false, newDefault: true, alreadyMigrated); if (flag4 != EnableExtendedAvatarStyles.Value) { EnableExtendedAvatarStyles.Value = flag4; flag = true; } } if (EnableChalkboardPersistence != null) { bool flag5 = ConfigMigrationPolicy.MigrateOldDefaultBool(EnableChalkboardPersistence.Value, oldDefault: false, newDefault: true, alreadyMigrated); if (flag5 != EnableChalkboardPersistence.Value) { EnableChalkboardPersistence.Value = flag5; flag = true; } } if (PlayerRenderSaverRadiusMeters != null) { int num5 = PlayerRenderSaverPolicy.ClampRadiusMeters(PlayerRenderSaverRadiusMeters.Value); if (num5 != PlayerRenderSaverRadiusMeters.Value) { PlayerRenderSaverRadiusMeters.Value = num5; flag = true; } } if (PlayerRenderSaverMaxVisiblePlayers != null) { int num6 = PlayerRenderSaverPolicy.ClampMaxVisiblePlayers(PlayerRenderSaverMaxVisiblePlayers.Value); if (num6 != PlayerRenderSaverMaxVisiblePlayers.Value) { PlayerRenderSaverMaxVisiblePlayers.Value = num6; flag = true; } } if (EnableHiddenDiagnostics != null && !EnableHiddenDiagnostics.Value) { EnableHiddenDiagnostics.Value = true; flag = true; } if (MaxChatCharacters != null) { int currentValue2 = ConfigMigrationPolicy.MigrateOldDefaultInt(MaxChatCharacters.Value, 1500, 3000, alreadyMigrated); currentValue2 = ConfigMigrationPolicy.MigrateOldDefaultInt(currentValue2, 2000, 3000, alreadyMigrated); if (currentValue2 != MaxChatCharacters.Value) { MaxChatCharacters.Value = currentValue2; flag = true; } } if (MaxProfileCharacters != null) { int currentValue3 = ConfigMigrationPolicy.MigrateOldDefaultInt(MaxProfileCharacters.Value, 1500, 3000, alreadyMigrated); currentValue3 = ConfigMigrationPolicy.MigrateOldDefaultInt(currentValue3, 2000, 3000, alreadyMigrated); if (currentValue3 != MaxProfileCharacters.Value) { MaxProfileCharacters.Value = currentValue3; flag = true; } } if (MaxSessionNameCharacters != null) { int currentValue4 = ConfigMigrationPolicy.MigrateOldDefaultInt(MaxSessionNameCharacters.Value, 1500, 3000, alreadyMigrated); currentValue4 = ConfigMigrationPolicy.MigrateOldDefaultInt(currentValue4, 2000, 3000, alreadyMigrated); if (currentValue4 != MaxSessionNameCharacters.Value) { MaxSessionNameCharacters.Value = currentValue4; flag = true; } } if (PingSoundMode != null) { string text2 = ConfigMigrationPolicy.MigrateOldDefaultString(PingSoundMode.Value, "click", "ticket", alreadyMigrated); if (!string.Equals(text2, PingSoundMode.Value, StringComparison.Ordinal)) { PingSoundMode.Value = text2; flag = true; } } if (EnableSpoons != null && ConfigMigrationPolicy.ShouldDisableDefaultSpoons(EnableSpoons.Value, SpoonsUpdatedUtc?.Value, alreadyMigrated)) { EnableSpoons.Value = false; flag = true; } if (EnableCommunityBanListSync != null) { bool flag6 = ConfigMigrationPolicy.MigrateOldDefaultBool(EnableCommunityBanListSync.Value, oldDefault: true, newDefault: false, alreadyMigrated5); if (flag6 != EnableCommunityBanListSync.Value) { EnableCommunityBanListSync.Value = flag6; flag = true; } } string ledger = ConfigMigrationPolicy.RecordMigration(ConfigMigrationVersion?.Value, "final-defaults-v2"); ledger = ConfigMigrationPolicy.RecordMigration(ledger, "sweep-15-default-v1"); ledger = ConfigMigrationPolicy.RecordMigration(ledger, "sweep-10-safety-v2"); ledger = ConfigMigrationPolicy.RecordMigration(ledger, "sweep-auto-opt-in-v3"); ledger = ConfigMigrationPolicy.RecordMigration(ledger, "community-ban-any-client-opt-in-v1"); if (ConfigMigrationVersion != null && !string.Equals(ConfigMigrationVersion.Value, ledger, StringComparison.Ordinal)) { ConfigMigrationVersion.Value = ledger; flag = true; } if (flag) { ((BaseUnityPlugin)this).Config.Save(); ManualLogSource log = Log; if (log != null) { log.LogInfo((object)"Applied BlueSage QoL one-time config migration for release defaults."); } } } private void StartWelcomeMessageLoop() { if (_welcomeMessageCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_welcomeMessageCoroutine); } _welcomeMessageCoroutine = ((MonoBehaviour)this).StartCoroutine(WelcomeMessageLoop()); } private IEnumerator WelcomeMessageLoop() { yield return (object)new WaitForSecondsRealtime(4f); while (true) { if (EnableWelcomeMessage == null || !EnableWelcomeMessage.Value) { ResetWelcomeMessageLobbyState(); yield return (object)new WaitForSecondsRealtime(2f); continue; } if (TryGetCurrentLobbyKeyForWelcomeMessage(out var lobbyKey)) { if (!string.Equals(_welcomeLobbyKey, lobbyKey, StringComparison.Ordinal)) { _welcomeLobbyKey = lobbyKey; _welcomeMessageShownForLobby = false; _welcomePrimaryShownForLobby = false; _welcomeContextShownForLobby = false; _welcomeVoiceStateLogKey = string.Empty; } if (!_welcomeMessageShownForLobby) { _welcomeMessageShownForLobby = ShowWelcomeMessageForLobby(); } } else { ResetWelcomeMessageLobbyState(); } yield return (object)new WaitForSecondsRealtime(2f); } } private void ResetWelcomeMessageLobbyState() { _welcomeLobbyKey = string.Empty; _welcomeMessageShownForLobby = false; _welcomePrimaryShownForLobby = false; _welcomeContextShownForLobby = false; _welcomeVoiceStateLogKey = string.Empty; } private static bool TryGetCurrentLobbyKeyForWelcomeMessage(out string lobbyKey) { lobbyKey = string.Empty; MultiplayerManager val = null; try { val = MonoSingleton.I; if ((Object)(object)val != (Object)null && val.LobbyStatus) { string lobbyCode = val.LobbyCode; if (!string.IsNullOrWhiteSpace(lobbyCode)) { lobbyKey = "code:" + lobbyCode.Trim(); return true; } } } catch { } if ((Object)(object)val == (Object)null) { return false; } if (TryGetVisibleLobbyCounts(out var memberCount, out var _) && memberCount.HasValue && memberCount.Value >= 1) { lobbyKey = "visible-lobby"; return true; } return false; } private bool ShowWelcomeMessageForLobby() { bool flag = (_welcomePrimaryShownForLobby = _welcomePrimaryShownForLobby || AddLocalNotification(NavigationCalloutPolicy.BuildPrimaryWelcomeMessage("0.2.4"))); bool flag2 = _welcomeContextShownForLobby; if (!flag2) { string text = BuildVoiceWelcomeGuidance(); flag2 = AddLocalNotification("Tip: " + text + " The game's Pomodoro/focus timer can earn tickets and XP for focus time; " + NavigationCalloutPolicy.BuildCommandBrowserHint() + " Quick status: /brb, /afk, /back. Local audit starts on; /auditlog status shows your private local logs. Green Steam-profile buttons verify player accounts."); } _welcomeContextShownForLobby = flag2; return flag && flag2; } private string BuildVoiceWelcomeGuidance() { bool? flag = null; bool? flag2 = null; bool? flag3 = null; try { SettingsData val = MonoSingleton.I?.SettingsData; if (val != null) { flag = val.PushToTalk; flag2 = val.VoiceChat; } VoiceManager i = MonoSingleton.I; if ((Object)(object)i != (Object)null) { flag3 = !i.IsVoiceLobbyOff; } } catch (Exception ex) { ManualLogSource log = Log; if (log != null) { log.LogDebug((object)("Welcome voice-state read deferred safely: " + ex.GetType().Name + ": " + ex.Message)); } } string text = ((flag == true) ? "Push to Talk" : ((flag == false) ? "Voice Activation" : "not ready")); string text2 = ((flag3 == true) ? "on" : ((flag3 == false) ? "off" : "starting")); string text3 = ((flag2 == true) ? "on" : ((flag2 == false) ? "off" : "unknown")); string text4 = _welcomeLobbyKey + "|" + text2 + "|" + text + "|" + text3; if (!string.Equals(_welcomeVoiceStateLogKey, text4, StringComparison.Ordinal)) { _welcomeVoiceStateLogKey = text4; ManualLogSource log2 = Log; if (log2 != null) { log2.LogInfo((object)("Welcome voice-state check: lobbyVoice=" + text2 + ", micMode=" + text + ", voicePlayback=" + text3 + ". No microphone device name was logged.")); } } return VoiceWelcomePolicy.Build(flag2, flag3, flag); } 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 (IsPluginLoaded("com.jai.ontogether.copychat")) { Log.LogWarning((object)"Legacy CopyChat is loaded; BlueSage Chat Copy is disabled to avoid duplicate click handlers."); } if (compatibility != null && (compatibility.AutoSweepAllowed != _compatibility.AutoSweepAllowed || compatibility.ManualSweepAllowed != _compatibility.ManualSweepAllowed || compatibility.ChatTweaksAllowed != _compatibility.ChatTweaksAllowed || compatibility.HostHealthRepairsAllowed != _compatibility.HostHealthRepairsAllowed)) { Log.LogInfo((object)("Compatibility changed after " + source + ". Restarting BlueSage-controlled maintenance loops with updated ownership.")); RestartSweepLoop(); RestartHostHealthLoop(); } } internal static string GetDefensiveCompatibilitySummary() { PluginCompatibilityResult pluginCompatibilityResult = Instance?._compatibility; if (pluginCompatibilityResult == null) { return "Compatibility: loaded-mod scan is not ready yet."; } List list = new List(); if (pluginCompatibilityResult.HushLoaded) { list.Add("Hush shares the native ban store; BlueSage keeps separate exact-ID previews and authenticated Helpers"); } if (pluginCompatibilityResult.EchoLoaded) { list.Add("Echo can intentionally copy names/outfits, so Clone Shield remains clues-not-proof"); } if (pluginCompatibilityResult.DesyncLoaded) { list.Add("Desync owns periodic lobby/persona repair; BlueSage Host Health is read-only"); } if (pluginCompatibilityResult.ChalkyLoaded) { list.Add("Chalky owns board persistence/sync; BlueSage stands down"); } if (pluginCompatibilityResult.FomoLoaded) { list.Add("Fomo may observe visible chat; authenticated Helper moderation/resolution uses non-chat Steam member data"); } if (pluginCompatibilityResult.AlphaLoaded && list.Count == 0) { list.Add("Alpha command utilities detected; no overlapping BlueSage owner changed"); } if (list.Count != 0) { return "Compatibility: " + string.Join(" • ", list.ToArray()) + "."; } return "Compatibility: no overlapping Andrew defensive/runtime surfaces detected."; } 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; } _sweepSchedule.Reset(); if (EnableAutoSweep.Value && AutoSweepAllowed) { EnsureSweepBackgroundExecution(); _sweepSchedule.Start(Time.realtimeSinceStartupAsDouble, SweepIntervalMinutes.Value); _sweepCoroutine = ((MonoBehaviour)this).StartCoroutine(SweepLoop()); } else { ReleaseSweepBackgroundExecution(); } } private void EnsureSweepBackgroundExecution() { if (!_ownsSweepRunInBackgroundOverride && !Application.runInBackground) { _runInBackgroundBeforeSweepOverride = Application.runInBackground; Application.runInBackground = true; _ownsSweepRunInBackgroundOverride = true; ManualLogSource log = Log; if (log != null) { log.LogInfo((object)"Asset Sweep enabled background-safe scheduling for its real-time timer; the prior Unity run-in-background state will be restored when automatic Sweep stops."); } } } private void ReleaseSweepBackgroundExecution() { if (_ownsSweepRunInBackgroundOverride) { bool runInBackgroundBeforeSweepOverride = _runInBackgroundBeforeSweepOverride; _ownsSweepRunInBackgroundOverride = false; _runInBackgroundBeforeSweepOverride = false; Application.runInBackground = runInBackgroundBeforeSweepOverride; ManualLogSource log = Log; if (log != null) { log.LogInfo((object)$"Asset Sweep restored Unity run-in-background to {runInBackgroundBeforeSweepOverride}."); } } } private IEnumerator SweepLoop() { while (true) { double realtimeSinceStartupAsDouble = Time.realtimeSinceStartupAsDouble; _sweepSchedule.ReconcileInterval(realtimeSinceStartupAsDouble, SweepIntervalMinutes.Value); bool num = _sweepSchedule.IsDue(realtimeSinceStartupAsDouble); bool vanillaStateAvailable = false; float vanillaTimerSeconds = 0f; float vanillaDurationSeconds = 0f; bool vanillaSweepActive = false; if (num) { vanillaStateAvailable = ReadNativeSweepCoordinationState(out vanillaTimerSeconds, out vanillaDurationSeconds, out vanillaSweepActive); } switch (NativeAssetSweepPolicy.DecideAutomaticSweep(num, _sweepOperationGate.IsRunning, vanillaStateAvailable, vanillaTimerSeconds, vanillaDurationSeconds, vanillaSweepActive)) { case NativeSweepCoordinationDecision.Run: RunSweep("automatic timer", requireManualPermission: false); break; case NativeSweepCoordinationDecision.SatisfiedByVanilla: _sweepSchedule.MarkAutomaticAttemptFinished(realtimeSinceStartupAsDouble, SweepIntervalMinutes.Value); Log.LogDebug((object)"[BlueSageSweep] pending automatic interval satisfied by the observed vanilla cleanup boundary."); break; } yield return (object)new WaitForSecondsRealtime(1f); } } private static bool ReadNativeSweepCoordinationState(out float vanillaTimerSeconds, out float vanillaDurationSeconds, out bool vanillaSweepActive) { vanillaTimerSeconds = 0f; vanillaDurationSeconds = 0f; vanillaSweepActive = false; try { object i = MonoSingleton.I; object obj = MonoSingleton.I?.SettingsData; Type type = AccessTools.TypeByName("MainSceneManager"); Type type2 = AccessTools.TypeByName("SettingsData"); FieldInfo fieldInfo = AccessTools.Field(type, "_sweepTimer"); FieldInfo fieldInfo2 = AccessTools.Field(type, "_sweepUI"); FieldInfo fieldInfo3 = AccessTools.Field(type2, "SweepDuration"); if (i == null || obj == null || fieldInfo == null || fieldInfo2 == null || fieldInfo3 == null) { return false; } vanillaTimerSeconds = Convert.ToSingle(fieldInfo.GetValue(i)); vanillaDurationSeconds = Convert.ToSingle(fieldInfo3.GetValue(obj)); object? value = fieldInfo2.GetValue(i); GameObject val = (GameObject)((value is GameObject) ? value : null); vanillaSweepActive = (Object)(object)val != (Object)null && val.activeSelf; return true; } catch { vanillaTimerSeconds = 0f; vanillaDurationSeconds = 0f; vanillaSweepActive = false; return false; } } internal bool RunSweep(string reason, bool requireManualPermission) { if (requireManualPermission && !ManualSweepAllowed) { Log.LogWarning((object)("Skipping asset cleanup via " + reason + ": a compatibility guard is active.")); return false; } if (!_sweepOperationGate.TryStart(reason, out var sequence)) { Log.LogInfo((object)$"[BlueSageSweep] utc={DateTime.UtcNow:O}; phase=skip; seq={sequence}; trigger={SanitizeSweepLogValue(reason)}; activeTrigger={SanitizeSweepLogValue(_sweepOperationGate.ActiveReason)}; reason=operation-in-flight"); return false; } try { _activeSweepOperationCoroutine = ((MonoBehaviour)this).StartCoroutine(CompleteSweepOperation(sequence, reason)); return true; } catch (Exception ex) { _sweepOperationGate.Complete(sequence); _activeSweepOperationCoroutine = null; Log.LogWarning((object)$"[BlueSageSweep] utc={DateTime.UtcNow:O}; phase=failed; seq={sequence}; trigger={SanitizeSweepLogValue(reason)}; error={ex.GetType().Name}"); return false; } } private static bool TryCreateNativeSweepRoutine(out IEnumerator nativeSweepRoutine) { nativeSweepRoutine = null; try { object i = MonoSingleton.I; if (i == null) { return false; } MethodInfo methodInfo = AccessTools.Method(i.GetType(), "SweepRoutine", Type.EmptyTypes, (Type[])null); if (methodInfo == null) { return false; } nativeSweepRoutine = methodInfo.Invoke(i, null) as IEnumerator; return nativeSweepRoutine != null; } catch (Exception ex) { ManualLogSource log = Log; if (log != null) { log.LogWarning((object)("Native sweep routine was unavailable; using compatibility cleanup: " + ex.GetType().Name + ": " + ex.Message)); } nativeSweepRoutine = null; return false; } } private IEnumerator CompleteSweepOperation(int sequence, string reason) { long managedBeforeBytes = GC.GetTotalMemory(forceFullCollection: false); SweepResourceSnapshot resourceBefore = SweepResourceEvidenceCapture.Capture(); SweepSessionSnapshot sessionBefore = SweepSessionEvidenceCapture.Capture(); Stopwatch stopwatch = Stopwatch.StartNew(); Log.LogInfo((object)$"[BlueSageSweep] utc={DateTime.UtcNow:O}; phase=start; seq={sequence}; trigger={SanitizeSweepLogValue(reason)}; managedBeforeMiB={(double)managedBeforeBytes / 1048576.0:0.0}; managedGc=disabled"); bool nativeSweepFailed = false; if (TryCreateNativeSweepRoutine(out var nativeSweepRoutine)) { while (true) { object obj = null; bool flag; try { flag = nativeSweepRoutine.MoveNext(); if (flag) { obj = nativeSweepRoutine.Current; } } catch (Exception ex) { Log.LogWarning((object)$"[BlueSageSweep] utc={DateTime.UtcNow:O}; phase=native-fallback; seq={sequence}; trigger={SanitizeSweepLogValue(reason)}; error={ex.GetType().Name}"); nativeSweepFailed = true; break; } if (!flag) { break; } yield return obj; } } if (nativeSweepRoutine == null || nativeSweepFailed) { AsyncOperation val; try { val = Resources.UnloadUnusedAssets(); } catch (Exception ex2) { stopwatch.Stop(); Log.LogWarning((object)$"[BlueSageSweep] utc={DateTime.UtcNow:O}; phase=failed; seq={sequence}; trigger={SanitizeSweepLogValue(reason)}; elapsedMs={stopwatch.ElapsedMilliseconds}; error={ex2.GetType().Name}"); FinishSweepOperation(sequence, reason); yield break; } if (val != null) { yield return val; } } SweepResourceSnapshot after = SweepResourceEvidenceCapture.Capture(); SweepSessionSnapshot after2 = SweepSessionEvidenceCapture.Capture(); stopwatch.Stop(); long totalMemory = GC.GetTotalMemory(forceFullCollection: false); double num = (double)(totalMemory - managedBeforeBytes) / 1048576.0; string text = SweepResourceEvidenceFormatter.Format(resourceBefore, after); string text2 = SweepSessionEvidenceFormatter.Format(sessionBefore, after2); Log.LogInfo((object)$"[BlueSageSweep] utc={DateTime.UtcNow:O}; phase=complete; seq={sequence}; trigger={SanitizeSweepLogValue(reason)}; elapsedMs={stopwatch.ElapsedMilliseconds}; managedBeforeMiB={(double)managedBeforeBytes / 1048576.0:0.0}; managedAfterMiB={(double)totalMemory / 1048576.0:0.0}; managedDeltaMiB={num:+0.0;-0.0;0.0}; managedGc=disabled; {text}; {text2}"); FinishSweepOperation(sequence, reason); } private void FinishSweepOperation(int sequence, string reason) { _sweepOperationGate.Complete(sequence); _activeSweepOperationCoroutine = null; if (string.Equals(reason, "automatic timer", StringComparison.Ordinal)) { _sweepSchedule.MarkAutomaticAttemptFinished(Time.realtimeSinceStartupAsDouble, SweepIntervalMinutes.Value); } } private static string SanitizeSweepLogValue(string value) { if (string.IsNullOrWhiteSpace(value)) { return "unspecified"; } return value.Trim().Replace(";", ",").Replace("\r", " ") .Replace("\n", " "); } internal static string BuildTimestampPrefix() { return TimestampFormatter.BuildPrefix(DateTime.Now, Use24HourTime.Value, TimestampSizePercent.Value, TimestampColor.Value); } internal static bool AddLocalNotification(string text) { try { TextChannelManager i = NetworkSingleton.I; if ((Object)(object)i == (Object)null) { return false; } i.AddNotification(text); return true; } catch (Exception ex) { ManualLogSource log = Log; if (log != null) { log.LogWarning((object)("Could not show local notification: " + ex.GetType().Name + ": " + ex.Message)); } return false; } } internal static string FormatBlueSageNotice(string message) { return "BlueSage " + message; } internal static string FormatBlueSageWarning(string message) { return "BlueSage " + message; } internal static string FormatBlueSageAction(string action, string detail, bool warning = false) { string text = (warning ? "FFD45D" : "70D6FF"); string text2 = (string.IsNullOrWhiteSpace(action) ? "Notice" : action.Trim()); string text3 = (string.IsNullOrWhiteSpace(detail) ? string.Empty : (" " + detail.Trim())); return "BlueSage " + text2 + "" + text3; } internal static bool HandleOutgoingChatCommand(byte[] message) { if ((Object)(object)Instance == (Object)null || message == null || Instance._commandRegistry == null) { return true; } string input; try { input = 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(input); if (!blueSageCommandResult.Handled) { return true; } foreach (string message2 in blueSageCommandResult.Messages) { AddLocalNotification(message2); } string commandName; string arguments; string text = (BlueSageCommandRegistry.TryParse(input, out commandName, out arguments) ? commandName.ToLowerInvariant() : "unknown"); ManualLogSource log2 = Log; if (log2 != null) { log2.LogInfo((object)("BlueSage command handled locally: /" + text + " (arguments redacted).")); } return false; } internal static IReadOnlyList GetCommandSuggestions(string input, int maxResults) { if ((Object)(object)Instance == (Object)null || Instance._commandRegistry == null) { return Array.Empty(); } return Instance._commandRegistry.Suggest(input, maxResults, CanSuggestCommand); } private void ApplyStyleStateMigration() { string text = (StatusBaseName?.Value ?? string.Empty).Trim(); bool flag = false; bool flag2 = false; bool flag3 = false; if (!string.IsNullOrWhiteSpace(text)) { DetectedDisplayNameState detectedDisplayNameState = StyleHelperNamePolicy.DetectCurrentState(text, text, StatusMessage?.Value ?? string.Empty, GetStatusColor(), StatusBrackets?.Value ?? "()", ShouldApplySpoons, SpoonCount?.Value ?? 5, GetSpoonLabel(), BuildStatusSuffix(), BuildSpoonSuffix()); string text2 = (detectedDisplayNameState.BaseName ?? string.Empty).Trim(); if (!string.Equals(text2, text, StringComparison.Ordinal)) { if (StatusBaseNameLegacyBackup != null && string.IsNullOrEmpty(StatusBaseNameLegacyBackup.Value)) { StatusBaseNameLegacyBackup.Value = text; } StatusBaseName.Value = text2; flag2 = true; flag3 = detectedDisplayNameState.UsedPlainTextFallback; flag = true; } } if (StyleStateMigrationVersion != null && !string.Equals(StyleStateMigrationVersion.Value, "0.2.4-legacy-style-repair-v1", StringComparison.Ordinal)) { StyleStateMigrationVersion.Value = "0.2.4-legacy-style-repair-v1"; flag = true; } if (!flag) { return; } ((BaseUnityPlugin)this).Config.Save(); if (flag2) { ManualLogSource log = Log; if (log != null) { log.LogWarning((object)("Status Helper repaired legacy local style state without exposing its contents. " + $"beforeLength={text.Length}, afterLength={(StatusBaseName.Value ?? string.Empty).Length}, " + $"plainTextFallback={flag3}. The original remains in the local rollback config entry.")); } } } internal static bool CanUseLobbySafety(out bool isHost) { string reason; return SteamIdModerationController.CanLocalIssue(out isHost, out reason); } internal static bool ShouldShowLobbySafetyTab(out LobbySafetyAccessState state, out string reason) { return SteamIdModerationController.ShouldShowLocalSurface(out state, out reason); } internal static bool CanManageHelpers() { bool isHost; return CanUseLobbySafety(out isHost) && isHost; } internal static bool IsHelperManagementCommandName(string commandName) { string text = (commandName ?? string.Empty).Trim().TrimStart(new char[1] { '/' }).ToLowerInvariant(); switch (text) { default: return text == "moderationhelper"; case "helper": case "lobbyhelper": case "lobbymod": case "modhelper": return true; } } internal static bool IsLobbySafetyCommandName(string commandName) { string text = (commandName ?? string.Empty).Trim().TrimStart(new char[1] { '/' }).ToLowerInvariant(); if (!IsHelperManagementCommandName(text)) { switch (text) { default: return text == "cloneincident"; case "sidlist": case "sidwho": case "sidlookup": case "sidban": case "sidunban": case "unbansid": case "sidincident": break; } } return true; } private static bool CanSuggestCommand(BlueSageCommand command) { if (command == null) { return false; } if (command.Matches("helper")) { return CanManageHelpers(); } bool isHost; if (command.Matches("sidlist") || command.Matches("sidwho") || command.Matches("sidban") || command.Matches("sidunban") || command.Matches("sidincident")) { return CanUseLobbySafety(out isHost); } return true; } internal static IReadOnlyList GetModerationTargetSuggestions(bool unban) { if (!CanUseLobbySafety(out var isHost)) { return Array.Empty(); } if (unban) { if (!isHost) { return Array.Empty(); } return SteamIdModerationController.GetBannedSteamIds().Take(12).ToArray(); } string[] array = (from item in CloneIncidentLedger.Snapshot.Reverse().Take(12) select item.Id).ToArray(); if (array.Length == 0) { return Array.Empty(); } return array; } internal static IReadOnlyList GetIdentityLookupSuggestions() { if (!CanUseLobbySafety(out var _)) { return Array.Empty(); } return PlayerIdentityEvidenceController.GetLookupSuggestions(); } internal string ShowCloneEvidenceFromQolMenu() { BlueSageCommandResult blueSageCommandResult = HandleSidListCommand(new BlueSageCommandContext("/sidlist", "sidlist", string.Empty)); foreach (string message in blueSageCommandResult.Messages) { AddLocalNotification(message); } object obj; if (blueSageCommandResult.Messages.Count <= 1) { obj = blueSageCommandResult.Messages.FirstOrDefault(); if (obj == null) { return "No Clone Shield evidence is available."; } } else { obj = "Clone Shield evidence was sent to local chat."; } return (string)obj; } internal string CheckModerationAccessFromQolMenu() { bool isHost; string reason; object obj = SteamIdModerationController.GetLocalAccessState(out isHost, out reason) switch { LobbySafetyAccessState.Host => "Helper / Safety access: you are the current lobby host.", LobbySafetyAccessState.HelperReady => "Helper / Safety access: this host added you as a Helper. The host still rechecks and performs each moderation action.", LobbySafetyAccessState.HelperWaitingForHost => "Helper / Safety waiting: " + reason, _ => "Helper / Safety access: " + reason, }; AddLocalNotification((string)obj); return (string)obj; } internal string UpdateHelperFromQolMenu(string steamId, bool add) { string text = (add ? "add " : "remove "); string? obj = HandleHelperCommand(new BlueSageCommandContext("/helper " + text + steamId, "helper", text + steamId)).Messages.FirstOrDefault() ?? "Helpers: no change."; AddLocalNotification(obj); return obj; } internal string ResetHelpersFromQolMenu() { string? obj = HandleHelperCommand(new BlueSageCommandContext("/helper reset", "helper", "reset")).Messages.FirstOrDefault() ?? "Helpers: no change."; AddLocalNotification(obj); return obj; } internal string PreviewSidBanFromQolMenu(string steamId) { BlueSageCommandResult blueSageCommandResult = HandleSidBanCommand(new BlueSageCommandContext("/sidban " + steamId, "sidban", steamId)); foreach (string message in blueSageCommandResult.Messages) { AddLocalNotification(message); } return blueSageCommandResult.Messages.FirstOrDefault() ?? "SID ban preview was not available."; } internal string ConfirmSidBanFromQolMenu(string steamId) { BlueSageCommandResult blueSageCommandResult = HandleSidBanCommand(new BlueSageCommandContext("/sidban " + steamId + " confirm", "sidban", steamId + " confirm")); foreach (string message in blueSageCommandResult.Messages) { AddLocalNotification(message); } return blueSageCommandResult.Messages.FirstOrDefault() ?? "SID ban confirmation was not available."; } internal string PreviewSidUnbanFromQolMenu(string steamId) { BlueSageCommandResult blueSageCommandResult = HandleSidUnbanCommand(new BlueSageCommandContext("/sidunban " + steamId, "sidunban", steamId)); foreach (string message in blueSageCommandResult.Messages) { AddLocalNotification(message); } return blueSageCommandResult.Messages.FirstOrDefault() ?? "SID unban preview was not available."; } internal string ConfirmSidUnbanFromQolMenu(string steamId) { BlueSageCommandResult blueSageCommandResult = HandleSidUnbanCommand(new BlueSageCommandContext("/sidunban " + steamId + " confirm", "sidunban", steamId + " confirm")); foreach (string message in blueSageCommandResult.Messages) { AddLocalNotification(message); } return blueSageCommandResult.Messages.FirstOrDefault() ?? "SID unban confirmation was not available."; } internal ModerationConfirmationReadiness GetModerationConfirmationReadiness(string action, string target) { PendingModerationAction pendingModerationAction = _pendingModerationAction; if (pendingModerationAction == null) { return new ModerationConfirmationReadiness(isReady: false, 0); } MultiplayerManager i = MonoSingleton.I; return ModerationProtocolPolicy.EvaluateConfirmation(action, target, ((i != null) ? i.LobbyCode : null) ?? string.Empty, DateTimeOffset.UtcNow.ToUnixTimeSeconds(), pendingModerationAction.Action, pendingModerationAction.TargetSteamId, pendingModerationAction.IncidentId, pendingModerationAction.LobbyCode, pendingModerationAction.ExpiresUnixSeconds); } internal string CopySteamIdFromQolMenu(string steamId) { if (!CanUseLobbySafety(out var _)) { return "Lobby Safety is available only to the current host or a Helper assigned by that host. No player or incident details were shown."; } if (!PlayerIdentityEvidenceController.TryResolveExact(steamId, out var evidence, out var error)) { return "Steam ID copy failed safely: " + error + ". Re-select a verified active row."; } GUIUtility.systemCopyBuffer = evidence.SteamId; return "Copied exact SteamID64 " + evidence.SteamId + "."; } internal string CopySavedBanSteamIdFromQolMenu(string steamId) { if (!CanManageHelpers()) { return "Only the verified current host can copy saved native-ban identities."; } IReadOnlyList records; string error; bool flag = SteamIdModerationController.TryGetNativeBanRecords(out records, out error); if (!ModerationProtocolPolicy.IsSteamId64(steamId) || !flag || !records.Any((NativeBanRecord record) => string.Equals(record.SteamId, steamId, StringComparison.Ordinal))) { string text = (string.IsNullOrWhiteSpace(error) ? "that exact native-ban row is no longer available" : error); return "Saved SteamID64 copy failed safely: " + text + ". Refresh the list."; } GUIUtility.systemCopyBuffer = steamId; return "Copied saved native-ban SteamID64 " + steamId + "."; } internal string CopyIdentityEvidenceFromQolMenu(string steamId) { if (!CanUseLobbySafety(out var _)) { return "Lobby Safety is available only to the current host or a Helper assigned by that host. No player or incident details were shown."; } if (!PlayerIdentityEvidenceController.TryResolveExact(steamId, out var evidence, out var error)) { return "Identity evidence copy failed safely: " + error + ". Re-select a verified active row."; } GUIUtility.systemCopyBuffer = PlayerIdentityEvidenceController.BuildCopyableEvidence(evidence); return "Copied the verified live identity record for row #" + (evidence.RosterIndex + 1) + " and Steam …" + PlayerIdentityEvidenceController.Suffix(evidence.SteamId) + "."; } internal string OpenSteamProfileFromQolMenu(string steamId) { //IL_004b: Unknown result type (might be due to invalid IL or missing references) if (!CanUseLobbySafety(out var _)) { return "Lobby Safety is available only to the current host or a Helper assigned by that host. No player or incident details were shown."; } if (!PlayerIdentityEvidenceController.TryResolveExact(steamId, out var evidence, out var error) || !SteamManager.Initialized || !ulong.TryParse(evidence.SteamId, out var result) || result == 0L) { return "Steam profile could not open safely: " + error + ". Re-select a verified active row."; } SteamFriends.ActivateGameOverlayToUser("steamid", new CSteamID(result)); return "Opened the selected Steam profile for verification."; } internal string OpenOfflineSteamProfileFromQolMenu(string steamId) { //IL_006b: Unknown result type (might be due to invalid IL or missing references) if (!CanManageHelpers()) { return "Only the verified current host can inspect an off-roster profile from this form."; } if (!ModerationProtocolPolicy.IsSteamId64(steamId) || !ulong.TryParse(steamId, out var result) || result == 0L) { return "Steam profile could not open safely: enter one exact 17-digit SteamID64."; } string error; IReadOnlyList verifiedCurrentSteamIds = PlayerIdentityEvidenceController.GetVerifiedCurrentSteamIds(out error); if (!string.IsNullOrWhiteSpace(error)) { return "Steam profile could not open safely while current lobby coverage is incomplete: " + error; } if (verifiedCurrentSteamIds.Contains(steamId, StringComparer.Ordinal)) { return "That SteamID64 is currently in the lobby. Select its verified live row instead."; } if (!SteamManager.Initialized) { return "Steam profile could not open safely because Steam is not initialized."; } SteamFriends.ActivateGameOverlayToUser("steamid", new CSteamID(result)); return "Opened the exact off-roster Steam profile for verification."; } 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 Automatic Cleanup or run one cleanup now", new string[2] { "sweep", "as" }, HandleAssetSweepCommand)); _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("chatreadability", "Toggle local chat text outline and shadow", new string[1] { "chatread" }, (BlueSageCommandContext context) => HandleToggleCommand(context, "Chat Readability", EnableChatReadability))); _commandRegistry.Register(new BlueSageCommand("chatbackdrop", "Toggle persistent local chat backdrop readability", new string[1] { "chatbg" }, (BlueSageCommandContext context) => HandleToggleCommand(context, "Chat Backdrop", EnablePersistentChatBackdrop))); _commandRegistry.Register(new BlueSageCommand("chatoutline", "Set chat outline intensity from 25 to 100", new string[1] { "outline" }, HandleChatOutlineCommand)); _commandRegistry.Register(new BlueSageCommand("chatscale", "Set whole local chat UI accessibility scale from 75 to 200 percent", new string[2] { "chatresize", "cr" }, HandleChatScaleCommand)); _commandRegistry.Register(new BlueSageCommand("chatfont", "Set local chat message font size from 75 to 200 percent", new string[2] { "chatfontsize", "cf" }, HandleChatFontCommand)); _commandRegistry.Register(new BlueSageCommand("chatheight", "Resize local chat window height from 100 to 200 percent without scaling text", new string[2] { "chatstretch", "chath" }, HandleChatHeightCommand)); _commandRegistry.Register(new BlueSageCommand("chatrows", "Set retained chat rows from 25 to 250; default/reset is 150", new string[2] { "chathistory", "chatr" }, HandleChatHistoryCommand)); _commandRegistry.Register(new BlueSageCommand("chatvanilla", "Reset whole chat UI scale, message font size, and window height to vanilla", new string[2] { "chatreset", "chatdefault" }, HandleChatVanillaCommand)); _commandRegistry.Register(new BlueSageCommand("chatoutlinecolor", "Set normal chat outline color as hex", new string[1] { "outlinecolor" }, (BlueSageCommandContext context) => HandleOutlineColorCommand(context, "Normal outline", ChatOutlineColor, "000000"))); _commandRegistry.Register(new BlueSageCommand("blackoutlinecolor", "Set true-black chat/name outline color as hex", new string[2] { "blackchatoutline", "blackoutline" }, (BlueSageCommandContext context) => HandleOutlineColorCommand(context, "Black text outline", BlackChatOutlineColor, "C0C0C0"))); _commandRegistry.Register(new BlueSageCommand("bettermove", "Toggle Shift run and Ctrl slow-walk", new string[1] { "bm" }, (BlueSageCommandContext context) => HandleToggleCommand(context, "BetterMove", EnableBetterMove))); _commandRegistry.Register(new BlueSageCommand("unlimitedconsumables", "Toggle reusable canteen items and chalk", new string[2] { "consumables", "infiniteconsumables" }, (BlueSageCommandContext context) => HandleToggleCommand(context, "Reusable Consumables", EnableUnlimitedConsumables))); _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("minimaplabels", "Toggle local minimap dot-to-ID-card clicks", new string[1] { "mml" }, (BlueSageCommandContext context) => HandleToggleCommand(context, "MiniMap ID Click", EnableMiniMapPlayerLabels))); _commandRegistry.Register(new BlueSageCommand("cloneshield", "Toggle public alerts when someone newly copies your avatar or styled name", new string[2] { "cloneguard", "identityshield" }, (BlueSageCommandContext context) => HandleToggleCommand(context, "Clone Shield", EnableCloneShield))); _commandRegistry.Register(new BlueSageCommand("playersaver", "Toggle local-only distant player render saver for large lobbies", new string[2] { "rendercull", "playercull" }, HandlePlayerRenderSaverCommand)); _commandRegistry.Register(new BlueSageCommand("communitybans", "Local additive community BanData status or refresh", new string[2] { "communityban", "bansync" }, HandleCommunityBanCommand)); _commandRegistry.Register(new BlueSageCommand("chalk", "View/save/delete local chalk data or request exact authorized load/clear actions", new string[1] { "chalkboard" }, HandleChalkCommand)); _commandRegistry.Register(new BlueSageCommand("chalksave", "Save the current chalkboard locally: /chalksave name [board index]", Array.Empty(), HandleChalkSaveCommand)); _commandRegistry.Register(new BlueSageCommand("chalkload", "Authorized exact shared chalkboard restore: /chalkload name board-index", new string[1] { "chalkrestore" }, HandleChalkLoadCommand)); _commandRegistry.Register(new BlueSageCommand("chalklist", "List local BlueSage chalkboard saves", Array.Empty(), (BlueSageCommandContext context) => BlueSageCommandResult.HandledWithMessages(ChalkboardPersistenceController.List()))); _commandRegistry.Register(new BlueSageCommand("copychat", "Toggle right-click-to-copy chat messages", new string[1] { "ccopy" }, (BlueSageCommandContext context) => HandleToggleCommand(context, "Chat Copy", EnableChatCopy))); _commandRegistry.Register(new BlueSageCommand("chatlinks", "Toggle safe chat link opening", new string[1] { "clinks" }, (BlueSageCommandContext context) => HandleToggleCommand(context, "Chat Links", EnableChatUrlLinks))); _commandRegistry.Register(new BlueSageCommand("auditlog", "Control local chat and BepInEx audit exports", new string[2] { "sessionaudit", "localaudit" }, HandleAuditLogCommand)); _commandRegistry.Register(new BlueSageCommand("qolmenu", "Open the BlueSage QoL menu", new string[1] { "qm" }, HandleQolMenuCommand)); _commandRegistry.Register(new BlueSageCommand("selfmove", "Self movement: status, save, teleport, fly, speed, jump, gravity, or reset", new string[2] { "bluemove", "qolmove" }, HandleSelfMovementCommand)); _commandRegistry.Register(new BlueSageCommand("style", "Build styled name or status text you can paste", new string[1] { "sty" }, HandleStyleCommand)); _commandRegistry.Register(new BlueSageCommand("styleui", "Open Style Helper for names, status, Spoons, colors, and templates", new string[1] { "sui" }, HandleStyleUiCommand)); _commandRegistry.Register(new BlueSageCommand("setpingcolor", "Set BlueSage mention highlight color", Array.Empty(), HandleSetPingColorCommand)); _commandRegistry.Register(new BlueSageCommand("pingsound", "Toggle or choose BlueSage local ping sound", Array.Empty(), HandlePingSoundCommand)); _commandRegistry.Register(new BlueSageCommand("helper", "Host-managed helpers: add, remove, list, or reset", new string[4] { "lobbyhelper", "lobbymod", "modhelper", "moderationhelper" }, HandleHelperCommand)); _commandRegistry.Register(new BlueSageCommand("sidlist", "Show local Clone Shield incident evidence and Steam ID suffixes", Array.Empty(), HandleSidListCommand)); _commandRegistry.Register(new BlueSageCommand("sidincident", "Review, resolve, or locally clear Clone Shield incidents", new string[1] { "cloneincident" }, HandleSidIncidentCommand)); _commandRegistry.Register(new BlueSageCommand("sidwho", "Find verified live players by name, persona, SteamID digits, or regex", new string[1] { "sidlookup" }, HandleSidWhoCommand)); _commandRegistry.Register(new BlueSageCommand("idsync", "Identity status/local refresh, Host repair, or authenticated Helper request", new string[2] { "identitysync", "rostersync" }, HandleIdentityResyncCommand)); _commandRegistry.Register(new BlueSageCommand("sidban", "Preview or confirm a Steam ID ban checked by the host", Array.Empty(), HandleSidBanCommand)); _commandRegistry.Register(new BlueSageCommand("sidunban", "Preview or confirm removal from the host's Steam ID ban list", new string[1] { "unbansid" }, HandleSidUnbanCommand)); _commandRegistry.Register(new BlueSageCommand("setname", "Set your BlueSage base display name", Array.Empty(), HandleSetNameCommand)); _commandRegistry.Register(new BlueSageCommand("status", "Set your BlueSage status message", new string[2] { "setstatus", "set-status" }, HandleStatusCommand)); _commandRegistry.Register(new BlueSageCommand("brb", "Set your BlueSage status to BRB", Array.Empty(), (BlueSageCommandContext context) => SetPresetStatus("BRB"))); _commandRegistry.Register(new BlueSageCommand("afk", "Set your BlueSage status to AFK", Array.Empty(), (BlueSageCommandContext context) => SetPresetStatus("AFK"))); _commandRegistry.Register(new BlueSageCommand("back", "Restore your status from before BRB/AFK, or clear it", Array.Empty(), HandleBackStatusCommand)); _commandRegistry.Register(new BlueSageCommand("clearstatus", "Clear your BlueSage status message", new string[1] { "clear-status" }, HandleClearStatusCommand)); _commandRegistry.Register(new BlueSageCommand("statuscolor", "Set your BlueSage status color", Array.Empty(), HandleStatusColorCommand)); _commandRegistry.Register(new BlueSageCommand("spoons", "Set, show, or hide your optional spoon tag", Array.Empty(), HandleSpoonsCommand)); _commandRegistry.Register(new BlueSageCommand("setmood", "Customize the short label shown in your optional 0/5 tag", new string[1] { "spoonlabel" }, HandleSetMoodCommand)); 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)); _commandRegistry.Register(new BlueSageCommand("boop", "BlueSage internal", Array.Empty(), HandleBoopCommand, isHidden: true)); _commandRegistry.Register(new BlueSageCommand("sagewave", "BlueSage internal", Array.Empty(), HandleSageWaveCommand, isHidden: true)); } private BlueSageCommandResult HandleBlueSageQoLCommand(BlueSageCommandContext context) { string text = ((context.Tokens.Count > 0) ? context.Tokens[0].ToLowerInvariant() : "help"); if (text == "status") { LogDetailedQolStatus(); return BlueSageCommandResult.HandledWithMessages(BuildQolStatusMessage()); } if (text == "diag" && string.Equals(context.CommandName, "bsqol", StringComparison.OrdinalIgnoreCase)) { return HandleHiddenDiagnosticsCommand(context); } if (text == "hosthealth") { return BlueSageCommandResult.HandledWithMessages(RunHostHealthCheck("manual command")); } List list = new List { "BlueSage basics: Home or /qolmenu, Insert or /styleui, /qol status, and type / then Tab or click a suggestion.", "Chat: /timestamps, /chatreadability, /chatbackdrop, /chatoutline, /chatscale, /chatfont, /chatheight, /chatrows, /chatvanilla, /copychat, /chatlinks.", "Style: /style, /setname, /status, /clearstatus, /brb, /afk, /back, /spoons, /setmood.", "Comfort: /as, /bettermove, /rg, /fa, /minimaplabels, /cloneshield. Chalkboards: /chalk boards, then authorized Host/Helper /chalk clear . Experimental tools are opt-in under Home > Toggles; type / to browse their commands." }; if (CanUseLobbySafety(out var isHost)) { list.Add(isHost ? "Host-only Helper management and evidence tools are grouped in Lobby Safety. /idsync status is read-only, refresh is local-only, and verified Host repair runs one bounded exact-correlation transaction." : "This host assigned you as a Helper. /idsync status is read-only, refresh is local-only, and /idsync request uses authenticated member data for current-Host revalidation."); } return BlueSageCommandResult.HandledWithMessages(list.ToArray()); } private BlueSageCommandResult HandleQolMenuCommand(BlueSageCommandContext context) { if ((Object)(object)_qolMenuWindow == (Object)null) { StartQolMenuWindow(); } if ((Object)(object)_qolMenuWindow == (Object)null) { return BlueSageCommandResult.HandledWithMessages("QoL menu could not start; check BepInEx console."); } _qolMenuWindow.ToggleVisible(); return BlueSageCommandResult.HandledWithMessages("QoL menu toggled. Use the BlueSage window to manage QoL features."); } private BlueSageCommandResult HandleStyleCommand(BlueSageCommandContext context) { RichTextStyleResult richTextStyleResult = RichTextStyleBuilder.TryBuild(context.Arguments, LockedMaxIdCardCharacters); if (!richTextStyleResult.Success) { return BlueSageCommandResult.HandledWithMessages(richTextStyleResult.Message); } GUIUtility.systemCopyBuffer = richTextStyleResult.GeneratedText; return BlueSageCommandResult.HandledWithMessages($"Style Helper: copied {richTextStyleResult.RawLength}/{LockedMaxIdCardCharacters} chars. Paste into an ID/name/profile/room field."); } private BlueSageCommandResult HandleStyleUiCommand(BlueSageCommandContext context) { if (!EnableStyleUi.Value) { return BlueSageCommandResult.HandledWithMessages("Style UI is disabled in config."); } if ((Object)(object)_styleHelperWindow == (Object)null) { StartStyleHelperWindow(); } if ((Object)(object)_styleHelperWindow == (Object)null) { return BlueSageCommandResult.HandledWithMessages("Style UI could not start; check BepInEx console."); } _styleHelperWindow.ToggleVisible(); return BlueSageCommandResult.HandledWithMessages("Style UI toggled. Use the BlueSage window to build and copy name/status text."); } internal string TryApplyStyledNameFromStyleUi(string styledName) { bool applied; return TryApplyStyledNameFromStyleUi(styledName, out applied); } internal string TryApplyStyledNameFromStyleUi(string styledName, out bool applied) { applied = false; string text = (styledName ?? string.Empty).Trim(); if (string.IsNullOrWhiteSpace(text)) { return "Style UI: build a style before applying it to your name."; } if (text.Length > LockedMaxIdCardCharacters) { return $"Style UI: styled name is too long ({text.Length}/{LockedMaxIdCardCharacters}). Shorten it or use fewer per-letter colors."; } DisplayNameDraft displayNameDraft = CaptureDisplayNameDraft(captureLiveBase: false); displayNameDraft.BaseName = text; BlueSageCommandResult blueSageCommandResult = TryApplyDisplayNameDraft(displayNameDraft, "Style UI: name updated.", out applied); if (blueSageCommandResult.Messages.Count <= 0) { return "Style UI: name updated."; } return blueSageCommandResult.Messages[0]; } internal string TryApplyPlainNameFromStyleUi(string plainName) { bool applied; return TryApplyPlainNameFromStyleUi(plainName, out applied); } internal string TryApplyPlainNameFromStyleUi(string plainName, out bool applied) { applied = false; string text = (plainName ?? string.Empty).Trim(); if (string.IsNullOrWhiteSpace(text) || string.Equals(text, "YOURNAMEHERE", StringComparison.OrdinalIgnoreCase)) { return "Style UI: type your name first."; } if (text.Length > LockedMaxIdCardCharacters) { return $"Style UI: name is too long ({text.Length}/{LockedMaxIdCardCharacters})."; } DisplayNameDraft displayNameDraft = CaptureDisplayNameDraft(captureLiveBase: false); displayNameDraft.BaseName = text; BlueSageCommandResult blueSageCommandResult = TryApplyDisplayNameDraft(displayNameDraft, "Style UI: name updated.", out applied); if (blueSageCommandResult.Messages.Count <= 0) { return "Style UI: name updated."; } return blueSageCommandResult.Messages[0]; } internal string TryApplyStatusFromStyleUi(string plainName, string status, string color) { bool applied; return TryApplyStatusFromStyleUi(plainName, status, color, out applied); } internal string TryApplyStatusFromStyleUi(string plainName, string status, string color, out bool applied) { applied = false; DisplayNameDraft displayNameDraft = CaptureDisplayNameDraft(captureLiveBase: true); string text = (plainName ?? string.Empty).Trim(); if (!string.IsNullOrWhiteSpace(text) && !string.Equals(text, "YOURNAMEHERE", StringComparison.OrdinalIgnoreCase)) { displayNameDraft.BaseName = text; } displayNameDraft.StatusMessage = (status ?? string.Empty).Trim(); if (RichTextStyleBuilder.TryNormalizeHex(color, out var hex)) { displayNameDraft.StatusColor = hex; } BlueSageCommandResult blueSageCommandResult = TryApplyDisplayNameDraft(displayNameDraft, "Style UI: status updated.", out applied); if (blueSageCommandResult.Messages.Count <= 0) { return "Style UI: status updated."; } return blueSageCommandResult.Messages[0]; } internal string TryClearStatusFromStyleUi() { bool applied; return TryClearStatusFromStyleUi(out applied); } internal string TryClearStatusFromStyleUi(out bool applied) { DisplayNameDraft displayNameDraft = CaptureDisplayNameDraft(captureLiveBase: true); displayNameDraft.StatusMessage = string.Empty; BlueSageCommandResult blueSageCommandResult = TryApplyDisplayNameDraft(displayNameDraft, "Style UI: status cleared.", out applied); if (blueSageCommandResult.Messages.Count <= 0) { return "Style UI: status cleared."; } return blueSageCommandResult.Messages[0]; } internal string GetCurrentPlayerDisplayNameForStyleUi() { return GetStatusBaseName(); } internal string GetCurrentPlayerPlainNameForStyleUi() { return RichTextStyleBuilder.ToPlainStyleEditorText(GetCurrentDisplayNameStateForStyleUi().BaseName); } internal string GetCurrentPlayerStyledNameForStyleUi() { return GetCurrentDisplayNameStateForStyleUi().BaseName; } internal DetectedDisplayNameState GetCurrentDisplayNameStateForStyleUi() { return StyleHelperNamePolicy.DetectCurrentState(GetLivePlayerDisplayName(), GetStatusBaseName(), StatusMessage?.Value ?? string.Empty, GetStatusColor(), StatusBrackets?.Value ?? "()", ShouldApplySpoons, SpoonCount?.Value ?? 5, GetSpoonLabel(), BuildStatusSuffix(), BuildSpoonSuffix()); } private static string GetLivePlayerDisplayName() { try { string text = MonoSingleton.I?.PlayerData?.Name?.Trim(); if (!string.IsNullOrWhiteSpace(text)) { return text; } } catch { } try { TextChannelManager i = NetworkSingleton.I; return ((i == null) ? null : i.UserName?.Trim()) ?? string.Empty; } catch { return string.Empty; } } private BlueSageCommandResult HandleSetPingColorCommand(BlueSageCommandContext context) { string value = context.Arguments.Trim(); if (string.IsNullOrWhiteSpace(value)) { return BlueSageCommandResult.HandledWithMessages("Ping Mentions color: #" + GetPingHighlightColor() + "."); } if (!RichTextStyleBuilder.TryNormalizeHex(value, out var hex)) { return BlueSageCommandResult.HandledWithMessages("Ping Mentions: expected a hex color like #9B59B6."); } PingHighlightColor.Value = hex; ((BaseUnityPlugin)this).Config.Save(); return BlueSageCommandResult.HandledWithMessages("Ping Mentions color set to #" + hex + "."); } private BlueSageCommandResult HandleBlueSageClueCommand(BlueSageCommandContext context) { string text = GetStatusBaseName(); if (string.IsNullOrWhiteSpace(text)) { text = "You"; } string text2 = EasterEggTracker.Discover("command_clue", "the secret BlueSage clue glyph"); return BlueSageCommandResult.HandledWithMessages("♥☻ " + text + " found a Clue! ☻♥ Screenshot this to @jollyblue (Blues) in Discord to make him giggle. " + text2); } private BlueSageCommandResult HandleBoopCommand(BlueSageCommandContext context) { string text = EasterEggTracker.Discover("command_boop", "the respectful server boop"); return BlueSageCommandResult.HandledWithMessages("Boop accepted. The lobby turtle has been emotionally supported. " + text); } private BlueSageCommandResult HandleSageWaveCommand(BlueSageCommandContext context) { string text = EasterEggTracker.Discover("command_sagewave", "the tiny Sage wave from behind the UI"); return BlueSageCommandResult.HandledWithMessages("Sage waves from the menu bar, then pretends nothing happened. " + text); } private BlueSageCommandResult HandleSetNameCommand(BlueSageCommandContext context) { string text = context.Arguments.Trim(); if (string.IsNullOrWhiteSpace(text)) { return BlueSageCommandResult.HandledWithMessages("Status Helper name: " + GetStatusBaseName() + "."); } bool applied; if (string.Equals(text, "reset", StringComparison.OrdinalIgnoreCase) || string.Equals(text, "default", StringComparison.OrdinalIgnoreCase)) { string steamPersonaName = GetSteamPersonaName(); if (string.IsNullOrWhiteSpace(steamPersonaName)) { return BlueSageCommandResult.HandledWithMessages("Status Helper: Steam persona name is not ready yet; no name change was made."); } DisplayNameDraft displayNameDraft = CaptureDisplayNameDraft(captureLiveBase: false); displayNameDraft.BaseName = steamPersonaName; return TryApplyDisplayNameDraft(displayNameDraft, "Status Helper: name reset to your current Steam persona name.", out applied); } DisplayNameDraft displayNameDraft2 = CaptureDisplayNameDraft(captureLiveBase: false); displayNameDraft2.BaseName = text; return TryApplyDisplayNameDraft(displayNameDraft2, "Status Helper: name updated.", out applied); } private BlueSageCommandResult HandleStatusCommand(BlueSageCommandContext context) { string text = context.Arguments.Trim(); if (string.IsNullOrWhiteSpace(text)) { return BlueSageCommandResult.HandledWithMessages(string.IsNullOrWhiteSpace(StatusMessage.Value) ? "Status Helper: no status set. Use /status BRB or /clearstatus." : ("Status Helper status: " + StatusMessage.Value + ".")); } DisplayNameDraft displayNameDraft = CaptureDisplayNameDraft(captureLiveBase: true); displayNameDraft.StatusMessage = text; bool applied; BlueSageCommandResult result = TryApplyDisplayNameDraft(displayNameDraft, "Status Helper: status updated.", out applied); if (applied) { _lastStatusBeforePreset = string.Empty; } return result; } private BlueSageCommandResult SetPresetStatus(string status) { DisplayNameDraft displayNameDraft = CaptureDisplayNameDraft(captureLiveBase: true); string text = (displayNameDraft.StatusMessage ?? string.Empty).Trim(); string lastStatusBeforePreset = _lastStatusBeforePreset; if (!string.Equals(text, "BRB", StringComparison.OrdinalIgnoreCase) && !string.Equals(text, "AFK", StringComparison.OrdinalIgnoreCase)) { lastStatusBeforePreset = text; } displayNameDraft.StatusMessage = status; bool applied; BlueSageCommandResult result = TryApplyDisplayNameDraft(displayNameDraft, "Status Helper: status set to " + status + ".", out applied); if (applied) { _lastStatusBeforePreset = lastStatusBeforePreset; } return result; } private BlueSageCommandResult HandleBackStatusCommand(BlueSageCommandContext context) { DisplayNameDraft displayNameDraft = CaptureDisplayNameDraft(captureLiveBase: true); string text = (displayNameDraft.StatusMessage = (_lastStatusBeforePreset ?? string.Empty).Trim()); string successMessage = (string.IsNullOrWhiteSpace(text) ? "Status Helper: back cleared your BRB/AFK status." : ("Status Helper: back restored " + text + ".")); bool applied; BlueSageCommandResult result = TryApplyDisplayNameDraft(displayNameDraft, successMessage, out applied); if (applied) { _lastStatusBeforePreset = string.Empty; } return result; } private BlueSageCommandResult HandleClearStatusCommand(BlueSageCommandContext context) { DisplayNameDraft displayNameDraft = CaptureDisplayNameDraft(captureLiveBase: true); displayNameDraft.StatusMessage = string.Empty; bool applied; BlueSageCommandResult result = TryApplyDisplayNameDraft(displayNameDraft, "Status Helper: status cleared.", out applied); if (applied) { _lastStatusBeforePreset = string.Empty; } return result; } private BlueSageCommandResult HandleStatusColorCommand(BlueSageCommandContext context) { string text = context.Arguments.Trim(); if (string.IsNullOrWhiteSpace(text)) { return BlueSageCommandResult.HandledWithMessages("Status Helper color: #" + GetStatusColor() + "."); } bool applied; if (string.Equals(text, "reset", StringComparison.OrdinalIgnoreCase) || string.Equals(text, "default", StringComparison.OrdinalIgnoreCase)) { DisplayNameDraft displayNameDraft = CaptureDisplayNameDraft(captureLiveBase: true); displayNameDraft.StatusColor = "FFD45D"; return TryApplyDisplayNameDraft(displayNameDraft, "Status Helper color: reset to #FFD45D.", out applied); } if (!RichTextStyleBuilder.TryNormalizeHex(text, out var hex)) { return BlueSageCommandResult.HandledWithMessages("Status Helper: expected a hex color like #FFD45D."); } DisplayNameDraft displayNameDraft2 = CaptureDisplayNameDraft(captureLiveBase: true); displayNameDraft2.StatusColor = hex; return TryApplyDisplayNameDraft(displayNameDraft2, "Status Helper color: #" + hex + ".", out applied); } private BlueSageCommandResult HandleSpoonsCommand(BlueSageCommandContext context) { string text = context.Arguments.Trim(); if (string.IsNullOrWhiteSpace(text) || string.Equals(text, "status", StringComparison.OrdinalIgnoreCase)) { return BlueSageCommandResult.HandledWithMessages("Spoons: " + FormatSpoonStatus() + "."); } bool applied; if (string.Equals(text, "off", StringComparison.OrdinalIgnoreCase)) { DisplayNameDraft displayNameDraft = CaptureDisplayNameDraft(captureLiveBase: true); displayNameDraft.SpoonsEnabled = false; return TryApplyDisplayNameDraft(displayNameDraft, "Spoons: hidden.", out applied); } if (string.Equals(text, "reset", StringComparison.OrdinalIgnoreCase) || string.Equals(text, "default", StringComparison.OrdinalIgnoreCase)) { DisplayNameDraft displayNameDraft2 = CaptureDisplayNameDraft(captureLiveBase: true); displayNameDraft2.SpoonCount = 5; displayNameDraft2.SpoonLabel = "sp"; displayNameDraft2.SpoonsEnabled = false; displayNameDraft2.SpoonsUpdatedUtc = DateTime.UtcNow.ToString("O"); return TryApplyDisplayNameDraft(displayNameDraft2, "Spoons: reset to hidden default [5/5sp]. Use /spoons on when you want it visible.", out applied); } if (string.Equals(text, "on", StringComparison.OrdinalIgnoreCase)) { DisplayNameDraft displayNameDraft3 = CaptureDisplayNameDraft(captureLiveBase: true); displayNameDraft3.SpoonsEnabled = true; return TryApplyDisplayNameDraft(displayNameDraft3, "Spoons: visible as [" + ClampSpoons(displayNameDraft3.SpoonCount) + "/5" + GetSpoonLabel(displayNameDraft3.SpoonLabel) + "].", out applied); } if (!int.TryParse(text, out var result)) { return BlueSageCommandResult.HandledWithMessages("Spoons: use /spoons 0 through /spoons 5, /spoons status, /spoons on, /spoons off, or /spoons reset."); } DisplayNameDraft displayNameDraft4 = CaptureDisplayNameDraft(captureLiveBase: true); displayNameDraft4.SpoonCount = ClampSpoons(result); displayNameDraft4.SpoonsEnabled = true; displayNameDraft4.SpoonsUpdatedUtc = DateTime.UtcNow.ToString("O"); return TryApplyDisplayNameDraft(displayNameDraft4, "Spoons: set to [" + displayNameDraft4.SpoonCount + "/5" + GetSpoonLabel(displayNameDraft4.SpoonLabel) + "].", out applied); } private void EnforceHiddenDiagnosticsLockedOn() { if (EnableHiddenDiagnostics != null && !EnableHiddenDiagnostics.Value) { EnableHiddenDiagnostics.Value = true; ((BaseUnityPlugin)this).Config.Save(); ManualLogSource log = Log; if (log != null) { log.LogInfo((object)"BlueSage diagnostics config was normalized to the 0.2.x locked-on state."); } } } private BlueSageCommandResult HandleSetMoodCommand(BlueSageCommandContext context) { string text = context.Arguments.Trim(); if (string.IsNullOrWhiteSpace(text) || string.Equals(text, "status", StringComparison.OrdinalIgnoreCase)) { return BlueSageCommandResult.HandledWithMessages("Mood label: " + GetSpoonLabel() + ". Use /setmood energy, /setmood spoons, or /setmood default."); } if (string.Equals(text, "default", StringComparison.OrdinalIgnoreCase) || string.Equals(text, "reset", StringComparison.OrdinalIgnoreCase)) { text = "sp"; } string text2 = new string(text.Where((char c) => char.IsLetterOrDigit(c) || c == '-' || c == '_').Take(12).ToArray()); if (string.IsNullOrWhiteSpace(text2)) { return BlueSageCommandResult.HandledWithMessages("Mood label: use 1-12 letters/numbers, '-' or '_'. Example: /setmood energy."); } DisplayNameDraft displayNameDraft = CaptureDisplayNameDraft(captureLiveBase: true); displayNameDraft.SpoonLabel = text2; displayNameDraft.SpoonsEnabled = true; displayNameDraft.SpoonsUpdatedUtc = DateTime.UtcNow.ToString("O"); bool applied; return TryApplyDisplayNameDraft(displayNameDraft, "Mood label: now [" + ClampSpoons(displayNameDraft.SpoonCount) + "/5" + text2 + "]. /setmood default restores [x/5sp].", out applied); } internal string ApplySpoonVisibilityFromMenu() { bool applied; return TrySetSpoonVisibilityFromMenu(ShouldApplySpoons, out applied); } internal string TrySetSpoonVisibilityFromMenu(bool enabled, out bool applied) { DisplayNameDraft displayNameDraft = CaptureDisplayNameDraft(captureLiveBase: true); displayNameDraft.SpoonsEnabled = enabled; BlueSageCommandResult blueSageCommandResult = TryApplyDisplayNameDraft(displayNameDraft, enabled ? ("Spoons: visible as [" + ClampSpoons(displayNameDraft.SpoonCount) + "/5" + GetSpoonLabel(displayNameDraft.SpoonLabel) + "].") : "Spoons: hidden.", out applied); if (blueSageCommandResult.Messages.Count <= 0) { if (!enabled) { return "Spoons: hidden."; } return "Spoons: visible."; } return blueSageCommandResult.Messages[0]; } internal string TryApplySpoonsFromStyleUi(int count, string label, bool enabled, string updatedUtc, out bool applied) { DisplayNameDraft displayNameDraft = CaptureDisplayNameDraft(captureLiveBase: true); displayNameDraft.SpoonCount = ClampSpoons(count); displayNameDraft.SpoonLabel = GetSpoonLabel(label); displayNameDraft.SpoonsEnabled = enabled; displayNameDraft.SpoonsUpdatedUtc = updatedUtc ?? string.Empty; BlueSageCommandResult blueSageCommandResult = TryApplyDisplayNameDraft(displayNameDraft, enabled ? ("Spoons: visible as [" + displayNameDraft.SpoonCount + "/5" + displayNameDraft.SpoonLabel + "].") : "Spoons: hidden.", out applied); if (blueSageCommandResult.Messages.Count <= 0) { return "Spoons updated."; } return blueSageCommandResult.Messages[0]; } internal void CaptureLiveStyledBaseBeforeSuffixChange() { } private BlueSageCommandResult ApplyStatusNameToPlayerData(string successMessage) { bool applied; return TryApplyDisplayNameDraft(CaptureDisplayNameDraft(captureLiveBase: false), successMessage, out applied); } private DisplayNameDraft CaptureDisplayNameDraft(bool captureLiveBase) { DisplayNameDraft displayNameDraft = new DisplayNameDraft { BaseName = GetStatusBaseName(), StatusMessage = (StatusMessage?.Value ?? string.Empty), StatusColor = GetStatusColor(), StatusBrackets = (StatusBrackets?.Value ?? "()"), SpoonsEnabled = ShouldApplySpoons, SpoonCount = ClampSpoons(SpoonCount?.Value ?? 5), SpoonLabel = GetSpoonLabel(), SpoonsUpdatedUtc = (SpoonsUpdatedUtc?.Value ?? string.Empty) }; if (captureLiveBase && string.IsNullOrWhiteSpace(displayNameDraft.BaseName)) { string baseName = GetCurrentDisplayNameStateForStyleUi().BaseName; if (!string.IsNullOrWhiteSpace(RichTextStyleBuilder.ToPlainStyleEditorText(baseName))) { displayNameDraft.BaseName = baseName; } } return displayNameDraft; } public string GetCompanionStyledBase() { return CaptureDisplayNameDraft(captureLiveBase: false).BaseName ?? string.Empty; } public bool TryApplyCompanionStyledBase(string baseName, out string resultMessage) { DisplayNameDraft displayNameDraft = CaptureDisplayNameDraft(captureLiveBase: false); displayNameDraft.BaseName = baseName ?? string.Empty; bool applied; BlueSageCommandResult blueSageCommandResult = TryApplyDisplayNameDraft(displayNameDraft, "Companion style change applied through BlueSage QoL.", out applied); resultMessage = ((blueSageCommandResult.Messages.Count > 0) ? blueSageCommandResult.Messages[0] : (applied ? "Companion style change applied through BlueSage QoL." : "Companion style change failed.")); return applied; } public bool TrySetCompanionHostRoleLabel(string richText, out string resultMessage) { return PlayerRoleLabelController.TrySetLocalHostLabelOverride(richText, out resultMessage); } public bool TryClearCompanionHostRoleLabel(out string resultMessage) { return PlayerRoleLabelController.TryClearLocalHostLabelOverride(out resultMessage); } public bool TryFocusCompanionHostConsole(out string resultMessage) { if ((Object)(object)_qolMenuWindow == (Object)null) { StartQolMenuWindow(); } if ((Object)(object)_qolMenuWindow == (Object)null || !_qolMenuWindow.ShowHostConsole()) { resultMessage = "Host Console unavailable; current verified host access is required."; return false; } resultMessage = "BlueSage Host Console opened."; return true; } public bool TryResolveCompanionIdentityTruthV1(ulong exactSteamId, ulong exactPurrNetId, out BlueSageIdentityTruthSnapshotV1 truth, out string resultMessage) { return PlayerBadgeController.TryResolveFreshIdentityTruth(exactSteamId, exactPurrNetId, out truth, out resultMessage); } public bool TryResolveCompanionIdentityTruthV2(ulong exactSteamId, out BlueSageIdentityTruthSnapshotV1 truth, out string resultMessage) { return PlayerBadgeController.TryResolveFreshIdentityTruthBySteam(exactSteamId, out truth, out resultMessage); } private BlueSageCommandResult TryApplyDisplayNameDraft(DisplayNameDraft draft, string successMessage, out bool applied) { //IL_0189: Unknown result type (might be due to invalid IL or missing references) //IL_018e: Unknown result type (might be due to invalid IL or missing references) //IL_0195: Unknown result type (might be due to invalid IL or missing references) //IL_0235: Unknown result type (might be due to invalid IL or missing references) //IL_0239: Unknown result type (might be due to invalid IL or missing references) //IL_023f: Unknown result type (might be due to invalid IL or missing references) applied = false; if (draft == null) { return BlueSageCommandResult.HandledWithMessages("Status Helper: player name is not ready yet."); } int num = (PlayerIdInfoLimitPatch.HasExpectedCoverage ? LockedMaxIdCardCharacters : Math.Min(LockedMaxIdCardCharacters, 1000)); if (!DisplayNameMutationPolicy.TryCompose(draft.BaseName, BuildStatusSuffix(draft), BuildSpoonSuffix(draft), num, out var displayName, out var error)) { ManualLogSource log = Log; if (log != null) { log.LogWarning((object)("Status Helper rejected a display-name draft before mutation. " + $"baseLength={(draft.BaseName ?? string.Empty).Length}, limit={num}, reason={error}")); } return BlueSageCommandResult.HandledWithMessages(error); } DataManager val; try { val = MonoSingleton.I; } catch { val = null; } if ((Object)(object)val == (Object)null || val.PlayerData == null) { return BlueSageCommandResult.HandledWithMessages("Status Helper: player data is not ready yet."); } TextChannelManager val2; try { val2 = NetworkSingleton.I; } catch { val2 = null; } if ((Object)(object)val2 == (Object)null || (Object)(object)val2.MainCustomizationController == (Object)null) { return BlueSageCommandResult.HandledWithMessages("Status Helper: native player identity sync is not ready yet."); } DisplayNameDraft previous = CaptureDisplayNameDraft(captureLiveBase: false); string name = val.PlayerData.Name; string userName = val2.UserName; UIManager val3 = null; string uiName = string.Empty; try { val3 = MonoSingleton.I; if ((Object)(object)val3 != (Object)null && (Object)(object)val3.PlayerText != (Object)null) { uiName = ((TMP_Text)val3.PlayerText).text; } val.PlayerData.Name = displayName; val.SavePlayerData(); PlayerIDInfo playerIdInfo = val.PlayerData.GetPlayerIdInfo(); string text = Encoding.Unicode.GetString(playerIdInfo.Name ?? new byte[0]).TrimEnd(new char[1]); if (!string.Equals(val.PlayerData.Name, displayName, StringComparison.Ordinal) || !string.Equals(text, displayName, StringComparison.Ordinal)) { throw new InvalidOperationException("native display-name readback did not match the requested value " + $"(requested={displayName.Length}, playerData={(val.PlayerData.Name ?? string.Empty).Length}, serialized={text.Length})"); } val2.UserName = displayName; val2.MainCustomizationController.UpdatePlayerInfo(playerIdInfo, default(RPCInfo)); if ((Object)(object)val3 != (Object)null && (Object)(object)val3.PlayerText != (Object)null) { ((TMP_Text)val3.PlayerText).text = displayName; } if (!string.Equals(val2.UserName, displayName, StringComparison.Ordinal)) { throw new InvalidOperationException("text-channel display-name readback did not match the requested value"); } if ((Object)(object)val3 != (Object)null && (Object)(object)val3.PlayerText != (Object)null && !string.Equals(((TMP_Text)val3.PlayerText).text, displayName, StringComparison.Ordinal)) { throw new InvalidOperationException("local UI display-name readback did not match the requested value"); } CommitDisplayNameDraft(draft); ((BaseUnityPlugin)this).Config.Save(); applied = true; return BlueSageCommandResult.HandledWithMessages(successMessage); } catch (Exception ex) { RestoreDisplayNameConfig(previous); TryRestoreNativeDisplayName(val, name, val2, userName, val3, uiName); Log.LogWarning((object)("Status Helper update failed: " + ex.GetType().Name + ": " + ex.Message)); return BlueSageCommandResult.HandledWithMessages("Status Helper: update failed; check BepInEx console."); } } private static void CommitDisplayNameDraft(DisplayNameDraft draft) { StatusBaseName.Value = (draft.BaseName ?? string.Empty).Trim(); StatusMessage.Value = (draft.StatusMessage ?? string.Empty).Trim(); StatusColor.Value = GetNormalizedStatusColor(draft.StatusColor); StatusBrackets.Value = (string.IsNullOrWhiteSpace(draft.StatusBrackets) ? "()" : draft.StatusBrackets); EnableSpoons.Value = draft.SpoonsEnabled; SpoonCount.Value = ClampSpoons(draft.SpoonCount); SpoonLabel.Value = GetSpoonLabel(draft.SpoonLabel); SpoonsUpdatedUtc.Value = draft.SpoonsUpdatedUtc ?? string.Empty; } private static void RestoreDisplayNameConfig(DisplayNameDraft previous) { if (previous == null) { return; } CommitDisplayNameDraft(previous); try { Plugin instance = Instance; if (instance != null) { ((BaseUnityPlugin)instance).Config.Save(); } } catch { } } private static void TryRestoreNativeDisplayName(DataManager dataManager, string playerName, TextChannelManager textChannelManager, string channelName, UIManager uiManager, string uiName) { //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_005f: 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) try { if (dataManager?.PlayerData != null) { dataManager.PlayerData.Name = playerName; dataManager.SavePlayerData(); } if ((Object)(object)textChannelManager != (Object)null) { textChannelManager.UserName = channelName; if ((Object)(object)textChannelManager.MainCustomizationController != (Object)null && dataManager?.PlayerData != null) { textChannelManager.MainCustomizationController.UpdatePlayerInfo(dataManager.PlayerData.GetPlayerIdInfo(), default(RPCInfo)); } } if ((Object)(object)uiManager != (Object)null && (Object)(object)uiManager.PlayerText != (Object)null) { ((TMP_Text)uiManager.PlayerText).text = uiName; } } catch { } } private static string BuildStatusDisplayName(string baseName) { string statusSuffix = BuildStatusSuffix(); string spoonSuffix = BuildSpoonSuffix(); return DisplayNameCompositionPolicy.Compose(baseName, statusSuffix, spoonSuffix); } private static string BuildStatusSuffix() { return BuildStatusSuffix(new DisplayNameDraft { StatusMessage = (StatusMessage?.Value ?? string.Empty), StatusColor = GetStatusColor(), StatusBrackets = (StatusBrackets?.Value ?? "()") }); } private static string BuildStatusSuffix(DisplayNameDraft draft) { return StatusSuffixPolicy.Build(draft?.StatusMessage ?? string.Empty, GetNormalizedStatusColor(draft?.StatusColor), draft?.StatusBrackets ?? "()"); } internal static string BuildSpoonSuffix() { return BuildSpoonSuffix(new DisplayNameDraft { SpoonsEnabled = ShouldApplySpoons, SpoonCount = (SpoonCount?.Value ?? 5), SpoonLabel = GetSpoonLabel() }); } private static string BuildSpoonSuffix(DisplayNameDraft draft) { if (draft == null || !draft.SpoonsEnabled) { return string.Empty; } int num = ClampSpoons(draft.SpoonCount); return RichTextStyleBuilder.BuildColorTag("70FFBD", $"[{num}/5{GetSpoonLabel(draft.SpoonLabel)}]"); } private static string FormatSpoonStatus() { int num = ClampSpoons(SpoonCount?.Value ?? 5); string text = SpoonsUpdatedUtc?.Value ?? string.Empty; string text2 = (string.IsNullOrWhiteSpace(text) ? "not stamped yet" : ("updated " + text)); string text3 = (ShouldApplySpoons ? "visible" : "hidden"); return $"{num}/5{GetSpoonLabel()}, {text3}, {text2}"; } private static string GetSpoonLabel() { return GetSpoonLabel(SpoonLabel?.Value); } private static string GetSpoonLabel(string value) { string text = new string((value ?? "sp").Where((char c) => char.IsLetterOrDigit(c) || c == '-' || c == '_').Take(12).ToArray()); if (!string.IsNullOrWhiteSpace(text)) { return text; } return "sp"; } private static int ClampSpoons(int value) { if (value < 0) { return 0; } if (value <= 5) { return value; } return 5; } private static void StampSpoonsUpdated() { if (SpoonsUpdatedUtc != null) { SpoonsUpdatedUtc.Value = DateTime.UtcNow.ToString("O"); } } 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() { return GetNormalizedStatusColor(StatusColor?.Value); } private static string GetSteamPersonaName() { try { if (!SteamManager.Initialized) { return string.Empty; } return (SteamFriends.GetPersonaName() ?? string.Empty).Trim(); } catch { return string.Empty; } } private static string GetNormalizedStatusColor(string value) { if (!RichTextStyleBuilder.TryNormalizeHex(value, out var hex)) { return "FFD45D"; } return hex; } internal static string GetPingHighlightColor() { if (!RichTextStyleBuilder.TryNormalizeHex(PingHighlightColor.Value, out var hex)) { return "9B59B6"; } return hex; } internal static string GetPingMentionColor() { if (!RichTextStyleBuilder.TryNormalizeHex(PingMentionColor.Value, out var hex)) { return "FFD700"; } return hex; } internal static string GetPingSoundMode() { string text = (PingSoundMode?.Value ?? "ticket").Trim().ToLowerInvariant(); switch (text) { case "change": case "error": case "task": case "ticket": return text; default: return "ticket"; } } private BlueSageCommandResult HandlePingSoundCommand(BlueSageCommandContext context) { string text = (context.Arguments ?? string.Empty).Trim().ToLowerInvariant(); if (!string.IsNullOrWhiteSpace(text)) { switch (text) { case "status": break; case "on": case "off": EnablePingSound.Value = text == "on"; ((BaseUnityPlugin)this).Config.Save(); return BlueSageCommandResult.HandledWithMessages("Ping Sound: " + (EnablePingSound.Value ? "on." : "off.")); case "click": case "change": case "error": case "task": case "ticket": PingSoundMode.Value = text; EnablePingSound.Value = true; ((BaseUnityPlugin)this).Config.Save(); MentionPingController.PlayConfiguredPing(); return BlueSageCommandResult.HandledWithMessages("Ping Sound mode set to " + text + "."); default: return BlueSageCommandResult.HandledWithMessages("Ping Sound: use /pingsound click, change, error, task, ticket, on, off, or status."); } } return BlueSageCommandResult.HandledWithMessages("Ping Sound: " + (EnablePingSound.Value ? "on" : "off") + ", mode=" + GetPingSoundMode() + ". Use /pingsound click|change|error|task|ticket|on|off."); } private void StartMentionPingController() { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Expected O, but got Unknown if (!((Object)(object)_mentionPingControllerObject != (Object)null)) { _mentionPingControllerObject = new GameObject("BlueSage_MentionPingController"); _mentionPingControllerObject.AddComponent(); Object.DontDestroyOnLoad((Object)(object)_mentionPingControllerObject); } } private void StartCommandTypeaheadController() { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Expected O, but got Unknown if (!((Object)(object)_commandTypeaheadControllerObject != (Object)null)) { _commandTypeaheadControllerObject = new GameObject("BlueSage_CommandTypeaheadController"); _commandTypeaheadControllerObject.AddComponent(); Object.DontDestroyOnLoad((Object)(object)_commandTypeaheadControllerObject); } } private void StartChatReadabilityController() { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Expected O, but got Unknown if (!((Object)(object)_chatReadabilityControllerObject != (Object)null)) { _chatReadabilityControllerObject = new GameObject("BlueSage_ChatReadabilityController"); _chatReadabilityControllerObject.AddComponent(); Object.DontDestroyOnLoad((Object)(object)_chatReadabilityControllerObject); } } private void StartSessionAuditController() { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Expected O, but got Unknown if (!((Object)(object)_sessionAuditControllerObject != (Object)null)) { _sessionAuditControllerObject = new GameObject("BlueSage_SessionAuditController"); _sessionAuditControllerObject.AddComponent(); Object.DontDestroyOnLoad((Object)(object)_sessionAuditControllerObject); } } private void StartSessionRunMarkerController() { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Expected O, but got Unknown if (!((Object)(object)_sessionRunMarkerControllerObject != (Object)null)) { _sessionRunMarkerControllerObject = new GameObject("BlueSage_SessionRunMarkerController"); _sessionRunMarkerControllerObject.AddComponent(); Object.DontDestroyOnLoad((Object)(object)_sessionRunMarkerControllerObject); } } private void StartStewardAnnouncementBridgeController() { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Expected O, but got Unknown if (!((Object)(object)_stewardAnnouncementBridgeControllerObject != (Object)null)) { _stewardAnnouncementBridgeControllerObject = new GameObject("BlueSage_StewardAnnouncementBridgeController"); _stewardAnnouncementBridgeControllerObject.AddComponent(); Object.DontDestroyOnLoad((Object)(object)_stewardAnnouncementBridgeControllerObject); } } internal static bool IsLobbyMassPingAuthorized(string senderSteamId) { if (!TryGetCurrentSteamLobby(out var _)) { return false; } if (string.IsNullOrWhiteSpace(senderSteamId) || !PlayerIdentityEvidenceController.TryResolveExact(senderSteamId, out var _, out var _)) { return false; } return SteamIdModerationController.IsAuthorizedCurrentIssuer(senderSteamId); } private void StartRescueReceiverCapabilityPublisher() { if (_rescueReceiverCapabilityCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_rescueReceiverCapabilityCoroutine); } _rescueReceiverCapabilityCoroutine = ((MonoBehaviour)this).StartCoroutine(RescueReceiverCapabilityLoop()); } private IEnumerator RescueReceiverCapabilityLoop() { while (true) { PublishRescueReceiverCapability(); yield return (object)new WaitForSecondsRealtime(5f); } } private void PublishRescueReceiverCapability() { //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_002d: 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_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Unknown result type (might be due to invalid IL or missing references) if (PluginShutdownController.IsShuttingDown) { return; } try { if (!OptInRescuePatch.IsReceiverAvailable || !SteamManager.Initialized || !TryGetRescueLobby(out var lobbyId) || lobbyId == CSteamID.Nil || !IsLocalCurrentLobbyMember(lobbyId)) { ClearRescueReceiverCapability(); return; } if (_rescueReceiverCapabilityLobby != CSteamID.Nil && _rescueReceiverCapabilityLobby != lobbyId) { ClearRescueReceiverCapability(); } string text = RescueReceiverCapabilityPolicy.BuildPayload("0.2.4", DateTimeOffset.UtcNow.ToUnixTimeSeconds()); if (text.Length == 0) { ClearRescueReceiverCapability(); return; } SteamMatchmaking.SetLobbyMemberData(lobbyId, "bluesage_qol_rescue_receiver_v1", text); _rescueReceiverCapabilityLobby = lobbyId; } catch { ClearRescueReceiverCapability(); } } private void ClearRescueReceiverCapability() { //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) if (PluginShutdownController.IsShuttingDown) { return; } CSteamID rescueReceiverCapabilityLobby = _rescueReceiverCapabilityLobby; _rescueReceiverCapabilityLobby = CSteamID.Nil; if (rescueReceiverCapabilityLobby == CSteamID.Nil || !SteamManager.Initialized) { return; } try { SteamMatchmaking.SetLobbyMemberData(rescueReceiverCapabilityLobby, "bluesage_qol_rescue_receiver_v1", string.Empty); } catch { } } private void ClearRescueReceiverCapability(SteamRuntimeSnapshot snapshot) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: 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) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) CSteamID val = ((_rescueReceiverCapabilityLobby != CSteamID.Nil) ? _rescueReceiverCapabilityLobby : (snapshot?.Lobby ?? CSteamID.Nil)); _rescueReceiverCapabilityLobby = CSteamID.Nil; if (snapshot == null || !snapshot.SteamWasReady || val == CSteamID.Nil) { return; } try { SteamMatchmaking.SetLobbyMemberData(val, "bluesage_qol_rescue_receiver_v1", string.Empty); } catch { } } private static bool IsLocalCurrentLobbyMember(CSteamID lobby) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) CSteamID steamID = SteamUser.GetSteamID(); int numLobbyMembers = SteamMatchmaking.GetNumLobbyMembers(lobby); for (int i = 0; i < numLobbyMembers; i++) { if (SteamMatchmaking.GetLobbyMemberByIndex(lobby, i) == steamID) { return true; } } return false; } internal static bool TryGetRescueLobby(out CSteamID lobbyId) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) if (PluginShutdownController.IsShuttingDown) { lobbyId = CSteamID.Nil; return false; } return TryGetCurrentSteamLobby(out lobbyId); } private BlueSageCommandResult HandleHelperCommand(BlueSageCommandContext context) { //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0051: 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 (!CanManageHelpers()) { return BlueSageCommandResult.HandledWithMessages("Helper management is available only to the current lobby host. No Helper details were shown."); } if (!TryGetCurrentSteamLobby(out var lobbyId)) { return BlueSageCommandResult.HandledWithMessages("Helpers: join a lobby first."); } CSteamID lobbyOwner = SteamMatchmaking.GetLobbyOwner(lobbyId); if (lobbyOwner == CSteamID.Nil || SteamUser.GetSteamID() != lobbyOwner || (Object)(object)NetworkManager.main == (Object)null || !NetworkManager.main.isHost) { return BlueSageCommandResult.HandledWithMessages("Helpers: this function is host only. You are not the current lobby host."); } string[] array = (context.Arguments ?? string.Empty).Split(new char[1] { ' ' }, 2, StringSplitOptions.RemoveEmptyEntries); string text = ((array.Length != 0) ? array[0].ToLowerInvariant() : "list"); string text2 = ((ulong)lobbyOwner).ToString(); HashSet unifiedHelpersForOwner = GetUnifiedHelpersForOwner(text2, importLegacy: true); switch (text) { case "list": case "status": { string text3 = ((unifiedHelpersForOwner.Count == 0) ? "none" : string.Join(", ", unifiedHelpersForOwner.Select(FormatSteamIdSuffix))); return BlueSageCommandResult.HandledWithMessages("Helpers: " + text3 + ". One host-scoped list grants @lobby and host-validated moderation requests."); } case "reset": case "clear": SaveUnifiedHelpers(text2, Array.Empty()); SteamIdModerationController.PublishHostCapability(); PlayerIdentityRoleHighlightController.RefreshAll(); return BlueSageCommandResult.HandledWithMessages("Helpers: cleared. Only the host remains authorized."); default: { if ((text != "add" && text != "remove" && text != "revoke") || array.Length < 2) { return BlueSageCommandResult.HandledWithMessages("Helpers: use /helper add , remove , list, or reset."); } if (!TryResolveLobbyMemberSteamId(array[1], out var steamId, out var error)) { return BlueSageCommandResult.HandledWithMessages("Helpers: " + error); } if (steamId == text2) { return BlueSageCommandResult.HandledWithMessages("Helpers: the host is already authorized."); } bool flag = text == "add"; bool flag2 = (flag ? unifiedHelpersForOwner.Add(steamId) : unifiedHelpersForOwner.Remove(steamId)); SaveUnifiedHelpers(text2, unifiedHelpersForOwner); SteamIdModerationController.PublishHostCapability(); PlayerIdentityRoleHighlightController.RefreshAll(); return BlueSageCommandResult.HandledWithMessages("Helpers: " + ((!flag2) ? "no change for " : (flag ? "authorized " : "revoked ")) + FormatSteamIdSuffix(steamId) + ". Helpers may use @lobby and request host-validated moderation actions."); } } } internal static HashSet GetUnifiedHelpersForOwner(string ownerSteamId, bool importLegacy) { string text = UnifiedHelperPolicy.MergeForOwner(ownerSteamId, LobbyHelpers?.Value ?? string.Empty, LobbyHelperOwnerSteamId?.Value ?? string.Empty, (!importLegacy) ? string.Empty : (LobbyPingDelegates?.Value ?? string.Empty), (!importLegacy) ? string.Empty : (LobbyPingDelegateOwnerSteamId?.Value ?? string.Empty), (!importLegacy) ? string.Empty : (LobbyModerationDelegates?.Value ?? string.Empty), (!importLegacy) ? string.Empty : (LobbyModerationDelegateOwnerSteamId?.Value ?? string.Empty)); HashSet hashSet = new HashSet(LobbyPingAuthorizationPolicy.ParseDelegates(text), StringComparer.Ordinal); if (importLegacy && (LobbyHelperOwnerSteamId == null || !string.Equals(LobbyHelperOwnerSteamId.Value, ownerSteamId, StringComparison.Ordinal) || !string.Equals(LobbyHelpers?.Value ?? string.Empty, text, StringComparison.Ordinal))) { SaveUnifiedHelpers(ownerSteamId, hashSet); } return hashSet; } private static void SaveUnifiedHelpers(string ownerSteamId, IEnumerable helpers) { string value = LobbyPingAuthorizationPolicy.SerializeDelegates(helpers ?? Array.Empty()); LobbyHelpers.Value = value; LobbyHelperOwnerSteamId.Value = ownerSteamId; LobbyPingDelegates.Value = value; LobbyPingDelegateOwnerSteamId.Value = ownerSteamId; LobbyModerationDelegates.Value = value; LobbyModerationDelegateOwnerSteamId.Value = ownerSteamId; Plugin instance = Instance; if (instance != null) { ((BaseUnityPlugin)instance).Config.Save(); } } private BlueSageCommandResult HandleSidListCommand(BlueSageCommandContext context) { if (!CanUseLobbySafety(out var _)) { return LobbySafetyDeniedResult(); } IReadOnlyList snapshot = CloneIncidentLedger.Snapshot; if (snapshot.Count == 0) { return BlueSageCommandResult.HandledWithMessages("Clone Shield evidence: no open current-lobby incidents. Open the Lobby Safety tab for the verified roster, Helper controls, and closed history."); } List list = new List { "Clone Shield: " + snapshot.Count + " open incident(s). Open the Lobby Safety tab for exact IDs, profile buttons, highlights, preview, resolve, and dismiss controls." }; foreach (CloneIncident item in snapshot.OrderByDescending((CloneIncident item) => item.LastObservedUnixSeconds).Take(3)) { list.Add(item.Id + " | protected victim …" + FormatSteamIdSuffix(item.VictimSteamId).Replace("Steam …", string.Empty) + " | suspect …" + FormatSteamIdSuffix(item.SuspectSteamId).Replace("Steam …", string.Empty) + " | " + item.MatchType + "."); } if (snapshot.Count > 3) { list.Add(snapshot.Count - 3 + " more open incident(s) are available in Lobby Safety without filling chat."); } return BlueSageCommandResult.HandledWithMessages(list.ToArray()); } private BlueSageCommandResult HandleSidIncidentCommand(BlueSageCommandContext context) { if (!CanUseLobbySafety(out var _)) { return LobbySafetyDeniedResult(); } string[] array = (context.Arguments ?? string.Empty).Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries); if (array.Length == 0 || string.Equals(array[0], "status", StringComparison.OrdinalIgnoreCase) || string.Equals(array[0], "list", StringComparison.OrdinalIgnoreCase)) { int count = CloneIncidentActionController.GetOpenIncidents().Count; int num = CloneIncidentActionController.GetIncidentHistory().Count((CloneIncident item) => item.State != CloneIncidentState.Open); return BlueSageCommandResult.HandledWithMessages("Clone incidents: " + count + " open, " + num + " closed locally. Use /sidincident resolve|dismiss, clear-player , dismiss-all, clear-history, or clear-all."); } switch (array[0].ToLowerInvariant()) { case "dismiss-all": case "dismissall": { CloneIncidentActionController.TryDismissAllOpenLocal(out string message3); return BlueSageCommandResult.HandledWithMessages(message3); } case "clear-history": case "clearhistory": { int num3 = CloneIncidentActionController.ClearClosedHistory(); return BlueSageCommandResult.HandledWithMessages("Cleared " + num3 + " closed local Clone Shield " + ((num3 == 1) ? "record" : "records") + ". Open incidents and lobby moderation state were unchanged."); } case "clear-all": case "clearall": { CloneIncidentActionController.TryDismissAllOpenLocal(out string _); int num2 = CloneIncidentActionController.ClearClosedHistory(); return BlueSageCommandResult.HandledWithMessages("Cleared the local Clone Shield incident view (" + num2 + " archived record" + ((num2 == 1) ? string.Empty : "s") + "). No bans, Helper state, or lobby moderation state changed."); } case "clear-player": case "clear-row": case "clearplayer": case "clearrow": if (array.Length >= 2) { if (!PlayerIdentityEvidenceController.TryResolveLookup(string.Join(" ", array.Skip(1)), out var evidence, out var error)) { return BlueSageCommandResult.HandledWithMessages("Clone incident player cleanup refused: " + error); } CloneIncidentActionController.TryDismissOpenForVerifiedPlayer(evidence.SteamId, out string message); return BlueSageCommandResult.HandledWithMessages(message); } break; } if (array.Length >= 2 && array[0].StartsWith("C", StringComparison.OrdinalIgnoreCase)) { string incidentId = array[0]; string text = array[1].ToLowerInvariant(); if (text == "dismiss") { CloneIncidentActionController.TryDismissLocal(incidentId, out string message4); return BlueSageCommandResult.HandledWithMessages(message4); } if (text == "resolve") { CloneIncidentActionController.TryResolveForLobby(incidentId, out string message5); return BlueSageCommandResult.HandledWithMessages(message5); } } return BlueSageCommandResult.HandledWithMessages("Clone incidents: use /sidincident status, resolve, dismiss, clear-player , dismiss-all, clear-history, or clear-all. Clear actions are local-only."); } private BlueSageCommandResult HandleSidWhoCommand(BlueSageCommandContext context) { if (!CanUseLobbySafety(out var _)) { return LobbySafetyDeniedResult(); } string text = (context.Arguments ?? string.Empty).Trim(); bool flag = text.StartsWith("copy ", StringComparison.OrdinalIgnoreCase); string text2 = (flag ? text.Substring("copy ".Length).Trim() : text); string[] array = text.Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries); if (array.Length != 0 && string.Equals(array[0], "list", StringComparison.OrdinalIgnoreCase)) { IReadOnlyList verifiedRoster = PlayerIdentityEvidenceController.GetVerifiedRoster(); return BlueSageCommandResult.HandledWithMessages((verifiedRoster.Count == 0) ? "Lobby Safety: no player rows currently pass both native-roster and Steam-lobby verification." : ("Lobby Safety: " + verifiedRoster.Count + " verified player row(s). Open the authorized Lobby Safety tab for paging, exact copy, profile verification, incident evidence, and host-validated actions without filling chat.")); } if (string.IsNullOrWhiteSpace(text)) { return BlueSageCommandResult.HandledWithMessages("SID lookup: use /sidwho list, or /sidwho .", "This command is read-only. /sidban still refuses names and requires preview plus confirmation."); } string error; IReadOnlyList readOnlyList = PlayerIdentityEvidenceController.ResolveLookupMatches(text2, out error); if (readOnlyList.Count == 0) { return BlueSageCommandResult.HandledWithMessages("SID lookup NOT RESOLVED: " + error + " No moderation action was taken."); } List list; if (readOnlyList.Count > 1) { list = new List(); list.Add("SID lookup: " + readOnlyList.Count + " verified current players match '" + text2 + "'. No player was selected and no moderation action was taken."); List list2 = list; foreach (PlayerIdentityEvidence item in readOnlyList.Take(8)) { list2.Add("row #" + (item.RosterIndex + 1) + " | " + item.DisplayName + " | " + item.SteamPersona + " | " + item.SteamId + " | " + item.Role); } if (readOnlyList.Count > 8) { list2.Add(readOnlyList.Count - 8 + " more match(es). Refine the query or use Lobby Safety's live filter."); } list2.Add("Read-only results only. Select the exact live row in Lobby Safety before any preview action."); return BlueSageCommandResult.HandledWithMessages(list2.ToArray()); } PlayerIdentityEvidence playerIdentityEvidence = readOnlyList[0]; if (flag) { GUIUtility.systemCopyBuffer = PlayerIdentityEvidenceController.BuildCopyableEvidence(playerIdentityEvidence); return BlueSageCommandResult.HandledWithMessages("SID lookup COPY: copied verified live row #" + (playerIdentityEvidence.RosterIndex + 1) + " with displayed name, raw TMP source, Steam persona, exact SteamID64, PlayerID, and current role. Nothing was sent to chat."); } list = new List(); list.Add("SID lookup VERIFIED | row #" + (playerIdentityEvidence.RosterIndex + 1) + " | " + playerIdentityEvidence.DisplayName); list.Add("SteamID64: " + playerIdentityEvidence.SteamId + " | PlayerID " + playerIdentityEvidence.NetworkPlayerId + " | ending …" + PlayerIdentityEvidenceController.Suffix(playerIdentityEvidence.SteamId) + " | " + playerIdentityEvidence.Role); List list3 = list; if (!string.IsNullOrWhiteSpace(playerIdentityEvidence.SteamPersona)) { list3.Add("Cached Steam persona: " + playerIdentityEvidence.SteamPersona + ". Use the green profile button to open this exact account."); } if (!string.Equals(playerIdentityEvidence.DisplayNameRaw, playerIdentityEvidence.DisplayName, StringComparison.Ordinal)) { list3.Add("Raw TMP name source is visible and copyable in Lobby Safety and the local chat audit JSONL."); } list3.Add("Read-only evidence only. Recheck the profile before /sidban; cloned names never authorize or select a ban target."); return BlueSageCommandResult.HandledWithMessages(list3.ToArray()); } private BlueSageCommandResult HandleIdentityResyncCommand(BlueSageCommandContext context) { return IdentityResyncController.HandleCommand(context); } private BlueSageCommandResult HandleSidBanCommand(BlueSageCommandContext context) { if (!CanUseLobbySafety(out var _)) { return LobbySafetyDeniedResult(); } string text = (context.Arguments ?? string.Empty).Trim(); int num; object obj; if (!text.Equals("confirm", StringComparison.OrdinalIgnoreCase)) { num = (text.EndsWith(" confirm", StringComparison.OrdinalIgnoreCase) ? 1 : 0); if (num == 0) { obj = text; goto IL_0079; } } else { num = 1; } obj = (text.Equals("confirm", StringComparison.OrdinalIgnoreCase) ? string.Empty : text.Substring(0, text.Length - " confirm".Length).Trim()); goto IL_0079; IL_0079: string text2 = (string)obj; if (num != 0) { return ConfirmPendingModeration("ban", text2); } if (string.IsNullOrWhiteSpace(text2)) { return BlueSageCommandResult.HandledWithMessages("SID ban: use a verified current row, C# incident, or exact SteamID64. The active host may also preview an exact off-roster SteamID64. Review the preview, then add confirm within 30 seconds."); } if (!SteamIdModerationController.CanLocalIssue(out var isHost2, out var reason)) { return BlueSageCommandResult.HandledWithMessages("SID ban: " + reason); } string error; string[] array = PlayerIdentityEvidenceController.GetVerifiedCurrentSteamIds(out error).ToArray(); if (!string.IsNullOrWhiteSpace(error)) { return BlueSageCommandResult.HandledWithMessages("SID ban: verified live roster unavailable: " + error + ". No preview was created."); } bool flag = ModerationProtocolPolicy.IsSteamId64(text2) && !array.Contains(text2, StringComparer.Ordinal); ModerationTarget target; string error2; if (flag) { if (!isHost2) { return BlueSageCommandResult.HandledWithMessages("SID ban: only the active lobby host can preview an exact SteamID64 that is not in the current lobby. Helpers must select a verified current player."); } target = new ModerationTarget(text2, "none"); } else if (!ModerationTargetResolver.TryResolve(text2, array, out target, out error2)) { return BlueSageCommandResult.HandledWithMessages("SID ban: " + error2); } if (CloneIncidentLedger.IsProtectedVictim(target.SteamId)) { return BlueSageCommandResult.HandledWithMessages("SID ban blocked: Steam " + FormatSteamIdSuffix(target.SteamId) + " is the protected victim in an open Clone Shield incident. Use the C# incident to target its suspect."); } if (SteamIdModerationController.IsAuthorizedCurrentIssuer(target.SteamId)) { return BlueSageCommandResult.HandledWithMessages("SID ban blocked: the selected Steam account is the current host or an assigned Helper. No preview or pending action was created."); } _pendingModerationAction = BuildPendingModeration("ban", target, flag); return BlueSageCommandResult.HandledWithMessages((flag ? "SID ban OFFLINE PREVIEW: " : "SID ban PREVIEW: SUSPECT ") + target.SteamId + ((target.IncidentId == "none") ? string.Empty : (" from " + target.IncidentId)) + (flag ? (". This exact ID is not in the current lobby. No kick will be attempted. Verify the Steam profile, then use /sidban " + target.SteamId + " confirm within 30 seconds.") : (". Victim IDs are protected. Verify the Steam profile, then use /sidban " + ((target.IncidentId == "none") ? target.SteamId : target.IncidentId) + " confirm within 30 seconds."))); } private BlueSageCommandResult HandleSidUnbanCommand(BlueSageCommandContext context) { if (!CanUseLobbySafety(out var _)) { return LobbySafetyDeniedResult(); } string text = (context.Arguments ?? string.Empty).Trim(); int num; object obj; if (!text.Equals("confirm", StringComparison.OrdinalIgnoreCase)) { num = (text.EndsWith(" confirm", StringComparison.OrdinalIgnoreCase) ? 1 : 0); if (num == 0) { obj = text; goto IL_0079; } } else { num = 1; } obj = (text.Equals("confirm", StringComparison.OrdinalIgnoreCase) ? string.Empty : text.Substring(0, text.Length - " confirm".Length).Trim()); goto IL_0079; IL_0079: string text2 = (string)obj; if (num != 0) { return ConfirmPendingModeration("unban", text2); } if (string.IsNullOrWhiteSpace(text2)) { return BlueSageCommandResult.HandledWithMessages("SID unban: host may use a full SteamID64 or unique 6+ digit suffix. Helpers must use the exact full SteamID64. Review the preview, then add confirm within 30 seconds."); } if (!SteamIdModerationController.CanLocalIssue(out var isHost2, out var reason)) { return BlueSageCommandResult.HandledWithMessages("SID unban: " + reason); } ModerationTarget target; if (isHost2) { if (!ModerationTargetResolver.TryResolve(text2, SteamIdModerationController.GetBannedSteamIds(), out target, out var error)) { return BlueSageCommandResult.HandledWithMessages("SID unban: " + error); } } else { if (!ModerationProtocolPolicy.IsSteamId64(text2)) { return BlueSageCommandResult.HandledWithMessages("SID unban: Helpers must provide the exact 17-digit SteamID64 so the host can resolve its host-owned native ban list safely."); } target = new ModerationTarget(text2, "none"); } _pendingModerationAction = BuildPendingModeration("unban", target, targetWasOffline: false); return BlueSageCommandResult.HandledWithMessages("SID unban PREVIEW: " + target.SteamId + ". Use /sidunban " + target.SteamId + " confirm within 30 seconds."); } private BlueSageCommandResult ConfirmPendingModeration(string action, string query) { if (!CanUseLobbySafety(out var _)) { _pendingModerationAction = null; return LobbySafetyDeniedResult(); } long num = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); MultiplayerManager i = MonoSingleton.I; string b = ((i != null) ? i.LobbyCode : null) ?? string.Empty; PendingModerationAction pendingModerationAction = _pendingModerationAction; if (pendingModerationAction == null || pendingModerationAction.Action != action || pendingModerationAction.ExpiresUnixSeconds < num || !string.Equals(pendingModerationAction.LobbyCode, b, StringComparison.Ordinal)) { _pendingModerationAction = null; return BlueSageCommandResult.HandledWithMessages("SID " + action + ": preview expired or lobby changed. Start again so the target is revalidated."); } if (string.IsNullOrWhiteSpace(query)) { return BlueSageCommandResult.HandledWithMessages("SID " + action + ": confirmation must repeat the previewed incident ID or full SteamID64; bare confirm is refused and no action was taken."); } string b2 = ((pendingModerationAction.IncidentId == "none") ? pendingModerationAction.TargetSteamId : pendingModerationAction.IncidentId); if (!string.Equals(query, b2, StringComparison.OrdinalIgnoreCase)) { return BlueSageCommandResult.HandledWithMessages("SID " + action + ": confirmation does not match the previewed target; no action was taken."); } if (action == "ban") { ModerationTarget target; string error2; if (pendingModerationAction.TargetWasOffline) { if (!SteamIdModerationController.CanLocalIssue(out var isHost2, out var reason) || !isHost2 || !ModerationProtocolPolicy.IsSteamId64(pendingModerationAction.TargetSteamId) || CloneIncidentLedger.IsProtectedVictim(pendingModerationAction.TargetSteamId)) { _pendingModerationAction = null; return BlueSageCommandResult.HandledWithMessages("SID ban offline revalidation failed: " + reason + " No action was taken."); } string error; IReadOnlyList verifiedCurrentSteamIds = PlayerIdentityEvidenceController.GetVerifiedCurrentSteamIds(out error); if (!string.IsNullOrWhiteSpace(error)) { _pendingModerationAction = null; return BlueSageCommandResult.HandledWithMessages("SID ban offline revalidation failed: " + error + ". No action was taken."); } if (verifiedCurrentSteamIds.Contains(pendingModerationAction.TargetSteamId, StringComparer.Ordinal)) { _pendingModerationAction = null; return BlueSageCommandResult.HandledWithMessages("SID ban offline revalidation failed: that SteamID joined the verified live roster after preview. Start a new live-player preview; no ban or kick was attempted."); } } else if (!ModerationTargetResolver.TryResolve((pendingModerationAction.IncidentId == "none") ? pendingModerationAction.TargetSteamId : pendingModerationAction.IncidentId, SteamIdModerationController.GetCurrentPlayerSteamIds(), out target, out error2) || !string.Equals(target.SteamId, pendingModerationAction.TargetSteamId, StringComparison.Ordinal) || CloneIncidentLedger.IsProtectedVictim(pendingModerationAction.TargetSteamId)) { _pendingModerationAction = null; return BlueSageCommandResult.HandledWithMessages("SID ban revalidation failed: " + (error2 ?? "target or victim role changed") + ". No action was taken."); } } _pendingModerationAction = null; string message; bool flag = SteamIdModerationController.TrySendHelperRequest(action, pendingModerationAction.TargetSteamId, pendingModerationAction.IncidentId, out message); return BlueSageCommandResult.HandledWithMessages((flag ? ("SID " + action + ": ") : ("SID " + action + " rejected: ")) + message); } private static PendingModerationAction BuildPendingModeration(string action, ModerationTarget target, bool targetWasOffline) { PendingModerationAction obj = new PendingModerationAction { Action = action, TargetSteamId = target.SteamId, IncidentId = target.IncidentId }; MultiplayerManager i = MonoSingleton.I; obj.LobbyCode = ((i != null) ? i.LobbyCode : null) ?? string.Empty; obj.TargetWasOffline = targetWasOffline; obj.ExpiresUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 30; return obj; } private static BlueSageCommandResult LobbySafetyDeniedResult() { bool isHost; string reason; LobbySafetyAccessState localAccessState = SteamIdModerationController.GetLocalAccessState(out isHost, out reason); return BlueSageCommandResult.HandledWithMessages((localAccessState == LobbySafetyAccessState.HelperWaitingForHost) ? (reason + " Protected data stayed locked and no moderation request was sent.") : "Lobby Safety is available only to the current host or a Helper assigned by that host. No player or incident details were shown."); } private static string SafeIncidentLabel(string value) { if (!string.IsNullOrWhiteSpace(value)) { return value; } return "unknown"; } private static bool TryGetCurrentSteamLobby(out CSteamID lobbyId) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: 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_0043: 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_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) lobbyId = CSteamID.Nil; if (PluginShutdownController.IsShuttingDown) { return false; } MultiplayerManager i = MonoSingleton.I; string s = ((i != null) ? i.LobbyCode : null); if (!SteamManager.Initialized || !ulong.TryParse(s, out var result) || result == 0L) { return false; } lobbyId = new CSteamID(result); return SteamMatchmaking.GetLobbyOwner(lobbyId) != CSteamID.Nil; } private static bool TryResolveLobbyMemberSteamId(string query, out string steamId, out string error) { steamId = string.Empty; if (!PlayerIdentityEvidenceController.TryResolveLookup(query, out var evidence, out error)) { return false; } steamId = evidence.SteamId; return true; } private static string FormatSteamIdSuffix(string steamId) { return "Steam …" + ((steamId != null && steamId.Length > 6) ? steamId.Substring(steamId.Length - 6) : steamId); } private void StartCloneShieldController() { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Expected O, but got Unknown if (!((Object)(object)_cloneShieldControllerObject != (Object)null)) { _cloneShieldControllerObject = new GameObject("BlueSage_CloneShieldController"); _cloneShieldControllerObject.AddComponent(); Object.DontDestroyOnLoad((Object)(object)_cloneShieldControllerObject); } } private void StartPlayerRenderSaverController() { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Expected O, but got Unknown if (!((Object)(object)_playerRenderSaverControllerObject != (Object)null)) { _playerRenderSaverControllerObject = new GameObject("BlueSage_PlayerRenderSaverController"); _playerRenderSaverControllerObject.AddComponent(); Object.DontDestroyOnLoad((Object)(object)_playerRenderSaverControllerObject); } } private void StartStyleHelperWindow() { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Expected O, but got Unknown if (!((Object)(object)_styleHelperWindow != (Object)null)) { GameObject val = new GameObject("BlueSage_StyleHelperWindow"); _styleHelperWindow = val.AddComponent(); Object.DontDestroyOnLoad((Object)(object)val); } } private void StartQolMenuWindow() { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Expected O, but got Unknown if (!((Object)(object)_qolMenuWindow != (Object)null)) { GameObject val = new GameObject("BlueSage_QolMenuWindow"); _qolMenuWindow = val.AddComponent(); Object.DontDestroyOnLoad((Object)(object)val); } } internal string ToggleStyleUiFromQolMenu() { if (!EnableStyleUi.Value) { return "Style UI is disabled in config."; } if ((Object)(object)_styleHelperWindow == (Object)null) { StartStyleHelperWindow(); } if ((Object)(object)_styleHelperWindow == (Object)null) { return "Style UI could not start; check BepInEx console."; } _styleHelperWindow.ToggleVisible(); return "Style UI toggled."; } internal string RunManualSweepFromQolMenu() { return SweepMenuActionPolicy.BuildCompletionMessage(RunSweep("QoL menu", requireManualPermission: true)); } internal string RunHostHealthFromQolMenu() { return RunHostHealthCheck("QoL menu"); } internal string RunQolStatusFromQolMenu() { LogDetailedQolStatus(); if (!AddLocalNotification(BuildQolStatusMessage())) { return "QoL status could not reach local chat yet; join a lobby or use /qol status after chat loads."; } return "QoL status was sent to your local chat."; } private void StartPerformanceOverlayController() { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Expected O, but got Unknown if (!((Object)(object)_performanceOverlayControllerObject != (Object)null)) { _performanceOverlayControllerObject = new GameObject("BlueSage_PublicPerformanceOverlay"); ((Object)_performanceOverlayControllerObject).hideFlags = (HideFlags)61; Object.DontDestroyOnLoad((Object)(object)_performanceOverlayControllerObject); _performanceOverlayControllerObject.AddComponent(); } } internal void RunSweepRestartFromMenu() { RestartSweepLoop(); } internal void ReconcileSweepScheduleFromMenu() { if (!EnableAutoSweep.Value || !AutoSweepAllowed) { return; } if (_sweepCoroutine == null) { RestartSweepLoop(); return; } double realtimeSinceStartupAsDouble = Time.realtimeSinceStartupAsDouble; _sweepSchedule.ReconcileInterval(realtimeSinceStartupAsDouble, SweepIntervalMinutes.Value); double num = Math.Max(0.0, _sweepSchedule.DueSeconds - realtimeSinceStartupAsDouble); ManualLogSource log = Log; if (log != null) { log.LogInfo((object)$"Asset Sweep interval reconciled to {_sweepSchedule.ConfiguredIntervalMinutes} minutes without restarting elapsed time; next automatic deadline in {num:0} seconds."); } } internal void RestartHostHealthFromMenu() { RestartHostHealthLoop(); } internal void SaveConfigFromQolMenu() { ((BaseUnityPlugin)this).Config.Save(); } internal string SaveSelfPositionFromQolMenu() { //IL_004c: Unknown result type (might be due to invalid IL or missing references) TextChannelManager i = NetworkSingleton.I; object obj = i?.MainPlayer; if (obj == null) { if (i == null) { obj = null; } else { PlayerMovementController mainMovementController = i.MainMovementController; obj = ((mainMovementController != null) ? ((Component)mainMovementController).transform : null); } } Transform val = (Transform)obj; if ((Object)(object)val == (Object)null || !((Component)val).gameObject.activeInHierarchy) { return "Self movement: your player position is not ready yet."; } string text = SelfMovementPolicy.FormatPosition(val.position); SelfSavedPosition.Value = text; ((BaseUnityPlugin)this).Config.Save(); return "Self movement: saved your current position " + text + "."; } internal string TeleportSelfToSavedFromQolMenu() { //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) if (!SelfMovementPolicy.TryParsePosition(SelfSavedPosition?.Value, out var position)) { return "Self movement: no saved position yet. Use Save Pos first."; } SelfMovementPatch.QueueTeleport(position); return "Self movement: queued teleport to your saved position " + SelfMovementPolicy.FormatPosition(position) + "."; } internal string ToggleNoclipFlyFromQolMenu() { if (EnableNoclipFly == null) { return "Self movement: noclip/fly is not ready yet."; } EnableNoclipFly.Value = !EnableNoclipFly.Value; OptInRescuePatch.Stop(); SelfMovementPatch.ResetVerticalVelocity(); ((BaseUnityPlugin)this).Config.Save(); if (!EnableNoclipFly.Value) { return "Self movement: noclip/fly off. Vanilla collision and movement resumed."; } return "Self movement: noclip/fly on. WASD moves, Space/E rises, Ctrl/Q lowers, Shift boosts, and Alt gives precision control."; } internal string SaveSelfMovementMultiplierFromQolMenu(string kind, string rawValue) { ConfigEntry val; float num; float num2; string arg; switch ((kind ?? string.Empty).Trim().ToLowerInvariant()) { case "speed": val = SelfSpeedMultiplier; num = 0.25f; num2 = 5f; arg = "speed"; break; case "jump": val = SelfJumpMultiplier; num = 0.25f; num2 = 5f; arg = "jump"; break; case "gravity": val = SelfGravityMultiplier; num = 0.1f; num2 = 5f; arg = "gravity"; break; default: return "Self movement: unknown multiplier."; } string text = (rawValue ?? string.Empty).Trim(); if (string.Equals(text, "reset", StringComparison.OrdinalIgnoreCase) || string.Equals(text, "default", StringComparison.OrdinalIgnoreCase) || string.Equals(text, "vanilla", StringComparison.OrdinalIgnoreCase)) { text = "1"; } if (val == null || !float.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { return $"Self movement: {arg} needs a multiplier from {num:0.##} to {num2:0.##}, or reset."; } val.Value = Mathf.Clamp(result, num, num2); ((BaseUnityPlugin)this).Config.Save(); return $"Self movement: {arg} saved at {val.Value:0.##}x."; } internal string ResetSelfMovementFromQolMenu() { EnableNoclipFly.Value = false; SelfSpeedMultiplier.Value = 1f; SelfJumpMultiplier.Value = 1f; SelfGravityMultiplier.Value = 1f; SelfMovementPatch.CleanupRuntimeState(); ((BaseUnityPlugin)this).Config.Save(); return "Self movement reset: noclip/fly off; speed, jump, and gravity are back to 1x vanilla."; } private BlueSageCommandResult HandleSelfMovementCommand(BlueSageCommandContext context) { string text = ((context.Tokens.Count > 0) ? context.Tokens[0].Trim().ToLowerInvariant() : "status"); switch (text) { case "status": case "?": return BlueSageCommandResult.HandledWithMessages(string.Format("Self movement: fly={0}, speed={1:0.##}x, jump={2:0.##}x, gravity={3:0.##}x, saved={4}.", FormatToggleState(ShouldApplyNoclipFly), LockedSelfSpeedMultiplier, LockedSelfJumpMultiplier, LockedSelfGravityMultiplier, string.IsNullOrWhiteSpace(SelfSavedPosition?.Value) ? "none" : SelfSavedPosition.Value)); case "save": case "savepos": return BlueSageCommandResult.HandledWithMessages(SaveSelfPositionFromQolMenu()); case "teleport": case "tp": case "saved": return BlueSageCommandResult.HandledWithMessages(TeleportSelfToSavedFromQolMenu()); case "reset": return BlueSageCommandResult.HandledWithMessages(ResetSelfMovementFromQolMenu()); case "fly": case "noclip": { string text2 = ((context.Tokens.Count > 1) ? context.Tokens[1].Trim().ToLowerInvariant() : "toggle"); if (text2 == "status" || text2 == "?") { return BlueSageCommandResult.HandledWithMessages("Self movement fly: " + FormatToggleState(ShouldApplyNoclipFly) + "."); } bool? flag = ParseToggleRequestWithCurrent(text2, ShouldApplyNoclipFly); if (!flag.HasValue) { return BlueSageCommandResult.HandledWithMessages("Self movement fly: use on, off, toggle, or status."); } if (flag.Value == ShouldApplyNoclipFly) { return BlueSageCommandResult.HandledWithMessages("Self movement fly: " + FormatToggleState(ShouldApplyNoclipFly) + "."); } return BlueSageCommandResult.HandledWithMessages(ToggleNoclipFlyFromQolMenu()); } case "speed": case "jump": case "gravity": { string rawValue = ((context.Tokens.Count > 1) ? context.Tokens[1] : string.Empty); return BlueSageCommandResult.HandledWithMessages(SaveSelfMovementMultiplierFromQolMenu(text, rawValue)); } default: return BlueSageCommandResult.HandledWithMessages("Self movement: use status, save, teleport, fly [on|off], speed <0.25-5>, jump <0.25-5>, gravity <0.1-5>, or reset."); } } private static string BuildQolStatusMessage() { return QolStatusTextPolicy.BuildPlayerSummary(NativeAssetSweepOwnership, EnableAutoSweep.Value && AutoSweepAllowed, ManualSweepAllowed); } private static void LogDetailedQolStatus() { ManualLogSource log = Log; if (log != null) { log.LogInfo((object)("QoL detailed status: " + BuildDetailedQolStatusMessage())); } } private static string BuildDetailedQolStatusMessage() { return string.Format("BlueSage QoL: AutomaticCleanup={0}, ManualCleanup={1}, HostHealth={2}, LeaveNotices={3}, Welcome={4}, Timestamps={5}({6}), ChatReadability={7}, ChatBackdrop={8}, ChatOutline={9}%, ChatUIScale={10}%, ChatFont={11}%, ChatWindowHeight={12}%, ChatRows={13}, OutlineColors=#{14}/#{15}, BlackNamesOutline={16}, BetterMove={17}, UnlimitedConsumables={18}, FocusAnywhere={19}, PlayerPanel={20}, SelfFly={21}, SelfMove={22:0.##}x/{23:0.##}x/{24:0.##}x, ReconnectGuard={25}, ReconnectMessage={26}, PingMentions={27}, PingSound={28}, CloneShield={29}, RenderSaver={30}({31}m/max{32}, hidden={33}), ChatCopy={34}, ChatLinks={35}, LocalAudit={36}, Spoons={37}, StyleUI={38}, AvatarSlots4-9={39}, Chalkboards={40}, MiniMapIDClick={41}, MiniMapFriends={42}, QoLMenu=Home or /qolmenu, LongerText=always on.", FormatToggleState(EnableAutoSweep.Value && AutoSweepAllowed), FormatToggleState(ManualSweepAllowed), FormatToggleState(EnableHostHealthMonitor.Value), FormatToggleState(ShouldApplyLeaveNotifications), FormatToggleState(EnableWelcomeMessage.Value), FormatToggleState(ShouldApplyChatTimestamps), Use24HourTime.Value ? "24-hour" : "12-hour AM/PM", FormatToggleState(ShouldApplyChatReadability), FormatToggleState(ShouldApplyPersistentChatBackdrop), LockedChatOutlineIntensityPercent, LockedChatUiScalePercent, LockedChatFontSizePercent, LockedChatWindowHeightPercent, LockedChatHistoryRows, LockedChatOutlineColorHex, LockedBlackChatOutlineColorHex, FormatToggleState(ShouldApplyBlackNamesOutline), FormatToggleState(ShouldApplyBetterMove), FormatToggleState(ShouldApplyUnlimitedConsumables), FormatToggleState(EnableFocusAnywhere.Value), FormatToggleState(EnableEnhancedPlayerPanel.Value), FormatToggleState(ShouldApplyNoclipFly), LockedSelfSpeedMultiplier, LockedSelfJumpMultiplier, LockedSelfGravityMultiplier, FormatToggleState(EnableReconnectGuard.Value), FormatToggleState(EnableReconnectAnnouncement.Value), FormatToggleState(EnablePingMentions.Value), FormatToggleState(EnablePingSound.Value), FormatToggleState(ShouldApplyCloneShield), FormatToggleState(ShouldApplyPlayerRenderSaver), LockedPlayerRenderSaverRadiusMeters, LockedPlayerRenderSaverMaxVisiblePlayers, PlayerRenderSaverController.LastHiddenRemotePlayers, FormatToggleState(ShouldApplyChatCopy), FormatToggleState(ShouldApplyChatUrlLinks), FormatToggleState(EnableLocalAuditExports?.Value ?? false), FormatToggleState(ShouldApplySpoons), FormatToggleState(EnableStyleUi.Value), FormatToggleState(EnableExtendedAvatarStyles?.Value ?? false), FormatToggleState(ChalkboardPersistenceController.Available), FormatToggleState(EnableMiniMapPlayerLabels.Value), FormatToggleState(ShouldApplyMiniMapFriendColors)); } private BlueSageCommandResult HandleChatOutlineCommand(BlueSageCommandContext context) { string text = ((context.Tokens.Count > 0) ? context.Tokens[0].Trim().ToLowerInvariant() : "status"); if (text == "status" || text == "?") { return BlueSageCommandResult.HandledWithMessages($"Chat Outline: {LockedChatOutlineIntensityPercent}%."); } if (!int.TryParse(text.TrimEnd(new char[1] { '%' }), out var result)) { return BlueSageCommandResult.HandledWithMessages("Chat Outline: use a number from 25 to 100, or status."); } int num = ChatReadabilityStylePolicy.ClampOutlineIntensityPercent(result); ChatOutlineIntensity.Value = num; ((BaseUnityPlugin)this).Config.Save(); ChatReadabilityPatch.RefreshVisibleChat(); return BlueSageCommandResult.HandledWithMessages($"Chat Outline: {num}%."); } private BlueSageCommandResult HandleChatScaleCommand(BlueSageCommandContext context) { string text = ((context.Tokens.Count > 0) ? context.Tokens[0].Trim() : "status"); if (string.Equals(text, "status", StringComparison.OrdinalIgnoreCase) || text == "?") { return BlueSageCommandResult.HandledWithMessages($"Chat UI Scale (whole panel): {LockedChatUiScalePercent}%."); } if (!ChatAccessibilityScalePolicy.TryParseScaleToken(text, out var percent)) { return BlueSageCommandResult.HandledWithMessages("Chat UI Scale grows the whole panel/input/backdrop/text. Use 75-200, decimal scale like 1.25, reset, or status. Use /chatfont for message text only."); } ChatUiScalePercent.Value = percent; ((BaseUnityPlugin)this).Config.Save(); ChatReadabilityPatch.RequestRefreshSoon(); return BlueSageCommandResult.HandledWithMessages($"Chat UI Scale (whole panel): {percent}%."); } private BlueSageCommandResult HandleChatFontCommand(BlueSageCommandContext context) { string text = ((context.Tokens.Count > 0) ? context.Tokens[0].Trim() : "status"); if (string.Equals(text, "status", StringComparison.OrdinalIgnoreCase) || text == "?") { return BlueSageCommandResult.HandledWithMessages($"Chat Font (message text only): {LockedChatFontSizePercent}%."); } if (string.Equals(text, "toggle", StringComparison.OrdinalIgnoreCase)) { int num = ((LockedChatFontSizePercent > 100) ? 100 : 125); ChatFontSizePercent.Value = num; ((BaseUnityPlugin)this).Config.Save(); ChatReadabilityPatch.RequestRefreshSoon(); return BlueSageCommandResult.HandledWithMessages($"Chat Font (message text only): {num}%."); } if (!ChatFontSizePolicy.TryParseFontSizeToken(text, out var percent)) { return BlueSageCommandResult.HandledWithMessages("Chat Font changes message text only. Use on/off/toggle, 75-200, decimal size like 1.25, reset, or status. Use /chatscale for the whole panel."); } ChatFontSizePercent.Value = percent; ((BaseUnityPlugin)this).Config.Save(); ChatReadabilityPatch.RequestRefreshSoon(); return BlueSageCommandResult.HandledWithMessages($"Chat Font (message text only): {percent}%."); } private BlueSageCommandResult HandleChatHeightCommand(BlueSageCommandContext context) { string text = ((context.Tokens.Count > 0) ? context.Tokens[0].Trim() : "status"); if (string.Equals(text, "status", StringComparison.OrdinalIgnoreCase) || text == "?") { return BlueSageCommandResult.HandledWithMessages($"Chat Window Height (vertical log space): {LockedChatWindowHeightPercent}%."); } if (string.Equals(text, "toggle", StringComparison.OrdinalIgnoreCase)) { int num = ((LockedChatWindowHeightPercent > 100) ? 100 : 125); ChatWindowHeightPercent.Value = num; ((BaseUnityPlugin)this).Config.Save(); ChatReadabilityPatch.RequestRefreshSoon(); return BlueSageCommandResult.HandledWithMessages($"Chat Window Height (vertical log space): {num}%."); } if (!ChatWindowHeightPolicy.TryParseHeightToken(text, out var percent)) { return BlueSageCommandResult.HandledWithMessages("Chat Window Height adds vertical log space only. Use on/off/toggle, 100-200, decimal size like 1.5, reset, or status."); } ChatWindowHeightPercent.Value = percent; ((BaseUnityPlugin)this).Config.Save(); ChatReadabilityPatch.RequestRefreshSoon(); return BlueSageCommandResult.HandledWithMessages($"Chat Window Height (vertical log space): {percent}%."); } private BlueSageCommandResult HandleChatHistoryCommand(BlueSageCommandContext context) { string text = ((context.Tokens.Count > 0) ? context.Tokens[0].Trim() : "status"); if (string.Equals(text, "status", StringComparison.OrdinalIgnoreCase) || text == "?") { return BlueSageCommandResult.HandledWithMessages($"Chat Rows: {LockedChatHistoryRows}. Default 150; range 25-250. Higher vanilla/other-mod limits remain untouched."); } if (string.Equals(text, "reset", StringComparison.OrdinalIgnoreCase) || string.Equals(text, "default", StringComparison.OrdinalIgnoreCase)) { ChatHistoryRows.Value = 150; ((BaseUnityPlugin)this).Config.Save(); return BlueSageCommandResult.HandledWithMessages("Chat Rows: reset to 150 (balanced default)."); } if (!int.TryParse(text, out var result)) { return BlueSageCommandResult.HandledWithMessages("Chat Rows: use 25-250, reset, or status."); } int num = ChatHistoryLimitPolicy.ClampRows(result); ChatHistoryRows.Value = num; ((BaseUnityPlugin)this).Config.Save(); return BlueSageCommandResult.HandledWithMessages($"Chat Rows: {num}. Larger histories cost more UI memory/work in busy lobbies."); } private BlueSageCommandResult HandleChatVanillaCommand(BlueSageCommandContext context) { ResetChatUiToVanilla(); return BlueSageCommandResult.HandledWithMessages("Chat UI reset: whole-panel scale=100%, message font=100%, window height=100%."); } internal string ResetChatUiToVanillaFromMenu() { ResetChatUiToVanilla(); return "Chat UI reset to vanilla whole-panel scale, message font size, and window height."; } private void ResetChatUiToVanilla() { if (ChatUiScalePercent != null) { ChatUiScalePercent.Value = 100; } if (ChatFontSizePercent != null) { ChatFontSizePercent.Value = 100; } if (ChatWindowHeightPercent != null) { ChatWindowHeightPercent.Value = 100; } ((BaseUnityPlugin)this).Config.Save(); ChatReadabilityPatch.RestoreChatWindowHeight(); ChatReadabilityPatch.RestoreChatAccessibilityScale(); ChatReadabilityPatch.RestoreChatFontSize(); ChatReadabilityPatch.RequestRefreshSoon(); } private BlueSageCommandResult HandlePlayerRenderSaverCommand(BlueSageCommandContext context) { string text = ((context.Tokens.Count > 0) ? context.Tokens[0].Trim().ToLowerInvariant() : "status"); switch (text) { case "status": case "?": return BlueSageCommandResult.HandledWithMessages(BuildPlayerRenderSaverStatus()); case "reset": PlayerRenderSaverRadiusMeters.Value = 45; PlayerRenderSaverMaxVisiblePlayers.Value = 32; ((BaseUnityPlugin)this).Config.Save(); if (!ShouldApplyPlayerRenderSaver) { PlayerRenderSaverController.RestoreAll(); } return BlueSageCommandResult.HandledWithMessages(BuildPlayerRenderSaverStatus()); case "radius": { if (context.Tokens.Count < 2 || !int.TryParse(context.Tokens[1].Trim().TrimEnd(new char[1] { 'm' }), out var result2)) { return BlueSageCommandResult.HandledWithMessages("Player Render Saver radius: use /playersaver radius 45, range 10-250 meters."); } PlayerRenderSaverRadiusMeters.Value = PlayerRenderSaverPolicy.ClampRadiusMeters(result2); ((BaseUnityPlugin)this).Config.Save(); return BlueSageCommandResult.HandledWithMessages(BuildPlayerRenderSaverStatus()); } case "max": case "maxvisible": { if (context.Tokens.Count < 2 || !int.TryParse(context.Tokens[1].Trim(), out var result)) { return BlueSageCommandResult.HandledWithMessages("Player Render Saver max visible: use /playersaver max 32, range 1-128 closest players."); } PlayerRenderSaverMaxVisiblePlayers.Value = PlayerRenderSaverPolicy.ClampMaxVisiblePlayers(result); ((BaseUnityPlugin)this).Config.Save(); return BlueSageCommandResult.HandledWithMessages(BuildPlayerRenderSaverStatus()); } default: { bool? flag = ParseToggleRequestWithCurrent(text, EnablePlayerRenderSaver.Value); if (!flag.HasValue) { return BlueSageCommandResult.HandledWithMessages("Player Render Saver: use on, off, toggle, status, reset, radius 45, or max 32."); } EnablePlayerRenderSaver.Value = flag.Value; ((BaseUnityPlugin)this).Config.Save(); if (!EnablePlayerRenderSaver.Value) { PlayerRenderSaverController.RestoreAll(); return BlueSageCommandResult.HandledWithMessages("Player Render Saver: off. Restored all hidden player renderers."); } return BlueSageCommandResult.HandledWithMessages(BuildPlayerRenderSaverStatus()); } } } private BlueSageCommandResult HandleChalkSaveCommand(BlueSageCommandContext context) { if (context.Tokens.Count < 1) { return BlueSageCommandResult.HandledWithMessages("Chalkboard save: use /chalksave name [board index]."); } string boardToken = ((context.Tokens.Count > 1) ? context.Tokens[1] : string.Empty); return BlueSageCommandResult.HandledWithMessages(ChalkboardPersistenceController.Save(context.Tokens[0], boardToken)); } private BlueSageCommandResult HandleChalkLoadCommand(BlueSageCommandContext context) { if (context.Tokens.Count < 2) { return BlueSageCommandResult.HandledWithMessages("Chalkboard restore: first run /chalk boards, then use /chalkload name ."); } return BlueSageCommandResult.HandledWithMessages(ChalkboardPersistenceController.Load(context.Tokens[0], context.Tokens[1])); } private BlueSageCommandResult HandleChalkCommand(BlueSageCommandContext context) { switch ((context.Tokens.Count > 0) ? context.Tokens[0].Trim().ToLowerInvariant() : "status") { case "status": case "help": case "?": return BlueSageCommandResult.HandledWithMessages(ChalkboardPersistenceController.Status(), "Use /chalk boards or /chalk refresh to map exact indices 0-2. Local: view/save/list/delete/refresh. Shared: Host-direct or authenticated Helper-to-Host load/clear after a fresh map. Legacy /chalksave, /chalkload, and /chalklist remain available."); case "boards": case "where": case "here": return BlueSageCommandResult.HandledWithMessages(ChalkboardPersistenceController.Boards()); case "refresh": return BlueSageCommandResult.HandledWithMessages(ChalkboardPersistenceController.RefreshMap()); case "view": { string boardToken = ((context.Tokens.Count > 1) ? context.Tokens[1] : string.Empty); return BlueSageCommandResult.HandledWithMessages(ChalkboardPersistenceController.View(boardToken)); } case "clear": { string boardToken3 = ((context.Tokens.Count > 1) ? context.Tokens[1] : string.Empty); return BlueSageCommandResult.HandledWithMessages(ChalkboardPersistenceController.TryClear(boardToken3)); } case "save": { if (context.Tokens.Count < 2) { return BlueSageCommandResult.HandledWithMessages("Chalkboard save: use /chalk save name [board index]."); } string boardToken2 = ((context.Tokens.Count > 2) ? context.Tokens[2] : string.Empty); return BlueSageCommandResult.HandledWithMessages(ChalkboardPersistenceController.Save(context.Tokens[1], boardToken2)); } case "load": case "restore": if (context.Tokens.Count < 3) { return BlueSageCommandResult.HandledWithMessages("Chalkboard restore: first run /chalk boards, then use /chalk load name ."); } return BlueSageCommandResult.HandledWithMessages(ChalkboardPersistenceController.Load(context.Tokens[1], context.Tokens[2])); case "list": return BlueSageCommandResult.HandledWithMessages(ChalkboardPersistenceController.List()); case "delete": if (context.Tokens.Count < 2) { return BlueSageCommandResult.HandledWithMessages("Chalkboard local delete: use /chalk delete name."); } return BlueSageCommandResult.HandledWithMessages(ChalkboardPersistenceController.Delete(context.Tokens[1])); default: return BlueSageCommandResult.HandledWithMessages("Chalkboards: /chalk boards|refresh, view , save name [index], list, delete name, load name , or clear . Index 3 is never accepted."); } } private static string BuildPlayerRenderSaverStatus() { return $"Player Render Saver: {FormatToggleState(ShouldApplyPlayerRenderSaver)}, radius={LockedPlayerRenderSaverRadiusMeters}m, maxVisible={LockedPlayerRenderSaverMaxVisiblePlayers}, candidates={PlayerRenderSaverController.LastCandidateRemotePlayers}, visible={PlayerRenderSaverController.LastVisibleRemotePlayers}, hidden={PlayerRenderSaverController.LastHiddenRemotePlayers}, renderersSuppressed={PlayerRenderSaverController.LastSuppressedRenderers}, state={PlayerRenderSaverController.LastRuntimeState}. Local-only optimization; chat and player list stay visible."; } private BlueSageCommandResult HandleOutlineColorCommand(BlueSageCommandContext context, string label, ConfigEntry entry, string fallback) { if (entry == null) { return BlueSageCommandResult.HandledWithMessages(label + ": config is not ready yet."); } string text = ((context.Tokens.Count > 0) ? context.Tokens[0].Trim() : "status"); if (string.Equals(text, "status", StringComparison.OrdinalIgnoreCase) || text == "?") { return BlueSageCommandResult.HandledWithMessages(label + ": #" + ChatReadabilityStylePolicy.NormalizeHexColor(entry.Value, fallback) + "."); } string text2 = ChatReadabilityStylePolicy.NormalizeHexColor(text, string.Empty); if (string.IsNullOrEmpty(text2)) { return BlueSageCommandResult.HandledWithMessages(label + ": use a six-character hex color like #000000, or status."); } entry.Value = text2; ((BaseUnityPlugin)this).Config.Save(); ChatReadabilityPatch.RefreshVisibleChat(); return BlueSageCommandResult.HandledWithMessages(label + ": #" + text2 + "."); } 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 HandleAssetSweepCommand(BlueSageCommandContext context) { if (context.Tokens.Count == 0) { return BlueSageCommandResult.HandledWithMessages(SweepMenuActionPolicy.BuildCompletionMessage(RunSweep("manual command", requireManualPermission: true))); } return HandleToggleCommand(context, "Automatic Cleanup", EnableAutoSweep, RestartSweepLoop, "now", () => SweepMenuActionPolicy.BuildCompletionMessage(RunSweep("manual command", requireManualPermission: true))); } private BlueSageCommandResult HandleTimestampCommand(BlueSageCommandContext context) { string text = ((context.Tokens.Count > 0) ? context.Tokens[0].Trim().ToLowerInvariant() : "toggle"); switch (text) { case "status": case "?": return BlueSageCommandResult.HandledWithMessages("Timestamps: chat=" + FormatToggleState(EnableChatTimestamps.Value) + ", join/leave=" + FormatToggleState(EnableNotificationTimestamps.Value) + ", format=" + (Use24HourTime.Value ? "24-hour" : "12-hour AM/PM") + "."); case "12": case "12h": case "12hr": Use24HourTime.Value = false; ((BaseUnityPlugin)this).Config.Save(); return BlueSageCommandResult.HandledWithMessages("Timestamps: 12-hour format with AM/PM."); case "24": case "24h": case "24hr": Use24HourTime.Value = true; ((BaseUnityPlugin)this).Config.Save(); return BlueSageCommandResult.HandledWithMessages("Timestamps: 24-hour format."); case "reset": case "default": EnableChatTimestamps.Value = true; EnableNotificationTimestamps.Value = true; Use24HourTime.Value = false; ((BaseUnityPlugin)this).Config.Save(); return BlueSageCommandResult.HandledWithMessages("Timestamps: reset to enabled 12-hour AM/PM defaults."); default: { bool? flag = ParseToggleRequestWithCurrent(text, EnableChatTimestamps.Value && EnableNotificationTimestamps.Value); if (!flag.HasValue) { return BlueSageCommandResult.HandledWithMessages("Timestamps: use on, off, toggle, status, 12, 24, or reset."); } 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 && HostHealthRepairsAllowed) { _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) { bool flag = reason.IndexOf("manual", StringComparison.OrdinalIgnoreCase) >= 0; if (!EnableHostHealthMonitor.Value && !flag) { 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)); bool hostHealthRepairsAllowed = HostHealthRepairsAllowed; 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 (hostHealthRepairsAllowed && hostHealthDecision.Actions.Contains(HostHealthAction.RefreshLobbyMetadata)) { RefreshLobbyMetadataHeartbeat(lobbyId, playerLimitCompanionStatus); } if (hostHealthRepairsAllowed && hostHealthDecision.Actions.Contains(HostHealthAction.RequestPersonaRefresh)) { RequestPersonaRefresh(playerSteamIds); } if (hostHealthDecision.Issues.Count > 0) { Log.LogWarning((object)("Host Health diagnostic issue via " + reason + ": " + text + ". No recreate-lobby warning is emitted without join-failure evidence.")); } string text2 = (hostHealthRepairsAllowed ? "Metadata refresh=on, persona refresh=on" : "Read-only compatibility mode; Desync owns metadata/persona repair"); return "Host Health: " + text + ". " + text2 + ". No recreate-lobby warning without join-failure evidence. " + 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 = true; ((BaseUnityPlugin)this).Config.Save(); Log.LogInfo((object)"BlueSage diagnostics remain locked on through 0.2.x Early Access community troubleshooting."); return BlueSageCommandResult.HandledWithMessages("BlueSage diagnostics: locked on through 0.2.x Early Access; /bluedevdebug off cannot disable this release. Shareable BepInEx diagnostics omit exact player/lobby identities; protected mappings stay in authorized local audit files. BepInEx log: " + GetBepInExLogPath()); case "status": case "?": return BlueSageCommandResult.HandledWithMessages("BlueSage diagnostics: locked on through 0.2.x Early Access. BepInEx log: " + GetBepInExLogPath()); default: return BlueSageCommandResult.HandledWithMessages("BlueSage diagnostics: use on, off, or status. BepInEx log: " + GetBepInExLogPath()); } } private void StartCommunityBanListMaintenance() { if (_communityBanListCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_communityBanListCoroutine); } _communityBanListCoroutine = ((MonoBehaviour)this).StartCoroutine(CommunityBanListMaintenanceLoop()); ConfigEntry enableCommunityBanListSync = EnableCommunityBanListSync; if (enableCommunityBanListSync != null && enableCommunityBanListSync.Value) { CommunityBanListController.SetNextAttempt(DateTime.UtcNow.AddSeconds(45.0)); } else { CommunityBanListController.MarkDisabled(); } } private IEnumerator CommunityBanListMaintenanceLoop() { yield return (object)new WaitForSecondsRealtime(45f); while (true) { bool completed = false; bool succeeded = false; if (EnableCommunityBanListSync != null && EnableCommunityBanListSync.Value) { yield return CommunityBanListController.ReconcileFromSource(delegate(bool result) { succeeded = result; completed = true; }); } else { CommunityBanListController.MarkDisabled(); } float num = ((completed && succeeded) ? 21600f : 900f); if (EnableCommunityBanListSync?.Value ?? false) { CommunityBanListController.SetNextAttempt(DateTime.UtcNow.AddSeconds(num)); } yield return (object)new WaitForSecondsRealtime(num); } } internal string RefreshCommunityBanListFromQolMenu() { ConfigEntry enableCommunityBanListSync = EnableCommunityBanListSync; if (enableCommunityBanListSync == null || !enableCommunityBanListSync.Value) { return "Community BanData refresh is off. Enable the local opt-in first."; } if (CommunityBanListController.RefreshInFlight) { ManualLogSource log = Log; if (log != null) { log.LogInfo((object)("Community BanData detailed status: " + CommunityBanListController.StatusText())); } return "Community BanData refresh is already running. " + CommunityBanListController.FriendlyStatusText(); } ((MonoBehaviour)this).StartCoroutine(RunCommunityBanManualRefresh()); return "Community BanData refresh started locally. It can append only validated missing pairs; it cannot kick, ban, or grant authority."; } internal void ReconcileCommunityBanMaintenanceFromQolMenu() { StartCommunityBanListMaintenance(); } private IEnumerator RunCommunityBanManualRefresh() { bool succeeded = false; yield return CommunityBanListController.ReconcileFromSource(delegate(bool result) { succeeded = result; }); ManualLogSource log = Log; if (log != null) { log.LogInfo((object)("Community BanData manual refresh " + (succeeded ? "completed. " : "failed closed. ") + CommunityBanListController.StatusText())); } AddLocalNotification(CommunityBanListController.FriendlyStatusText()); } private BlueSageCommandResult HandleCommunityBanCommand(BlueSageCommandContext context) { string text = ((context.Tokens.Count > 0) ? context.Tokens[0].Trim().ToLowerInvariant() : "status"); ManualLogSource log = Log; if (log != null) { log.LogInfo((object)("Community BanData detailed status: " + CommunityBanListController.StatusText())); } switch (text) { case "status": case "?": return BlueSageCommandResult.HandledWithMessages(CommunityBanListController.FriendlyStatusText()); case "attestation": return BlueSageCommandResult.HandledWithMessages(CommunityBanListController.FriendlyStatusText(), "Attestation: local BlueSageAudit/community-ban-source-attestation.json; bounded operational fields only, with no player identities."); case "refresh": case "validate": return BlueSageCommandResult.HandledWithMessages(RefreshCommunityBanListFromQolMenu()); case "on": case "off": EnableCommunityBanListSync.Value = text == "on"; ((BaseUnityPlugin)this).Config.Save(); StartCommunityBanListMaintenance(); return BlueSageCommandResult.HandledWithMessages(CommunityBanListController.FriendlyStatusText()); default: return BlueSageCommandResult.HandledWithMessages("Community BanData: use /communitybans status, refresh, on, off, or attestation. Refresh is local/additive and never performs a live moderation action."); } } private BlueSageCommandResult HandleAuditLogCommand(BlueSageCommandContext context) { string text = (context.Arguments ?? string.Empty).Trim().ToLowerInvariant(); if (!string.IsNullOrWhiteSpace(text)) { switch (text) { case "status": case "?": break; case "on": EnableLocalAuditExports.Value = true; ((BaseUnityPlugin)this).Config.Save(); SessionAuditController.ReconcileEnabledState(); return BlueSageCommandResult.HandledWithMessages(SessionAuditController.StatusText()); case "off": EnableLocalAuditExports.Value = false; ((BaseUnityPlugin)this).Config.Save(); SessionAuditController.ReconcileEnabledState(); return BlueSageCommandResult.HandledWithMessages("Local session audit: off. Existing files remain local and are not deleted. /auditlog on resumes capture."); case "path": case "folder": case "copy": return BlueSageCommandResult.HandledWithMessages(SessionAuditController.CopyAuditFolder()); case "snapshot": case "flush": return BlueSageCommandResult.HandledWithMessages(SessionAuditController.SnapshotNow()); default: return BlueSageCommandResult.HandledWithMessages("Local session audit: use /auditlog status, on, off, path, or snapshot. Files are local-only and never uploaded automatically."); } } return BlueSageCommandResult.HandledWithMessages(SessionAuditController.StatusText()); } 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) { if (ShouldEmitHiddenDiagnostics && input != null) { TryGetVisibleLobbyCounts(out var memberCount, out var maxPlayers); TryGetSteamLobbyCounts(lobbyId, out var memberCount2, out var maxPlayers2); TryGetLobbySurfaceMetadata(lobbyId, out var _, out var tags, out var visibility); string text = ((_lastHostHealthDecision == null) ? "none" : ((_lastHostHealthDecision.Issues.Count == 0) ? "healthy" : string.Join("|", _lastHostHealthDecision.Issues.Select((HostHealthIssue issue) => issue.ToString()).ToArray()))); string text2 = BuildPlayerRosterFingerprint(playerSteamIds); PlayerLimitCompanionStatus playerLimitCompanionStatus = CapturePlayerLimitCompanionStatus(); bool isHost; LobbySafetyAccessState localAccessState = SteamIdModerationController.GetLocalAccessState(out isHost, out tags); string diagnosticRosterSummary = PlayerIdentityEvidenceController.GetDiagnosticRosterSummary(); string text3 = BuildProcessMemoryDiagnosticSummary(); string diagnosticSummary = BlueSageEnhancedPanelButtonMarker.GetDiagnosticSummary(); string diagnosticStateSummary = ChatReadabilityPatch.GetDiagnosticStateSummary(); string diagnosticSummary2 = VoicePositionRateGuardPatch.GetDiagnosticSummary(); Log.LogInfo((object)($"[BlueSageDiag] utc={DateTime.UtcNow:O}; " + "build=0.2.4+20260730.1-public-24414155-release; reason=" + SanitizeDiagnosticValue(reason) + "; " + $"netIsHost={input.IsHost}; " + $"isHostMachine={input.IsHostMachine}; " + $"lobbyValid={input.LobbyValid}; " + "identityDetail=private-audit-only; " + $"panelSteamIds={input.PlayerSteamIds.Count}; " + "identityRoster=" + diagnosticRosterSummary + "; processMemory=" + text3 + "; panelObjects=" + diagnosticSummary + "; chatState=" + diagnosticStateSummary + "; voicePosition=" + diagnosticSummary2 + "; " + $"lobbySafety={localAccessState}; " + $"serverFlagCount={input.ServerPlayerCount}; " + "visibleLobby=" + FormatNullableCount(memberCount) + "/" + FormatNullableCount(maxPlayers) + "; steamLobbyMembers=" + FormatNullableCount(memberCount2) + "; steamLobbyLimit=" + FormatNullableCount(maxPlayers2) + "; lobbyVisibility=" + SanitizeDiagnosticValue(visibility) + "; " + $"loadedPlugins={Chainloader.PluginInfos.Count}; " + playerLimitCompanionStatus.ToDiagnosticText() + "; lastHostHealth=" + text + "; panelRosterFingerprint=" + text2 + "; panelSteamIdList=omitted-public")); } } private static string BuildProcessMemoryDiagnosticSummary() { try { using Process process = Process.GetCurrentProcess(); return "managedMiB=" + Math.Round((double)GC.GetTotalMemory(forceFullCollection: false) / 1048576.0, 1) + ",workingSetMiB=" + Math.Round((double)process.WorkingSet64 / 1048576.0, 1) + ",privateMiB=" + Math.Round((double)process.PrivateMemorySize64 / 1048576.0, 1) + ",handles=" + process.HandleCount + ",gc=" + GC.CollectionCount(0) + "/" + GC.CollectionCount(1) + "/" + GC.CollectionCount(2); } catch (Exception ex) { return "unavailable-" + ex.GetType().Name; } } private static string BuildPlayerRosterFingerprint(string[] playerSteamIds) { if (playerSteamIds == null || playerSteamIds.Length == 0) { return "none"; } string[] array = playerSteamIds.Where((string value) => !string.IsNullOrWhiteSpace(value)).OrderBy((string value) => value, StringComparer.Ordinal).ToArray(); if (array.Length == 0) { return "none"; } ulong num = 14695981039346656037uL; string[] array2 = array; foreach (string text in array2) { foreach (char c in text) { num ^= c; num *= 1099511628211L; } num ^= 0x7C; num *= 1099511628211L; } return $"{array.Length}:{num:X16}"; } 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_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Unknown result type (might be due to invalid IL or missing references) if (PluginShutdownController.IsShuttingDown) { return; } 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(); string text2 = DateTime.UtcNow.Ticks.ToString(); bool num = SteamMatchmaking.SetLobbyData(lobbyId.Value, "bluesage_qol_heartbeat", text); bool flag = SteamMatchmaking.SetLobbyData(lobbyId.Value, "heartbeat", text2); int num2 = RefreshLongRunningLobbySurfaceMetadata(lobbyId.Value); PublishPlayerLimitCompanionMetadata(lobbyId.Value, playerLimitStatus); if (num || flag) { string text3 = playerLimitStatus?.ToDiagnosticText() ?? "PlayerLimit=unknown"; Log.LogInfo((object)string.Format("Host Health H1: refreshed Steam lobby heartbeat metadata at {0}; heartbeatCompat={1}; preservedSurfaceKeys={2}; {3}.", text, flag ? "ok" : "failed", num2, text3)); } 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 int RefreshLongRunningLobbySurfaceMetadata(CSteamID lobbyId) { //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) int num = 0; string[] array = new string[6] { "Name", "focus", "mature", "chill", "break", "modded" }; foreach (string text in array) { try { string lobbyData = SteamMatchmaking.GetLobbyData(lobbyId, text); if (!string.IsNullOrWhiteSpace(lobbyData) && SteamMatchmaking.SetLobbyData(lobbyId, text, lobbyData)) { num++; } } catch (Exception ex) { Log.LogDebug((object)("Host Health H1: skipped lobby surface metadata key '" + text + "': " + ex.GetType().Name + ": " + ex.Message)); } } return num; } private static void PublishPlayerLimitCompanionMetadata(CSteamID lobbyId, PlayerLimitCompanionStatus playerLimitStatus) { //IL_000c: 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) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) if (playerLimitStatus != null && playerLimitStatus.IsLoaded) { SetOptionalLobbyData(lobbyId, "bluesage_playerlimit_cap", playerLimitStatus.EffectiveMaxLobbySize); SetOptionalLobbyData(lobbyId, "bluesage_playerlimit_default", playerLimitStatus.EffectiveDefaultLobbySize); SetOptionalLobbyData(lobbyId, "bluesage_playerlimit_shift", playerLimitStatus.EffectiveShiftSkipRate); if (playerLimitStatus.ChatRelayPatchEnabled.HasValue) { SteamMatchmaking.SetLobbyData(lobbyId, "bluesage_playerlimit_chat_relay", playerLimitStatus.ChatRelayPatchEnabled.Value ? "true" : "false"); } } } private static void SetOptionalLobbyData(CSteamID lobbyId, string key, int? value) { //IL_0009: Unknown result type (might be due to invalid IL or missing references) if (value.HasValue) { SteamMatchmaking.SetLobbyData(lobbyId, key, value.Value.ToString()); } } internal static PlayerLimitCompanionStatus GetPlayerLimitCompanionStatus() { return CapturePlayerLimitCompanionStatus(); } 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_00ab: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Invalid comparison between Unknown and I4 //IL_0278: Unknown result type (might be due to invalid IL or missing references) if (!EnableReconnectGuard.Value) { return true; } if (_reconnectMenuFallbackInProgress) { Log.LogInfo((object)"Reconnect Guard: menu fallback is already in progress; allowing normal disconnect flow."); return true; } if (_cleanMenuReconnectActive) { Log.LogInfo((object)$"Reconnect Guard: generation {_cleanMenuReconnectGeneration} already owns recovery in phase {_cleanMenuReconnectPhase}; suppressing duplicate ConnectionLost callback."); return false; } if (_lastLobbyRejoinInProgress) { Log.LogInfo((object)"Reconnect Guard: saved-lobby recovery is already in progress; suppressing re-entrant disconnect handling."); return false; } 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; } TryCaptureReconnectLobbyCode(); 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, 5, 20, 3, flag2, isIntentionalLeave: false, _cleanMenuReconnectActive, ((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 true; case ReconnectGuardAction.StopManagedRetry: Log.LogWarning((object)("Reconnect Guard: " + reconnectGuardDecision.Reason)); StopManagedReconnect(reconnectGuardDecision.Reason); return true; case ReconnectGuardAction.WaitForManagedRetryCadence: Log.LogInfo((object)("Reconnect Guard: " + reconnectGuardDecision.Reason)); return true; case ReconnectGuardAction.ScheduleManagedRetry: case ReconnectGuardAction.RunManagedRetryNow: Log.LogWarning((object)("Reconnect Guard: " + reconnectGuardDecision.Reason)); if (flag) { MarkAndrewReconnectIntentionalLeave(); } StartCleanMenuReconnect($"native {notificationStatus}", nativeTeardownAlreadyStarting: true); return true; default: Log.LogWarning((object)$"Reconnect Guard: unexpected policy action {reconnectGuardDecision.Action}; allowing base reconnect flow."); return true; } } private void StartCleanMenuReconnect(string source, bool nativeTeardownAlreadyStarting) { if (_cleanMenuReconnectActive || _cleanMenuReconnectCoroutine != null) { Log.LogInfo((object)$"Reconnect Guard: existing generation {_cleanMenuReconnectGeneration} retained; ignored overlapping start from {source}."); return; } _cleanMenuReconnectActive = true; _managedReconnectSuccessAnnounced = false; _reconnectReadySampleCount = 0; _reconnectQuorumReadySince = -1f; _cleanMenuReconnectCoroutine = ((MonoBehaviour)this).StartCoroutine(CleanMenuReconnectLoop(++_cleanMenuReconnectGeneration, source, nativeTeardownAlreadyStarting)); } private IEnumerator CleanMenuReconnectLoop(int generation, string source, bool nativeTeardownAlreadyStarting) { _cleanMenuReconnectPhase = "native-teardown"; Log.LogWarning((object)$"Reconnect Guard generation {generation}: {source}; native teardown must reach the clean menu before any join attempt."); AddLocalNotification("BlueSage reconnect is cleaning up the old session first."); if (!nativeTeardownAlreadyStarting) { if (!IsCleanMenuReadyForReconnect(requireBlueSageJoinIdle: false, out var reason)) { RequestNativeReturnToMenuForReconnect(); } else { Log.LogWarning((object)$"Reconnect Guard generation {generation}: initial menu state is already clean; skipped redundant native ReturnMenu ({reason})."); } } float menuWaited = 0f; int consecutiveMenuReadyFrames = 0; while (menuWaited < 10f) { yield return null; menuWaited += Time.unscaledDeltaTime; if (generation != _cleanMenuReconnectGeneration) { yield break; } consecutiveMenuReadyFrames = (IsCleanMenuReadyForReconnect(out var reason2) ? (consecutiveMenuReadyFrames + 1) : 0); if (consecutiveMenuReadyFrames >= 3) { Log.LogWarning((object)$"Reconnect Guard generation {generation}: clean menu confirmed after {menuWaited:0.0}s ({reason2})."); break; } } if (consecutiveMenuReadyFrames < 3) { FinishCleanMenuReconnect(generation, "native teardown did not reach a clean menu; no rejoin was attempted"); AddLocalNotification("BlueSage reconnect stopped safely at cleanup; use the menu to rejoin."); yield break; } string text = (_lastReconnectLobbyCode ?? string.Empty).Trim(); if (!ulong.TryParse(text, out var result) || result == 0L) { FinishCleanMenuReconnect(generation, "saved lobby id was unavailable after native teardown"); yield break; } _cleanMenuReconnectPhase = "joining-saved-lobby"; _lastLobbyRejoinInProgress = true; Log.LogWarning((object)$"Reconnect Guard generation {generation}: joining saved lobby {RedactLobbyCode(text)} from clean menu with isChangeLobby=false."); AddLocalNotification("BlueSage reconnect is joining the saved lobby from the clean menu."); try { MonoSingleton.I.JoinByCode(text, false); } catch (Exception ex) { Log.LogWarning((object)$"Reconnect Guard generation {generation}: clean-menu join could not start: {ex.GetType().Name}: {ex.Message}"); FinishCleanMenuReconnect(generation, "clean-menu join threw before it started"); yield break; } _cleanMenuReconnectPhase = "validating-playable-scene"; float connectedSince = -1f; float validationWaited = 0f; while (validationWaited < 45f) { yield return null; validationWaited += Time.unscaledDeltaTime; if (generation != _cleanMenuReconnectGeneration) { yield break; } bool flag = IsNetworkClientConnected(); if (flag && connectedSince < 0f) { connectedSince = Time.unscaledTime; } else if (!flag) { connectedSince = -1f; } ReconnectRuntimeHealthDecision reconnectRuntimeHealthDecision = EvaluateManagedReconnectRuntime(connectedSince); if (reconnectRuntimeHealthDecision.Action == ReconnectRuntimeHealthAction.ConfirmSuccess) { Log.LogWarning((object)$"Reconnect Guard generation {generation}: playable reconnect confirmed after {validationWaited:0.0}s: {reconnectRuntimeHealthDecision.Reason}"); AnnounceManagedReconnectSuccess(); FinishCleanMenuReconnect(generation, "playable scene confirmed"); yield break; } } Log.LogWarning((object)$"Reconnect Guard generation {generation}: saved-lobby join did not produce a playable scene within {45}s; returning through native cleanup."); AddLocalNotification("BlueSage reconnect joined incompletely; cleaning up instead of looping."); if (!IsCleanMenuReadyForReconnect(requireBlueSageJoinIdle: false, out var reason3)) { RequestNativeReturnToMenuForReconnect(); } else { Log.LogWarning((object)$"Reconnect Guard generation {generation}: terminal menu state is already clean; skipped redundant native ReturnMenu ({reason3})."); } float fallbackMenuWaited = 0f; int fallbackMenuReadyFrames = 0; while (fallbackMenuWaited < 10f) { yield return null; fallbackMenuWaited += Time.unscaledDeltaTime; fallbackMenuReadyFrames = (IsCleanMenuReadyForReconnect(requireBlueSageJoinIdle: false, out var _) ? (fallbackMenuReadyFrames + 1) : 0); if (fallbackMenuReadyFrames >= 3) { break; } } FinishCleanMenuReconnect(generation, "saved-lobby validation failed without another same-lobby retry"); if (fallbackMenuReadyFrames < 3 || !TryStartReplacementLobbyDiscovery()) { AddLocalNotification("BlueSage reconnect stopped at the menu without starting another loop."); } } private void FinishCleanMenuReconnect(int generation, string reason) { if (generation == _cleanMenuReconnectGeneration) { Log.LogWarning((object)$"Reconnect Guard generation {generation} finished in phase {_cleanMenuReconnectPhase}: {reason}."); _cleanMenuReconnectActive = false; _cleanMenuReconnectPhase = "idle"; _cleanMenuReconnectCoroutine = null; _lastLobbyRejoinInProgress = false; _reconnectMenuFallbackInProgress = false; _reconnectReadySampleCount = 0; _reconnectQuorumReadySince = -1f; } } private static bool IsCleanMenuReadyForReconnect(out string reason) { return IsCleanMenuReadyForReconnect(requireBlueSageJoinIdle: true, out reason); } private static bool IsCleanMenuReadyForReconnect(bool requireBlueSageJoinIdle, out string reason) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) try { Scene activeScene = SceneManager.GetActiveScene(); MultiplayerManager i = MonoSingleton.I; bool flag = (Object)(object)MonoSingleton.I != (Object)null; bool flag2 = !IsNetworkClientConnected(); bool flag3 = (Object)(object)i == (Object)null || !i.LobbyStatus; bool flag4 = (Object)(object)i != (Object)null && i.IsConnecting; bool flag5 = (Object)(object)Instance == (Object)null || !Instance._lastLobbyRejoinInProgress; ReconnectMenuReadinessState state = new ReconnectMenuReadinessState(((Scene)(ref activeScene)).isLoaded, ((Scene)(ref activeScene)).buildIndex, flag, flag2, flag3, flag5); reason = $"scene={((Scene)(ref activeScene)).buildIndex}, menu={flag}, transportStopped={flag2}, lobbyCleared={flag3}, vanillaIsConnecting={flag4}, blueSageJoinIdle={flag5}, requireBlueSageJoinIdle={requireBlueSageJoinIdle}"; return requireBlueSageJoinIdle ? ReconnectTerminalCleanupPolicy.IsReadyForNewJoin(state) : ReconnectTerminalCleanupPolicy.IsReady(state); } catch (Exception ex) { reason = "menu readiness unavailable: " + ex.GetType().Name; return false; } } 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}."); float connectedSince = -1f; Log.LogWarning((object)$"Reconnect Guard initial wait {intervalSeconds}s before first canonical saved-lobby cleanup/rejoin attempt so lobby state can settle."); AddLocalNotification($"BlueSage reconnect will try in {intervalSeconds}s."); float initialWaited = 0f; while (initialWaited < (float)intervalSeconds) { yield return null; initialWaited += Time.unscaledDeltaTime; bool flag = IsNetworkClientConnected(); if (flag && connectedSince < 0f) { connectedSince = Time.unscaledTime; } else if (!flag) { connectedSince = -1f; } ReconnectRuntimeHealthDecision reconnectRuntimeHealthDecision = EvaluateManagedReconnectRuntime(connectedSince); if (reconnectRuntimeHealthDecision.Action == ReconnectRuntimeHealthAction.ConfirmSuccess) { Log.LogWarning((object)("Reconnect Guard confirmed usable runtime during initial wait: " + reconnectRuntimeHealthDecision.Reason)); AnnounceManagedReconnectSuccess(); StopManagedReconnect("client connected during initial wait"); yield break; } if (reconnectRuntimeHealthDecision.Action == ReconnectRuntimeHealthAction.ReturnToMenu) { Log.LogWarning((object)("Reconnect Guard could not confirm a usable runtime during initial wait: " + reconnectRuntimeHealthDecision.Reason)); StopManagedReconnect(reconnectRuntimeHealthDecision.Reason); HandleReconnectFailureFallback(reconnectRuntimeHealthDecision.Reason); yield break; } } while (_managedReconnectAttempt < maxAttempts) { bool flag2 = IsNetworkClientConnected(); if (flag2 && connectedSince < 0f) { connectedSince = Time.unscaledTime; } else if (!flag2) { connectedSince = -1f; } ReconnectRuntimeHealthDecision reconnectRuntimeHealthDecision2 = EvaluateManagedReconnectRuntime(connectedSince); if (reconnectRuntimeHealthDecision2.Action == ReconnectRuntimeHealthAction.ConfirmSuccess) { Log.LogWarning((object)("Reconnect Guard confirmed usable runtime: " + reconnectRuntimeHealthDecision2.Reason)); AnnounceManagedReconnectSuccess(); StopManagedReconnect("client connected"); yield break; } if (reconnectRuntimeHealthDecision2.Action == ReconnectRuntimeHealthAction.ReturnToMenu) { Log.LogWarning((object)("Reconnect Guard could not confirm a usable runtime: " + reconnectRuntimeHealthDecision2.Reason)); StopManagedReconnect(reconnectRuntimeHealthDecision2.Reason); HandleReconnectFailureFallback(reconnectRuntimeHealthDecision2.Reason); yield break; } TryGetVisibleLobbyCounts(out var memberCount, out var maxPlayers); bool flag3 = memberCount.HasValue && memberCount.Value >= 1; _managedReconnectAttempt++; _lastManagedReconnectAttemptAt = Time.unscaledTime; if (!flag2) { string text = (flag3 ? $"{memberCount}/{maxPlayers}" : "not confirmed"); Log.LogWarning((object)("Reconnect Guard: transport is disconnected with lobby " + text + "; using canonical saved-lobby cleanup/rejoin instead of calling StartClient directly.")); StopManagedReconnect("switching to canonical lobby rejoin"); HandleReconnectFailureFallback("transport disconnected; canonical lobby rejoin required"); yield break; } Log.LogWarning((object)$"Reconnect Guard attempt {_managedReconnectAttempt}/{maxAttempts}: {reconnectRuntimeHealthDecision2.Reason}"); float waited = 0f; while (waited < (float)intervalSeconds) { yield return null; waited += Time.unscaledDeltaTime; bool flag4 = IsNetworkClientConnected(); if (flag4 && connectedSince < 0f) { connectedSince = Time.unscaledTime; } else if (!flag4) { connectedSince = -1f; } ReconnectRuntimeHealthDecision reconnectRuntimeHealthDecision3 = EvaluateManagedReconnectRuntime(connectedSince); if (reconnectRuntimeHealthDecision3.Action == ReconnectRuntimeHealthAction.ConfirmSuccess) { Log.LogWarning((object)("Reconnect Guard confirmed usable runtime during wait: " + reconnectRuntimeHealthDecision3.Reason)); AnnounceManagedReconnectSuccess(); StopManagedReconnect("client connected during wait"); yield break; } if (reconnectRuntimeHealthDecision3.Action == ReconnectRuntimeHealthAction.ReturnToMenu) { Log.LogWarning((object)("Reconnect Guard could not confirm a usable runtime during wait: " + reconnectRuntimeHealthDecision3.Reason)); StopManagedReconnect(reconnectRuntimeHealthDecision3.Reason); HandleReconnectFailureFallback(reconnectRuntimeHealthDecision3.Reason); yield break; } } } StopManagedReconnect($"max attempts reached ({maxAttempts})"); HandleReconnectFailureFallback($"max attempts reached ({maxAttempts})"); } private void TryCaptureReconnectLobbyCode() { //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) try { MultiplayerManager i = MonoSingleton.I; if ((Object)(object)i == (Object)null || !i.LobbyStatus) { return; } string lobbyCode = i.LobbyCode; if (string.IsNullOrWhiteSpace(lobbyCode)) { return; } lobbyCode = lobbyCode.Trim(); if (ulong.TryParse(lobbyCode, out var result) && result != 0L) { if (!string.Equals(_lastReconnectLobbyCode, lobbyCode, StringComparison.Ordinal)) { _lastReconnectLobbyCode = lobbyCode; _lastLobbyRejoinInProgress = false; _replacementLobbyDiscoveryAttempted = false; Log.LogInfo((object)"Reconnect Guard saved current lobby id for last-lobby fallback."); } CSteamID val = new CSteamID(result); CSteamID lobbyOwner = SteamMatchmaking.GetLobbyOwner(val); if (lobbyOwner != CSteamID.Nil) { _lastReconnectLobbyOwnerSteamId = ((ulong)lobbyOwner).ToString(); } string lobbyData = SteamMatchmaking.GetLobbyData(val, "Name"); _lastReconnectLobbyNameFingerprint = GetLobbyNameFingerprint(lobbyData); } } catch (Exception ex) { Log.LogWarning((object)("Reconnect Guard could not capture current lobby id: " + ex.GetType().Name + ": " + ex.Message)); } } private void HandleReconnectFailureFallback(string reason) { if (!TryRejoinLastLobbyBeforeMenuFallback(reason)) { AddLocalNotification("BlueSage reconnect could not confirm a usable lobby; returning to menu."); ReturnToMenuAfterFailedReconnectCore(); } } private bool TryRejoinLastLobbyBeforeMenuFallback(string reason) { if (_cleanMenuReconnectActive || string.IsNullOrWhiteSpace(_lastReconnectLobbyCode)) { return false; } if (!ulong.TryParse(_lastReconnectLobbyCode, out var result) || result == 0L) { Log.LogWarning((object)"Reconnect Guard saved lobby id was invalid; skipping last-lobby rejoin fallback."); return false; } Log.LogWarning((object)("Reconnect Guard legacy fallback redirected into one clean-menu generation: " + reason + ".")); StartCleanMenuReconnect("legacy fallback redirected: " + reason, nativeTeardownAlreadyStarting: false); return true; } private IEnumerator LastLobbyRejoinFallbackWait() { Log.LogWarning((object)"Reconnect Guard last-lobby rejoin attempted; waiting before menu fallback."); float connectedSince = -1f; float waited = 0f; float maxWaitSeconds = 13f; while (waited < maxWaitSeconds) { yield return null; waited += Time.unscaledDeltaTime; bool flag = IsNetworkClientConnected(); if (flag && connectedSince < 0f) { connectedSince = Time.unscaledTime; } else if (!flag) { connectedSince = -1f; } ReconnectRuntimeHealthDecision reconnectRuntimeHealthDecision = EvaluateManagedReconnectRuntime(connectedSince); if (reconnectRuntimeHealthDecision.Action == ReconnectRuntimeHealthAction.ConfirmSuccess) { Log.LogWarning((object)("Reconnect Guard confirmed usable runtime after last-lobby rejoin: " + reconnectRuntimeHealthDecision.Reason)); AnnounceManagedReconnectSuccess(); _lastLobbyRejoinInProgress = false; _lastLobbyRejoinFallbackCoroutine = null; yield break; } } _lastLobbyRejoinInProgress = false; _lastLobbyRejoinFallbackCoroutine = null; if (!TryStartReplacementLobbyDiscovery()) { AddLocalNotification("BlueSage reconnect could not rejoin the last lobby; returning to menu."); ReturnToMenuAfterFailedReconnectCore(); } } private bool TryStartReplacementLobbyDiscovery() { if (_replacementLobbyDiscoveryAttempted || string.IsNullOrWhiteSpace(_lastReconnectLobbyOwnerSteamId) || _replacementLobbyDiscoveryCoroutine != null) { return false; } _replacementLobbyDiscoveryAttempted = true; _lastLobbyRejoinInProgress = true; _replacementLobbyDiscoveryCoroutine = ((MonoBehaviour)this).StartCoroutine(ReplacementLobbyDiscoveryLoop()); AddLocalNotification("BlueSage reconnect is checking whether the same host re-created the public lobby."); return true; } private IEnumerator ReplacementLobbyDiscoveryLoop() { int[] array = new int[5] { 5, 5, 10, 10, 15 }; int[] array2 = array; foreach (int num in array2) { yield return (object)new WaitForSecondsRealtime((float)num); List candidates = new List(); for (int distance = 1; distance <= 3; distance++) { Task searchTask = StartReplacementLobbySearch(distance); if (searchTask == null) { continue; } float taskWaited = 0f; while (!searchTask.IsCompleted && taskWaited < 8f) { yield return null; taskWaited += Time.unscaledDeltaTime; } if (!searchTask.IsCompleted || searchTask.IsFaulted || searchTask.IsCanceled) { continue; } foreach (object item in ReadTaskResultItems(searchTask)) { string text = ReadLobbyString(item, "lobbyId"); string candidateName = ReadLobbyString(item, "Name"); if (IsReplacementLobbyCandidate(text, candidateName) && !candidates.Contains(text)) { candidates.Add(text); } } } if (candidates.Count == 1) { _lastReconnectLobbyCode = candidates[0]; _lastLobbyRejoinInProgress = false; _replacementLobbyDiscoveryCoroutine = null; Log.LogWarning((object)"Reconnect Guard found one validated replacement lobby owned by the original host; starting one clean-menu join generation."); AddLocalNotification("BlueSage found the host's replacement lobby and is reconnecting."); StartCleanMenuReconnect("validated same-host replacement lobby discovered", nativeTeardownAlreadyStarting: false); yield break; } if (candidates.Count > 1) { Log.LogWarning((object)"Reconnect Guard found multiple same-host replacement candidates; refusing to guess."); break; } } _lastLobbyRejoinInProgress = false; _replacementLobbyDiscoveryCoroutine = null; AddLocalNotification("BlueSage could not uniquely validate a replacement lobby; returning to menu."); ReturnToMenuAfterFailedReconnectCore(); } private static Task StartReplacementLobbySearch(int distance) { try { MultiplayerManager i = MonoSingleton.I; object obj = AccessTools.Field(typeof(MultiplayerManager), "_lobbyManager")?.GetValue(i); object obj2 = obj?.GetType().GetProperty("CurrentProvider")?.GetValue(obj, null); MethodInfo? obj3 = obj2?.GetType().GetMethod("SearchLobbiesAsync", new Type[3] { typeof(int), typeof(int), typeof(Dictionary) }); Dictionary dictionary = new Dictionary { { "public", "t" }, { "version", ConstData.Version.ToLowerInvariant() } }; return obj3?.Invoke(obj2, new object[3] { distance, 50, dictionary }) as Task; } catch (Exception ex) { ManualLogSource log = Log; if (log != null) { log.LogWarning((object)("Reconnect Guard replacement-lobby search could not start: " + ex.GetType().Name + ": " + ex.Message)); } return null; } } private static IEnumerable ReadTaskResultItems(Task task) { if (!(task?.GetType().GetProperty("Result")?.GetValue(task, null) is IEnumerable enumerable)) { yield break; } foreach (object item in enumerable) { yield return item; } } private bool IsReplacementLobbyCandidate(string candidateId, string candidateName) { //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) if (!ulong.TryParse(candidateId, out var result) || result == 0L || string.Equals(candidateId, _lastReconnectLobbyCode, StringComparison.Ordinal)) { return false; } CSteamID val = default(CSteamID); ((CSteamID)(ref val))..ctor(result); if (!string.Equals(((ulong)SteamMatchmaking.GetLobbyOwner(val)).ToString(), _lastReconnectLobbyOwnerSteamId, StringComparison.Ordinal)) { return false; } if (!string.Equals(SteamMatchmaking.GetLobbyData(val, "public"), "t", StringComparison.OrdinalIgnoreCase) || string.Equals(SteamMatchmaking.GetLobbyData(val, "request"), "t", StringComparison.OrdinalIgnoreCase) || !string.Equals(SteamMatchmaking.GetLobbyData(val, "version"), ConstData.Version.ToLowerInvariant(), StringComparison.OrdinalIgnoreCase)) { return false; } int numLobbyMembers = SteamMatchmaking.GetNumLobbyMembers(val); int lobbyMemberLimit = SteamMatchmaking.GetLobbyMemberLimit(val); if (numLobbyMembers < 1 || lobbyMemberLimit < 1 || numLobbyMembers >= lobbyMemberLimit - 1) { return false; } string lobbyNameFingerprint = GetLobbyNameFingerprint(candidateName); if (!string.IsNullOrEmpty(_lastReconnectLobbyNameFingerprint)) { return string.Equals(lobbyNameFingerprint, _lastReconnectLobbyNameFingerprint, StringComparison.Ordinal); } return true; } private static string ReadLobbyString(object lobby, string memberName) { if (lobby == null) { return string.Empty; } return (lobby.GetType().GetProperty(memberName)?.GetValue(lobby, null) ?? lobby.GetType().GetField(memberName)?.GetValue(lobby))?.ToString() ?? string.Empty; } private static string GetLobbyNameFingerprint(string lobbyName) { string text = new string((from character in Regex.Replace(lobbyName ?? string.Empty, "<[^>]*>", string.Empty) where !char.IsWhiteSpace(character) select character).ToArray()).ToLowerInvariant(); if (!text.Contains("♪blues♪")) { return text; } return "♪blues♪"; } private static bool IsNetworkClientConnected() { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Invalid comparison between Unknown and I4 try { return (Object)(object)NetworkManager.main != (Object)null && (int)NetworkManager.main.clientState == 1; } catch (Exception ex) { ManualLogSource log = Log; if (log != null) { log.LogWarning((object)("Reconnect Guard could not read PurrNet client state: " + ex.GetType().Name + ": " + ex.Message)); } return false; } } private static ReconnectRuntimeHealthDecision EvaluateManagedReconnectRuntime(float connectedSince) { bool num = IsNetworkClientConnected(); int? memberCount; int? maxPlayers; bool localUserIsMember; bool flag = TryGetAuthoritativeLobbyState(out memberCount, out maxPlayers, out localUserIsMember) && memberCount.HasValue && memberCount.Value >= 1; string reason; bool flag2 = IsGameRuntimeUsableForReconnect(out reason); bool flag3 = IsVoiceRuntimeReadyForReconnect(); bool flag4 = num && flag && localUserIsMember && flag2; float readyStableSeconds = 0f; if ((Object)(object)Instance != (Object)null) { Instance._reconnectReadySampleCount = (flag4 ? (Instance._reconnectReadySampleCount + 1) : 0); if (flag4) { if (Instance._reconnectQuorumReadySince < 0f) { Instance._reconnectQuorumReadySince = Time.unscaledTime; } readyStableSeconds = Time.unscaledTime - Instance._reconnectQuorumReadySince; } else { Instance._reconnectQuorumReadySince = -1f; } } ReconnectRuntimeHealthDecision reconnectRuntimeHealthDecision = ReconnectRuntimeHealthPolicy.Evaluate(new ReconnectRuntimeHealthInput(secondsSinceClientTransportConnected: (num && connectedSince >= 0f) ? new float?(Time.unscaledTime - connectedSince) : ((float?)null), clientTransportConnected: num, requireVisibleLobby: true, visibleLobbyConfirmed: flag, localUserIsAuthoritativeLobbyMember: localUserIsMember, gameRuntimeUsable: flag2, voiceRuntimeReady: flag3, consecutiveReadySamples: Instance?._reconnectReadySampleCount ?? 0, postConnectValidationSeconds: 8, readyStableSeconds: readyStableSeconds)); if (reconnectRuntimeHealthDecision.Action == ReconnectRuntimeHealthAction.ConfirmSuccess) { return reconnectRuntimeHealthDecision; } return new ReconnectRuntimeHealthDecision(reconnectRuntimeHealthDecision.Action, $"{reconnectRuntimeHealthDecision.Reason} Runtime probe: {reason}; voiceReady={flag3}."); } private static bool TryGetAuthoritativeLobbyState(out int? memberCount, out int? maxPlayers, out bool localUserIsMember) { //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0074: 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_007c: Unknown result type (might be due to invalid IL or missing references) memberCount = null; maxPlayers = null; localUserIsMember = false; try { MultiplayerManager i = MonoSingleton.I; if (!ulong.TryParse((i != null) ? i.LobbyCode : null, out var result) || result == 0L || !SteamManager.Initialized) { return false; } CSteamID val = default(CSteamID); ((CSteamID)(ref val))..ctor(result); CSteamID steamID = SteamUser.GetSteamID(); int numLobbyMembers = SteamMatchmaking.GetNumLobbyMembers(val); memberCount = numLobbyMembers; maxPlayers = SteamMatchmaking.GetLobbyMemberLimit(val); for (int j = 0; j < numLobbyMembers; j++) { if (SteamMatchmaking.GetLobbyMemberByIndex(val, j) == steamID) { localUserIsMember = true; break; } } return true; } catch (Exception ex) { ManualLogSource log = Log; if (log != null) { log.LogWarning((object)("Reconnect Guard could not read authoritative Steam lobby membership: " + ex.GetType().Name + ": " + ex.Message)); } return false; } } private static bool IsVoiceRuntimeReadyForReconnect() { //IL_0027: Unknown result type (might be due to invalid IL or missing references) try { MultiplayerManager i = MonoSingleton.I; string text = ((i != null) ? i.LobbyCode : null); if (!ulong.TryParse(text, out var result) || result == 0L) { return false; } if (string.Equals(SteamMatchmaking.GetLobbyData(new CSteamID(result), "voice"), "f", StringComparison.OrdinalIgnoreCase)) { return true; } VoiceManager i2 = MonoSingleton.I; return (Object)(object)i2 != (Object)null && i2.IsJoined && string.Equals(i2.LobbyIDName, text, StringComparison.Ordinal); } catch (Exception ex) { ManualLogSource log = Log; if (log != null) { log.LogWarning((object)("Reconnect Guard could not verify Vivox lobby alignment: " + ex.GetType().Name + ": " + ex.Message)); } return false; } } private static bool IsGameRuntimeUsableForReconnect(out string reason) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0202: Unknown result type (might be due to invalid IL or missing references) reason = "unknown"; try { Scene activeScene = SceneManager.GetActiveScene(); if (!((Scene)(ref activeScene)).isLoaded || ((Scene)(ref activeScene)).buildIndex != 2) { reason = $"scene={((Scene)(ref activeScene)).buildIndex}, loaded={((Scene)(ref activeScene)).isLoaded}"; return false; } MultiplayerManager i = MonoSingleton.I; if ((Object)(object)i == (Object)null || !i.LobbyStatus) { reason = string.Format("multiplayer={0}, lobbyStatus={1}", ((Object)(object)i == (Object)null) ? "missing" : "present", (i != null) ? new bool?(i.LobbyStatus) : ((bool?)null)); return false; } string text = Instance?._lastReconnectLobbyCode; if (!string.IsNullOrWhiteSpace(text) && !string.Equals(i.LobbyCode, text, StringComparison.Ordinal)) { reason = "active lobby does not match the reconnect target"; return false; } TextChannelManager i2 = NetworkSingleton.I; if ((Object)(object)i2 == (Object)null || (Object)(object)i2.MainPlayer == (Object)null || (Object)(object)i2.MainPlayerController == (Object)null || !((Component)i2.MainPlayer).gameObject.activeInHierarchy || !((Component)i2.MainPlayerController).gameObject.activeInHierarchy) { reason = string.Format("chat={0}, player={1}, controller={2}", ((Object)(object)i2 == (Object)null) ? "missing" : "present", (Object)(object)i2?.MainPlayer != (Object)null, (Object)(object)i2?.MainPlayerController != (Object)null); return false; } PlayerPanelController i3 = NetworkSingleton.I; if ((Object)(object)i3 == (Object)null || i3.PlayerSteamIDs == null || i3.PlayerControllers == null || i3.PlayerTransforms == null || !SteamManager.Initialized) { reason = string.Format("panel={0}, steamInitialized={1}", ((Object)(object)i3 == (Object)null) ? "missing" : "present", SteamManager.Initialized); return false; } string localSteamId = ((ulong)SteamUser.GetSteamID()).ToString(); int num = i3.PlayerSteamIDs.Count((string value) => string.Equals(value, localSteamId, StringComparison.Ordinal)); int num2 = i3.PlayerSteamIDs.FindIndex((string value) => string.Equals(value, localSteamId, StringComparison.Ordinal)); bool flag = num == 1 && num2 >= 0 && num2 < i3.PlayerControllers.Count && num2 < i3.PlayerTransforms.Count && (Object)(object)i3.PlayerControllers[num2] != (Object)null && (Object)(object)i3.PlayerTransforms[num2] != (Object)null; reason = (flag ? "scene, lobby, chat/player controller, and unique local roster entry are playable" : $"localRosterOccurrences={num}, localIndex={num2}, controllers={i3.PlayerControllers.Count}, transforms={i3.PlayerTransforms.Count}"); return flag; } catch (Exception ex) { reason = "probe exception " + ex.GetType().Name; ManualLogSource log = Log; if (log != null) { log.LogWarning((object)("Reconnect Guard could not read local runtime readiness: " + ex.GetType().Name + ": " + ex.Message)); } return false; } } private void StopManagedReconnect(string reason) { if (_cleanMenuReconnectCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_cleanMenuReconnectCoroutine); _cleanMenuReconnectCoroutine = null; _cleanMenuReconnectGeneration++; } if (_cleanMenuReconnectActive) { Log.LogWarning((object)("Reconnect Guard stopped generation in phase " + _cleanMenuReconnectPhase + ": " + reason + ".")); } _cleanMenuReconnectActive = false; _cleanMenuReconnectPhase = "idle"; if (_managedReconnectCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_managedReconnectCoroutine); _managedReconnectCoroutine = null; } if (_managedReconnectActive) { Log.LogWarning((object)("Reconnect Guard stopped: " + reason + ".")); } _managedReconnectActive = false; _managedReconnectAttempt = 0; _lastManagedReconnectAttemptAt = -1f; _reconnectReadySampleCount = 0; _reconnectQuorumReadySince = -1f; } private void AnnounceManagedReconnectSuccess() { if (_managedReconnectSuccessAnnounced) { return; } _managedReconnectSuccessAnnounced = true; if (EnableReconnectAnnouncement.Value) { string text = "Auto Reconnected by Blues - BlueSage QoL Tweaks Mod".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 (!TrySendLobbyChatMessage(message)) { AddLocalNotification(message); } _reconnectAnnouncementCoroutine = null; } internal bool TrySendDoNotPressLobbyMessage() { return TrySendLobbyChatMessage("@Blue I found the tiny BlueSage lobby joke :3"); } internal static bool TrySendLobbyChatMessage(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)("Lobby 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; } internal static bool IsPluginLoaded(string pluginGuid) { return Chainloader.PluginInfos.Keys.Any((string key) => string.Equals(key, pluginGuid, StringComparison.OrdinalIgnoreCase)); } private static void RequestNativeReturnToMenuForReconnect() { //IL_0028: Unknown result type (might be due to invalid IL or missing references) Plugin instance = Instance; if ((Object)(object)instance != (Object)null) { instance._reconnectMenuFallbackInProgress = true; } try { MultiplayerManager i = MonoSingleton.I; if ((Object)(object)i != (Object)null) { i._notificationState = (NotificationStatus)3; } MainSceneManager i2 = MonoSingleton.I; if ((Object)(object)i2 != (Object)null) { i2.ReturnMenu(false); } else if (i != null) { i.QuitSession(); } } catch (Exception ex) { Log.LogWarning((object)("Reconnect Guard could not request native menu teardown: " + ex.GetType().Name + ": " + ex.Message)); } } private static void ReturnToMenuAfterFailedReconnectCore() { //IL_00a1: Unknown result type (might be due to invalid IL or missing references) Plugin instance = Instance; if ((Object)(object)instance != (Object)null) { instance._reconnectMenuFallbackInProgress = true; } try { try { MultiplayerManager i = MonoSingleton.I; if (i != null) { i.QuitSession(); } } catch (Exception ex) { Log.LogWarning((object)("Reconnect Guard could not quit unhealthy session before menu fallback: " + ex.GetType().Name + ": " + ex.Message)); } MainSceneManager i2 = MonoSingleton.I; if (!((Object)(object)i2 == (Object)null)) { AccessTools.Field(typeof(MainSceneManager), "_returnMenuStarted")?.SetValue(i2, false); MultiplayerManager i3 = MonoSingleton.I; if ((Object)(object)i3 != (Object)null) { i3._notificationState = (NotificationStatus)3; } i2.ReturnMenu(false); } } catch (Exception ex2) { Log.LogWarning((object)("Reconnect Guard fallback to menu failed: " + ex2.GetType().Name + ": " + ex2.Message)); } finally { if ((Object)(object)instance != (Object)null) { ((MonoBehaviour)instance).StartCoroutine(instance.ClearReconnectMenuFallbackFlagSoon()); } } } private IEnumerator ClearReconnectMenuFallbackFlagSoon() { yield return null; yield return null; _reconnectMenuFallbackInProgress = false; } private static string RedactLobbyCode(string lobbyCode) { if (string.IsNullOrWhiteSpace(lobbyCode)) { return "[none]"; } lobbyCode = lobbyCode.Trim(); if (lobbyCode.Length <= 4) { return "[redacted]"; } return "[redacted:" + lobbyCode.Substring(lobbyCode.Length - 4) + "]"; } 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; } } private static bool TryGetSteamLobbyCounts(CSteamID? lobbyId, out int? memberCount, out int? maxPlayers) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) memberCount = null; maxPlayers = null; try { if (!lobbyId.HasValue || (ulong)lobbyId.Value == 0L) { return false; } int numLobbyMembers = SteamMatchmaking.GetNumLobbyMembers(lobbyId.Value); int lobbyMemberLimit = SteamMatchmaking.GetLobbyMemberLimit(lobbyId.Value); if (numLobbyMembers > 0) { memberCount = numLobbyMembers; } if (lobbyMemberLimit > 0) { maxPlayers = lobbyMemberLimit; } return memberCount.HasValue || maxPlayers.HasValue; } catch (Exception ex) { ManualLogSource log = Log; if (log != null) { log.LogWarning((object)("Host Health could not read Steam lobby count/limit: " + ex.GetType().Name + ": " + ex.Message)); } return false; } } private static bool TryGetLobbySurfaceMetadata(CSteamID? lobbyId, out string title, out string tags, out string visibility) { //IL_0178: Unknown result type (might be due to invalid IL or missing references) //IL_018c: Unknown result type (might be due to invalid IL or missing references) //IL_01c7: Unknown result type (might be due to invalid IL or missing references) //IL_0202: Unknown result type (might be due to invalid IL or missing references) title = "?"; tags = "?"; visibility = "?"; bool flag = false; 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) { if (TryReadStringProperty(obj2, out var value, "Name", "Title", "LobbyName", "SessionName")) { title = value; flag = true; } if (TryReadStringProperty(obj2, out var value2, "Tags", "TagString", "SearchTags")) { tags = value2; flag = true; } bool value4; if (TryReadStringProperty(obj2, out var value3, "Visibility", "LobbyType", "Type")) { visibility = value3; flag = true; } else if (TryReadBoolProperty(obj2, out value4, "IsPublic", "Public")) { visibility = (value4 ? "public" : "private"); flag = true; } } } catch (Exception ex) { ManualLogSource log = Log; if (log != null) { log.LogWarning((object)("Host Health could not read reflected lobby surface metadata: " + ex.GetType().Name + ": " + ex.Message)); } } if (lobbyId.HasValue && (ulong)lobbyId.Value != 0L) { title = FirstUseful(title, ReadLobbyData(lobbyId.Value, "name", "title", "lobby_name", "session_name")); tags = FirstUseful(tags, ReadLobbyData(lobbyId.Value, "tags", "tag", "search_tags", "description")); visibility = FirstUseful(visibility, ReadLobbyData(lobbyId.Value, "visibility", "public", "lobby_type", "type")); flag = flag || title != "?" || tags != "?" || visibility != "?"; } return flag; } private static string ReadLobbyData(CSteamID lobbyId, params string[] keys) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) foreach (string text in keys) { try { string lobbyData = SteamMatchmaking.GetLobbyData(lobbyId, text); if (!string.IsNullOrWhiteSpace(lobbyData)) { return lobbyData; } } catch { } } return "?"; } private static bool TryReadStringProperty(object target, out string value, params string[] names) { value = string.Empty; foreach (string name in names) { object obj = target.GetType().GetProperty(name)?.GetValue(target, null); if (obj != null && !string.IsNullOrWhiteSpace(obj.ToString())) { value = obj.ToString(); return true; } } return false; } private static bool TryReadBoolProperty(object target, out bool value, params string[] names) { value = false; foreach (string name in names) { object obj = target.GetType().GetProperty(name)?.GetValue(target, null); if (obj is bool flag) { value = flag; return true; } if (obj != null && bool.TryParse(obj.ToString(), out var result)) { value = result; return true; } } return false; } private static string FirstUseful(string current, string candidate) { if (!(current == "?") || string.IsNullOrWhiteSpace(candidate) || !(candidate != "?")) { return current; } return candidate; } private static string SanitizeDiagnosticValue(string value) { if (string.IsNullOrWhiteSpace(value)) { return "?"; } return value.Replace("\r", " ").Replace("\n", " ").Replace(";", ",") .Trim(); } } internal static class PluginShutdownController { private static readonly PluginShutdownPolicy Policy = new PluginShutdownPolicy(); internal static bool IsShuttingDown => Policy.IsShuttingDown; internal static void Begin(string trigger, Action steamCleanup, Action localCleanup) { if (!Policy.TryBegin(trigger)) { return; } SteamRuntimeSnapshot obj = SteamRuntimeFacade.SnapshotForShutdown(); if (Policy.TryClaimSteamCleanup()) { try { steamCleanup?.Invoke(obj); } finally { Policy.FinishSteamCleanup(); } } if (!Policy.TryClaimLocalCleanup()) { return; } try { localCleanup?.Invoke(); } finally { Policy.FinishLocalCleanup(); } } } internal sealed class QolMenuWindow : MonoBehaviour { private sealed class ThanksCard { internal string Name { get; } internal string Received { get; } internal IReadOnlyList Messages { get; } internal ThanksCard(string name, string received, params string[] messages) { Name = name; Received = received; Messages = messages ?? Array.Empty(); } } private enum FeatureSectionTone { Comfort, Chat, Identity, Personalization, Experimental } private const float MinWindowWidth = 920f; private const float MinWindowHeight = 640f; private const float ResizeGripSize = 42f; private const float FooterHeight = 120f; private const float ContentChromeHeight = 272f; private const float ContentHorizontalChrome = 96f; private const string DoNotPressEggKey = "menu_do_not_press"; private static readonly ThanksCard[] CommunityCrewCards = new ThanksCard[2] { new ThanksCard("bloo_bamboo", "Quality \"the sacrifice\" Tester", "Large-lobby QA, crash logs, and patient reconnect testing when things go sideways."), new ThanksCard("Rowan", "Partner in Crime ♥", "BlueSage's trusted co-conspirator for testing, experiments, and keeping Blue honest.") }; private static readonly SupporterIdentityRollup[] SupporterRollups = SupporterLedgerPolicy.RollUp(SupporterLedgerData.Transactions); private Rect _windowRect = new Rect(120f, 95f, 960f, 720f); private bool _visible; private bool _hasOpened; private int _tab; private string _message = "Manage BlueSage QoL features without memorizing slash commands."; private string _sweepIntervalText = string.Empty; private string _pingHighlightText = string.Empty; private string _pingMentionText = string.Empty; private string _pingSoundModeText = string.Empty; private string _chatOutlineIntensityText = string.Empty; private string _chatUiScaleText = string.Empty; private string _chatFontSizeText = string.Empty; private string _chatWindowHeightText = string.Empty; private string _chatHistoryRowsText = string.Empty; private string _chatOutlineColorText = string.Empty; private string _blackChatOutlineColorText = string.Empty; private string _playerRenderSaverRadiusText = string.Empty; private string _playerRenderSaverMaxVisibleText = string.Empty; private string _chatLimitText = string.Empty; private string _profileLimitText = string.Empty; private string _sessionLimitText = string.Empty; private string _selfSpeedText = string.Empty; private string _selfJumpText = string.Empty; private string _selfGravityText = string.Empty; private int _safetyRosterPage; private string _selectedSafetySteamId = string.Empty; private string _safetyRosterQuery = string.Empty; private int _nativeBanPage; private string _selectedBannedSteamId = string.Empty; private string _offlineBanSteamIdText = string.Empty; private bool _showClosedIncidentHistory; private int _selectedSupporterIndex; private Vector2 _contentScroll; private GUIStyle _windowStyle; private GUIStyle _headerBoxStyle; private GUIStyle _headerStyle; private GUIStyle _labelStyle; private GUIStyle _smallStyle; private GUIStyle _buttonStyle; private GUIStyle _activeButtonStyle; private GUIStyle _enabledButtonStyle; private GUIStyle _disabledButtonStyle; private GUIStyle _experimentalButtonStyle; private GUIStyle _boxStyle; private GUIStyle _sectionBoxStyle; private GUIStyle _comfortSectionBoxStyle; private GUIStyle _chatSectionBoxStyle; private GUIStyle _identitySectionBoxStyle; private GUIStyle _personalizationSectionBoxStyle; private GUIStyle _experimentalBoxStyle; private GUIStyle _sectionHeaderStyle; private GUIStyle _navigationHintStyle; private GUIStyle _footerBoxStyle; private GUIStyle _footerTextStyle; private GUIStyle _textFieldStyle; private Texture2D _windowBackgroundTexture; private bool _isResizing; private string _renderedThemeKey = string.Empty; private int _renderedTextureGeneration; private BlueSageUiThemePalette _theme; private string _stateLabel = "Saved"; private string _stateDetail = "Loaded saved menu values."; private readonly StatisticsPulseReader _statisticsPulseReader = new StatisticsPulseReader(); private StatisticsPulseSnapshot _statisticsPulse; private string _statisticsPulseUnavailableReason = "Native statistics have not been read yet."; private string _statisticsPulseNote = "Achievements are counted only when you press Refresh."; private readonly Dictionary _settingUndoValues = new Dictionary(StringComparer.Ordinal); public void ToggleVisible() { //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) _visible = !_visible; BlueSageWindowCoordinator.SetVisible(BlueSagePublicWindow.QolMenu, _visible); if (!_visible) { BlueSageWindowHoverScope.UnregisterWindow(47058); } if (_visible) { WindowFitResult windowFitResult = BlueSageWindowCoordinator.ResolveForOpen(BlueSagePublicWindow.QolMenu, _hasOpened, _windowRect, 920f, 640f, Screen.width, Screen.height); _windowRect = new Rect(windowFitResult.X, windowFitResult.Y, windowFitResult.Width, windowFitResult.Height); _hasOpened = true; HydrateFields(); RefreshStatisticsPulse(includeAchievements: false); MarkSaved("Loaded saved menu values. Edit Customize fields to start a draft."); } } internal bool ShowHostConsole() { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) if (!Plugin.ShouldShowLobbySafetyTab(out var state, out var _) || state != LobbySafetyAccessState.Host) { return false; } _tab = 4; _contentScroll = Vector2.zero; if (!_visible) { ToggleVisible(); } return true; } private void OnGUI() { //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Expected O, but got Unknown //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) if (!_visible) { BlueSageWindowCoordinator.SetVisible(BlueSagePublicWindow.QolMenu, visible: false); return; } BlueSageWindowCoordinator.SetVisible(BlueSagePublicWindow.QolMenu, visible: true); EnsureStyles(); ClampWindowToScreen(); BlueSageWindowHoverScope.RegisterWindow(47058, _windowRect, 0); _windowRect = GUI.Window(47058, _windowRect, new WindowFunction(DrawWindow), string.Empty, _windowStyle); ClampWindowToScreen(); } private void DrawWindow(int id) { //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_00c7: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0156: Unknown result type (might be due to invalid IL or missing references) GUI.tooltip = string.Empty; LobbySafetyAccessState state; string reason; bool flag = Plugin.ShouldShowLobbySafetyTab(out state, out reason); if (_tab == 4 && !flag) { _tab = 0; _contentScroll = Vector2.zero; ClearLobbySafetyTransientState(); } if ((Object)(object)_windowBackgroundTexture != (Object)null) { GUI.DrawTexture(new Rect(0f, 0f, ((Rect)(ref _windowRect)).width, ((Rect)(ref _windowRect)).height), (Texture)(object)_windowBackgroundTexture); } DrawHeader(); GUILayout.Space(10f); DrawTabs(); GUILayout.Space(8f); _contentScroll = GUILayout.BeginScrollView(_contentScroll, false, true, GUIStyle.none, GUI.skin.verticalScrollbar, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(GetContentScrollHeight()) }); if (_tab == 0) { DrawMainTab(); } else if (_tab == 1) { DrawTogglesTab(); } else if (_tab == 2) { DrawCustomizeTab(); } else if (_tab == 3) { DrawCreditsOtherTab(); } else if (flag) { DrawLobbySafetyTab(); } else { DrawMainTab(); } GUILayout.EndScrollView(); GUILayout.FlexibleSpace(); DrawFooter(); DrawResizeGrip(); GUI.DragWindow(new Rect(0f, 0f, ((Rect)(ref _windowRect)).width - 138f, 34f)); } private void DrawHeader() { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: Expected O, but got Unknown //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Unknown result type (might be due to invalid IL or missing references) //IL_00ee: Expected O, but got Unknown //IL_011a: Unknown result type (might be due to invalid IL or missing references) //IL_0129: Unknown result type (might be due to invalid IL or missing references) //IL_0139: Expected O, but got Unknown //IL_0165: Unknown result type (might be due to invalid IL or missing references) //IL_0174: Unknown result type (might be due to invalid IL or missing references) //IL_0184: Expected O, but got Unknown GUI.Box(new Rect(0f, 0f, ((Rect)(ref _windowRect)).width, 38f), GUIContent.none, _headerBoxStyle); GUI.Label(new Rect(18f, 7f, ((Rect)(ref _windowRect)).width - 330f, 28f), "▣ BlueSage QoL Menu v0.2.4", _headerStyle); if (GUI.Button(new Rect(((Rect)(ref _windowRect)).width - 308f, 5f, 108f, 28f), new GUIContent("Thunderstore", "Open the BlueSage QoL Tweaks Thunderstore page."), _buttonStyle)) { Application.OpenURL("https://thunderstore.io/c/on-together/p/Blues/BlueSage_QoL_Tweaks_Beta/"); } if (GUI.Button(new Rect(((Rect)(ref _windowRect)).width - 192f, 5f, 58f, 28f), new GUIContent("Ko-fi", "Support Blue's community modding work on Ko-fi."), _buttonStyle)) { Application.OpenURL("https://ko-fi.com/Q5Q1JRPW"); } if (GUI.Button(new Rect(((Rect)(ref _windowRect)).width - 126f, 5f, 76f, 28f), new GUIContent("Discord", "Open Blue's On-Together community Discord."), _buttonStyle)) { Application.OpenURL("https://discord.gg/JujMEwtN3q"); } if (GUI.Button(new Rect(((Rect)(ref _windowRect)).width - 42f, 5f, 30f, 28f), new GUIContent("X", "Close this window"), _activeButtonStyle)) { _visible = false; BlueSageWindowCoordinator.SetVisible(BlueSagePublicWindow.QolMenu, visible: false); BlueSageWindowHoverScope.UnregisterWindow(47058); } } private float GetContentScrollHeight() { //IL_0002: Unknown result type (might be due to invalid IL or missing references) return Mathf.Max(24f, BlueSageHelpFrame.Resolve(BlueSageHelpFrameKind.QolMenu, _windowRect).ContentHeight); } private float GetContentLayoutWidth() { return Mathf.Max(280f, ((Rect)(ref _windowRect)).width - 96f); } private void DrawTabs() { GUILayout.BeginHorizontal(Array.Empty()); GUILayout.FlexibleSpace(); GUILayout.Label("v0.2.4 • Updated 2026-07-30", _smallStyle, Array.Empty()); GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(Array.Empty()); DrawTabButton(0, "Main", "Fast actions, how-to examples, current feature status, and the quickest path to Style Helper."); DrawTabButton(1, "Toggles", "Turn major QoL features on/off with one normal On/Off button per row."); DrawTabButton(2, "Customize", "Edit movement, ping, sweep, and text-limit settings. Names, status, and Spoons live in Style Helper."); DrawTabButton(3, "Credits / Other", "Themes, credits, community notes, and one optional lobby joke."); if (Plugin.ShouldShowLobbySafetyTab(out var state, out var _)) { bool flag = state == LobbySafetyAccessState.Host; bool flag2 = state == LobbySafetyAccessState.Host || state == LobbySafetyAccessState.HelperReady; DrawTabButton(4, flag ? "Host Console" : "Safety", (state == LobbySafetyAccessState.Host) ? "Host-only Helper management plus verified identities, Clone Shield evidence, and careful moderation actions." : (flag2 ? "Assigned Helper access: verified identities, Clone Shield evidence, and host-validated actions." : "Helper assignment detected. Protected details stay locked until the host publishes a fresh compatible 0.2.0 Helper capability.")); } GUILayout.EndHorizontal(); } private void DrawTabButton(int index, string label, string tooltip) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Expected O, but got Unknown //IL_0044: 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) bool flag = _tab == index; if (GUILayout.Button(new GUIContent(label, tooltip), flag ? _activeButtonStyle : _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(34f) })) { _tab = index; _contentScroll = Vector2.zero; } } private void DrawMainTab() { //IL_00c0: Unknown result type (might be due to invalid IL or missing references) //IL_00ec: Expected O, but got Unknown //IL_012d: Unknown result type (might be due to invalid IL or missing references) //IL_0159: Expected O, but got Unknown //IL_01ab: Unknown result type (might be due to invalid IL or missing references) //IL_01e2: Expected O, but got Unknown GUILayout.BeginVertical(_boxStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandHeight(false) }); GUILayout.Label("Welcome to BlueSage QoL", _sectionHeaderStyle, Array.Empty()); GUILayout.Label("Comfort, clarity, personalization, and safer community tools—each one can be adjusted without memorizing commands.", _smallStyle, Array.Empty()); GUILayout.Space(8f); GUILayout.Label("Quick actions", _labelStyle, Array.Empty()); GUILayout.BeginHorizontal(Array.Empty()); float num = Mathf.Max(150f, (((Rect)(ref _windowRect)).width - 108f) / 3f); SweepMenuActionState sweepMenuActionState = SweepMenuActionPolicy.Resolve(Plugin.ManualSweepAllowed); bool enabled = GUI.enabled; GUI.enabled = enabled && sweepMenuActionState.Enabled; bool num2 = GUILayout.Button(new GUIContent(sweepMenuActionState.Label, sweepMenuActionState.Tooltip), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(num), GUILayout.Height(34f) }); GUI.enabled = enabled; if (num2) { _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.RunManualSweepFromQolMenu() : "Plugin is not ready."); MarkAction("Run Cleanup Now requested."); } if (GUILayout.Button(new GUIContent("Run QoL Status", "Shows the same easy feature summary as /qol status so players can quickly see what is on or off."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(num), GUILayout.Height(34f) })) { if ((Object)(object)Plugin.Instance != (Object)null) { _message = Plugin.Instance.RunQolStatusFromQolMenu(); } else { _message = "Plugin is not ready."; } MarkAction(_message); } bool isStyleHelperVisible = Plugin.IsStyleHelperVisible; if (GUILayout.Button(new GUIContent(isStyleHelperVisible ? "Close Style Helper" : "Open Style Helper", "Open or close Style Helper for name styling, status tags, templates, and Spoons."), isStyleHelperVisible ? _enabledButtonStyle : _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(num), GUILayout.Height(34f) })) { _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.ToggleStyleUiFromQolMenu() : "Plugin is not ready."); MarkAction("Style Helper toggled."); } GUILayout.EndHorizontal(); GUILayout.Space(10f); DrawMainStatusStrip(); DrawFocusPulse(); DrawLobbySafetyAccessBanner(); GUILayout.Space(12f); GUILayout.Label("Getting started", _labelStyle, Array.Empty()); GUILayout.Label(NavigationCalloutPolicy.BuildMenuNavigationHint(_theme.NavigationKeywordHex), _navigationHintStyle, Array.Empty()); GUILayout.Label("Use /styleui for name, status, and Spoons identity edits. Name Styler changes your visible name style; Status Styler changes only the activity tag and optional Spoons tag.", _smallStyle, Array.Empty()); GUILayout.Label("Chat actions: plain left-click keeps the vanilla ID card. Ctrl-left-click copies message text. Shift-left-click a safe http/https link once to copy and warn, then repeat to open.", _smallStyle, Array.Empty()); GUILayout.Label("Quick checks: /qol status or Run QoL Status shows enabled features. Type / in chat to browse commands with autocomplete.", _smallStyle, Array.Empty()); GUILayout.Label("When something feels noisy: open Toggles and turn off only that feature. Use Customize for ping colors, ping sound, sweep timing, and text limits.", _smallStyle, Array.Empty()); GUILayout.Space(12f); DrawCommunityThanks(); GUILayout.EndVertical(); } private void DrawMainStatusStrip() { float contentLayoutWidth = GetContentLayoutWidth(); bool num = ((Rect)(ref _windowRect)).width < 1040f; float width = (num ? ((contentLayoutWidth - 8f) / 2f) : ((contentLayoutWidth - 24f) / 4f)); GUILayout.BeginHorizontal(Array.Empty()); ConfigEntry enableChatReadability = Plugin.EnableChatReadability; DrawStatusCard("Comfort", (enableChatReadability != null && enableChatReadability.Value) ? "Ready" : "Adjusted", "Chat, movement, and notices", width); GUILayout.Space(8f); ConfigEntry enableStyleUi = Plugin.EnableStyleUi; DrawStatusCard("Styling", (enableStyleUi != null && enableStyleUi.Value) ? "Ready" : "Off", "Names, status, Spoons, templates", width); if (!num) { GUILayout.Space(8f); ConfigEntry enableReconnectGuard = Plugin.EnableReconnectGuard; DrawStatusCard("Reconnect Guard", (enableReconnectGuard != null && enableReconnectGuard.Value) ? "Ready" : "Off", "Saved-lobby recovery protection", width); GUILayout.Space(8f); ConfigEntry enablePlayerRenderSaver = Plugin.EnablePlayerRenderSaver; DrawStatusCard("Experimental", (enablePlayerRenderSaver != null && enablePlayerRenderSaver.Value) ? "On" : "Off", "Player Render Saver only", width, experimental: true); } GUILayout.EndHorizontal(); if (num) { GUILayout.Space(8f); GUILayout.BeginHorizontal(Array.Empty()); ConfigEntry enableReconnectGuard2 = Plugin.EnableReconnectGuard; DrawStatusCard("Reconnect Guard", (enableReconnectGuard2 != null && enableReconnectGuard2.Value) ? "Ready" : "Off", "Saved-lobby recovery protection", width); GUILayout.Space(8f); ConfigEntry enablePlayerRenderSaver2 = Plugin.EnablePlayerRenderSaver; DrawStatusCard("Experimental", (enablePlayerRenderSaver2 != null && enablePlayerRenderSaver2.Value) ? "On" : "Off", "Player Render Saver only", width, experimental: true); GUILayout.EndHorizontal(); } } private void DrawStatusCard(string title, string state, string detail, float width, bool experimental = false) { GUILayout.BeginVertical(experimental ? _experimentalBoxStyle : _sectionBoxStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(width), GUILayout.Height(86f) }); GUILayout.Label(title + " • " + state, _labelStyle, Array.Empty()); GUILayout.Label(detail, _smallStyle, Array.Empty()); GUILayout.EndVertical(); } private void DrawFocusPulse() { //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Expected O, but got Unknown //IL_0236: Unknown result type (might be due to invalid IL or missing references) //IL_024b: Expected O, but got Unknown //IL_00ed: Unknown result type (might be due to invalid IL or missing references) //IL_0102: Expected O, but got Unknown GUILayout.Space(8f); GUILayout.BeginVertical(_identitySectionBoxStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandWidth(true) }); GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label("Focus Pulse", _sectionHeaderStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandWidth(true) }); if (GUILayout.Button(new GUIContent("Refresh", "Refresh these read-only values and count unlocked achievements once."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(112f), GUILayout.Height(30f) })) { RefreshStatisticsPulse(includeAchievements: true); _message = ((_statisticsPulse != null) ? "Focus Pulse refreshed from the native statistics data." : "Focus Pulse is unavailable on this game build."); MarkAction(_message); } GUILayout.EndHorizontal(); if (_statisticsPulse == null) { GUILayout.Label("Native statistics unavailable. " + _statisticsPulseUnavailableReason, _smallStyle, Array.Empty()); GUILayout.Label(new GUIContent("Full Report - Open with ESC", "The full achievement gallery remains in the native Report. Focus Pulse never opens or changes it."), _smallStyle, Array.Empty()); GUILayout.EndVertical(); return; } float contentLayoutWidth = GetContentLayoutWidth(); float width = Mathf.Max(140f, (contentLayoutWidth - 24f) / 4f); GUILayout.BeginHorizontal(Array.Empty()); DrawFocusPulseMetric("Total focus", _statisticsPulse.TotalFocusLabel, width); GUILayout.Space(8f); DrawFocusPulseMetric("Today's sessions", _statisticsPulse.TodaySessions.ToString(), width); GUILayout.Space(8f); DrawFocusPulseMetric("Current streak", StatisticsPulsePresentationPolicy.BuildStreakValue(_statisticsPulse.CurrentStreak, _statisticsPulse.LongestStreak), width); GUILayout.Space(8f); DrawFocusPulseMetric("Task checks recorded", _statisticsPulse.TaskChecks.ToString(), width); GUILayout.EndHorizontal(); GUILayout.Space(8f); GUILayout.BeginHorizontal(Array.Empty()); DrawFocusPulseMetric("Achievements unlocked", _statisticsPulse.AchievementsLabel, width); GUILayout.Space(8f); GUILayout.BeginVertical(_sectionBoxStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.ExpandWidth(true), GUILayout.Height(64f) }); GUILayout.Label(new GUIContent("Full Report - Open with ESC", "The full achievement gallery remains in the native Report. Focus Pulse never opens or changes it."), _labelStyle, Array.Empty()); GUILayout.Label("Focus Pulse reads the game's saved totals without opening or changing the Report. " + _statisticsPulseNote, _smallStyle, Array.Empty()); GUILayout.EndVertical(); GUILayout.EndHorizontal(); GUILayout.EndVertical(); } private void DrawFocusPulseMetric(string title, string value, float width) { GUILayout.BeginVertical(_sectionBoxStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(width), GUILayout.Height(64f) }); GUILayout.Label(title, _smallStyle, Array.Empty()); GUILayout.Label(value, _labelStyle, Array.Empty()); GUILayout.EndVertical(); } private void RefreshStatisticsPulse(bool includeAchievements) { if (!_statisticsPulseReader.TryCapturePulse(out var snapshot, out var unavailableReason)) { _statisticsPulse = null; _statisticsPulseUnavailableReason = (string.IsNullOrWhiteSpace(unavailableReason) ? "This game build does not expose the expected read-only surface." : unavailableReason); _statisticsPulseNote = "The rest of the QoL menu remains available."; return; } _statisticsPulseUnavailableReason = string.Empty; _statisticsPulseNote = "Achievements are counted only when you press Refresh."; string unavailableReason2 = string.Empty; if (includeAchievements && _statisticsPulseReader.TryCountAchievements(out var unlocked, out var total, out unavailableReason2)) { snapshot = snapshot.WithAchievementProgress(unlocked, total); _statisticsPulseNote = "Achievement progress refreshed once from the native gallery."; } else if (includeAchievements) { _statisticsPulseNote = (string.IsNullOrWhiteSpace(unavailableReason2) ? "Achievement progress is unavailable on this game build." : unavailableReason2); } _statisticsPulse = snapshot; } private void DrawLobbySafetyAccessBanner() { //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Expected O, but got Unknown //IL_00c6: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) if (Plugin.ShouldShowLobbySafetyTab(out var state, out var _)) { bool flag = state == LobbySafetyAccessState.Host || state == LobbySafetyAccessState.HelperReady; object obj = state switch { LobbySafetyAccessState.Host => "HOST • Helper management and Lobby Safety ready", LobbySafetyAccessState.HelperReady => "HELPER • Lobby Safety ready", _ => "HELPER DETECTED • Waiting for compatible host capability", }; GUILayout.Space(8f); GUILayout.BeginHorizontal(_identitySectionBoxStyle, Array.Empty()); GUILayout.Label((string)obj, _smallStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandWidth(true) }); if (GUILayout.Button(new GUIContent("Open Helper / Safety", flag ? "Open authorized Helper and Lobby Safety tools." : "Open the access status. Protected player details and actions remain locked until the host publishes a fresh compatible v3 member-data capability."), flag ? _enabledButtonStyle : _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(210f), GUILayout.Height(30f) })) { _tab = 4; _contentScroll = Vector2.zero; _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.CheckModerationAccessFromQolMenu() : "Helper / Safety is not ready yet."); MarkAction(_message); } GUILayout.EndHorizontal(); } } private void DrawCommunityThanks() { //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Expected O, but got Unknown GUILayout.BeginVertical(_identitySectionBoxStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandWidth(true) }); GUILayout.Label("♥ COMMUNITY SUPPORTERS ♥", _sectionHeaderStyle, Array.Empty()); GUILayout.Label("15 community supporters shared 16 public thank-you messages. Their thank-you wall lives in Credits / Other.", _smallStyle, Array.Empty()); if (GUILayout.Button(new GUIContent("Open Community Supporters in Credits / Other", "Open the community thank-you wall. Supporter totals and public messages are shown; account and payment identifiers are never included."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(34f) })) { OpenCommunitySupporters(); } GUILayout.EndVertical(); } private void OpenCommunitySupporters() { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) _tab = 3; _contentScroll = Vector2.zero; _message = "Community Supporters opened in Credits / Other."; MarkAction(_message); } private void DrawSupporterCredits() { GUILayout.Label("15 community supporters • 16 public thank-you messages", _labelStyle, Array.Empty()); GUILayout.Label("Choose a supporter to read their public messages. Only each person's combined support is shown; account and payment identifiers are excluded.", _smallStyle, Array.Empty()); GUILayout.Space(8f); DrawSupporterDirectory(); GUILayout.Space(10f); DrawSelectedSupporterMessages(); } private void DrawSupporterDirectory() { //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Expected O, but got Unknown float contentLayoutWidth = GetContentLayoutWidth(); int thanksGridColumnCount = GetThanksGridColumnCount(contentLayoutWidth); float num = (float)(thanksGridColumnCount - 1) * 8f; float num2 = Mathf.Max(180f, (contentLayoutWidth - num) / (float)thanksGridColumnCount); for (int i = 0; i < SupporterRollups.Length; i += thanksGridColumnCount) { GUILayout.BeginHorizontal(Array.Empty()); int num3 = Math.Min(SupporterRollups.Length, i + thanksGridColumnCount); for (int j = i; j < num3; j++) { SupporterIdentityRollup supporterIdentityRollup = SupporterRollups[j]; bool flag = j == _selectedSupporterIndex; if (GUILayout.Button(new GUIContent(SupporterLedgerPolicy.SanitizeForDisplay(supporterIdentityRollup.Name) + " ♥ " + SupporterLedgerPolicy.FormatReceived(supporterIdentityRollup.TotalReceived), flag ? "Close this supporter's thank-you messages." : "Read this supporter's public thank-you messages."), flag ? _activeButtonStyle : _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(num2), GUILayout.Height(34f) })) { SelectSupporterInCredits(j); } if (j + 1 < num3) { GUILayout.Space(8f); } } GUILayout.FlexibleSpace(); GUILayout.EndHorizontal(); if (num3 < SupporterRollups.Length) { GUILayout.Space(6f); } } } private void SelectSupporterInCredits(int index) { if (index >= 0 && index < SupporterRollups.Length) { _selectedSupporterIndex = ((_selectedSupporterIndex == index) ? (-1) : index); _tab = 3; _message = ((_selectedSupporterIndex < 0) ? "Community supporter details closed." : ("Community supporter selected: " + SupporterRollups[index].Name + ".")); MarkAction(_message); } } private void DrawSelectedSupporterMessages() { if (_selectedSupporterIndex < 0 || _selectedSupporterIndex >= SupporterRollups.Length) { return; } SupporterIdentityRollup supporterIdentityRollup = SupporterRollups[_selectedSupporterIndex]; SupporterTransaction[] publicMessages = SupporterLedgerPolicy.GetPublicMessages(supporterIdentityRollup); GUILayout.BeginVertical(_identitySectionBoxStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandWidth(true) }); GUILayout.Label(SupporterLedgerPolicy.SanitizeForDisplay(supporterIdentityRollup.Name) + " ♥ " + SupporterLedgerPolicy.FormatReceived(supporterIdentityRollup.TotalReceived) + " total", _sectionHeaderStyle, Array.Empty()); if (publicMessages.Length == 0) { GUILayout.Label("Supported with love and chose not to leave a public message.", _smallStyle, Array.Empty()); } GUILayout.EndVertical(); for (int i = 0; i < publicMessages.Length; i++) { SupporterTransaction supporterTransaction = publicMessages[i]; GUILayout.Space(6f); GUILayout.BeginVertical(_sectionBoxStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandWidth(true) }); GUILayout.Label("♥ " + SupporterLedgerPolicy.FormatDateLabel(supporterTransaction.DateTimeUtc), _labelStyle, Array.Empty()); GUILayout.Label(SupporterLedgerPolicy.SanitizeForDisplay(supporterTransaction.Message), _smallStyle, Array.Empty()); GUILayout.EndVertical(); if (i + 1 < publicMessages.Length) { GUILayout.Space(6f); } } } private void DrawCommunityCrew() { DrawThanksCardGrid(CommunityCrewCards); } private void DrawThanksCardGrid(IReadOnlyList cards) { float contentLayoutWidth = GetContentLayoutWidth(); int thanksGridColumnCount = GetThanksGridColumnCount(contentLayoutWidth); float num = (float)(thanksGridColumnCount - 1) * 8f; float cardWidth = Mathf.Max(220f, (contentLayoutWidth - num) / (float)thanksGridColumnCount); for (int i = 0; i < cards.Count; i += thanksGridColumnCount) { GUILayout.BeginHorizontal(Array.Empty()); int num2 = Math.Min(cards.Count, i + thanksGridColumnCount); for (int j = i; j < num2; j++) { ThanksCard thanksCard = cards[j]; DrawThanksCard(thanksCard.Name, thanksCard.Received, thanksCard.Messages, cardWidth); if (j + 1 < num2) { GUILayout.Space(8f); } } GUILayout.FlexibleSpace(); GUILayout.EndHorizontal(); if (num2 < cards.Count) { GUILayout.Space(8f); } } } private static int GetThanksGridColumnCount(float availableWidth) { if (availableWidth >= 1400f) { return 4; } if (availableWidth >= 1020f) { return 3; } if (availableWidth >= 680f) { return 2; } return 1; } private void DrawThanksCard(string name, string received, IReadOnlyList messages, float cardWidth) { GUILayout.BeginVertical(_boxStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(cardWidth), GUILayout.MinHeight(118f) }); GUILayout.Label(name, _labelStyle, Array.Empty()); GUILayout.Label(received, _smallStyle, Array.Empty()); foreach (string message in messages) { if (!string.IsNullOrWhiteSpace(message)) { GUILayout.Label(message, _smallStyle, Array.Empty()); } } GUILayout.EndVertical(); } private void DrawTogglesTab() { GUILayout.BeginVertical(_boxStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandHeight(false) }); GUILayout.Label("Click a feature's On/Off button, then read the footer for what changed. Your saved choices remain yours.", _smallStyle, Array.Empty()); GUILayout.Space(8f); int num = ((((Rect)(ref _windowRect)).width >= 1320f) ? 3 : ((!(((Rect)(ref _windowRect)).width >= 880f)) ? 1 : 2)); float contentLayoutWidth = GetContentLayoutWidth(); float width = (contentLayoutWidth - 12f * (float)(num - 1)) / (float)num; if (num == 1) { DrawFeatureSection("Comfort & Movement", "Reduce friction and stay informed while you move.", DrawComfortMovementToggles, contentLayoutWidth, FeatureSectionTone.Comfort); GUILayout.Space(12f); DrawFeatureSection("Chat & Readability", "Improve chat clarity, context, and everyday usability.", DrawChatReadabilityToggles, contentLayoutWidth, FeatureSectionTone.Chat); GUILayout.Space(12f); DrawFeatureSection("Identity & Safety", "Protect identity and reduce reconnect friction.", DrawIdentitySafetyToggles, contentLayoutWidth, FeatureSectionTone.Identity); GUILayout.Space(12f); DrawFeatureSection("Personalization", "Customize how your BlueSage experience looks and feels.", DrawPersonalizationToggles, contentLayoutWidth, FeatureSectionTone.Personalization); GUILayout.Space(12f); DrawFeatureSection("Experimental (Opt-In)", "Unproven local-only tools stay separate and default off.", DrawExperimentalToggles, contentLayoutWidth, FeatureSectionTone.Experimental); } else { GUILayout.BeginHorizontal(Array.Empty()); DrawFeatureSection("Comfort & Movement", "Reduce friction and stay informed while you move.", DrawComfortMovementToggles, width, FeatureSectionTone.Comfort); GUILayout.Space(12f); DrawFeatureSection("Chat & Readability", "Improve chat clarity, context, and everyday usability.", DrawChatReadabilityToggles, width, FeatureSectionTone.Chat); if (num == 3) { GUILayout.Space(12f); DrawFeatureSection("Identity & Safety", "Protect identity and reduce reconnect friction.", DrawIdentitySafetyToggles, width, FeatureSectionTone.Identity); } GUILayout.EndHorizontal(); GUILayout.Space(12f); GUILayout.BeginHorizontal(Array.Empty()); if (num == 2) { DrawFeatureSection("Identity & Safety", "Protect identity and reduce reconnect friction.", DrawIdentitySafetyToggles, width, FeatureSectionTone.Identity); GUILayout.Space(12f); DrawFeatureSection("Personalization", "Customize how your BlueSage experience looks and feels.", DrawPersonalizationToggles, width, FeatureSectionTone.Personalization); } else { float width2 = (contentLayoutWidth - 12f) / 2f; DrawFeatureSection("Personalization", "Customize how your BlueSage experience looks and feels.", DrawPersonalizationToggles, width2, FeatureSectionTone.Personalization); GUILayout.Space(12f); DrawFeatureSection("Experimental (Opt-In)", "Unproven local-only tools stay separate and default off.", DrawExperimentalToggles, width2, FeatureSectionTone.Experimental); } GUILayout.EndHorizontal(); if (num == 2) { GUILayout.Space(12f); DrawFeatureSection("Experimental (Opt-In)", "Unproven local-only tools stay separate and default off.", DrawExperimentalToggles, contentLayoutWidth, FeatureSectionTone.Experimental); } } GUILayout.EndVertical(); } private void DrawComfortMovementToggles() { DrawToggle("Automatic Cleanup", Plugin.EnableAutoSweep, "On for fresh installs at a user-controlled 10-minute interval; an existing saved Off choice stays Off. BlueSage coordinates its unused-asset cleanup with the game's native sweep. Run Cleanup Now, F8, and /sweep now remain independent; the game's timer is unchanged.", delegate { Plugin.Instance?.RunSweepRestartFromMenu(); }); DrawToggle("Host Health", Plugin.EnableHostHealthMonitor, "Background lobby-health checks for long sessions. Most useful for hosts; quiet/local unless a health action has something to report.", delegate { Plugin.Instance?.RestartHostHealthFromMenu(); }); DrawToggle("Leave Notices", Plugin.EnableLeaveNotifications, "Shows you a local notice when someone leaves. Other players do not see it, and overlapping chat mods can make BlueSage back off."); DrawToggle("Welcome Message", Plugin.EnableWelcomeMessage, "Shows two local tips once per lobby: Home/Insert navigation, commands/audit/profile checks, your saved Push to Talk or Voice Activation setting, and the game's Pomodoro/focus ticket + XP reminder. Never posts globally."); DrawToggle("BetterMove", Plugin.EnableBetterMove, "Local movement helper: hold Shift to run and Ctrl to slow-walk. It only affects your character input."); DrawToggle("Reusable Consumables", Plugin.EnableUnlimitedConsumables, "Once you own a canteen item or chalk color, using it no longer lowers the count, so you do not keep spending tickets to replace it. Clothing, bait, rods, and bobbers stay vanilla. Turn Off to restore normal item use immediately."); DrawToggle("Focus Anywhere", Plugin.EnableFocusAnywhere, "Lets focus mode start from more places instead of only vanilla focus spots."); } private void DrawChatReadabilityToggles() { DrawToggle("Chat + notice timestamps", Plugin.EnableChatTimestamps, "Adds local timestamps to chat plus join/leave/system notices. Use this if you like knowing when messages happened.", delegate { Plugin.EnableNotificationTimestamps.Value = Plugin.EnableChatTimestamps.Value; Plugin.Instance?.SaveConfigFromQolMenu(); }); DrawTimestampFormatRow(); DrawToggle("Chat Readability", Plugin.EnableChatReadability, "Adds local outline/shadow to chat text so white or colored messages stay readable on bright maps. Does not change chat for others.", ChatReadabilityPatch.RefreshVisibleChat); DrawToggle("Persistent Chat Backdrop", Plugin.EnablePersistentChatBackdrop, "Keeps the chat panel/backdrop easier to read when the game exposes it. Local UI only; does not rewrite messages.", ChatReadabilityPatch.RefreshVisibleChat); DrawToggle("Ping Mentions", Plugin.EnablePingMentions, "Highlights messages that mention your name. Authorized lobby-wide pings are handled automatically for the host and assigned Helpers."); DrawToggle("Ping Sound", Plugin.EnablePingSound, "Plays your selected local ping sound when a message mentions you. Change the sound in Customize."); DrawToggle("Copy Chat Messages", Plugin.EnableChatCopy, "Plain left-click keeps the vanilla ID card. Ctrl-left-click copies only the message text. Names, timestamps, statuses, and sender labels are left out."); DrawToggle("Chat Links", Plugin.EnableChatUrlLinks, "Recolors safe web links. Shift-left-click once copies/warns, then repeat within 10 seconds to open."); DrawToggle("Local Audit Exports", Plugin.EnableLocalAuditExports, "Keeps a bounded local rendered-chat transcript and running BepInEx log mirror for troubleshooting or incident review. Nothing uploads automatically. Use /auditlog path or snapshot when you need the files.", SessionAuditController.ReconcileEnabledState); DrawLocalAuditActions(showIdentityNote: false); } private void DrawIdentitySafetyToggles() { DrawToggle("Reconnect Guard", Plugin.EnableReconnectGuard, "After the game reports a real disconnect, allows up to 10 seconds for clean-menu teardown, makes one saved-lobby join, then validates a stable playable session for up to 45 seconds."); DrawToggle("Reconnect Message", Plugin.EnableReconnectAnnouncement, "Sends the small BlueSage reconnect success message after a guarded reconnect works."); DrawToggle("Enhanced Player Panel", Plugin.EnableEnhancedPlayerPanel, "Adds local @mention and ID-card buttons. Hover the game's green Steam-profile button for a verified ID check. Native report, ignore, kick, and ban buttons stay reachable."); DrawToggle("Clone Shield", Plugin.EnableCloneShield, "Warns the lobby once when someone newly matches your avatar/outfit or exact styled name. The message is a clue, not proof, and asks everyone to check Steam profiles before acting."); DrawCommunityBanControls(); DrawToggle("MiniMap ID Click", Plugin.EnableMiniMapPlayerLabels, "Click minimap player dots to open ID cards when BlueSage can safely match the player. Floating labels stay removed for performance."); DrawToggle("MiniMap Friend Colors", Plugin.EnableMiniMapFriendColors, "Colors Steam friends green on your minimap. Uses cached Steam relationship data and stands down when the standalone MinimapFriends mod is installed."); } private void DrawPersonalizationToggles() { DrawSpoonsToggle(); DrawToggle("Style UI", Plugin.EnableStyleUi, "Enables /styleui for names, status tags, Spoons, templates, and styled text."); DrawToggle("Performance Overlay", Plugin.EnablePerformanceOverlay, "Shows the public draggable one-line FPS/PING/RAM bar. Hosts show PING -- honestly; connected clients show active-session round-trip latency when available. The bar's × is only a shortcut—you can always restore it here."); DrawToggle("Avatar Presets 4-9", Plugin.EnableExtendedAvatarStyles, "Adds six reversible avatar preset slots to the vanilla dropdown. Slots 1-3 remain game-owned and are never replaced.", AvatarStyleSlotsBridge.RefreshEnabledState); DrawToggle("Chalkboard Recovery", Plugin.EnableChalkboardPersistence, "Keeps local recovery copies. Use /chalk boards for 0 Stage, 1 Classroom A, and 2 Classroom B. View, save, list, delete, and refresh stay on this computer. Only the Host or an authenticated Helper can request a shared load or clear; BlueSage rechecks the exact board and saves recovery first. Existing Chalky takes ownership when installed."); } private void DrawCommunityBanControls() { //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Expected O, but got Unknown //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Expected O, but got Unknown DrawToggle("Community Safety List", Plugin.EnableCommunityBanListSync, "Off by default. Any QoL client may opt in to local additive validation. Refresh validates the bounded upstream source atomically, appends only missing pairs to this client's native ban store, and preserves every existing row. It cannot kick, ban, or grant authority. Use /communitybans status|refresh|attestation for complete bounded state without identities.", delegate { Plugin.Instance?.ReconcileCommunityBanMaintenanceFromQolMenu(); }); GUILayout.BeginHorizontal(Array.Empty()); if (GUILayout.Button(new GUIContent("Safety List Status", "Off by default. Any QoL client may opt in to local additive validation. Refresh validates the bounded upstream source atomically, appends only missing pairs to this client's native ban store, and preserves every existing row. It cannot kick, ban, or grant authority. Use /communitybans status|refresh|attestation for complete bounded state without identities."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(28f) })) { _message = CommunityBanListController.FriendlyStatusText(); MarkAction(_message); } if (GUILayout.Button(new GUIContent("Refresh Safety List", "Check the trusted community safety list now. Your existing local list stays unchanged unless the new copy validates completely."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(28f) })) { _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.RefreshCommunityBanListFromQolMenu() : "Community BanData refresh is not ready yet."); MarkAction(_message); } GUILayout.EndHorizontal(); GUILayout.Label(CommunityBanListController.FriendlyStatusText(), _smallStyle, Array.Empty()); } private void DrawExperimentalToggles() { DrawExperimentalToggle("EXPERIMENTAL - Player Render Saver", Plugin.EnablePlayerRenderSaver, "Opt-in local-only performance test for huge lobbies. Hides far/overflow remote player renderers on your PC only; chat and the player list stay visible. Use /playersaver status to confirm actual renderer suppression.", delegate { if (!Plugin.ShouldApplyPlayerRenderSaver) { PlayerRenderSaverController.RestoreAll(); } }); GUILayout.Space(4f); GUILayout.Label("Local-only performance test for very large lobbies. It never hides you from other players.", _smallStyle, Array.Empty()); } private void DrawTimestampFormatRow() { //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Expected O, but got Unknown //IL_00b8: Unknown result type (might be due to invalid IL or missing references) //IL_00ff: Expected O, but got Unknown GUILayout.Label("Timestamp format", _labelStyle, Array.Empty()); GUILayout.BeginHorizontal(Array.Empty()); if (GUILayout.Button(new GUIContent("12-hour AM/PM", "Show timestamps like 3:37 PM."), (Plugin.Use24HourTime != null && !Plugin.Use24HourTime.Value) ? _activeButtonStyle : _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.ExpandWidth(true), GUILayout.Height(30f) })) { Plugin.Use24HourTime.Value = false; Plugin.Instance?.SaveConfigFromQolMenu(); _message = "Timestamps: 12-hour format with AM/PM."; MarkSaved(_message); } GUILayout.Space(6f); if (GUILayout.Button(new GUIContent("24-hour", "Show timestamps like 15:37 without AM/PM."), (Plugin.Use24HourTime != null && Plugin.Use24HourTime.Value) ? _activeButtonStyle : _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.ExpandWidth(true), GUILayout.Height(30f) })) { Plugin.Use24HourTime.Value = true; Plugin.Instance?.SaveConfigFromQolMenu(); _message = "Timestamps: 24-hour format."; MarkSaved(_message); } GUILayout.EndHorizontal(); } private void DrawFeatureSection(string title, string description, Action drawContent, float width, FeatureSectionTone tone) { GUILayout.BeginVertical(GetFeatureSectionStyle(tone), (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(width), GUILayout.ExpandHeight(false) }); GUILayout.Label(title, _sectionHeaderStyle, Array.Empty()); GUILayout.Label(description, _smallStyle, Array.Empty()); GUILayout.Space(5f); drawContent?.Invoke(); GUILayout.EndVertical(); } private GUIStyle GetFeatureSectionStyle(FeatureSectionTone tone) { return (GUIStyle)(tone switch { FeatureSectionTone.Comfort => _comfortSectionBoxStyle, FeatureSectionTone.Chat => _chatSectionBoxStyle, FeatureSectionTone.Identity => _identitySectionBoxStyle, FeatureSectionTone.Personalization => _personalizationSectionBoxStyle, FeatureSectionTone.Experimental => _experimentalBoxStyle, _ => _sectionBoxStyle, }); } private Color GetFeatureSectionAccent(FeatureSectionTone tone) { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0035: 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_0043: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) return (Color)(tone switch { FeatureSectionTone.Comfort => _theme.ComfortAccent, FeatureSectionTone.Chat => _theme.ChatAccent, FeatureSectionTone.Identity => _theme.IdentityAccent, FeatureSectionTone.Personalization => _theme.PersonalizationAccent, FeatureSectionTone.Experimental => _theme.ExperimentalAccent, _ => _theme.LabelText, }); } private void DrawCustomizeTab() { //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_008c: Expected O, but got Unknown GUILayout.BeginVertical(_boxStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandHeight(false) }); GUILayout.Label("Customize safely", _sectionHeaderStyle, Array.Empty()); GUILayout.Label("Every editable row has Save and Undo. Undo discards an unsaved draft or restores the value from before your last Save.", _smallStyle, Array.Empty()); bool isStyleHelperVisible = Plugin.IsStyleHelperVisible; if (GUILayout.Button(new GUIContent(isStyleHelperVisible ? "Close Style Helper" : "Open Style Helper", "Open or close the helper for names, status tags, colors, templates, and Spoons."), isStyleHelperVisible ? _enabledButtonStyle : _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(34f) })) { _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.ToggleStyleUiFromQolMenu() : "Plugin is not ready."); MarkAction("Style Helper toggled."); } GUILayout.Space(8f); bool num = ((Rect)(ref _windowRect)).width >= 1280f; float contentLayoutWidth = GetContentLayoutWidth(); float num2 = (num ? ((contentLayoutWidth - 12f) / 2f) : contentLayoutWidth); if (num) { GUILayout.BeginHorizontal(Array.Empty()); GUILayout.BeginVertical((GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(num2) }); DrawSettingsSection("General", "Safe housekeeping timing.", DrawGeneralSettings, num2); GUILayout.Space(12f); DrawSelfMovementSection(num2); GUILayout.Space(12f); DrawSettingsSection("Mentions & Pings", "Choose how local alerts look and sound.", DrawPingSettings, num2); GUILayout.EndVertical(); GUILayout.Space(12f); DrawSettingsSection("Chat & Readability", "Scale text, space, outline, and retained history independently.", DrawChatSettings, num2); GUILayout.EndHorizontal(); GUILayout.Space(12f); DrawSettingsSection("Text Limits", "Defaults favor stability; power users may raise them.", DrawTextLimitSettings, contentLayoutWidth); } else { DrawSettingsSection("General", "Safe housekeeping timing.", DrawGeneralSettings, num2); GUILayout.Space(12f); DrawSelfMovementSection(num2); GUILayout.Space(12f); DrawSettingsSection("Mentions & Pings", "Choose how local alerts look and sound.", DrawPingSettings, num2); GUILayout.Space(12f); DrawSettingsSection("Chat & Readability", "Scale text, space, outline, and retained history independently.", DrawChatSettings, num2); GUILayout.Space(12f); DrawSettingsSection("Text Limits", "Defaults favor stability; power users may raise them.", DrawTextLimitSettings, num2); } GUILayout.EndVertical(); } private void DrawGeneralSettings() { DrawTextSetting("Automatic cleanup interval (minutes)", ref _sweepIntervalText, "Save 5-180", SaveSweepInterval, () => Plugin.SweepIntervalMinutes?.Value.ToString() ?? "10"); } private void DrawSelfMovementSection(float width) { DrawSettingsSection("Self movement", "Optional self-only movement and rescue-from-a-glitch controls.", DrawSelfMovementTools, width); } private void DrawPingSettings() { DrawTextSetting("Ping highlight color", ref _pingHighlightText, "Save", delegate { SaveHex(Plugin.PingHighlightColor, _pingHighlightText, "Ping highlight"); }, () => Plugin.PingHighlightColor?.Value ?? "9B59B6"); DrawTextSetting("Ping @mention color", ref _pingMentionText, "Save", delegate { SaveHex(Plugin.PingMentionColor, _pingMentionText, "Ping mention"); }, () => Plugin.PingMentionColor?.Value ?? "FFD700"); DrawTextSetting("Ping sound mode", ref _pingSoundModeText, "Save", SavePingSoundMode, () => Plugin.GetPingSoundMode()); } private void DrawChatSettings() { //IL_0183: Unknown result type (might be due to invalid IL or missing references) //IL_01a6: Expected O, but got Unknown GUILayout.Label("Chat font size grows message text only. Whole chat UI scale grows the panel/input/backdrop too.", _smallStyle, Array.Empty()); GUILayout.Label("Chat window height adds vertical log space. Native Global/Local tabs stay game-owned.", _smallStyle, Array.Empty()); GUILayout.Space(5f); DrawTextSetting("Chat outline intensity", ref _chatOutlineIntensityText, "Save 25-100", SaveOutlineIntensity, () => Plugin.LockedChatOutlineIntensityPercent.ToString()); DrawTextSetting("Whole chat UI scale", ref _chatUiScaleText, "Save 75-200", SaveChatUiScale, () => Plugin.LockedChatUiScalePercent.ToString()); DrawTextSetting("Chat font size", ref _chatFontSizeText, "Save 75-200", SaveChatFontSize, () => Plugin.LockedChatFontSizePercent.ToString()); DrawTextSetting("Chat window height", ref _chatWindowHeightText, "Save 100-200", SaveChatWindowHeight, () => Plugin.LockedChatWindowHeightPercent.ToString()); DrawTextSetting("Retained chat rows", ref _chatHistoryRowsText, "Save 25-250", SaveChatHistoryRows, () => Plugin.LockedChatHistoryRows.ToString()); if (GUILayout.Button(new GUIContent("Reset Chat UI to Vanilla", "Sets whole chat UI scale, message font size, and window height back to 100% and restores the saved vanilla transform where possible."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) })) { _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.ResetChatUiToVanillaFromMenu() : "Plugin is not ready."); HydrateFields(); MarkSaved("Chat UI reset to vanilla."); } DrawTextSetting("Normal outline color", ref _chatOutlineColorText, "Save", delegate { SaveHex(Plugin.ChatOutlineColor, _chatOutlineColorText, "Normal outline"); }, () => Plugin.LockedChatOutlineColorHex); DrawTextSetting("Black text outline color", ref _blackChatOutlineColorText, "Save", delegate { SaveHex(Plugin.BlackChatOutlineColor, _blackChatOutlineColorText, "Black text outline"); }, () => Plugin.LockedBlackChatOutlineColorHex); DrawToggle("BlackNamesOutline", Plugin.BlackNamesOutline, "Uses the black-text outline color for true-black chat/name rows. On by default for readable black styles; turn off to force the normal outline color.", ChatReadabilityPatch.RefreshVisibleChat); } private void DrawTextLimitSettings() { GUILayout.Label("Default is 3000. Values above 3000 may wrap, clip, or look odd in some game UI.", _smallStyle, Array.Empty()); DrawTextSetting("Chat text limit", ref _chatLimitText, "Save 1-4000", delegate { SaveCharacterLimit(Plugin.MaxChatCharacters, _chatLimitText, "Chat text", InputFieldLimitPolicy.ClampChatCharacters, delegate(int value) { _chatLimitText = value.ToString(); }); }, () => Plugin.LockedMaxChatCharacters.ToString()); DrawTextSetting("Name/ID limit", ref _profileLimitText, "Save 1-4000", delegate { SaveCharacterLimit(Plugin.MaxProfileCharacters, _profileLimitText, "Name and ID card text", InputFieldLimitPolicy.ClampProfileCharacters, delegate(int value) { _profileLimitText = value.ToString(); }); }, () => Plugin.LockedMaxIdCardCharacters.ToString()); DrawTextSetting("Room name limit", ref _sessionLimitText, "Save 1-4000", delegate { SaveCharacterLimit(Plugin.MaxSessionNameCharacters, _sessionLimitText, "Room name text", InputFieldLimitPolicy.ClampSessionNameCharacters, delegate(int value) { _sessionLimitText = value.ToString(); }); }, () => Plugin.LockedMaxSessionNameCharacters.ToString()); } private void DrawSettingsSection(string title, string description, Action drawContent, float width) { GUILayout.BeginVertical(_sectionBoxStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(width), GUILayout.ExpandHeight(false) }); GUILayout.Label(title, _sectionHeaderStyle, Array.Empty()); GUILayout.Label(description, _smallStyle, Array.Empty()); GUILayout.Space(5f); drawContent?.Invoke(); GUILayout.EndVertical(); } private void DrawCreditsOtherTab() { GUILayout.BeginVertical(_boxStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandHeight(false) }); GUILayout.Label("Tools, safety, and credits", _sectionHeaderStyle, Array.Empty()); GUILayout.Label("Less-used controls live here so the everyday tabs stay calm. Each section keeps its existing behavior and exact hover help.", _smallStyle, Array.Empty()); GUILayout.Space(8f); bool num = ((Rect)(ref _windowRect)).width >= 1280f; float contentLayoutWidth = GetContentLayoutWidth(); float num2 = (num ? ((contentLayoutWidth - 12f) / 2f) : contentLayoutWidth); DrawSettingsSection("♥ COMMUNITY SUPPORTERS ♥", "A simple thank-you wall for the people who helped BlueSage keep building, with private payment details excluded.", DrawSupporterCredits, contentLayoutWidth); GUILayout.Space(12f); DrawSettingsSection("Community crew", "Testing, crash evidence, and trusted co-conspirators who help BlueSage stay honest.", DrawCommunityCrew, contentLayoutWidth); GUILayout.Space(12f); if (num) { GUILayout.BeginHorizontal(Array.Empty()); GUILayout.BeginVertical((GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(num2) }); DrawSettingsSection("About BlueSage QoL", "Community-built comfort, clarity, and safer lobby tools.", DrawAboutAndCredits, num2); GUILayout.EndVertical(); GUILayout.Space(12f); GUILayout.BeginVertical((GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(num2) }); DrawSettingsSection("Menu appearance", "Choose one shared theme for the QoL Menu, Style Helper, and command popups.", DrawThemeSelector, num2); GUILayout.Space(12f); DrawFeatureSection("Experimental performance", "Player Render Saver stays opt-in while large-lobby renderer suppression is tested.", DrawPlayerRenderSaverTuning, num2, FeatureSectionTone.Experimental); GUILayout.EndVertical(); GUILayout.EndHorizontal(); } else { DrawSettingsSection("Menu appearance", "Choose one shared theme for the QoL Menu, Style Helper, and command popups.", DrawThemeSelector, num2); GUILayout.Space(12f); DrawFeatureSection("Experimental performance", "Player Render Saver stays opt-in while large-lobby renderer suppression is tested.", DrawPlayerRenderSaverTuning, num2, FeatureSectionTone.Experimental); GUILayout.Space(12f); DrawSettingsSection("About BlueSage QoL", "Community-built comfort, clarity, and safer lobby tools.", DrawAboutAndCredits, num2); } GUILayout.EndVertical(); } private void DrawAboutAndCredits() { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Expected O, but got Unknown GUILayout.Label("BlueSage QoL Tweaks brings common comfort, readability, personalization, reconnect, and community-safety tools into one guided menu.", _smallStyle, Array.Empty()); GUILayout.Label("Asset Sweep asks Unity to unload assets that are no longer used. It never deletes files or touches profiles. MiniMap ID Click stays lightweight; floating name labels remain removed after large-lobby performance reports.", _smallStyle, Array.Empty()); if (GUILayout.Button(new GUIContent("Run Host Health", "Optional host-focused lobby-health check. Most players only need Run QoL Status on Main."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) })) { _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.RunHostHealthFromQolMenu() : "Plugin is not ready."); MarkAction("Host Health check requested."); } GUILayout.Space(8f); GUILayout.Label("Optional lobby joke", _labelStyle, Array.Empty()); GUILayout.Label("Sends one playful lobby chat line the first time. It never changes moderation or game state.", _smallStyle, Array.Empty()); DrawDoNotPressButton(); GUILayout.Label("Share finds in Blue's Discord if you uncover another harmless oddity.", _smallStyle, Array.Empty()); } private void DrawPlayerRenderSaverTuning() { GUILayout.Label("Player Render Saver", _labelStyle, Array.Empty()); GUILayout.Label("Opt-in local tuning for very large lobbies. It never hides chat or player-list rows. Use /playersaver status to verify actual renderer suppression.", _smallStyle, Array.Empty()); DrawTextSetting("Render saver radius", ref _playerRenderSaverRadiusText, "Save 10-250", SavePlayerRenderSaverRadius, () => Plugin.LockedPlayerRenderSaverRadiusMeters.ToString()); DrawTextSetting("Render saver max visible", ref _playerRenderSaverMaxVisibleText, "Save 1-128", SavePlayerRenderSaverMaxVisible, () => Plugin.LockedPlayerRenderSaverMaxVisiblePlayers.ToString()); } private void DrawLobbySafetyTab() { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_00e4: Unknown result type (might be due to invalid IL or missing references) //IL_0107: Expected O, but got Unknown //IL_023a: Unknown result type (might be due to invalid IL or missing references) //IL_025d: Expected O, but got Unknown //IL_029c: Unknown result type (might be due to invalid IL or missing references) //IL_02bf: Expected O, but got Unknown bool isHost; string reason; LobbySafetyAccessState localAccessState = SteamIdModerationController.GetLocalAccessState(out isHost, out reason); if (localAccessState == LobbySafetyAccessState.Denied) { _tab = 0; _contentScroll = Vector2.zero; ClearLobbySafetyTransientState(); GUILayout.Label("Lobby Safety is available only to the current host or a Helper assigned by that host. No player or incident details were shown.", _smallStyle, Array.Empty()); return; } GUILayout.BeginVertical(_boxStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandHeight(false) }); GUILayout.Label(isHost ? "Host Console" : "Safety", _sectionHeaderStyle, Array.Empty()); if (isHost) { DrawHostReadinessCard(); GUILayout.Space(10f); } DrawIdentityResyncControls(localAccessState, isHost); GUILayout.Space(10f); if (localAccessState == LobbySafetyAccessState.HelperWaitingForHost) { GUILayout.Label("Helper assigned • secure host channel pending", _labelStyle, Array.Empty()); GUILayout.Label("Protected player details stay locked until the host publishes a compatible capability. This unlocks automatically; no command is required.", _smallStyle, Array.Empty()); GUILayout.Space(8f); if (GUILayout.Button(new GUIContent("Refresh Access Now", "Optional immediate check for the current v3 member-data capability. Automatic checks continue without this button. Technical state: " + reason), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(32f) })) { _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.CheckModerationAccessFromQolMenu() : "Helper / Safety is not ready yet."); MarkAction("Helper access rechecked. Waiting for a fresh compatible host capability if the tools remain locked."); } GUILayout.EndVertical(); return; } GUILayout.Label(isHost ? "Host access. Act only after checking the Steam profile for the exact current row; Clone Shield is an advisory clue, not proof." : "Helper access assigned by this host. Act only after checking the Steam profile for the exact current row; the host rechecks every request.", _smallStyle, Array.Empty()); GUILayout.Space(8f); string status; IReadOnlyList verifiedRoster = PlayerIdentityEvidenceController.GetVerifiedRoster(out status); DrawVerifiedSafetyRoster(verifiedRoster, status, isHost); GUILayout.Space(10f); GUILayout.BeginVertical(_identitySectionBoxStyle, Array.Empty()); GUILayout.Label("Defensive compatibility", _labelStyle, Array.Empty()); GUILayout.Label(Plugin.GetDefensiveCompatibilitySummary(), _smallStyle, Array.Empty()); GUILayout.Label("The optional community safety list checks trusted entries locally. Existing rows remain untouched; an invalid refresh is discarded and cannot kick, ban, or grant authority. " + CommunityBanListController.FriendlyStatusText(), _smallStyle, Array.Empty()); GUILayout.EndVertical(); GUILayout.Space(10f); GUILayout.BeginVertical(_identitySectionBoxStyle, Array.Empty()); GUILayout.Label("Local audit evidence", _labelStyle, Array.Empty()); DrawLocalAuditActions(showIdentityNote: true); GUILayout.EndVertical(); GUILayout.Space(10f); GUILayout.BeginHorizontal(Array.Empty()); if (GUILayout.Button(new GUIContent("Check My Access", "Shows whether you are the current host, a host-assigned Helper, or a regular player."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(32f) })) { _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.CheckModerationAccessFromQolMenu() : "Lobby Safety is not ready yet."); MarkAction(_message); } if (isHost && GUILayout.Button(new GUIContent("Clear All Helpers", "Remove every Helper from your persistent host-owned list. Your own host access remains."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(32f) })) { _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.ResetHelpersFromQolMenu() : "Lobby Safety is not ready yet."); MarkAction(_message); } GUILayout.EndHorizontal(); GUILayout.Space(10f); DrawCloneIncidentReview(); if (isHost) { GUILayout.Space(12f); DrawOfflineBanEntry(); GUILayout.Space(12f); DrawNativeBanRecords(); } GUILayout.EndVertical(); } private void DrawHostReadinessCard() { //IL_0150: Unknown result type (might be due to invalid IL or missing references) //IL_016e: Expected O, but got Unknown HostConsoleSnapshot hostConsoleSnapshot = HostConsoleController.Capture(); string obj = ((hostConsoleSnapshot.OverallState == HostConsoleOverallState.Ready) ? "24/7 Ready" : ((hostConsoleSnapshot.OverallState == HostConsoleOverallState.Degraded) ? "24/7 Degraded" : "24/7 Unavailable")); GUIStyle val = ((hostConsoleSnapshot.OverallState == HostConsoleOverallState.Ready) ? _enabledButtonStyle : ((hostConsoleSnapshot.OverallState == HostConsoleOverallState.Degraded) ? _experimentalButtonStyle : _disabledButtonStyle)); GUILayout.BeginVertical(_identitySectionBoxStyle, Array.Empty()); GUILayout.Label(obj, _sectionHeaderStyle, Array.Empty()); GUILayout.Label(hostConsoleSnapshot.Reason, _smallStyle, Array.Empty()); GUILayout.Label("Host " + hostConsoleSnapshot.HostState + " • Roster " + hostConsoleSnapshot.RosterState + " (" + HostConsoleController.LastVerifiedRosterCount + " verified) • Cleanup " + hostConsoleSnapshot.CleanupState + " • Reconnect " + hostConsoleSnapshot.ReconnectState, _smallStyle, Array.Empty()); GUILayout.Label(HostConsoleController.LastPlayerLimitSummary + " • Provider " + hostConsoleSnapshot.ProviderState + " • Relay " + hostConsoleSnapshot.RelayState, _smallStyle, Array.Empty()); if (GUILayout.Button(new GUIContent("Audit Native UI", "Read-only check for native mute, ignore, report, and host-remove surfaces. No moderation action fires."), val, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) })) { _message = HostConsoleController.AuditNativeModerationUi(); MarkAction(_message); } if (HostConsoleController.HasCapabilitySnapshot) { BlueSageHostCapabilitySnapshotV1 lastCapabilitySnapshot = HostConsoleController.LastCapabilitySnapshot; GUILayout.Space(8f); GUILayout.Label(lastCapabilitySnapshot.Title, _labelStyle, Array.Empty()); GUILayout.Label(lastCapabilitySnapshot.Summary, _smallStyle, Array.Empty()); GUILayout.BeginHorizontal(Array.Empty()); if ((lastCapabilitySnapshot.Actions & BlueSageHostCapabilityActionMaskV1.RefreshSnapshot) != BlueSageHostCapabilityActionMaskV1.None && GUILayout.Button("Refresh Private Status", _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(28f) })) { HostConsoleController.TryExecuteCapability(BlueSageHostCapabilityActionV1.RefreshSnapshot, out _message); MarkAction(_message); } if ((lastCapabilitySnapshot.Actions & BlueSageHostCapabilityActionMaskV1.ResetMovement) != BlueSageHostCapabilityActionMaskV1.None && GUILayout.Button("Reset Private Movement", _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(28f) })) { HostConsoleController.TryExecuteCapability(BlueSageHostCapabilityActionV1.ResetMovement, out _message); MarkAction(_message); } if ((lastCapabilitySnapshot.Actions & BlueSageHostCapabilityActionMaskV1.AuditNativeModeration) != BlueSageHostCapabilityActionMaskV1.None && GUILayout.Button("Private Audit", _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(28f) })) { HostConsoleController.TryExecuteCapability(BlueSageHostCapabilityActionV1.AuditNativeModeration, out _message); MarkAction(_message); } GUILayout.EndHorizontal(); } GUILayout.EndVertical(); } private void DrawIdentityResyncControls(LobbySafetyAccessState accessState, bool isHost) { //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Expected O, but got Unknown //IL_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Expected O, but got Unknown GUILayout.BeginVertical(_identitySectionBoxStyle, Array.Empty()); GUILayout.Label("Steam / PurrNet / lobby identity truth", _labelStyle, Array.Empty()); GUILayout.Label(IdentityResyncController.StatusText(), _smallStyle, Array.Empty()); GUILayout.BeginHorizontal(Array.Empty()); if (GUILayout.Button(new GUIContent("Rebuild Local View", "Rebuild only BlueSage-owned local presentation from already-available native state. This does not delete stale native/PurrNet objects or run moderation."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) })) { _message = IdentityResyncController.ExecuteMenuAction("refresh"); MarkAction(_message); } if (isHost || accessState == LobbySafetyAccessState.HelperReady) { string obj = (isHost ? "Recheck Exact Rows" : "Request Host Recheck"); string text = (isHost ? "Run one generation-bound exact-correlation recheck after the quiet/cooldown gates. This does not delete stale native/PurrNet objects." : "Ask a compatible 0.2.4+ host to run one authenticated exact-correlation recheck. The host revalidates Helper, lobby, owner, capability, generation, nonce, and exact row. This does not delete stale native/PurrNet objects."); if (GUILayout.Button(new GUIContent(obj, text), _experimentalButtonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) })) { _message = IdentityResyncController.ExecuteMenuAction(isHost ? "repair" : "request"); MarkAction(_message); } } GUILayout.EndHorizontal(); GUILayout.Label("Truth is read-only. Conflicting rows stay locked; a recheck can recapture exact correlation but cannot remove a game/network ghost. Private callers receive no ambiguity bypass.", _smallStyle, Array.Empty()); GUILayout.EndVertical(); } private void DrawCloneIncidentReview() { //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Expected O, but got Unknown //IL_00d9: Unknown result type (might be due to invalid IL or missing references) //IL_00fc: Expected O, but got Unknown //IL_021c: Unknown result type (might be due to invalid IL or missing references) //IL_023f: Expected O, but got Unknown //IL_0257: Unknown result type (might be due to invalid IL or missing references) //IL_027a: Expected O, but got Unknown //IL_02a1: Unknown result type (might be due to invalid IL or missing references) //IL_02c4: Expected O, but got Unknown //IL_02ef: Unknown result type (might be due to invalid IL or missing references) //IL_0312: Expected O, but got Unknown //IL_034c: Unknown result type (might be due to invalid IL or missing references) //IL_036f: Expected O, but got Unknown //IL_06a8: Unknown result type (might be due to invalid IL or missing references) //IL_06cb: Expected O, but got Unknown //IL_041b: Unknown result type (might be due to invalid IL or missing references) //IL_044f: Expected O, but got Unknown //IL_04a6: Unknown result type (might be due to invalid IL or missing references) //IL_04c9: Expected O, but got Unknown //IL_04f3: Unknown result type (might be due to invalid IL or missing references) //IL_0516: Expected O, but got Unknown IReadOnlyList openIncidents = CloneIncidentActionController.GetOpenIncidents(); GUILayout.BeginVertical(_identitySectionBoxStyle, Array.Empty()); GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label("Clone Shield incidents • " + openIncidents.Count + " open", _sectionHeaderStyle, Array.Empty()); GUILayout.FlexibleSpace(); if (GUILayout.Button(new GUIContent(_showClosedIncidentHistory ? "Hide History" : "Show History", "Show or hide resolved and locally dismissed incident history."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(104f), GUILayout.Height(28f) })) { _showClosedIncidentHistory = !_showClosedIncidentHistory; } GUILayout.EndHorizontal(); GUILayout.Label("C# is a local shortcut. Victim and suspect SteamID64 values remain exact; resolve clears the role highlights for QoL clients that receive the verified host/Helper notice.", _smallStyle, Array.Empty()); if (openIncidents.Count > 0 && GUILayout.Button(new GUIContent("Dismiss All Open Locally", "Clear every open Clone Shield advisory and highlight on this client only. This never bans, changes Helpers, or publishes lobby state. Use /sidincident dismiss-all for the same action."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) })) { CloneIncidentActionController.TryDismissAllOpenLocal(out _message); MarkAction(_message); GUILayout.EndVertical(); return; } if (openIncidents.Count == 0) { GUILayout.Label("No open incidents. Clone Shield highlights and moderation protections are clear on this client.", _smallStyle, Array.Empty()); } foreach (CloneIncident item in openIncidents.Take(4)) { GUILayout.BeginVertical(_boxStyle, Array.Empty()); GUILayout.Label(item.Id + " • " + item.MatchType + " • " + item.StableId, _labelStyle, Array.Empty()); GUILayout.Label("Protected victim: " + item.VictimLabel + " • …" + PlayerIdentityEvidenceController.Suffix(item.VictimSteamId), _smallStyle, Array.Empty()); GUILayout.Label("Suspect: " + item.SuspectLabel + " • …" + PlayerIdentityEvidenceController.Suffix(item.SuspectSteamId), _smallStyle, Array.Empty()); GUILayout.BeginHorizontal(Array.Empty()); if (GUILayout.Button(new GUIContent("Select Victim", "Select the exact protected-victim row in the verified live roster above. Ban controls remain blocked for this row."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(28f) })) { SelectSafetyPlayer(item.VictimSteamId); } if (GUILayout.Button(new GUIContent("Select Suspect", "Select the exact suspected-copier row in the verified live roster above for profile verification."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(28f) })) { SelectSafetyPlayer(item.SuspectSteamId); } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(Array.Empty()); if (GUILayout.Button(new GUIContent("Copy Victim ID", "Copy the protected victim's exact SteamID64."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(28f) })) { CloneIncidentActionController.TryCopySteamId(item.Id, suspect: false, out _message); MarkAction(_message); } if (GUILayout.Button(new GUIContent("Copy Suspect ID", "Copy the suspect's exact SteamID64."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(28f) })) { CloneIncidentActionController.TryCopySteamId(item.Id, suspect: true, out _message); MarkAction(_message); } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(Array.Empty()); if (GUILayout.Button(new GUIContent("Preview Suspect Ban", "Create a safe preview for the exact suspect. Confirmation remains separate."), _experimentalButtonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(28f) })) { _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.PreviewSidBanFromQolMenu(item.SuspectSteamId) : "SID preview is not ready yet."); MarkAction(_message); } ModerationConfirmationReadiness moderationConfirmationReadiness = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.GetModerationConfirmationReadiness("ban", item.SuspectSteamId) : new ModerationConfirmationReadiness(isReady: false, 0)); GUI.enabled = moderationConfirmationReadiness.IsReady; if (GUILayout.Button(new GUIContent(moderationConfirmationReadiness.IsReady ? $"Confirm Suspect Ban • {moderationConfirmationReadiness.SecondsRemaining}s" : "Preview Required", moderationConfirmationReadiness.IsReady ? "Confirm the matching exact-suspect preview before its visible countdown expires." : "Create a matching suspect-ban preview first; confirmation stays disabled until then."), moderationConfirmationReadiness.IsReady ? _enabledButtonStyle : _disabledButtonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(28f) })) { _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.ConfirmSidBanFromQolMenu(item.SuspectSteamId) : "SID confirmation is not ready yet."); MarkAction(_message); } GUI.enabled = true; GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(Array.Empty()); if (GUILayout.Button(new GUIContent("Resolve for Lobby", "Host or Helper: mark this incident handled and clear its victim/suspect highlights for compatible QoL clients."), _enabledButtonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(28f) })) { CloneIncidentActionController.TryResolveForLobby(item.Id, out _message); MarkAction(_message); } if (GUILayout.Button(new GUIContent("Dismiss Locally", "Remove this advisory incident and its highlights on this client only. No moderation state changes."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(28f) })) { CloneIncidentActionController.TryDismissLocal(item.Id, out _message); MarkAction(_message); } GUILayout.EndHorizontal(); GUILayout.EndVertical(); } if (openIncidents.Count > 4) { GUILayout.Label("Showing the newest 4 open incidents below the live roster. Use /sidlist for a compact summary, dismiss handled entries, or dismiss all locally to clear advisory clutter.", _smallStyle, Array.Empty()); } if (_showClosedIncidentHistory) { IReadOnlyList readOnlyList = (from item in CloneIncidentActionController.GetIncidentHistory() where item.State != CloneIncidentState.Open select item).Take(12).ToArray(); GUILayout.Space(8f); GUILayout.Label("Closed history • " + readOnlyList.Count + ((readOnlyList.Count == 12) ? "+" : string.Empty), _labelStyle, Array.Empty()); foreach (CloneIncident item2 in readOnlyList) { GUILayout.Label(item2.Id + " • " + item2.State.ToString() + " • victim …" + PlayerIdentityEvidenceController.Suffix(item2.VictimSteamId) + " • suspect …" + PlayerIdentityEvidenceController.Suffix(item2.SuspectSteamId), _smallStyle, Array.Empty()); } if (GUILayout.Button(new GUIContent("Clear Closed History", "Remove resolved and locally dismissed history. Open incidents are never removed by this button."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(28f) })) { int num = CloneIncidentActionController.ClearClosedHistory(); _message = "Cleared " + num + " closed Clone Shield " + ((num == 1) ? "incident" : "incidents") + "."; MarkAction(_message); } } GUILayout.EndVertical(); } private void SelectSafetyPlayer(string steamId) { _safetyRosterQuery = string.Empty; _selectedSafetySteamId = steamId ?? string.Empty; int num = PlayerIdentityEvidenceController.GetVerifiedRoster().ToList().FindIndex((PlayerIdentityEvidence item) => string.Equals(item.SteamId, _selectedSafetySteamId, StringComparison.Ordinal)); if (num >= 0) { _safetyRosterPage = num / 8; } } private void DrawVerifiedSafetyRoster(IReadOnlyList liveRoster, string rosterStatus, bool isHost) { //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00ec: Expected O, but got Unknown //IL_01eb: Unknown result type (might be due to invalid IL or missing references) //IL_021b: Expected O, but got Unknown //IL_0250: Unknown result type (might be due to invalid IL or missing references) //IL_0280: Expected O, but got Unknown //IL_04b8: Unknown result type (might be due to invalid IL or missing references) //IL_04e8: Expected O, but got Unknown //IL_0388: Unknown result type (might be due to invalid IL or missing references) //IL_03b7: Expected O, but got Unknown //IL_06c8: Unknown result type (might be due to invalid IL or missing references) //IL_06eb: Expected O, but got Unknown //IL_072e: Unknown result type (might be due to invalid IL or missing references) //IL_0751: Expected O, but got Unknown //IL_0823: Unknown result type (might be due to invalid IL or missing references) //IL_0846: Expected O, but got Unknown //IL_07b3: Unknown result type (might be due to invalid IL or missing references) //IL_07d6: Expected O, but got Unknown //IL_08b2: Unknown result type (might be due to invalid IL or missing references) //IL_08d5: Expected O, but got Unknown //IL_09ab: Unknown result type (might be due to invalid IL or missing references) //IL_09ce: Expected O, but got Unknown //IL_0a64: Unknown result type (might be due to invalid IL or missing references) //IL_0a98: Expected O, but got Unknown GUILayout.BeginVertical(_identitySectionBoxStyle, Array.Empty()); GUILayout.Label("Find a verified live player", _labelStyle, Array.Empty()); GUILayout.Label("Type part of the displayed name, raw TMP tags, Steam persona, 4+ SteamID digits, _host, _self, or a regex pattern. Matching never grants ban authority; it only filters rows whose live game and lobby identity agrees.", _smallStyle, Array.Empty()); GUILayout.Label(rosterStatus, _smallStyle, Array.Empty()); GUILayout.BeginHorizontal(Array.Empty()); string safetyRosterQuery = _safetyRosterQuery; _safetyRosterQuery = GUILayout.TextField(_safetyRosterQuery ?? string.Empty, 96, _textFieldStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) }); if (!string.Equals(safetyRosterQuery, _safetyRosterQuery, StringComparison.Ordinal)) { _safetyRosterPage = 0; _selectedSafetySteamId = string.Empty; } if (GUILayout.Button(new GUIContent("Clear", "Show every currently verified row again."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(72f), GUILayout.Height(30f) })) { _safetyRosterQuery = string.Empty; _safetyRosterPage = 0; _selectedSafetySteamId = string.Empty; } GUILayout.EndHorizontal(); string error; IReadOnlyList readOnlyList = PlayerIdentityEvidenceController.FilterVerifiedRoster(liveRoster, _safetyRosterQuery, out error); if (!string.IsNullOrWhiteSpace(error)) { GUILayout.Label(error, _smallStyle, Array.Empty()); } GUILayout.EndVertical(); GUILayout.Space(8f); int num = Math.Max(1, (readOnlyList.Count + 8 - 1) / 8); _safetyRosterPage = Mathf.Clamp(_safetyRosterPage, 0, num - 1); GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label($"Verified active roster • {readOnlyList.Count}/{liveRoster.Count} matching • page {_safetyRosterPage + 1}/{num}", _labelStyle, Array.Empty()); GUILayout.FlexibleSpace(); GUI.enabled = _safetyRosterPage > 0; if (GUILayout.Button(new GUIContent("◀ Previous", "Show the previous eight verified current-lobby rows."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(104f), GUILayout.Height(28f) })) { _safetyRosterPage--; _selectedSafetySteamId = string.Empty; } GUI.enabled = _safetyRosterPage + 1 < num; if (GUILayout.Button(new GUIContent("Next ▶", "Show the next eight verified current-lobby rows."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(104f), GUILayout.Height(28f) })) { _safetyRosterPage++; _selectedSafetySteamId = string.Empty; } GUI.enabled = true; GUILayout.EndHorizontal(); GUILayout.Label("Select a row below; BlueSage keeps the exact SteamID64 attached to that selection. No retyping is needed.", _smallStyle, Array.Empty()); if (readOnlyList.Count == 0) { GUILayout.Label(string.IsNullOrWhiteSpace(_safetyRosterQuery) ? "The verified player roster is not ready yet. Close and reopen the player panel, then return here." : "No current verified player matches this filter. Clear it or add more exact detail.", _smallStyle, Array.Empty()); return; } int num2 = _safetyRosterPage * 8; int num3 = Math.Min(readOnlyList.Count, num2 + 8); for (int i = num2; i < num3; i++) { PlayerIdentityEvidence playerIdentityEvidence = readOnlyList[i]; bool flag = string.Equals(_selectedSafetySteamId, playerIdentityEvidence.SteamId, StringComparison.Ordinal); string text = $"#{playerIdentityEvidence.RosterIndex + 1:00} {playerIdentityEvidence.DisplayName} • …{PlayerIdentityEvidenceController.Suffix(playerIdentityEvidence.SteamId)} • {playerIdentityEvidence.Role}"; string text2 = (flag ? "Selected. Click again to clear this player selection." : "Select this exact verified player-to-SteamID row."); if (GUILayout.Button(new GUIContent(text, text2), flag ? _activeButtonStyle : _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) })) { _selectedSafetySteamId = (flag ? string.Empty : playerIdentityEvidence.SteamId); MarkAction(flag ? "Cleared the selected player." : ("Selected " + playerIdentityEvidence.DisplayName + " (Steam …" + PlayerIdentityEvidenceController.Suffix(playerIdentityEvidence.SteamId) + ").")); } } PlayerIdentityEvidence playerIdentityEvidence2 = readOnlyList.FirstOrDefault((PlayerIdentityEvidence item) => string.Equals(item.SteamId, _selectedSafetySteamId, StringComparison.Ordinal)); if (playerIdentityEvidence2 == null) { _selectedSafetySteamId = string.Empty; GUILayout.Space(8f); GUILayout.Label("No player is selected. Click the exact verified row you intend to inspect; BlueSage never silently retargets actions after filtering or roster changes.", _smallStyle, Array.Empty()); return; } GUILayout.Space(10f); GUILayout.BeginVertical(_identitySectionBoxStyle, Array.Empty()); GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label("Selected player", _sectionHeaderStyle, Array.Empty()); GUILayout.FlexibleSpace(); if (GUILayout.Button(new GUIContent("Clear Selection", "Deselect this player and disarm every selection-scoped moderation control."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(130f), GUILayout.Height(28f) })) { _selectedSafetySteamId = string.Empty; MarkAction("Cleared the selected player."); GUILayout.EndHorizontal(); GUILayout.EndVertical(); return; } GUILayout.EndHorizontal(); GUILayout.Label(playerIdentityEvidence2.DisplayName + " • " + playerIdentityEvidence2.Role, _labelStyle, Array.Empty()); GUILayout.Label("Steam persona: " + (string.IsNullOrWhiteSpace(playerIdentityEvidence2.SteamPersona) ? "not cached" : playerIdentityEvidence2.SteamPersona), _smallStyle, Array.Empty()); GUILayout.Label("SteamID64: " + playerIdentityEvidence2.SteamId, _smallStyle, Array.Empty()); GUILayout.Label("Verified live row #" + (playerIdentityEvidence2.RosterIndex + 1) + " • PlayerID " + playerIdentityEvidence2.NetworkPlayerId, _smallStyle, Array.Empty()); GUILayout.Label("Raw rendered name / TMP tags (copyable audit source):", _smallStyle, Array.Empty()); GUILayout.TextArea(playerIdentityEvidence2.DisplayNameRaw ?? string.Empty, _textFieldStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.MinHeight(42f), GUILayout.MaxHeight(70f) }); bool isLocal = playerIdentityEvidence2.IsLocal; bool flag2 = playerIdentityEvidence2.Role.Split(new char[1] { '•' }).Any((string role) => role.Trim().StartsWith("HOST", StringComparison.Ordinal)); bool flag3 = playerIdentityEvidence2.Role.Split(new char[1] { '•' }).Any((string role) => role.Trim().StartsWith("HELPER", StringComparison.Ordinal)); bool flag4 = playerIdentityEvidence2.Role.Contains("VICTIM"); GUILayout.BeginHorizontal(Array.Empty()); if (GUILayout.Button(new GUIContent("Copy SteamID64", "Copy this exact verified 17-digit Steam ID to the clipboard."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(32f) })) { _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.CopySteamIdFromQolMenu(playerIdentityEvidence2.SteamId) : "Copy is not ready yet."); MarkAction(_message); } if (GUILayout.Button(new GUIContent("Open Steam Profile", "Open this exact Steam account in the Steam overlay for identity verification."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(32f) })) { _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.OpenSteamProfileFromQolMenu(playerIdentityEvidence2.SteamId) : "Steam profile lookup is not ready yet."); MarkAction(_message); } if (Plugin.CanManageHelpers()) { GUI.enabled = !flag2; if (GUILayout.Button(new GUIContent(flag3 ? "Remove Helper" : "Add Helper", "Manage your persistent host-owned Helper list. Exact SteamIDs keep access stable across lobbies you recreate."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(32f) })) { _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.UpdateHelperFromQolMenu(playerIdentityEvidence2.SteamId, !flag3) : "Helper controls are not ready yet."); MarkAction(_message); } } GUILayout.EndHorizontal(); if (GUILayout.Button(new GUIContent("Copy Identity Evidence", "Copy one local audit record containing the displayed name, raw TMP source, cached Steam persona, exact SteamID64, verified row, PlayerID, and current role. Nothing is sent to chat."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(32f) })) { _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.CopyIdentityEvidenceFromQolMenu(playerIdentityEvidence2.SteamId) : "Identity evidence copy is not ready yet."); MarkAction(_message); } if ((playerIdentityEvidence2.Role.Contains("VICTIM") || playerIdentityEvidence2.Role.Contains("SUSPECT")) && GUILayout.Button(new GUIContent("Clear This Player's Incident Flags Locally", "Dismiss every open Clone Shield incident involving this exact verified row on this client. Lifecycle evidence remains in the local audit; no ban, Helper, or lobby state changes."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(32f) })) { CloneIncidentActionController.TryDismissOpenForVerifiedPlayer(playerIdentityEvidence2.SteamId, out _message); MarkAction(_message); } bool flag5 = isLocal || flag2 || flag3 || flag4; ModerationConfirmationReadiness moderationConfirmationReadiness = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.GetModerationConfirmationReadiness("ban", playerIdentityEvidence2.SteamId) : new ModerationConfirmationReadiness(isReady: false, 0)); HostModerationSurfaceDecision hostModerationSurfaceDecision = HostModerationSurfacePolicy.Evaluate(isHost, exactTargetReady: true, isLocal, moderationConfirmationReadiness.IsReady); string obj = (flag4 ? "Protected Victim" : (isLocal ? "Protected Self" : (flag2 ? "Protected Host" : (flag3 ? "Protected Helper" : "Preview SID Ban")))); string text3 = (flag5 ? "This verified role is protected from BlueSage ban previews." : "Create a safe preview only. The exact target is revalidated and still requires a separate confirmation."); GUI.enabled = !flag5 && (!isHost || hostModerationSurfaceDecision.CanArmHostRemove); GUILayout.BeginHorizontal(Array.Empty()); if (GUILayout.Button(new GUIContent(obj, text3), _experimentalButtonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(32f) })) { _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.PreviewSidBanFromQolMenu(playerIdentityEvidence2.SteamId) : "SID preview is not ready yet."); MarkAction(_message); } GUI.enabled = !flag5 && moderationConfirmationReadiness.IsReady && (!isHost || hostModerationSurfaceDecision.CanExecuteHostRemove); if (GUILayout.Button(new GUIContent(moderationConfirmationReadiness.IsReady ? $"Confirm Exact SID Ban • {moderationConfirmationReadiness.SecondsRemaining}s" : "Preview Required", moderationConfirmationReadiness.IsReady ? "Confirm the matching exact-player preview before its visible countdown expires." : "Create a matching SID-ban preview first; confirmation stays disabled until then."), moderationConfirmationReadiness.IsReady ? _enabledButtonStyle : _disabledButtonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(32f) })) { _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.ConfirmSidBanFromQolMenu(playerIdentityEvidence2.SteamId) : "SID confirmation is not ready yet."); MarkAction(_message); } GUI.enabled = true; GUILayout.EndHorizontal(); GUILayout.Label("Your own row is marked SELF and automatically protected; a host never adds itself as a Helper. A ban preview never acts immediately. Recheck the profile, then use the explicit confirmation shown locally. Protected victims, host, Helpers, self, and ambiguous rows are blocked.", _smallStyle, Array.Empty()); GUILayout.EndVertical(); } private void DrawNativeBanRecords() { //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_00d1: Expected O, but got Unknown //IL_0106: Unknown result type (might be due to invalid IL or missing references) //IL_0136: Expected O, but got Unknown //IL_038d: Unknown result type (might be due to invalid IL or missing references) //IL_03bd: Expected O, but got Unknown //IL_0250: Unknown result type (might be due to invalid IL or missing references) //IL_03f9: Unknown result type (might be due to invalid IL or missing references) //IL_041c: Expected O, but got Unknown //IL_045f: Unknown result type (might be due to invalid IL or missing references) //IL_0482: Expected O, but got Unknown //IL_027f: Expected O, but got Unknown //IL_0530: Unknown result type (might be due to invalid IL or missing references) //IL_0564: Expected O, but got Unknown IReadOnlyList records; string error; bool flag = SteamIdModerationController.TryGetNativeBanRecords(out records, out error); int num = Math.Max(1, (records.Count + 8 - 1) / 8); _nativeBanPage = Mathf.Clamp(_nativeBanPage, 0, num - 1); GUILayout.BeginVertical(_identitySectionBoxStyle, Array.Empty()); GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label($"Native banned players • {records.Count} saved • page {_nativeBanPage + 1}/{num}", _sectionHeaderStyle, Array.Empty()); GUILayout.FlexibleSpace(); GUI.enabled = _nativeBanPage > 0; if (GUILayout.Button(new GUIContent("◀ Previous", "Show the previous eight host-native ban records."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(104f), GUILayout.Height(28f) })) { _nativeBanPage--; _selectedBannedSteamId = string.Empty; } GUI.enabled = _nativeBanPage + 1 < num; if (GUILayout.Button(new GUIContent("Next ▶", "Show the next eight host-native ban records."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(104f), GUILayout.Height(28f) })) { _nativeBanPage++; _selectedBannedSteamId = string.Empty; } GUI.enabled = true; GUILayout.EndHorizontal(); GUILayout.Label("Host-only view of the game's paired saved nickname and SteamID64 records. Select a row to copy its exact ID or create a separate unban preview. Click the selected row again, or use Clear Selection, to disarm it.", _smallStyle, Array.Empty()); if (!flag) { _selectedBannedSteamId = string.Empty; GUILayout.Label("Native banned-player list is NOT AVAILABLE: " + error + ". No empty-list claim or unban action is shown.", _smallStyle, Array.Empty()); GUILayout.EndVertical(); return; } if (records.Count == 0) { _selectedBannedSteamId = string.Empty; GUILayout.Label("The game's native banned-player list is empty.", _smallStyle, Array.Empty()); GUILayout.EndVertical(); return; } int num2 = _nativeBanPage * 8; int num3 = Math.Min(records.Count, num2 + 8); for (int i = num2; i < num3; i++) { NativeBanRecord nativeBanRecord = records[i]; bool flag2 = string.Equals(_selectedBannedSteamId, nativeBanRecord.SteamId, StringComparison.Ordinal); string text = $"#{i + 1:00} {nativeBanRecord.DisplayName} • …{PlayerIdentityEvidenceController.Suffix(nativeBanRecord.SteamId)}"; string text2 = (flag2 ? "Selected. Click again to clear this native-ban selection." : "Select this exact paired native ban record."); if (GUILayout.Button(new GUIContent(text, text2), flag2 ? _activeButtonStyle : _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) })) { _selectedBannedSteamId = (flag2 ? string.Empty : nativeBanRecord.SteamId); MarkAction(flag2 ? "Cleared the selected native ban." : ("Selected native ban for " + nativeBanRecord.DisplayName + " (Steam …" + PlayerIdentityEvidenceController.Suffix(nativeBanRecord.SteamId) + ").")); } } NativeBanRecord nativeBanRecord2 = records.FirstOrDefault((NativeBanRecord record) => string.Equals(record.SteamId, _selectedBannedSteamId, StringComparison.Ordinal)); if (nativeBanRecord2 == null) { _selectedBannedSteamId = string.Empty; GUILayout.Space(8f); GUILayout.Label("No native ban is selected. Click the exact saved row you intend to inspect; BlueSage never silently selects or retargets an unban action.", _smallStyle, Array.Empty()); GUILayout.EndVertical(); return; } GUILayout.Space(8f); GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label("Selected native ban: " + nativeBanRecord2.DisplayName + " • SteamID64 " + nativeBanRecord2.SteamId, _labelStyle, Array.Empty()); GUILayout.FlexibleSpace(); if (GUILayout.Button(new GUIContent("Clear Selection", "Deselect this native ban and disarm every selection-scoped unban control."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(130f), GUILayout.Height(28f) })) { _selectedBannedSteamId = string.Empty; MarkAction("Cleared the selected native ban."); GUILayout.EndHorizontal(); GUILayout.EndVertical(); return; } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(Array.Empty()); if (GUILayout.Button(new GUIContent("Copy Banned SteamID64", "Copy this exact saved native-ban Steam ID."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(32f) })) { _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.CopySavedBanSteamIdFromQolMenu(nativeBanRecord2.SteamId) : "Copy is not ready yet."); MarkAction(_message); } if (GUILayout.Button(new GUIContent("Preview SID Unban", "Create a safe unban preview only. A separate exact confirmation is still required."), _experimentalButtonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(32f) })) { _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.PreviewSidUnbanFromQolMenu(nativeBanRecord2.SteamId) : "SID unban preview is not ready yet."); MarkAction(_message); } ModerationConfirmationReadiness moderationConfirmationReadiness = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.GetModerationConfirmationReadiness("unban", nativeBanRecord2.SteamId) : new ModerationConfirmationReadiness(isReady: false, 0)); GUI.enabled = moderationConfirmationReadiness.IsReady; if (GUILayout.Button(new GUIContent(moderationConfirmationReadiness.IsReady ? $"Confirm SID Unban • {moderationConfirmationReadiness.SecondsRemaining}s" : "Preview Required", moderationConfirmationReadiness.IsReady ? "Confirm the matching saved-ban preview before its visible countdown expires." : "Create a matching SID-unban preview first; confirmation stays disabled until then."), moderationConfirmationReadiness.IsReady ? _enabledButtonStyle : _disabledButtonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(32f) })) { _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.ConfirmSidUnbanFromQolMenu(nativeBanRecord2.SteamId) : "SID unban confirmation is not ready yet."); MarkAction(_message); } GUI.enabled = true; GUILayout.EndHorizontal(); GUILayout.Label("Preview never removes a ban. Recheck the saved nickname and SteamID64, then use the exact confirmation shown locally within 30 seconds.", _smallStyle, Array.Empty()); GUILayout.EndVertical(); } private void ClearLobbySafetyTransientState() { _selectedSafetySteamId = string.Empty; _selectedBannedSteamId = string.Empty; _safetyRosterPage = 0; _nativeBanPage = 0; _offlineBanSteamIdText = string.Empty; _showClosedIncidentHistory = false; } private void DrawOfflineBanEntry() { //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Expected O, but got Unknown //IL_00f0: Unknown result type (might be due to invalid IL or missing references) //IL_0113: Expected O, but got Unknown //IL_0155: Unknown result type (might be due to invalid IL or missing references) //IL_0178: Expected O, but got Unknown //IL_0223: Unknown result type (might be due to invalid IL or missing references) //IL_0257: Expected O, but got Unknown //IL_029f: Unknown result type (might be due to invalid IL or missing references) //IL_02cf: Expected O, but got Unknown GUILayout.BeginVertical(_identitySectionBoxStyle, Array.Empty()); GUILayout.Label("Add exact off-roster SteamID64", _sectionHeaderStyle, Array.Empty()); GUILayout.Label("Host only. Use this when another trusted host gives you an exact SteamID64 for advance protection. BlueSage saves the game's native ban entry without pretending the absent player was kicked.", _smallStyle, Array.Empty()); GUILayout.Label("Always open and verify the Steam profile first. Reports and names never auto-fill or authorize this field.", _smallStyle, Array.Empty()); GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label(new GUIContent("SteamID64", "Paste the exact 17-digit SteamID64. Display names, profile names, suffixes, and URLs are intentionally refused here."), _labelStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(100f) }); string text = GUILayout.TextField(_offlineBanSteamIdText ?? string.Empty, 17, _textFieldStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) }); if (!string.Equals(text, _offlineBanSteamIdText, StringComparison.Ordinal)) { _offlineBanSteamIdText = text; MarkDraft("Offline SteamID64 changed. Verify its profile, then preview it."); } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(Array.Empty()); if (GUILayout.Button(new GUIContent("Open Steam Profile", "Open this exact SteamID64 in the Steam overlay before previewing a ban."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(32f) })) { _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.OpenOfflineSteamProfileFromQolMenu(_offlineBanSteamIdText) : "Steam profile lookup is not ready yet."); MarkAction(_message); } if (GUILayout.Button(new GUIContent("Preview Offline SID Ban", "Create a 30-second host-only preview. This does not save or kick anything."), _experimentalButtonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(32f) })) { _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.PreviewSidBanFromQolMenu(_offlineBanSteamIdText) : "SID preview is not ready yet."); MarkAction(_message); } ModerationConfirmationReadiness moderationConfirmationReadiness = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.GetModerationConfirmationReadiness("ban", _offlineBanSteamIdText) : new ModerationConfirmationReadiness(isReady: false, 0)); GUI.enabled = moderationConfirmationReadiness.IsReady; if (GUILayout.Button(new GUIContent(moderationConfirmationReadiness.IsReady ? $"Confirm Preview • {moderationConfirmationReadiness.SecondsRemaining}s" : "Preview Required", moderationConfirmationReadiness.IsReady ? "Confirm this exact offline-SID preview before its visible countdown expires." : "Create a matching offline-SID preview first; confirmation stays disabled until then."), moderationConfirmationReadiness.IsReady ? _enabledButtonStyle : _disabledButtonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(32f) })) { _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.ConfirmSidBanFromQolMenu(_offlineBanSteamIdText) : "SID confirmation is not ready yet."); MarkAction(_message); } GUI.enabled = true; if (GUILayout.Button(new GUIContent("Clear Field", "Clear the local draft. This does not change saved bans."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(112f), GUILayout.Height(32f) })) { _offlineBanSteamIdText = string.Empty; MarkSaved("Cleared the offline SteamID64 draft. Saved native bans were unchanged."); } GUILayout.EndHorizontal(); GUILayout.Label("Every preview expires after 30 seconds or a lobby change. Saved entries appear below and can be reversed with Preview SID Unban plus confirmation.", _smallStyle, Array.Empty()); GUILayout.EndVertical(); } private void DrawLocalAuditActions(bool showIdentityNote) { //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Expected O, but got Unknown //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: Expected O, but got Unknown GUILayout.Label(SessionAuditController.StatusText(), _smallStyle, Array.Empty()); if (showIdentityNote) { GUILayout.Label("Chat records preserve the displayed raw TMP name/message beside plain text. Exact SteamID64 and Steam persona fields are populated only for this verified host/Helper role. Files rotate locally and are never uploaded automatically.", _smallStyle, Array.Empty()); } GUILayout.BeginHorizontal(Array.Empty()); if (GUILayout.Button(new GUIContent("Snapshot Now", "Create a fresh timestamped copy of the true live BepInEx LogOutput.log, flush the running audit, and copy the new snapshot folder path."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) })) { _message = SessionAuditController.SnapshotNow(); MarkAction(_message); } if (GUILayout.Button(new GUIContent("Copy Audit Folder", "Copy the local session folder path so you can review or attach the files manually."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) })) { _message = SessionAuditController.CopyAuditFolder(); MarkAction(_message); } GUILayout.EndHorizontal(); } private void DrawSelfMovementTools() { //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Expected O, but got Unknown //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Expected O, but got Unknown //IL_00f5: Unknown result type (might be due to invalid IL or missing references) //IL_0127: Expected O, but got Unknown //IL_01b9: Unknown result type (might be due to invalid IL or missing references) //IL_01dc: Expected O, but got Unknown GUILayout.Label("These controls only move your own character. Save Pos stores one local spot; teleport returns you there. Noclip/fly resets off whenever BlueSage loads or unloads.", _smallStyle, Array.Empty()); GUILayout.BeginHorizontal(Array.Empty()); if (GUILayout.Button(new GUIContent("Save Pos", "Save your current local player position. It persists locally until you replace it."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) })) { _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.SaveSelfPositionFromQolMenu() : "Self movement is not ready yet."); MarkMovementResult(_message); } if (GUILayout.Button(new GUIContent("Teleport Saved", "Teleport only your own character to the position saved by Save Pos."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) })) { _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.TeleportSelfToSavedFromQolMenu() : "Self movement is not ready yet."); MarkMovementResult(_message); } if (GUILayout.Button(new GUIContent(Plugin.ShouldApplyNoclipFly ? "Noclip / Fly: On" : "Noclip / Fly: Off", "Self-only fly: WASD, Space/E up, Ctrl/Q down, Shift boost, Alt precision. Always starts off."), Plugin.ShouldApplyNoclipFly ? _enabledButtonStyle : _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) })) { _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.ToggleNoclipFlyFromQolMenu() : "Self movement is not ready yet."); MarkMovementResult(_message); } GUILayout.EndHorizontal(); DrawSelfMovementSetting("Speed x", ref _selfSpeedText, "speed", "0.25-5; 1 is vanilla. This also scales fly speed."); DrawSelfMovementSetting("Jump x", ref _selfJumpText, "jump", "0.25-5; 1 is vanilla."); DrawSelfMovementSetting("Gravity x", ref _selfGravityText, "gravity", "0.1-5; 1 is vanilla."); if (GUILayout.Button(new GUIContent("Reset Self Movement", "Turn fly off and return speed, jump, and gravity to 1x vanilla. Your saved position is kept."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) })) { _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.ResetSelfMovementFromQolMenu() : "Self movement is not ready yet."); HydrateSelfMovementFields(); MarkMovementResult(_message); } } private void DrawSelfMovementSetting(string label, ref string value, string kind, string rangeHelp) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Expected O, but got Unknown //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00cc: Expected O, but got Unknown //IL_011c: Unknown result type (might be due to invalid IL or missing references) //IL_014c: Expected O, but got Unknown GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label(new GUIContent(label, "Self-only multiplier. " + rangeHelp), _labelStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(110f) }); string text = GUILayout.TextField(value ?? string.Empty, 12, _textFieldStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(28f) }); if (!string.Equals(text, value, StringComparison.Ordinal)) { value = text; MarkDraft(label + " changed. Save it when ready."); } if (GUILayout.Button(new GUIContent("Save", "Save " + label + ". " + rangeHelp), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(100f), GUILayout.Height(28f) })) { _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.SaveSelfMovementMultiplierFromQolMenu(kind, value) : "Self movement is not ready yet."); HydrateSelfMovementFields(); MarkMovementResult(_message); } if (GUILayout.Button(new GUIContent("Reset", "Return " + label + " to 1x vanilla."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(100f), GUILayout.Height(28f) })) { _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.SaveSelfMovementMultiplierFromQolMenu(kind, "reset") : "Self movement is not ready yet."); HydrateSelfMovementFields(); MarkMovementResult(_message); } GUILayout.EndHorizontal(); } private void DrawThemeSelector() { //IL_00fb: Unknown result type (might be due to invalid IL or missing references) //IL_011e: Expected O, but got Unknown //IL_0146: Unknown result type (might be due to invalid IL or missing references) //IL_0169: Expected O, but got Unknown //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Expected O, but got Unknown GUILayout.Label("Menu theme", _labelStyle, Array.Empty()); GUILayout.Label("Applies to the QoL Menu, Style Helper, and command/typeahead popups. Pick what is easiest on your eyes.", _smallStyle, Array.Empty()); string[] names = BlueSageUiTheme.Names; for (int i = 0; i < names.Length; i += 2) { GUILayout.BeginHorizontal(Array.Empty()); for (int j = 0; j < 2 && i + j < names.Length; j++) { string text = names[i + j]; bool flag = string.Equals(BlueSageUiTheme.Normalize(Plugin.UiThemePreset?.Value), BlueSageUiTheme.Normalize(text), StringComparison.OrdinalIgnoreCase); if (GUILayout.Button(new GUIContent(flag ? (text + " ✓") : text, "Switch all BlueSage helper UI to " + text + "."), flag ? _activeButtonStyle : _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) })) { SetTheme(text); } } GUILayout.EndHorizontal(); } if (GUILayout.Button(new GUIContent("Cycle Theme", "Rotate through every available BlueSage menu theme."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) })) { SetTheme(BlueSageUiTheme.Next(Plugin.UiThemePreset?.Value)); } if (GUILayout.Button(new GUIContent("Reset to BlueSage Harbor", "Return BlueSage helper UI to the balanced cream, coral, sage, and midnight default theme."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) })) { SetTheme("BlueSage Harbor"); } } private void SetTheme(string themeName) { if (Plugin.UiThemePreset == null) { _message = "Theme config is not ready yet."; return; } Plugin.UiThemePreset.Value = BlueSageUiTheme.Normalize(themeName); Plugin.Instance?.SaveConfigFromQolMenu(); _message = "Theme set to " + BlueSageUiTheme.Current.DisplayName + ". Other BlueSage UI updates on the next draw."; MarkSaved("Theme saved."); } private void DrawDoNotPressButton() { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Expected O, but got Unknown if (GUILayout.Button(new GUIContent("DO NOT PRESS", "Extremely dangerous. Definitely not a harmless one-time easter egg. Probably."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(168f), GUILayout.Height(28f) })) { bool num = !EasterEggTracker.HasDiscovered("menu_do_not_press"); string text = EasterEggTracker.Discover("menu_do_not_press", "BlueSage lobby joke discovered"); if (num) { bool flag = (Object)(object)Plugin.Instance != (Object)null && Plugin.Instance.TrySendDoNotPressLobbyMessage(); _message = (flag ? (text + ". One playful line was sent to lobby chat.") : (text + ". Chat was unavailable, so nothing left this client.")); MarkAction("Optional lobby joke requested."); } else { _message = "Already discovered. Repeat presses stay local and send nothing."; MarkAction("Optional lobby joke checked again."); } } } private void DrawFooter() { //IL_0002: Unknown result type (might be due to invalid IL or missing references) HelpFrameLayout layout = BlueSageHelpFrame.Resolve(BlueSageHelpFrameKind.QolMenu, _windowRect); BlueSageHelpFrame.BeginFooter(_footerBoxStyle, layout); string stateLine = "State: " + _stateLabel + " — " + _stateDetail; BlueSageHelpFrame.DrawStateAndHelp(_footerTextStyle, layout, stateLine, BuildScopedFooterTooltip()); BlueSageHelpFrame.EndFooter(); } private string BuildScopedFooterTooltip() { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) Vector2 val = ((Event.current != null) ? Event.current.mousePosition : Vector2.zero); if (BlueSageWindowHoverScope.IsWindowHovered(47058)) { Rect val2 = new Rect(0f, 0f, ((Rect)(ref _windowRect)).width, ((Rect)(ref _windowRect)).height); if (((Rect)(ref val2)).Contains(val) && !string.IsNullOrWhiteSpace(GUI.tooltip)) { return GUI.tooltip; } } return "Tip: hover a visible QoL Menu control for exact help."; } private void DrawToggle(string label, ConfigEntry entry, Action afterChange = null) { DrawToggle(label, entry, string.Empty, afterChange); } private void DrawToggle(string label, ConfigEntry entry, string tooltip, Action afterChange = null) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Expected O, but got Unknown //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Expected O, but got Unknown if (entry != null) { GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label(new GUIContent(label, tooltip), _labelStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(190f) }); if (GUILayout.Button(new GUIContent(entry.Value ? "On" : "Off", tooltip), entry.Value ? _enabledButtonStyle : _disabledButtonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(88f), GUILayout.Height(30f) })) { entry.Value = !entry.Value; Plugin.Instance?.SaveConfigFromQolMenu(); _message = label + ": " + (entry.Value ? "on." : "off."); afterChange?.Invoke(); MarkSaved(label + " saved " + (entry.Value ? "On." : "Off.")); } GUILayout.EndHorizontal(); } } private void DrawSpoonsToggle() { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Expected O, but got Unknown //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Expected O, but got Unknown if (Plugin.EnableSpoons == null) { return; } GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label(new GUIContent("Spoons", "Shows or hides your compact [1/5sp] display-name tag. Edit the number in Style Helper > Status."), _labelStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(190f) }); if (GUILayout.Button(new GUIContent(Plugin.EnableSpoons.Value ? "On" : "Off", "Shows or hides your compact [1/5sp] display-name tag. Edit the number in Style Helper > Status."), Plugin.EnableSpoons.Value ? _enabledButtonStyle : _disabledButtonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(88f), GUILayout.Height(30f) })) { bool flag = !Plugin.EnableSpoons.Value; bool applied = false; _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.TrySetSpoonVisibilityFromMenu(flag, out applied) : "Spoons could not change because the player is not ready yet."); if (applied) { MarkSaved("Spoons saved " + (flag ? "On." : "Off.")); } else { MarkDraft("Spoons stayed unchanged; fix the reported problem and try again."); } } GUILayout.EndHorizontal(); } private void DrawExperimentalToggle(string label, ConfigEntry entry, string tooltip, Action afterChange = null) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Expected O, but got Unknown //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Expected O, but got Unknown if (entry != null) { GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label(new GUIContent(label, tooltip), _labelStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(190f) }); if (GUILayout.Button(new GUIContent(entry.Value ? "On" : "Off", tooltip), entry.Value ? _experimentalButtonStyle : _disabledButtonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(88f), GUILayout.Height(30f) })) { entry.Value = !entry.Value; Plugin.Instance?.SaveConfigFromQolMenu(); _message = label + ": " + (entry.Value ? "on." : "off."); afterChange?.Invoke(); MarkSaved(label + " saved " + (entry.Value ? "On." : "Off.")); } GUILayout.EndHorizontal(); } } private void DrawTextSetting(string label, ref string value, string buttonLabel, Action saveAction, Func savedValueProvider) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Expected O, but got Unknown //IL_009a: Unknown result type (might be due to invalid IL or missing references) //IL_00ca: Expected O, but got Unknown //IL_0136: Unknown result type (might be due to invalid IL or missing references) //IL_0166: Expected O, but got Unknown string text = BuildTextSettingTooltip(label); GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label(new GUIContent(label, text), _labelStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(170f) }); string text2 = value ?? string.Empty; string text3 = GUILayout.TextField(text2, 120, _textFieldStyle, (GUILayoutOption[])(object)new GUILayoutOption[3] { GUILayout.MinWidth(130f), GUILayout.ExpandWidth(true), GUILayout.Height(28f) }); if (!string.Equals(text3, text2, StringComparison.Ordinal)) { MarkDraft(label + " changed. Press Save or Undo."); } value = text3; if (GUILayout.Button(new GUIContent(buttonLabel, text), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(112f), GUILayout.Height(28f) })) { string text4 = ((savedValueProvider != null) ? savedValueProvider() : string.Empty); saveAction(); string b = ((savedValueProvider != null) ? savedValueProvider() : string.Empty); if (!string.Equals(text4, b, StringComparison.Ordinal)) { _settingUndoValues[label] = text4; } } string text5 = (_settingUndoValues.ContainsKey(label) ? "Restore the value from before the last successful Save." : "Discard this unsaved edit and reload the saved value."); if (GUILayout.Button(new GUIContent("Undo", text5), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(80f), GUILayout.Height(28f) })) { if (_settingUndoValues.TryGetValue(label, out var value2)) { value = value2; saveAction(); _settingUndoValues.Remove(label); _message = label + " restored to its previous saved value."; MarkSaved(label + " change undone."); } else { value = ((savedValueProvider != null) ? savedValueProvider() : string.Empty); _message = label + " draft discarded; saved value restored."; MarkSaved(label + " draft discarded."); } } GUILayout.EndHorizontal(); } private static string BuildTextSettingTooltip(string label) { switch (label) { case "Ping sound mode": return "Use click, change, error, task, or ticket. Save plays a preview."; case "Ping highlight color": return "Color behind the whole pinged message. Use hex like FFD700."; case "Ping @mention color": return "Color for the @name or @lobby token. Default is ping-yellow."; case "Chat outline intensity": return "Percent from 25 to 100. Default 75 is a stronger readable outline; 100 is crispest."; case "Whole chat UI scale": return "Percent from 75 to 200. Default 100 keeps vanilla size; larger values scale the whole chat panel, input, backdrop, visible text, and outlines."; case "Chat font size": return "Percent from 75 to 200. Default 100 keeps vanilla text size; larger values enlarge only message text without scaling the chat shell."; case "Chat window height": return "Percent from 100 to 200. Default 100 keeps vanilla height; larger values add vertical chat space and deeper visible scrollback without scaling text."; case "Normal outline color": return "Hex outline color for normal chat text. Default is black: 000000."; case "Black text outline color": return "Hex outline color for true-black styled chat/name text. Default is light grey: C0C0C0."; case "Render saver radius": return "Meters around you that stay fully rendered when Player Render Saver is on. Default 45; safe range 10-250."; case "Render saver max visible": return "Closest remote players kept visible even outside the radius. Default 32; safe range 1-128."; case "Spoons 0-5": return "Set your optional spoon tag from 0 to 5. Use /spoons off or Hide Spoon Tag to remove it from your name."; case "Chat text limit": case "Room name limit": case "Name/ID limit": return "Default/recommended is 3000. Power users can raise up to 4000, but long text may wrap or clip."; case "Retained chat rows": return "25-250 rows. Default 150 balances useful scrollback with busy-lobby UI cost; BlueSage never lowers a higher vanilla or other-mod limit."; default: return "Edit this safe local config value, then Save."; } } private void HydrateFields() { _sweepIntervalText = Plugin.SweepIntervalMinutes?.Value.ToString() ?? "10"; _pingHighlightText = Plugin.PingHighlightColor?.Value ?? "9B59B6"; _pingMentionText = Plugin.PingMentionColor?.Value ?? "FFD700"; _pingSoundModeText = Plugin.GetPingSoundMode(); _chatOutlineIntensityText = Plugin.LockedChatOutlineIntensityPercent.ToString(); _chatUiScaleText = Plugin.LockedChatUiScalePercent.ToString(); _chatFontSizeText = Plugin.LockedChatFontSizePercent.ToString(); _chatWindowHeightText = Plugin.LockedChatWindowHeightPercent.ToString(); _chatHistoryRowsText = Plugin.LockedChatHistoryRows.ToString(); _chatOutlineColorText = Plugin.LockedChatOutlineColorHex; _blackChatOutlineColorText = Plugin.LockedBlackChatOutlineColorHex; _playerRenderSaverRadiusText = Plugin.LockedPlayerRenderSaverRadiusMeters.ToString(); _playerRenderSaverMaxVisibleText = Plugin.LockedPlayerRenderSaverMaxVisiblePlayers.ToString(); _chatLimitText = Plugin.LockedMaxChatCharacters.ToString(); _profileLimitText = Plugin.LockedMaxIdCardCharacters.ToString(); _sessionLimitText = Plugin.LockedMaxSessionNameCharacters.ToString(); HydrateSelfMovementFields(); MarkSaved("Loaded saved Customize values."); } private void HydrateSelfMovementFields() { _selfSpeedText = Plugin.LockedSelfSpeedMultiplier.ToString("0.##", CultureInfo.InvariantCulture); _selfJumpText = Plugin.LockedSelfJumpMultiplier.ToString("0.##", CultureInfo.InvariantCulture); _selfGravityText = Plugin.LockedSelfGravityMultiplier.ToString("0.##", CultureInfo.InvariantCulture); } private string BuildStatusLine() { string text = (Plugin.CanManageHelpers() ? $", Helpers={Plugin.LockedLobbyHelperCount}" : string.Empty); object[] array = new object[48]; array[0] = OnOff(Plugin.EnableAutoSweep); array[1] = OnOff(Plugin.EnableHostHealthMonitor); array[2] = OnOff(Plugin.EnableLeaveNotifications); array[3] = OnOff(Plugin.EnableWelcomeMessage); array[4] = OnOff(Plugin.EnableChatTimestamps); ConfigEntry use24HourTime = Plugin.Use24HourTime; array[5] = ((use24HourTime != null && !use24HourTime.Value) ? "12hr" : "24hr"); array[6] = OnOff(Plugin.EnableChatReadability); array[7] = OnOff(Plugin.EnablePersistentChatBackdrop); array[8] = Plugin.LockedChatOutlineIntensityPercent; array[9] = Plugin.LockedChatUiScalePercent; array[10] = Plugin.LockedChatFontSizePercent; array[11] = Plugin.LockedChatWindowHeightPercent; array[12] = Plugin.LockedChatHistoryRows; array[13] = Plugin.LockedChatOutlineColorHex; array[14] = Plugin.LockedBlackChatOutlineColorHex; array[15] = OnOff(Plugin.BlackNamesOutline); array[16] = OnOff(Plugin.EnableBetterMove); array[17] = OnOff(Plugin.EnableFocusAnywhere); array[18] = OnOff(Plugin.EnableEnhancedPlayerPanel); array[19] = OnOff(Plugin.EnableReconnectGuard); array[20] = OnOff(Plugin.EnableReconnectAnnouncement); array[21] = OnOff(Plugin.EnablePingMentions); array[22] = text; array[23] = OnOff(Plugin.EnablePingSound); array[24] = OnOff(Plugin.EnableNoclipFly); array[25] = Plugin.LockedSelfSpeedMultiplier; array[26] = Plugin.LockedSelfJumpMultiplier; array[27] = Plugin.LockedSelfGravityMultiplier; array[28] = OnOff(Plugin.EnableCloneShield); array[29] = OnOff(Plugin.EnablePlayerRenderSaver); array[30] = Plugin.LockedPlayerRenderSaverRadiusMeters; array[31] = Plugin.LockedPlayerRenderSaverMaxVisiblePlayers; array[32] = PlayerRenderSaverController.LastCandidateRemotePlayers; array[33] = PlayerRenderSaverController.LastHiddenRemotePlayers; array[34] = PlayerRenderSaverController.LastSuppressedRenderers; array[35] = PlayerRenderSaverController.LastRuntimeState; array[36] = OnOff(Plugin.EnableChatCopy); array[37] = OnOff(Plugin.EnableChatUrlLinks); array[38] = OnOff(Plugin.EnableLocalAuditExports); array[39] = OnOff(Plugin.EnableSpoons); array[40] = OnOff(Plugin.EnableStyleUi); array[41] = OnOff(Plugin.EnableExtendedAvatarStyles); array[42] = OnOff(Plugin.EnableChalkboardPersistence); array[43] = OnOff(Plugin.EnableMiniMapPlayerLabels); array[44] = OnOff(Plugin.EnableMiniMapFriendColors); array[45] = Plugin.LockedMaxChatCharacters; array[46] = Plugin.LockedMaxIdCardCharacters; array[47] = Plugin.LockedMaxSessionNameCharacters; return string.Format("AutomaticCleanup={0}, HostHealth={1}, LeaveNotices={2}, Welcome={3}, Timestamps={4}/{5}, ChatReadability={6}, ChatBackdrop={7}, ChatOutline={8}%, ChatUIScale={9}%, ChatFont={10}%, ChatWindowHeight={11}%, ChatRows={12}, OutlineColors=#{13}/#{14}, BlackNamesOutline={15}, BetterMove={16}, FocusAnywhere={17}, PlayerPanel={18}, ReconnectGuard={19}, ReconnectMessage={20}, Ping={21}{22}, PingSound={23}, SelfFly={24}, SelfMove={25:0.##}x/{26:0.##}x/{27:0.##}x, CloneShield={28}, RenderSaver={29}({30}m/max{31}, candidates={32}, hidden={33}, renderers={34}, state={35}), CopyChat={36}, ChatLinks={37}, LocalAudit={38}, Spoons={39}, StyleUI={40}, AvatarSlots4-9={41}, Chalkboards={42}, MiniMapIDClick={43}, MiniMapFriends={44}, TextLimits={45}/{46}/{47}.", array); } private static string OnOff(ConfigEntry entry) { if (entry == null || !entry.Value) { return "off"; } return "on"; } private void MarkDraft(string detail) { _stateLabel = "Draft"; _stateDetail = detail ?? "Changes are local until you save or undo."; } private void MarkSaved(string detail) { _stateLabel = "Saved"; _stateDetail = detail ?? "Ready."; } private void MarkAction(string detail) { _stateLabel = "Done"; _stateDetail = detail ?? "Done."; } private void MarkMovementResult(string detail) { string text = detail ?? string.Empty; if (string.IsNullOrWhiteSpace(text) || text.IndexOf("not ready", StringComparison.OrdinalIgnoreCase) >= 0 || text.IndexOf("no saved position", StringComparison.OrdinalIgnoreCase) >= 0 || text.IndexOf("unknown multiplier", StringComparison.OrdinalIgnoreCase) >= 0 || text.IndexOf("use a number", StringComparison.OrdinalIgnoreCase) >= 0) { MarkDraft((text.Length > 0) ? text : "Self movement action did not run."); } else { MarkSaved(text); } } private void SaveSweepInterval() { if (!int.TryParse(_sweepIntervalText, out var result)) { _message = "Sweep interval must be a number from 5 to 180."; MarkDraft("Fix sweep interval to a number from 5 to 180."); return; } int value = SweepSettingsPolicy.ClampIntervalMinutes(result); Plugin.SweepIntervalMinutes.Value = value; Plugin.Instance?.SaveConfigFromQolMenu(); Plugin.Instance?.ReconcileSweepScheduleFromMenu(); _sweepIntervalText = value.ToString(); _message = "Sweep interval saved: " + value + " minutes."; MarkSaved("Sweep interval saved."); } private void SaveHex(ConfigEntry entry, string value, string label) { if (entry != null) { if (!RichTextStyleBuilder.TryNormalizeHex(value, out var hex)) { _message = label + ": use a hex color like FF66CC or #83E."; MarkDraft("Fix " + label + " to a valid hex color."); return; } entry.Value = hex; Plugin.Instance?.SaveConfigFromQolMenu(); _message = label + " saved: #" + hex + "."; MarkSaved(label + " saved."); } } private void SaveText(ConfigEntry entry, string value, string label) { if (entry != null) { entry.Value = (value ?? string.Empty).Trim(); Plugin.Instance?.SaveConfigFromQolMenu(); _message = label + " saved."; MarkSaved(label + " saved."); } } private void SaveCharacterLimit(ConfigEntry entry, string value, string label, Func clamp, Action hydrate) { if (entry != null) { if (!int.TryParse(value, out var result)) { _message = label + ": enter a number from 1 to 4000. Default/recommended is 3000."; MarkDraft("Fix " + label + " to a number from 1 to 4000."); return; } int num = (entry.Value = clamp(result)); Plugin.Instance?.SaveConfigFromQolMenu(); hydrate(num); _message = ((num > 3000) ? (label + " saved at " + num + ". Heads up: past 3000 may wrap, clip, or look visually messy.") : (label + " saved at " + num + ".")); MarkSaved(label + " saved."); } } private void SaveOutlineIntensity() { if (Plugin.ChatOutlineIntensity != null) { if (!int.TryParse((_chatOutlineIntensityText ?? string.Empty).Trim().TrimEnd(new char[1] { '%' }), out var result)) { _message = "Chat outline intensity: enter a number from 25 to 100."; MarkDraft("Fix chat outline intensity to a number from 25 to 100."); return; } int value = ChatReadabilityStylePolicy.ClampOutlineIntensityPercent(result); Plugin.ChatOutlineIntensity.Value = value; Plugin.Instance?.SaveConfigFromQolMenu(); ChatReadabilityPatch.RefreshVisibleChat(); _chatOutlineIntensityText = value.ToString(); _message = "Chat outline intensity saved: " + value + "%."; MarkSaved("Chat outline intensity saved."); } } private void SaveChatUiScale() { if (Plugin.ChatUiScalePercent != null) { if (!ChatAccessibilityScalePolicy.TryParseScaleToken((_chatUiScaleText ?? string.Empty).Trim(), out var percent)) { _message = "Whole chat UI scale: enter 75-200, decimal scale like 1.25, or reset. Use Chat font size for text-only growth."; MarkDraft("Fix whole chat UI scale to a number from 75 to 200."); return; } Plugin.ChatUiScalePercent.Value = percent; Plugin.Instance?.SaveConfigFromQolMenu(); ChatReadabilityPatch.RequestRefreshSoon(); _chatUiScaleText = percent.ToString(); _message = "Whole chat UI scale saved: " + percent + "%. This grows the panel, input, backdrop, text, and outlines together."; MarkSaved("Whole chat UI scale saved."); } } private void SaveChatFontSize() { if (Plugin.ChatFontSizePercent != null) { if (!ChatFontSizePolicy.TryParseFontSizeToken((_chatFontSizeText ?? string.Empty).Trim(), out var percent)) { _message = "Chat font size: enter 75-200, decimal size like 1.25, on/off, or reset. This changes message text only."; MarkDraft("Fix chat font size to a number from 75 to 200."); return; } Plugin.ChatFontSizePercent.Value = percent; Plugin.Instance?.SaveConfigFromQolMenu(); ChatReadabilityPatch.RequestRefreshSoon(); _chatFontSizeText = percent.ToString(); _message = "Chat font size saved: " + percent + "%. Message text grows without scaling the chat panel."; MarkSaved("Chat font size saved."); } } private void SaveChatWindowHeight() { if (Plugin.ChatWindowHeightPercent != null) { if (!ChatWindowHeightPolicy.TryParseHeightToken((_chatWindowHeightText ?? string.Empty).Trim(), out var percent)) { _message = "Chat window height: enter 100-200, decimal size like 1.5, or reset. This adds vertical log space only."; MarkDraft("Fix chat window height to a number from 100 to 200."); return; } Plugin.ChatWindowHeightPercent.Value = percent; Plugin.Instance?.SaveConfigFromQolMenu(); ChatReadabilityPatch.RequestRefreshSoon(); _chatWindowHeightText = percent.ToString(); _message = "Chat window height saved: " + percent + "%. Drag the vanilla chat handle if you want to reposition it."; MarkSaved("Chat window height saved."); } } private void SaveChatHistoryRows() { if (Plugin.ChatHistoryRows != null) { string text = (_chatHistoryRowsText ?? string.Empty).Trim(); if (string.Equals(text, "reset", StringComparison.OrdinalIgnoreCase) || string.Equals(text, "default", StringComparison.OrdinalIgnoreCase)) { text = 150.ToString(); } if (!int.TryParse(text, out var result)) { _message = "Retained chat rows: enter 25-250 or reset. Default 150 is recommended for busy lobbies."; MarkDraft("Fix retained chat rows to a number from 25 to 250."); return; } int value = ChatHistoryLimitPolicy.ClampRows(result); Plugin.ChatHistoryRows.Value = value; Plugin.Instance?.SaveConfigFromQolMenu(); _chatHistoryRowsText = value.ToString(); _message = "Retained chat rows saved: " + value + ". Higher values use more UI memory/work; higher vanilla or other-mod limits are never reduced."; MarkSaved("Retained chat rows saved."); } } private void SavePlayerRenderSaverRadius() { if (Plugin.PlayerRenderSaverRadiusMeters != null) { if (!int.TryParse((_playerRenderSaverRadiusText ?? string.Empty).Trim().TrimEnd(new char[1] { 'm' }), out var result)) { _message = "Render saver radius: enter meters from 10 to 250."; MarkDraft("Fix render saver radius to a number from 10 to 250."); return; } int value = PlayerRenderSaverPolicy.ClampRadiusMeters(result); Plugin.PlayerRenderSaverRadiusMeters.Value = value; Plugin.Instance?.SaveConfigFromQolMenu(); _playerRenderSaverRadiusText = value.ToString(); _message = "Render saver radius saved: " + value + "m."; MarkSaved("Render saver radius saved."); } } private void SavePlayerRenderSaverMaxVisible() { if (Plugin.PlayerRenderSaverMaxVisiblePlayers != null) { if (!int.TryParse((_playerRenderSaverMaxVisibleText ?? string.Empty).Trim(), out var result)) { _message = "Render saver max visible: enter a number from 1 to 128."; MarkDraft("Fix render saver max visible to a number from 1 to 128."); return; } int value = PlayerRenderSaverPolicy.ClampMaxVisiblePlayers(result); Plugin.PlayerRenderSaverMaxVisiblePlayers.Value = value; Plugin.Instance?.SaveConfigFromQolMenu(); _playerRenderSaverMaxVisibleText = value.ToString(); _message = "Render saver max visible saved: " + value + "."; MarkSaved("Render saver max visible saved."); } } private void SavePingSoundMode() { string text = (_pingSoundModeText ?? string.Empty).Trim().ToLowerInvariant(); switch (text) { case "click": case "change": case "error": case "task": case "ticket": Plugin.PingSoundMode.Value = text; Plugin.EnablePingSound.Value = true; Plugin.Instance?.SaveConfigFromQolMenu(); _pingSoundModeText = text; MentionPingController.PlayConfiguredPing(); _message = "Ping sound mode saved: " + text + "."; MarkSaved("Ping sound mode saved."); break; default: _message = "Ping sound mode: use click, change, error, task, or ticket."; MarkDraft("Fix ping sound mode to click, change, error, task, or ticket."); break; } } private void ClampWindowToScreen() { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) WindowFitResult windowFitResult = BlueSageWindowCoordinator.ResolveForFrame(BlueSagePublicWindow.QolMenu, _windowRect, 920f, 640f, Screen.width, Screen.height); _windowRect = new Rect(windowFitResult.X, windowFitResult.Y, windowFitResult.Width, windowFitResult.Height); } private void OnDisable() { BlueSageWindowCoordinator.SetVisible(BlueSagePublicWindow.QolMenu, visible: false); BlueSageWindowHoverScope.UnregisterWindow(47058); } private void DrawResizeGrip() { //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Expected O, but got Unknown //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Invalid comparison between Unknown and I4 //IL_00ea: Unknown result type (might be due to invalid IL or missing references) //IL_00f0: Invalid comparison between Unknown and I4 //IL_00a7: 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) Rect val = default(Rect); ((Rect)(ref val))..ctor(((Rect)(ref _windowRect)).width - 42f - 8f, ((Rect)(ref _windowRect)).height - 42f - 8f, 42f, 42f); GUI.Box(val, new GUIContent("↘", "Drag this larger corner handle to resize the QoL Menu."), _buttonStyle); Event current = Event.current; if (current != null) { if ((int)current.type == 0 && ((Rect)(ref val)).Contains(current.mousePosition)) { _isResizing = true; current.Use(); } else if (_isResizing && (int)current.type == 3) { ref Rect windowRect = ref _windowRect; ((Rect)(ref windowRect)).width = ((Rect)(ref windowRect)).width + current.delta.x; ref Rect windowRect2 = ref _windowRect; ((Rect)(ref windowRect2)).height = ((Rect)(ref windowRect2)).height + current.delta.y; ClampWindowToScreen(); current.Use(); } else if (_isResizing && (int)current.rawType == 1) { _isResizing = false; current.Use(); } } } private void EnsureStyles() { //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Expected O, but got Unknown //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Expected O, but got Unknown //IL_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_00c7: Unknown result type (might be due to invalid IL or missing references) //IL_00d3: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Expected O, but got Unknown //IL_00f2: Unknown result type (might be due to invalid IL or missing references) //IL_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_00ff: Unknown result type (might be due to invalid IL or missing references) //IL_0106: Unknown result type (might be due to invalid IL or missing references) //IL_0112: Unknown result type (might be due to invalid IL or missing references) //IL_0121: Expected O, but got Unknown //IL_012c: Unknown result type (might be due to invalid IL or missing references) //IL_0131: Unknown result type (might be due to invalid IL or missing references) //IL_0139: Unknown result type (might be due to invalid IL or missing references) //IL_0140: Unknown result type (might be due to invalid IL or missing references) //IL_014c: Unknown result type (might be due to invalid IL or missing references) //IL_015b: Expected O, but got Unknown //IL_0166: Unknown result type (might be due to invalid IL or missing references) //IL_016b: Unknown result type (might be due to invalid IL or missing references) //IL_0172: Unknown result type (might be due to invalid IL or missing references) //IL_017a: Unknown result type (might be due to invalid IL or missing references) //IL_0181: Unknown result type (might be due to invalid IL or missing references) //IL_0188: Unknown result type (might be due to invalid IL or missing references) //IL_0194: Unknown result type (might be due to invalid IL or missing references) //IL_019f: Unknown result type (might be due to invalid IL or missing references) //IL_01a9: Unknown result type (might be due to invalid IL or missing references) //IL_01b8: Expected O, but got Unknown //IL_01e2: Unknown result type (might be due to invalid IL or missing references) //IL_01ed: Unknown result type (might be due to invalid IL or missing references) //IL_01f8: Unknown result type (might be due to invalid IL or missing references) //IL_0216: Unknown result type (might be due to invalid IL or missing references) //IL_0221: Unknown result type (might be due to invalid IL or missing references) //IL_0226: Unknown result type (might be due to invalid IL or missing references) //IL_0230: Unknown result type (might be due to invalid IL or missing references) //IL_023b: Unknown result type (might be due to invalid IL or missing references) //IL_0259: Unknown result type (might be due to invalid IL or missing references) //IL_0264: Unknown result type (might be due to invalid IL or missing references) //IL_0269: Unknown result type (might be due to invalid IL or missing references) //IL_0273: Unknown result type (might be due to invalid IL or missing references) //IL_027e: Unknown result type (might be due to invalid IL or missing references) //IL_029c: Unknown result type (might be due to invalid IL or missing references) //IL_02a7: Unknown result type (might be due to invalid IL or missing references) //IL_02ac: Unknown result type (might be due to invalid IL or missing references) //IL_02b6: Unknown result type (might be due to invalid IL or missing references) //IL_02c1: Unknown result type (might be due to invalid IL or missing references) //IL_02dd: Unknown result type (might be due to invalid IL or missing references) //IL_02e2: Unknown result type (might be due to invalid IL or missing references) //IL_02eb: Unknown result type (might be due to invalid IL or missing references) //IL_02f5: Expected O, but got Unknown //IL_02f5: Unknown result type (might be due to invalid IL or missing references) //IL_0301: Unknown result type (might be due to invalid IL or missing references) //IL_0310: Unknown result type (might be due to invalid IL or missing references) //IL_031c: Unknown result type (might be due to invalid IL or missing references) //IL_032b: Expected O, but got Unknown //IL_0336: Unknown result type (might be due to invalid IL or missing references) //IL_033b: Unknown result type (might be due to invalid IL or missing references) //IL_0344: Unknown result type (might be due to invalid IL or missing references) //IL_034e: Expected O, but got Unknown //IL_034e: Unknown result type (might be due to invalid IL or missing references) //IL_0353: Unknown result type (might be due to invalid IL or missing references) //IL_035d: Expected O, but got Unknown //IL_035d: Unknown result type (might be due to invalid IL or missing references) //IL_0362: Unknown result type (might be due to invalid IL or missing references) //IL_036c: Expected O, but got Unknown //IL_036c: Unknown result type (might be due to invalid IL or missing references) //IL_0378: Unknown result type (might be due to invalid IL or missing references) //IL_0383: Unknown result type (might be due to invalid IL or missing references) //IL_0392: Unknown result type (might be due to invalid IL or missing references) //IL_039e: Unknown result type (might be due to invalid IL or missing references) //IL_03ad: Expected O, but got Unknown //IL_03b5: Unknown result type (might be due to invalid IL or missing references) //IL_03cc: Unknown result type (might be due to invalid IL or missing references) //IL_03e3: Unknown result type (might be due to invalid IL or missing references) //IL_03fa: Unknown result type (might be due to invalid IL or missing references) //IL_0410: Unknown result type (might be due to invalid IL or missing references) //IL_0415: Unknown result type (might be due to invalid IL or missing references) //IL_0421: Unknown result type (might be due to invalid IL or missing references) //IL_042c: Unknown result type (might be due to invalid IL or missing references) //IL_043b: Unknown result type (might be due to invalid IL or missing references) //IL_0447: Unknown result type (might be due to invalid IL or missing references) //IL_0456: Expected O, but got Unknown //IL_045d: Unknown result type (might be due to invalid IL or missing references) //IL_0462: Unknown result type (might be due to invalid IL or missing references) //IL_046a: Unknown result type (might be due to invalid IL or missing references) //IL_0476: Unknown result type (might be due to invalid IL or missing references) //IL_0485: Expected O, but got Unknown //IL_048c: Unknown result type (might be due to invalid IL or missing references) //IL_0491: Unknown result type (might be due to invalid IL or missing references) //IL_049d: Expected O, but got Unknown //IL_04a8: Unknown result type (might be due to invalid IL or missing references) //IL_04ad: Unknown result type (might be due to invalid IL or missing references) //IL_04b6: Unknown result type (might be due to invalid IL or missing references) //IL_04c0: Expected O, but got Unknown //IL_04c0: Unknown result type (might be due to invalid IL or missing references) //IL_04c5: Unknown result type (might be due to invalid IL or missing references) //IL_04cf: Expected O, but got Unknown //IL_04cf: Unknown result type (might be due to invalid IL or missing references) //IL_04db: Unknown result type (might be due to invalid IL or missing references) //IL_04e6: Unknown result type (might be due to invalid IL or missing references) //IL_04f5: Unknown result type (might be due to invalid IL or missing references) //IL_0501: Unknown result type (might be due to invalid IL or missing references) //IL_0510: Expected O, but got Unknown //IL_051b: Unknown result type (might be due to invalid IL or missing references) //IL_0520: Unknown result type (might be due to invalid IL or missing references) //IL_0527: Unknown result type (might be due to invalid IL or missing references) //IL_052f: Unknown result type (might be due to invalid IL or missing references) //IL_0536: Unknown result type (might be due to invalid IL or missing references) //IL_053d: Unknown result type (might be due to invalid IL or missing references) //IL_0549: Unknown result type (might be due to invalid IL or missing references) //IL_0558: Expected O, but got Unknown _theme = BlueSageUiTheme.Current; if (_windowStyle == null || !string.Equals(_renderedThemeKey, _theme.Key, StringComparison.Ordinal) || _renderedTextureGeneration != BlueSageUiTheme.RuntimeTextureGeneration || !BlueSageUiTheme.AreRuntimeTexturesAlive()) { _renderedThemeKey = _theme.Key; _windowBackgroundTexture = BlueSageUiTheme.GetSolidTexture(_theme.Window); GUIStyle val = new GUIStyle(GUI.skin.window) { padding = new RectOffset(16, 16, 46, 14) }; val.normal.background = _windowBackgroundTexture; val.normal.textColor = _theme.LabelText; _windowStyle = val; GUIStyle val2 = new GUIStyle(GUI.skin.box); val2.normal.background = BlueSageUiTheme.GetSolidTexture(_theme.Header); _headerBoxStyle = val2; GUIStyle val3 = new GUIStyle(GUI.skin.label) { fontSize = 18, fontStyle = (FontStyle)1 }; val3.normal.textColor = _theme.HeaderText; _headerStyle = val3; GUIStyle val4 = new GUIStyle(GUI.skin.label) { fontSize = 14, fontStyle = (FontStyle)1 }; val4.normal.textColor = _theme.LabelText; _labelStyle = val4; GUIStyle val5 = new GUIStyle(GUI.skin.label) { richText = false, fontSize = 15, fontStyle = (FontStyle)1, wordWrap = true }; val5.normal.textColor = Color.Lerp(_theme.SmallText, _theme.LabelText, 0.35f); _smallStyle = val5; _buttonStyle = BlueSageUiTheme.CreateNeutralButtonStyle(GUI.skin.button, _theme, 13); _activeButtonStyle = BlueSageUiTheme.CreateStateButtonStyle(_buttonStyle, _theme.ActiveAccent, _theme.FieldFocusedBorder, _theme.ActiveStateText, 13); _enabledButtonStyle = BlueSageUiTheme.CreateStateButtonStyle(_buttonStyle, _theme.EnabledAccent, Color.Lerp(_theme.EnabledAccent, Color.white, 0.28f), _theme.EnabledButtonText, 13); _disabledButtonStyle = BlueSageUiTheme.CreateStateButtonStyle(_buttonStyle, _theme.DisabledAccent, Color.Lerp(_theme.DisabledAccent, Color.white, 0.2f), _theme.DisabledButtonText, 13); _experimentalButtonStyle = BlueSageUiTheme.CreateStateButtonStyle(_buttonStyle, _theme.ExperimentalAccent, Color.Lerp(_theme.ExperimentalAccent, Color.black, 0.25f), _theme.ExperimentalButtonText, 13); GUIStyle val6 = new GUIStyle(GUI.skin.box) { padding = new RectOffset(12, 12, 10, 10) }; val6.normal.background = BlueSageUiTheme.GetSolidTexture(_theme.Panel); val6.normal.textColor = _theme.LabelText; _boxStyle = val6; GUIStyle val7 = new GUIStyle(GUI.skin.box) { padding = new RectOffset(14, 14, 12, 12), margin = new RectOffset(0, 0, 0, 0), border = new RectOffset(1, 1, 1, 1) }; val7.normal.background = BlueSageUiTheme.GetPanelTexture(_theme.SectionPanel, _theme.SectionBorder); val7.normal.textColor = _theme.LabelText; _sectionBoxStyle = val7; _comfortSectionBoxStyle = CreateAccentedSectionStyle(_theme.ComfortAccent); _chatSectionBoxStyle = CreateAccentedSectionStyle(_theme.ChatAccent); _identitySectionBoxStyle = CreateAccentedSectionStyle(_theme.IdentityAccent); _personalizationSectionBoxStyle = CreateAccentedSectionStyle(_theme.PersonalizationAccent); GUIStyle val8 = new GUIStyle(_sectionBoxStyle); val8.normal.background = BlueSageUiTheme.GetPanelTexture(_theme.SectionPanel, _theme.ExperimentalBorder); val8.normal.textColor = _theme.LabelText; _experimentalBoxStyle = val8; GUIStyle val9 = new GUIStyle(_labelStyle) { fontSize = 17 }; val9.normal.textColor = _theme.LabelText; _sectionHeaderStyle = val9; _navigationHintStyle = new GUIStyle(_smallStyle) { richText = true }; GUIStyle val10 = new GUIStyle(GUI.skin.box) { padding = new RectOffset(14, 56, 10, 10), border = new RectOffset(1, 1, 1, 1) }; val10.normal.background = BlueSageUiTheme.GetPanelTexture(_theme.FooterBackground, _theme.FooterBorder); val10.normal.textColor = _theme.FooterText; _footerBoxStyle = val10; GUIStyle val11 = new GUIStyle(GUI.skin.label) { richText = false, fontSize = 14, fontStyle = (FontStyle)1, wordWrap = true }; val11.normal.textColor = _theme.FooterText; _footerTextStyle = val11; _textFieldStyle = BlueSageUiTheme.CreateTextInputStyle(GUI.skin.textField, _theme, 14, wordWrap: true); _renderedTextureGeneration = BlueSageUiTheme.RuntimeTextureGeneration; } } private GUIStyle CreateAccentedSectionStyle(Color accent) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000b: 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_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Expected O, but got Unknown GUIStyle val = new GUIStyle(_sectionBoxStyle); val.normal.background = BlueSageUiTheme.GetPanelTexture(_theme.SectionPanel, accent); val.normal.textColor = _theme.LabelText; return val; } private static void SetAllStateTextColors(GUIStyle style, Color text) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_001e: 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_0036: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) style.normal.textColor = text; style.hover.textColor = text; style.active.textColor = text; style.focused.textColor = text; style.onNormal.textColor = text; style.onHover.textColor = text; style.onActive.textColor = text; style.onFocused.textColor = text; } } internal sealed class SessionAuditController : MonoBehaviour { private sealed class PendingModerationSave { internal string Action = string.Empty; internal string Source = string.Empty; internal string SteamId = string.Empty; internal string DisplayNameRaw = string.Empty; internal string Persona = string.Empty; internal string IssuerSteamId = string.Empty; internal string IncidentId = string.Empty; internal string TargetRole = string.Empty; } private const long MaxChatPartBytes = 16777216L; private const long MaxBepInExPartBytes = 67108864L; private const int MaxChatParts = 4; private const int MaxBepInExParts = 2; private const int MaxRetainedSessions = 12; private const int RetentionDays = 14; private const int MirrorChunkBytes = 524288; private static SessionAuditController _instance; private static PendingModerationSave _pendingModerationSave; private readonly Dictionary _nativeBanSnapshot = new Dictionary(StringComparer.Ordinal); private readonly AuditMirrorTransferBuffer _bepInExMirrorBuffer = new AuditMirrorTransferBuffer(); private string _sessionDirectory = string.Empty; private string _chatPath = string.Empty; private string _bepInExPath = string.Empty; private StreamWriter _chatWriter; private FileStream _bepInExWriter; private long _bepInExSourceOffset; private int _chatPart = 1; private int _bepInExPart = 1; private float _nextMaintenanceAt; private bool _nativeBanSnapshotReady; private bool _initializationFailed; private bool _wasDisabled; private bool _suppressNextBanBaselineEvents; internal static string AuditRoot { get { try { return Path.Combine(Paths.BepInExRootPath, "BlueSageAudit"); } catch { return Path.Combine(Environment.CurrentDirectory, "BepInEx", "BlueSageAudit"); } } } internal static bool Enabled { get { if (Plugin.EnableLocalAuditExports != null) { return Plugin.EnableLocalAuditExports.Value; } return false; } } internal static string CurrentSessionDirectory => _instance?._sessionDirectory ?? AuditRoot; internal static string CurrentChatPath => _instance?._chatPath ?? Path.Combine(AuditRoot, "chat-audit-not-started.jsonl"); internal static string CurrentBepInExPath => _instance?._bepInExPath ?? Path.Combine(AuditRoot, "bepinex-not-started.log"); private void Awake() { _instance = this; Object.DontDestroyOnLoad((Object)(object)((Component)this).gameObject); EnsureInitialized(); } private void Update() { if (!(Time.unscaledTime < _nextMaintenanceAt)) { _nextMaintenanceAt = Time.unscaledTime + 1f; if (!Enabled) { PauseCapture(); return; } EnsureInitialized(); MirrorBepInExLog(524288); PollNativeBanStore(); } } private void OnDestroy() { if (Enabled) { MirrorBepInExLog(8388608); PollNativeBanStore(); } CloseWriters(); if (_instance == this) { _instance = null; } } internal static void RecordChat(string displayNameRaw, string messageRaw, bool isLocal, int senderIndex, AuthoritativeChatSenderContext senderContext) { if (!Enabled || (Object)(object)_instance == (Object)null) { return; } _instance.EnsureInitialized(); if (_instance._chatWriter != null) { bool isHost; bool num = Plugin.CanUseLobbySafety(out isHost); string identityState = (num ? "unverified-current-row" : "redacted-normal-client"); string observerAuthority = ((!num) ? "normal-client" : (isHost ? "host" : "helper")); string senderRole = string.Empty; string steamId = string.Empty; string rosterNameRaw = string.Empty; string steamPersona = string.Empty; int senderIndex2 = senderIndex; if (num && senderContext != null && senderContext.ExactTupleCaptured) { identityState = "verified-current-row"; senderRole = senderContext.Role; steamId = senderContext.SteamId; rosterNameRaw = senderContext.RosterNameRaw; steamPersona = senderContext.SteamPersona; senderIndex2 = senderContext.NativeRowIndex; } _instance.AppendChatLine(SessionAuditPolicy.BuildChatRecord(UtcNow(), isLocal ? "local" : "global", senderIndex2, identityState, observerAuthority, senderRole, steamId, displayNameRaw ?? string.Empty, rosterNameRaw, steamPersona, messageRaw ?? string.Empty, CurrentAuditLobbyId())); } } internal static void RecordNotification(string text) { if (Enabled && !((Object)(object)_instance == (Object)null)) { _instance.EnsureInitialized(); _instance.AppendChatLine(SessionAuditPolicy.BuildNotificationRecord(UtcNow(), text ?? string.Empty, CurrentAuditLobbyId())); } } internal static void RecordRunMarker(string classification, string ageBucket, string markerPath) { if (Enabled && !((Object)(object)_instance == (Object)null)) { _instance.EnsureInitialized(); _instance.AppendChatLine(SessionAuditPolicy.BuildRunMarkerRecord(UtcNow(), classification ?? string.Empty, ageBucket ?? string.Empty, Path.GetFileName(markerPath ?? string.Empty))); } } internal static void RecordIdentityResync(string source, string outcome, int targetedRows, int healedRows, int lockedRows, long durationMilliseconds, string reason) { if (Enabled && !((Object)(object)_instance == (Object)null)) { _instance.EnsureInitialized(); string text = (reason ?? string.Empty).Replace("\r", " ").Replace("\n", " ").Replace("|", "/") .Trim(); if (text.Length > 160) { text = text.Substring(0, 160); } string textRaw = "identity-resync|source=" + (source ?? string.Empty) + "|outcome=" + (outcome ?? string.Empty) + "|targetedRows=" + Math.Max(0, targetedRows) + "|healedRows=" + Math.Max(0, healedRows) + "|lockedRows=" + Math.Max(0, lockedRows) + "|durationMilliseconds=" + Math.Max(0L, durationMilliseconds) + "|reason=" + text; _instance.AppendChatLine(SessionAuditPolicy.BuildNotificationRecord(UtcNow(), textRaw, string.Empty)); } } internal static void RecordModeration(string action, string source, string outcome, string steamId, string displayNameRaw, string persona, string issuerSteamId = "", string incidentId = "", string targetRole = "") { if (!Enabled || (Object)(object)_instance == (Object)null || !Plugin.CanUseLobbySafety(out var _)) { return; } _instance.EnsureInitialized(); if (_instance._nativeBanSnapshotReady && ModerationProtocolPolicy.IsSteamId64(steamId)) { if (string.Equals(action, "ban-added", StringComparison.Ordinal)) { _instance._nativeBanSnapshot[steamId] = new NativeBanRecord(steamId, displayNameRaw); } else if (string.Equals(action, "ban-removed", StringComparison.Ordinal)) { _instance._nativeBanSnapshot.Remove(steamId); } } _instance.AppendChatLine(SessionAuditPolicy.BuildModerationRecord(UtcNow(), action ?? string.Empty, source ?? string.Empty, outcome ?? string.Empty, steamId ?? string.Empty, displayNameRaw ?? string.Empty, persona ?? string.Empty, CurrentAuditLobbyId(), issuerSteamId ?? string.Empty, incidentId ?? string.Empty, targetRole ?? string.Empty)); } internal static void ArmPersistedModerationAudit(string action, string source, string steamId, string displayNameRaw, string persona, string issuerSteamId, string incidentId, string targetRole) { if (Enabled && !((Object)(object)_instance == (Object)null) && Plugin.CanUseLobbySafety(out var isHost) && isHost) { _pendingModerationSave = new PendingModerationSave { Action = (action ?? string.Empty), Source = (source ?? string.Empty), SteamId = (steamId ?? string.Empty), DisplayNameRaw = (displayNameRaw ?? string.Empty), Persona = (persona ?? string.Empty), IssuerSteamId = (issuerSteamId ?? string.Empty), IncidentId = (incidentId ?? string.Empty), TargetRole = (targetRole ?? string.Empty) }; } } internal static void CancelPersistedModerationAudit() { _pendingModerationSave = null; } internal static void CapturePersistedNativeBanSave() { if (!Enabled || (Object)(object)_instance == (Object)null || !Plugin.CanUseLobbySafety(out var isHost) || !isHost) { _pendingModerationSave = null; return; } PendingModerationSave pendingModerationSave = _pendingModerationSave; _pendingModerationSave = null; if (pendingModerationSave != null) { RecordModeration(pendingModerationSave.Action, pendingModerationSave.Source, "persisted", pendingModerationSave.SteamId, pendingModerationSave.DisplayNameRaw, pendingModerationSave.Persona, pendingModerationSave.IssuerSteamId, pendingModerationSave.IncidentId, pendingModerationSave.TargetRole); _instance.SynchronizeNativeBanSnapshot(); } else { _instance.PollNativeBanStore("native-ban-save-observed"); } } internal static string StatusText() { if (!Enabled) { return "Local session audit: off. Use /auditlog on to resume local chat and BepInEx exports."; } if ((Object)(object)_instance == (Object)null || _instance._initializationFailed || _instance._chatWriter == null || _instance._bepInExWriter == null) { return "Local session audit: configured on but NOT WRITING. Use /auditlog on to retry; check the BepInEx log for the local file error before relying on incident evidence."; } bool isHost; string text = ((!Plugin.CanUseLobbySafety(out isHost)) ? "normal-client transcript; exact Steam IDs are not added by BlueSage" : (isHost ? "exact identity enrichment active for host" : "exact identity enrichment active for assigned Helper")); return "Local session audit: on; " + text + ". Chat JSONL and a running BepInEx mirror are stored under " + CurrentSessionDirectory + ". Snapshot Now creates a fresh timestamped copy of the true live BepInEx LogOutput.log."; } internal static string CopyAuditFolder() { GUIUtility.systemCopyBuffer = CurrentSessionDirectory; return "Local audit folder copied. It stays on this computer until you choose to share it."; } internal static string SnapshotNow() { if (!Enabled || (Object)(object)_instance == (Object)null) { return "Local session audit is off."; } _instance.EnsureInitialized(); if (_instance._initializationFailed || _instance._chatWriter == null || _instance._bepInExWriter == null) { return "Local audit snapshot FAILED: one or both local writers are unavailable. Use /auditlog on to retry, then /auditlog status before relying on the export."; } _instance.MirrorBepInExLog(8388608); if (_instance._initializationFailed || _instance._chatWriter == null || _instance._bepInExWriter == null) { return "Local audit snapshot FAILED while mirroring BepInEx output. Use /auditlog on to retry, then /auditlog status before relying on the export."; } _instance.WriteRosterCheckpoint(); _instance.PollNativeBanStore(); try { _instance._chatWriter.Flush(); _instance._bepInExWriter.Flush(); } catch (Exception ex) { _instance.MarkCaptureFailed("snapshot flush", ex); return "Local audit snapshot FAILED while flushing files. Use /auditlog on to retry, then /auditlog status before relying on the export."; } if (!AuditSnapshotPolicy.TryCreate(Path.Combine(Paths.BepInExRootPath, "LogOutput.log"), _instance._sessionDirectory, DateTime.UtcNow, Guid.NewGuid().ToString("N").Substring(0, 8), out var snapshotDirectory, out var capturedBytes, out var error)) { return "Local audit snapshot FAILED while copying the true BepInEx LogOutput.log: " + error; } GUIUtility.systemCopyBuffer = snapshotDirectory; return "Local audit snapshot saved: " + snapshotDirectory + " (" + capturedBytes + " bytes from the true BepInEx LogOutput.log). The snapshot folder path was copied."; } internal static void ReconcileEnabledState() { if ((Object)(object)_instance == (Object)null) { return; } if (Enabled) { bool wasDisabled = _instance._wasDisabled; if (_instance._wasDisabled) { _instance.AdvanceBepInExSourceOffsetToEnd(); _instance._wasDisabled = false; } _instance._initializationFailed = false; _instance.EnsureInitialized(); if (wasDisabled) { _instance.AppendChatLine(SessionAuditPolicy.BuildNotificationRecord(UtcNow(), "Local audit capture resumed after an opt-out gap; events from the disabled period were not backfilled.", CurrentAuditLobbyId())); } } else { _instance.PauseCapture(); } } private void EnsureInitialized() { if (_initializationFailed || !Enabled || (_chatWriter != null && _bepInExWriter != null)) { return; } try { Directory.CreateDirectory(AuditRoot); PruneOldSessions(); if (string.IsNullOrWhiteSpace(_sessionDirectory)) { string text = DateTime.UtcNow.ToString("yyyyMMddTHHmmssZ") + "-" + Guid.NewGuid().ToString("N").Substring(0, 8); _sessionDirectory = Path.Combine(AuditRoot, "session-" + text); Directory.CreateDirectory(_sessionDirectory); File.WriteAllText(Path.Combine(_sessionDirectory, "README.txt"), "BlueSage QoL local session audit\r\nContains a chat/notification JSONL transcript and a running mirror of BepInEx/LogOutput.log.\r\nChat raw fields are the rendered UI arguments seen after the game's/mods' earlier patches, not a pristine network payload.\r\nExact SteamID/persona enrichment is written only while this client is the verified host or an assigned Helper.\r\nThe BepInEx mirror faithfully preserves whatever the game and installed mods already write, which may include player identifiers.\r\nThese files are never uploaded automatically. Review them before sharing.\r\n", new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); PruneOldSessions(); } OpenChatWriter(); OpenBepInExWriter(); MirrorBepInExLog(8388608); } catch (Exception ex) { _initializationFailed = true; CloseWriters(); ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("Local audit export could not start: " + ex.GetType().Name + ": " + ex.Message)); } } } private void OpenChatWriter(bool truncate = false) { _chatPath = Path.Combine(_sessionDirectory, PartName("chat-audit", _chatPart, ".jsonl")); _chatWriter = new StreamWriter(new FileStream(_chatPath, truncate ? FileMode.Create : FileMode.Append, FileAccess.Write, FileShare.Read), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)) { AutoFlush = true }; } private void OpenBepInExWriter(bool truncate = false) { _bepInExPath = Path.Combine(_sessionDirectory, PartName("bepinex", _bepInExPart, ".log")); _bepInExWriter = new FileStream(_bepInExPath, truncate ? FileMode.Create : FileMode.Append, FileAccess.Write, FileShare.Read); } private void AppendChatLine(string line) { if (_chatWriter == null || string.IsNullOrWhiteSpace(line)) { return; } try { if (_chatWriter.BaseStream.Length >= 16777216) { _chatWriter.Dispose(); _chatWriter = null; _chatPart++; OpenChatWriter(truncate: true); PruneAuditParts("chat-audit*.jsonl", _chatPath, 4); } _chatWriter.WriteLine(line); } catch (Exception ex) { MarkCaptureFailed("chat write", ex); } } private void MirrorBepInExLog(int maximumBytes) { if (_bepInExWriter == null || maximumBytes <= 0) { return; } string path = Path.Combine(Paths.BepInExRootPath, "LogOutput.log"); try { using FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); if (fileStream.Length < _bepInExSourceOffset) { _bepInExSourceOffset = 0L; } fileStream.Position = _bepInExSourceOffset; int num = maximumBytes; while (num > 0 && fileStream.Position < fileStream.Length) { if (_bepInExWriter.Length >= 67108864) { _bepInExWriter.Dispose(); _bepInExWriter = null; _bepInExPart++; OpenBepInExWriter(truncate: true); PruneAuditParts("bepinex*.log", _bepInExPath, 2); } byte[] array = _bepInExMirrorBuffer.Acquire(fileStream.Length, fileStream.Position, num); if (array == null) { break; } int num2 = fileStream.Read(array, 0, Math.Min(array.Length, num)); if (num2 <= 0) { break; } _bepInExWriter.Write(array, 0, num2); _bepInExSourceOffset += num2; num -= num2; } _bepInExWriter.Flush(); } catch (FileNotFoundException) { } catch (Exception ex2) { MarkCaptureFailed("BepInEx mirror", ex2); } } private void PollNativeBanStore(string observedSource = "native-ban-store-observed") { if (!Plugin.CanUseLobbySafety(out var isHost) || !isHost) { return; } if (!SteamIdModerationController.TryGetNativeBanRecords(out var records, out var error)) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogDebug((object)("Native ban audit observation skipped safely: " + error)); } return; } if (!_nativeBanSnapshotReady) { foreach (NativeBanRecord item in records) { _nativeBanSnapshot[item.SteamId] = item; if (!_suppressNextBanBaselineEvents) { WriteNativeBanChange("ban-existing", item); } } _nativeBanSnapshotReady = true; _suppressNextBanBaselineEvents = false; return; } Dictionary now = records.GroupBy((NativeBanRecord nativeBanRecord) => nativeBanRecord.SteamId, StringComparer.Ordinal).ToDictionary, string, NativeBanRecord>((IGrouping group) => group.Key, (IGrouping group) => group.First(), StringComparer.Ordinal); NativeBanRecord[] array = records.Where((NativeBanRecord nativeBanRecord) => !_nativeBanSnapshot.ContainsKey(nativeBanRecord.SteamId)).ToArray(); NativeBanRecord[] array2 = _nativeBanSnapshot.Values.Where((NativeBanRecord prior) => !now.ContainsKey(prior.SteamId)).ToArray(); NativeBanRecord value; NativeBanRecord[] array3 = records.Where((NativeBanRecord nativeBanRecord) => _nativeBanSnapshot.TryGetValue(nativeBanRecord.SteamId, out value) && !string.Equals(nativeBanRecord.DisplayNameRaw, value.DisplayNameRaw, StringComparison.Ordinal)).ToArray(); _nativeBanSnapshot.Clear(); foreach (KeyValuePair item2 in now) { _nativeBanSnapshot[item2.Key] = item2.Value; } NativeBanRecord[] array4 = array; foreach (NativeBanRecord record in array4) { WriteNativeBanChange("ban-added", record, observedSource); } array4 = array2; foreach (NativeBanRecord record2 in array4) { WriteNativeBanChange("ban-removed", record2, observedSource); } array4 = array3; foreach (NativeBanRecord record3 in array4) { WriteNativeBanChange("ban-name-updated", record3, observedSource); } } private void WriteNativeBanChange(string action, NativeBanRecord record, string source = "native-ban-store-observed") { RecordModeration(action, (action == "ban-existing") ? "native-ban-store-baseline" : source, "persisted", record.SteamId, record.DisplayNameRaw, ReadSteamPersona(record.SteamId)); } private void SynchronizeNativeBanSnapshot() { if (!SteamIdModerationController.TryGetNativeBanRecords(out var records, out var _)) { return; } _nativeBanSnapshot.Clear(); foreach (NativeBanRecord item in records) { _nativeBanSnapshot[item.SteamId] = item; } _nativeBanSnapshotReady = true; _suppressNextBanBaselineEvents = false; } private void WriteRosterCheckpoint() { if (!Plugin.CanUseLobbySafety(out var _)) { return; } foreach (PlayerIdentityEvidence item in PlayerIdentityEvidenceController.GetVerifiedRoster()) { RecordModeration("roster-checkpoint", "manual-audit-snapshot", item.Role, item.SteamId, item.DisplayNameRaw, item.SteamPersona, "", "", item.Role + ";rosterIndex=" + item.RosterIndex + ";playerId=" + item.NetworkPlayerId); } } private void CloseWriters() { try { _chatWriter?.Flush(); } catch { } try { _chatWriter?.Dispose(); } catch { } try { _bepInExWriter?.Flush(); } catch { } try { _bepInExWriter?.Dispose(); } catch { } _chatWriter = null; _bepInExWriter = null; } private void MarkCaptureFailed(string operation, Exception ex) { _initializationFailed = true; CloseWriters(); ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("Local audit " + operation + " failed; capture is NOT WRITING until /auditlog on retries: " + ex.GetType().Name + ": " + ex.Message)); } } private void PauseCapture() { CloseWriters(); AdvanceBepInExSourceOffsetToEnd(); _nativeBanSnapshot.Clear(); _nativeBanSnapshotReady = false; _suppressNextBanBaselineEvents = true; _wasDisabled = true; } private void AdvanceBepInExSourceOffsetToEnd() { try { using FileStream fileStream = new FileStream(Path.Combine(Paths.BepInExRootPath, "LogOutput.log"), FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); _bepInExSourceOffset = fileStream.Length; } catch (FileNotFoundException) { } catch (Exception ex2) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogDebug((object)("BepInEx audit opt-out cursor update skipped safely: " + ex2.GetType().Name + ": " + ex2.Message)); } } } private static void PruneOldSessions() { string text = Path.GetFullPath(AuditRoot).TrimEnd(new char[1] { Path.DirectorySeparatorChar }); char directorySeparatorChar = Path.DirectorySeparatorChar; string value = text + directorySeparatorChar; DirectoryInfo[] array = (from directory in new DirectoryInfo(AuditRoot).GetDirectories("session-*") orderby directory.CreationTimeUtc descending select directory).ToArray(); DateTime dateTime = DateTime.UtcNow.AddDays(-14.0); for (int num = 0; num < array.Length; num++) { DirectoryInfo directoryInfo = array[num]; string text2 = Path.GetFullPath(directoryInfo.FullName).TrimEnd(new char[1] { Path.DirectorySeparatorChar }); directorySeparatorChar = Path.DirectorySeparatorChar; if ((text2 + directorySeparatorChar).StartsWith(value, StringComparison.OrdinalIgnoreCase) && (num >= 12 || directoryInfo.CreationTimeUtc < dateTime)) { try { directoryInfo.Delete(recursive: true); } catch { } } } } private void PruneAuditParts(string pattern, string currentPath, int maximumParts) { try { FileInfo[] array = (from file in new DirectoryInfo(_sessionDirectory).GetFiles(pattern) where !string.Equals(file.FullName, currentPath, StringComparison.OrdinalIgnoreCase) orderby file.LastWriteTimeUtc select file).ToArray(); int num = Math.Max(0, array.Length + 1 - maximumParts); for (int num2 = 0; num2 < num; num2++) { array[num2].Delete(); } } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogDebug((object)("Local audit old-part cleanup skipped safely: " + ex.GetType().Name + ": " + ex.Message)); } } } private static string PartName(string stem, int part, string extension) { if (part > 1) { return stem + ".part" + part.ToString("00") + extension; } return stem + extension; } private static string UtcNow() { return DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); } private static string CurrentLobbyId() { //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000a: 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) try { CSteamID lobbyId; return (Plugin.TryGetRescueLobby(out lobbyId) && lobbyId != CSteamID.Nil) ? ((ulong)lobbyId).ToString() : string.Empty; } catch { return string.Empty; } } private static string CurrentAuditLobbyId() { if (!Plugin.CanUseLobbySafety(out var _)) { return string.Empty; } return CurrentLobbyId(); } private static string ReadSteamPersona(string steamId) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) try { ulong result; return ulong.TryParse(steamId, out result) ? (SteamFriends.GetFriendPersonaName(new CSteamID(result)) ?? string.Empty) : string.Empty; } catch { return string.Empty; } } } internal sealed class SessionRunMarkerController : MonoBehaviour { private const string MarkerFileName = "run-marker.v1"; private static readonly object Sync = new object(); private static SessionRunMarkerController _instance; private string _path = string.Empty; private SessionRunMarker _current; private float _nextHeartbeatAt; private void Awake() { _instance = this; _path = Path.Combine(SessionAuditController.AuditRoot, "run-marker.v1"); Directory.CreateDirectory(SessionAuditController.AuditRoot); SessionAuditController.RecordRunMarker(ReadAndClassifyPrior().ToString().ToLowerInvariant(), AgeBucket(ReadPriorHeartbeatUtcTicks()), _path); Process currentProcess = Process.GetCurrentProcess(); long num = SafeProcessStartUtcTicks(currentProcess); long ticks = DateTime.UtcNow.Ticks; _current = SessionRunMarkerPolicy.CreateRunning("0.2.4+20260730.1-public-24414155-release", currentProcess.Id, (num > 0) ? num : ticks, ticks, "runtime"); AtomicWrite(_path, SessionRunMarkerPolicy.Serialize(_current)); _nextHeartbeatAt = Time.realtimeSinceStartup + 5f; } private void Update() { if (!PluginShutdownController.IsShuttingDown && _current != null && !(Time.realtimeSinceStartup < _nextHeartbeatAt)) { _nextHeartbeatAt = Time.realtimeSinceStartup + 5f; _current = SessionRunMarkerPolicy.WithHeartbeat(_current, DateTime.UtcNow.Ticks, "runtime"); AtomicWrite(_path, SessionRunMarkerPolicy.Serialize(_current)); } } internal static void MarkCleanClose() { lock (Sync) { if (!((Object)(object)_instance == (Object)null) && _instance._current != null && !string.IsNullOrEmpty(_instance._path)) { _instance._current = SessionRunMarkerPolicy.CreateCleanClose(_instance._current, DateTime.UtcNow.Ticks); AtomicWrite(_instance._path, SessionRunMarkerPolicy.Serialize(_instance._current)); } } } private SessionRunClassification ReadAndClassifyPrior() { if (!TryReadMarker(out var marker)) { if (!File.Exists(_path)) { return SessionRunClassification.None; } return SessionRunClassification.Uncertain; } bool processExists = false; bool processStartMatches = false; try { using Process process = Process.GetProcessById(marker.ProcessId); processExists = !process.HasExited; processStartMatches = SafeProcessStartUtcTicks(process) == marker.StartedUtcTicks; } catch { processExists = false; processStartMatches = false; } return SessionRunMarkerPolicy.ClassifyPrior(marker, DateTime.UtcNow.Ticks, processExists, processStartMatches); } private long ReadPriorHeartbeatUtcTicks() { if (!TryReadMarker(out var marker)) { return 0L; } return marker.HeartbeatUtcTicks; } private bool TryReadMarker(out SessionRunMarker marker) { marker = null; try { return File.Exists(_path) && SessionRunMarkerPolicy.TryParse(File.ReadAllText(_path), out marker); } catch { return false; } } private static string AgeBucket(long heartbeatUtcTicks) { if (heartbeatUtcTicks <= 0) { return "unknown"; } long num = Math.Max(0L, (DateTime.UtcNow.Ticks - heartbeatUtcTicks) / 10000000); if (num <= 15) { return "0-15s"; } if (num <= 60) { return "16-60s"; } if (num <= 300) { return "1-5m"; } return "over-5m"; } private static long SafeProcessStartUtcTicks(Process process) { try { return process.StartTime.ToUniversalTime().Ticks; } catch { return 0L; } } private static void AtomicWrite(string path, string content) { string text = path + ".tmp"; string text2 = path + ".bak"; File.WriteAllText(text, content); if (File.Exists(path)) { try { File.Replace(text, path, text2, ignoreMetadataErrors: true); TryDelete(text2); return; } catch { TryDelete(text); return; } } File.Move(text, path); } private static void TryDelete(string path) { try { if (File.Exists(path)) { File.Delete(path); } } catch { } } } internal sealed class StatisticsPulseReader { private sealed class StatisticsSurface { public Type StatisticsDataType { get; } public Type DataManagerType { get; } public Type AchievementManagerType { get; } public Type AchievementType { get; } public StatisticsSurface(Type statisticsDataType, Type dataManagerType, Type achievementManagerType, Type achievementType) { StatisticsDataType = statisticsDataType; DataManagerType = dataManagerType; AchievementManagerType = achievementManagerType; AchievementType = achievementType; } } private const BindingFlags ReadFlags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; public bool TryCapturePulse(out StatisticsPulseSnapshot snapshot, out string unavailableReason) { snapshot = null; unavailableReason = string.Empty; try { if (!TryResolveSurface(out var surface, out unavailableReason)) { return false; } Object val = FindLiveObject(surface.DataManagerType); if (val == (Object)null) { unavailableReason = "Native DataManager is not live."; return false; } object obj = ReadRequiredMember(val, surface.DataManagerType, "StatisticsData"); if (obj == null || !surface.StatisticsDataType.IsInstanceOfType(obj)) { unavailableReason = "Native StatisticsData is unavailable."; return false; } float totalFocusSeconds = ReadSingle(obj, surface.StatisticsDataType, "TotalFocusDuration"); int todaySessions = ReadInt32(obj, surface.StatisticsDataType, "TodaySessionCount"); int currentStreak = ReadInt32(obj, surface.StatisticsDataType, "CurrentStreak"); int longestStreak = ReadInt32(obj, surface.StatisticsDataType, "LongestStreak"); int taskChecks = ReadInt32(obj, surface.StatisticsDataType, "ToDoCompleted"); if (!StatisticsPulseSnapshot.TryCreate(totalFocusSeconds, todaySessions, currentStreak, longestStreak, taskChecks, null, null, out snapshot)) { unavailableReason = "Native statistics contain an invalid focus duration."; return false; } return true; } catch (Exception ex) { snapshot = null; unavailableReason = "Native statistics read failed: " + ex.GetType().Name; return false; } } public bool TryCountAchievements(out int unlocked, out int total, out string unavailableReason) { unlocked = 0; total = 0; unavailableReason = string.Empty; try { if (!TryResolveSurface(out var surface, out unavailableReason)) { return false; } Object val = FindLiveObject(surface.AchievementManagerType); if (val == (Object)null) { unavailableReason = "Native AchievementManager is not live."; return false; } MethodInfo method = surface.AchievementManagerType.GetMethod("GetAchievement", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { surface.AchievementType }, null); if (method == null) { unavailableReason = "Native achievement reader is unavailable."; return false; } Array values = Enum.GetValues(surface.AchievementType); total = values.Length; bool flag = default(bool); foreach (object item in values) { object obj = method.Invoke(method.IsStatic ? null : val, new object[1] { item }); int num; if (obj is bool) { flag = (bool)obj; num = 1; } else { num = 0; } if (((uint)num & (flag ? 1u : 0u)) != 0) { unlocked++; } } return true; } catch (Exception ex) { unlocked = 0; total = 0; unavailableReason = "Native achievement read failed: " + ex.GetType().Name; return false; } } private static bool TryResolveSurface(out StatisticsSurface surface, out string unavailableReason) { surface = null; unavailableReason = string.Empty; Assembly assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault((Assembly assembly2) => string.Equals(assembly2.GetName().Name, "Assembly-CSharp", StringComparison.Ordinal)); if (assembly == null || !StatisticsTypeResolutionPolicy.IsExactGameSurface(assembly.GetName().Name, (string name) => assembly.GetType(name, throwOnError: false) != null)) { unavailableReason = "Native statistics are not available on this game build."; return false; } Type type = assembly.GetType("StatisticsData", throwOnError: false); Type type2 = assembly.GetType("DataManager", throwOnError: false); Type type3 = assembly.GetType("AchievementManager", throwOnError: false); Type type4 = assembly.GetType("Achievement", throwOnError: false); if (type == null || type2 == null || type3 == null || type4 == null || !type4.IsEnum) { unavailableReason = "Native statistics surface is incomplete."; return false; } surface = new StatisticsSurface(type, type2, type3, type4); return true; } private static Object FindLiveObject(Type type) { Object[] array = Resources.FindObjectsOfTypeAll(type); if (array == null) { return null; } Object[] array2 = array; foreach (Object val in array2) { if (val != null && !(val == (Object)null)) { return val; } } return null; } private static object ReadRequiredMember(object target, Type ownerType, string name) { FieldInfo field = ownerType.GetField(name, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (field != null) { return field.GetValue(field.IsStatic ? null : target); } MethodInfo methodInfo = ownerType.GetProperty(name, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetGetMethod(nonPublic: true); if (methodInfo != null) { return methodInfo.Invoke(methodInfo.IsStatic ? null : target, null); } throw new MissingMemberException(ownerType.FullName, name); } private static float ReadSingle(object target, Type ownerType, string name) { return Convert.ToSingle(ReadRequiredMember(target, ownerType, name), CultureInfo.InvariantCulture); } private static int ReadInt32(object target, Type ownerType, string name) { return Convert.ToInt32(ReadRequiredMember(target, ownerType, name), CultureInfo.InvariantCulture); } } internal sealed class NativeBanRecord { internal string SteamId { get; } internal string DisplayNameRaw { get; } internal string DisplayName { get; } internal NativeBanRecord(string steamId, string displayNameRaw) { SteamId = steamId ?? string.Empty; DisplayNameRaw = displayNameRaw ?? string.Empty; DisplayName = PlayerMentionProvider.StripRichText(DisplayNameRaw).Trim(); if (DisplayName.Length == 0) { DisplayName = "Saved native ban"; } } } internal enum LobbySafetyAccessState { Denied, Host, HelperReady, HelperWaitingForHost } internal static class SteamIdModerationController { private const int MemberDataPollIntervalMilliseconds = 1000; internal const string LobbySafetyDeniedMessage = "Lobby Safety is available only to the current host or a Helper assigned by that host. No player or incident details were shown."; private const string CapabilityKey = "bluesage_qol_moderation"; private const string HelpersKey = "bluesage_helpers"; private const string LegacyHelpersKey = "bluesage_mod_helpers"; private const string RescueHelpersKey = "bluesage_rescue_helpers"; private static readonly HashSet SeenNonces = new HashSet(StringComparer.Ordinal); private static readonly Queue NonceOrder = new Queue(); private static readonly Dictionary LastIssuerRequest = new Dictionary(StringComparer.Ordinal); private static long _nextMemberDataPollUnixMilliseconds; private static string _memberDataLobbyKey = string.Empty; internal static void PublishHostCapability() { //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Unknown result type (might be due to invalid IL or missing references) if (PluginShutdownController.IsShuttingDown) { return; } try { if (TryGetOwnedHostLobby(out var lobby, out var ownerSteamId)) { long issuedUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); SteamMatchmaking.SetLobbyData(lobby, "bluesage_qol_moderation", ModerationCapabilityPolicy.Build(ownerSteamId, issuedUnixSeconds)); string text = LobbyPingAuthorizationPolicy.SerializeDelegates(Plugin.GetUnifiedHelpersForOwner(ownerSteamId, importLegacy: true)); string text2 = ownerSteamId + "|" + text; SteamMatchmaking.SetLobbyData(lobby, "bluesage_helpers", text2); SteamMatchmaking.SetLobbyData(lobby, "bluesage_mod_helpers", text2); SteamMatchmaking.SetLobbyData(lobby, "bluesage_rescue_helpers", text2); PlayerRoleLabelController.PublishHostHeartbeat(); } } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogDebug((object)("Moderation capability publish skipped: " + ex.GetType().Name + ": " + ex.Message)); } } } internal static void ClearHostCapability() { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0038: 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) if (PluginShutdownController.IsShuttingDown) { return; } try { if (TryGetOwnedHostLobby(out var lobby, out var _)) { SteamMatchmaking.SetLobbyData(lobby, "bluesage_qol_moderation", string.Empty); SteamMatchmaking.SetLobbyData(lobby, "bluesage_helpers", string.Empty); SteamMatchmaking.SetLobbyData(lobby, "bluesage_mod_helpers", string.Empty); SteamMatchmaking.SetLobbyData(lobby, "bluesage_rescue_helpers", string.Empty); PlayerRoleLabelController.ClearPublishedHostLabel(); } } catch { } } internal static void ClearHostCapabilityForShutdown(SteamRuntimeSnapshot snapshot) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0028: 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_0054: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Unknown result type (might be due to invalid IL or missing references) if (snapshot == null || !snapshot.SteamWasReady || !snapshot.LocalWasOwner || snapshot.Lobby == CSteamID.Nil) { return; } try { SteamMatchmaking.SetLobbyData(snapshot.Lobby, "bluesage_qol_moderation", string.Empty); SteamMatchmaking.SetLobbyData(snapshot.Lobby, "bluesage_helpers", string.Empty); SteamMatchmaking.SetLobbyData(snapshot.Lobby, "bluesage_mod_helpers", string.Empty); SteamMatchmaking.SetLobbyData(snapshot.Lobby, "bluesage_rescue_helpers", string.Empty); } catch { } } internal static bool CanLocalIssue(out bool isHost, out string reason) { LobbySafetyAccessState localAccessState = GetLocalAccessState(out isHost, out reason); if (localAccessState != LobbySafetyAccessState.Host) { return localAccessState == LobbySafetyAccessState.HelperReady; } return true; } internal static bool ShouldShowLocalSurface(out LobbySafetyAccessState state, out string reason) { state = GetLocalAccessState(out var _, out reason); return state != LobbySafetyAccessState.Denied; } internal static LobbySafetyAccessState GetLocalAccessState(out bool isHost, out string reason) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_002f: 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_008f: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) isHost = false; reason = "Lobby Safety is available only to the current host or a Helper assigned by that host. No player or incident details were shown."; try { if (!Plugin.TryGetRescueLobby(out var lobbyId) || lobbyId == CSteamID.Nil || !SteamManager.Initialized) { return LobbySafetyAccessState.Denied; } string text = ((ulong)SteamMatchmaking.GetLobbyOwner(lobbyId)).ToString(); string text2 = ((ulong)SteamUser.GetSteamID()).ToString(); isHost = string.Equals(text, text2, StringComparison.Ordinal) && (Object)(object)NetworkManager.main != (Object)null && NetworkManager.main.isHost; if (isHost) { reason = "You are the verified current lobby host."; return LobbySafetyAccessState.Host; } if (!IsPublishedHelper(lobbyId, text, text2)) { return LobbySafetyAccessState.Denied; } if (!ModerationCapabilityPolicy.IsFresh(SteamMatchmaking.GetLobbyData(lobbyId, "bluesage_qol_moderation"), text, DateTimeOffset.UtcNow.ToUnixTimeSeconds())) { reason = "This lobby lists you as a Helper, but the host has not published a fresh compatible 0.2.0 v3 member-data capability. The status tab stays visible, but protected player data and actions remain locked. No request is sent until the capability matches."; return LobbySafetyAccessState.HelperWaitingForHost; } reason = "This host assigned you as a Helper and published a fresh compatible v3 member-data capability."; return LobbySafetyAccessState.HelperReady; } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogDebug((object)("Lobby Safety access check failed closed: " + ex.GetType().Name + ": " + ex.Message)); } reason = "Lobby Safety is available only to the current host or a Helper assigned by that host. No player or incident details were shown."; return LobbySafetyAccessState.Denied; } } internal static bool IsLocalHost() { bool isHost; string reason; return CanLocalIssue(out isHost, out reason) && isHost; } internal static bool IsAuthorizedCurrentIssuer(string steamId) { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_00bb: Unknown result type (might be due to invalid IL or missing references) //IL_00dc: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_006f: 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 (!ModerationProtocolPolicy.IsSteamId64(steamId) || !Plugin.TryGetRescueLobby(out var lobbyId) || lobbyId == CSteamID.Nil || !SteamManager.Initialized) { return false; } string text = ((ulong)SteamMatchmaking.GetLobbyOwner(lobbyId)).ToString(); if (string.Equals(text, steamId, StringComparison.Ordinal)) { return true; } if ((Object)(object)NetworkManager.main != (Object)null && NetworkManager.main.isHost && SteamUser.GetSteamID() == SteamMatchmaking.GetLobbyOwner(lobbyId)) { return ModerationAuthorizationPolicy.IsAuthorizedIssuer(steamId, text, Plugin.LobbyHelpers?.Value ?? string.Empty, Plugin.LobbyHelperOwnerSteamId?.Value ?? string.Empty, hasLobby: true); } return ModerationCapabilityPolicy.IsFresh(SteamMatchmaking.GetLobbyData(lobbyId, "bluesage_qol_moderation"), text, DateTimeOffset.UtcNow.ToUnixTimeSeconds()) && IsPublishedHelper(lobbyId, text, steamId); } catch { return false; } } internal static bool IsPublishedHelper(CSteamID lobby, string ownerSteamId, string steamId) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) return GetPublishedHelpers(lobby, ownerSteamId).Contains(steamId); } internal static bool TryGetFreshCurrentCapability(out CSteamID lobby, out string ownerSteamId, out string capability) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0044: 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_004a: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) lobby = CSteamID.Nil; ownerSteamId = string.Empty; capability = string.Empty; try { if (!SteamManager.Initialized || !Plugin.TryGetRescueLobby(out lobby) || lobby == CSteamID.Nil) { return false; } CSteamID lobbyOwner = SteamMatchmaking.GetLobbyOwner(lobby); if (lobbyOwner == CSteamID.Nil) { return false; } ownerSteamId = ((ulong)lobbyOwner).ToString(); capability = SteamMatchmaking.GetLobbyData(lobby, "bluesage_qol_moderation") ?? string.Empty; return ModerationCapabilityPolicy.IsFresh(capability, ownerSteamId, DateTimeOffset.UtcNow.ToUnixTimeSeconds()); } catch { lobby = CSteamID.Nil; ownerSteamId = string.Empty; capability = string.Empty; return false; } } internal static bool IsUniqueCurrentLobbyMember(CSteamID lobby, string steamId) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) if (lobby != CSteamID.Nil) { return IsUniqueSteamLobbyMember(lobby, steamId); } return false; } internal static HashSet GetPublishedHelpers(CSteamID lobby, string ownerSteamId) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) string lobbyData = SteamMatchmaking.GetLobbyData(lobby, "bluesage_helpers"); if (string.IsNullOrWhiteSpace(lobbyData)) { lobbyData = SteamMatchmaking.GetLobbyData(lobby, "bluesage_mod_helpers"); } string[] array = (lobbyData ?? string.Empty).Split(new char[1] { '|' }, 2); if (array.Length != 2 || !string.Equals(array[0], ownerSteamId, StringComparison.Ordinal)) { return new HashSet(StringComparer.Ordinal); } return new HashSet(LobbyPingAuthorizationPolicy.ParseDelegates(array[1]), StringComparer.Ordinal); } internal static bool TrySendHelperRequest(string action, string targetSteamId, string incidentId, out string message) { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_008c: Unknown result type (might be due to invalid IL or missing references) if (!CanLocalIssue(out var isHost, out message)) { return false; } if (isHost) { string issuerSteamId = ((ulong)SteamUser.GetSteamID()).ToString(); return TryExecuteHostAction(action, targetSteamId, incidentId, issuerSteamId, out message); } string nonce = Guid.NewGuid().ToString("N").Substring(0, 16); string text = ModerationProtocolPolicy.BuildMemberData(action, targetSteamId, incidentId, DateTimeOffset.UtcNow.ToUnixTimeSeconds(), nonce); if (string.IsNullOrWhiteSpace(text) || !Plugin.TryGetRescueLobby(out var lobbyId) || lobbyId == CSteamID.Nil) { message = "The moderation request could not reach the host; no action was taken."; return false; } try { SteamMatchmaking.SetLobbyMemberData(lobbyId, "bluesage_moderation_request_v2", text); } catch (Exception ex) { message = "The non-chat moderation request failed safely (" + ex.GetType().Name + "); no action was taken."; return false; } message = "Moderation request published through non-chat Steam member data for authoritative QoL-host validation. The host performs the native action; no local ban was written and no chat line was sent."; return true; } internal static void PollHostMemberDataRequests() { //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_00a7: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00bd: Unknown result type (might be due to invalid IL or missing references) long num = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); if (num < _nextMemberDataPollUnixMilliseconds) { return; } _nextMemberDataPollUnixMilliseconds = num + 1000; if (!TryGetOwnedHostLobby(out var lobby, out var ownerSteamId)) { return; } string text = ((ulong)lobby).ToString(); if (!string.Equals(_memberDataLobbyKey, text, StringComparison.Ordinal)) { _memberDataLobbyKey = text; SeenNonces.Clear(); NonceOrder.Clear(); LastIssuerRequest.Clear(); } int numLobbyMembers = SteamMatchmaking.GetNumLobbyMembers(lobby); for (int i = 0; i < numLobbyMembers; i++) { CSteamID lobbyMemberByIndex = SteamMatchmaking.GetLobbyMemberByIndex(lobby, i); string text2 = ((ulong)lobbyMemberByIndex).ToString(); if (!string.Equals(text2, ownerSteamId, StringComparison.Ordinal) && ModerationProtocolPolicy.TryParseMemberData(SteamMatchmaking.GetLobbyMemberData(lobby, lobbyMemberByIndex, "bluesage_moderation_request_v2"), out var request)) { ProcessMemberDataRequest(lobby, ownerSteamId, text2, request); } } } private static void ProcessMemberDataRequest(CSteamID lobby, string ownerSteamId, string senderSteamId, ModerationRequest request) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) long num = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (ModerationProtocolPolicy.IsFresh(request, num) && PlayerIdentityEvidenceController.TryResolveExact(senderSteamId, out var _, out var _) && IsUniqueSteamLobbyMember(lobby, senderSteamId) && ModerationAuthorizationPolicy.IsAuthorizedIssuer(senderSteamId, ownerSteamId, Plugin.LobbyHelpers?.Value ?? string.Empty, Plugin.LobbyHelperOwnerSteamId?.Value ?? string.Empty, hasLobby: true) && RememberNonce(request.Nonce) && (!LastIssuerRequest.TryGetValue(senderSteamId, out var value) || num - value >= 2)) { LastIssuerRequest[senderSteamId] = num; string message; bool num2 = TryExecuteHostAction(request.Action, request.TargetSteamId, request.IncidentId, senderSteamId, out message); ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)("Host non-chat moderation request from Steam …" + Suffix(senderSteamId) + ": " + message)); } Plugin.AddLocalNotification((num2 ? "Authorized helper action: " : "Helper action rejected: ") + message); } } internal static bool TryExecuteHostAction(string action, string targetSteamId, string incidentId, string issuerSteamId, out string message) { //IL_00b3: Unknown result type (might be due to invalid IL or missing references) message = "No moderation action was taken."; if (!TryGetOwnedHostLobby(out var lobby, out var ownerSteamId)) { message = "Only the active Steam/PurrNet lobby host can write native moderation state."; return false; } if (!ModerationAuthorizationPolicy.IsAuthorizedIssuer(issuerSteamId, ownerSteamId, Plugin.LobbyHelpers?.Value ?? string.Empty, Plugin.LobbyHelperOwnerSteamId?.Value ?? string.Empty, hasLobby: true)) { message = "The issuing Steam ID is not on this host's Helper list."; return false; } string incidentId2 = ((!string.Equals(issuerSteamId, ownerSteamId, StringComparison.Ordinal) && !string.Equals(incidentId, "none", StringComparison.OrdinalIgnoreCase)) ? ("issuer-local:" + incidentId) : incidentId); if (!(action == "ban")) { if (action == "unban") { return TryUnban(targetSteamId, issuerSteamId, incidentId2, out message); } return false; } return TryBan(lobby, ownerSteamId, issuerSteamId, targetSteamId, incidentId2, out message); } internal static string[] GetCurrentPlayerSteamIds() { string error; return PlayerIdentityEvidenceController.GetVerifiedCurrentSteamIds(out error).ToArray(); } internal static string[] GetBannedSteamIds() { try { return MonoSingleton.I?.BanData?.BanServerPlayers?.Where(ModerationProtocolPolicy.IsSteamId64).Distinct(StringComparer.Ordinal).ToArray() ?? Array.Empty(); } catch { return Array.Empty(); } } internal static IReadOnlyList GetNativeBanRecords() { if (!TryGetNativeBanRecords(out var records, out var _)) { return Array.Empty(); } return records; } internal static bool TryGetNativeBanRecords(out IReadOnlyList records, out string error) { records = Array.Empty(); error = string.Empty; if (!Plugin.CanManageHelpers()) { error = "current client is not the verified host"; return false; } try { BanData val = MonoSingleton.I?.BanData; if (val?.BanServerPlayers == null || val.BanServerPlayerNicks == null || val.BanServerPlayers.Count != val.BanServerPlayerNicks.Count) { error = "native ban storage is unavailable or misaligned"; return false; } List list = new List(); HashSet hashSet = new HashSet(StringComparer.Ordinal); for (int i = 0; i < val.BanServerPlayers.Count; i++) { string text = val.BanServerPlayers[i] ?? string.Empty; if (ModerationProtocolPolicy.IsSteamId64(text)) { if (!hashSet.Add(text)) { error = "native ban storage contains duplicate SteamID64 rows"; return false; } list.Add(new NativeBanRecord(text, val.BanServerPlayerNicks[i] ?? string.Empty)); } } records = list; return true; } catch (Exception ex) { error = "native ban storage read failed safely (" + ex.GetType().Name + ")"; return false; } } private static bool TryBan(CSteamID lobby, string ownerSteamId, string issuerSteamId, string targetSteamId, string incidentId, out string message) { //IL_00ac: Unknown result type (might be due to invalid IL or missing references) //IL_03fe: Unknown result type (might be due to invalid IL or missing references) //IL_0406: Unknown result type (might be due to invalid IL or missing references) //IL_040c: Unknown result type (might be due to invalid IL or missing references) message = "No ban was made."; PlayerPanelController panel = NetworkSingleton.I; if (panel?.PlayerSteamIDs == null || panel.PlayerIDs == null || panel.PlayerSteamIDs.Count != panel.PlayerIDs.Count) { message = "The native player roster is missing or misaligned."; return false; } int targetIndex = panel.PlayerSteamIDs.FindIndex((string id) => string.Equals(id, targetSteamId, StringComparison.Ordinal)); int num = panel.PlayerSteamIDs.Count((string id) => string.Equals(id, targetSteamId, StringComparison.Ordinal)); int num2 = CountSteamLobbyMembers(lobby, targetSteamId); bool flag = targetIndex >= 0 && targetIndex < panel.PlayerIDs.Count && panel.PlayerIDs.Count((PlayerID id) => ((PlayerID)(ref id)).Equals(panel.PlayerIDs[targetIndex])) == 1; bool flag2 = targetIndex >= 0 && num == 1 && targetIndex < panel.PlayerIDs.Count && flag && num2 == 1; bool flag3 = OfflineBanPolicy.IsConfirmedAbsentFromCurrentLobby(num2); bool targetIsProtectedVictim = CloneIncidentLedger.IsProtectedVictim(targetSteamId); if (!flag2 && !flag3) { message = "Target failed the Steam/PurrNet roster alignment check. No live or offline ban was saved."; return false; } if (flag2) { if (!ModerationAuthorizationPolicy.CanBanTarget(targetSteamId, ownerSteamId, Plugin.LobbyHelpers?.Value ?? string.Empty, issuerSteamId, targetIsProtectedVictim, targetIsPresent: true)) { message = "Target failed the live Steam/PurrNet safety checks (protected victim, host, helper, issuer, or ambiguous)."; return false; } } else if (!OfflineBanPolicy.CanSave(ownerSteamId, issuerSteamId, targetSteamId, Plugin.LobbyHelpers?.Value ?? string.Empty, targetIsProtectedVictim, targetIsAbsent: true, out message)) { return false; } DataManager i = MonoSingleton.I; BanData val = i?.BanData; if (val?.BanServerPlayers == null || val.BanServerPlayerNicks == null || val.BanServerPlayers.Count != val.BanServerPlayerNicks.Count) { message = "Native ban storage is unavailable or its paired lists are inconsistent."; return false; } if (val.BanServerPlayers.Contains(targetSteamId)) { message = "Steam …" + Suffix(targetSteamId) + " is already in the host's native ban list."; return true; } string text = (flag3 ? SafeOfflinePersona(targetSteamId) : SafePersona(targetSteamId)); string displayNameRaw = text; string persona = text; string targetRole = (flag3 ? "ABSENT" : "PLAYER"); if (flag2 && PlayerIdentityEvidenceController.TryResolveExact(targetSteamId, out var evidence, out var _)) { displayNameRaw = evidence.DisplayNameRaw; persona = evidence.SteamPersona; targetRole = evidence.Role; } val.BanServerPlayers.Add(targetSteamId); val.BanServerPlayerNicks.Add(text); SessionAuditController.ArmPersistedModerationAudit("ban-added", flag3 ? "bluesage-sidban-offline" : "bluesage-sidban-live", targetSteamId, displayNameRaw, persona, issuerSteamId, incidentId, targetRole); try { i.SaveBanData(); } catch (Exception ex) { SessionAuditController.CancelPersistedModerationAudit(); val.BanServerPlayers.RemoveAt(val.BanServerPlayers.Count - 1); val.BanServerPlayerNicks.RemoveAt(val.BanServerPlayerNicks.Count - 1); message = "Native ban save failed and was rolled back: " + ex.GetType().Name + "."; return false; } if (flag3) { message = "Offline native ban saved for exact SteamID64 " + targetSteamId + ". The player was not in the current lobby, so no kick was attempted. Use /sidunban " + targetSteamId + " to reverse it."; return true; } ChalkboardPersistenceController.NotifyModerationBoardReview(targetSteamId); PlayerController val2 = NetworkSingleton.I?.MainPlayerController; if ((Object)(object)val2 == (Object)null) { message = "Persisted native ban for Steam …" + Suffix(targetSteamId) + ", but the current-player kick could not be confirmed."; return true; } try { val2.BanRPC(panel.PlayerIDs[targetIndex], true, default(RPCInfo)); message = "Native ban applied to suspect Steam …" + Suffix(targetSteamId) + ((incidentId == "none") ? "." : (" from " + incidentId + ".")) + " Victim IDs were not targeted."; return true; } catch (Exception ex2) { message = "Persisted native ban for Steam …" + Suffix(targetSteamId) + ", but live kick raised " + ex2.GetType().Name + "."; return true; } } private static bool TryUnban(string targetSteamId, string issuerSteamId, string incidentId, out string message) { message = "No unban was made."; DataManager i = MonoSingleton.I; BanData val = i?.BanData; if (val?.BanServerPlayers == null || val.BanServerPlayerNicks == null || val.BanServerPlayers.Count != val.BanServerPlayerNicks.Count) { message = "Native ban storage is unavailable or its paired lists are inconsistent."; return false; } int num = val.BanServerPlayers.FindIndex((string id) => string.Equals(id, targetSteamId, StringComparison.Ordinal)); if (num < 0) { message = "Steam …" + Suffix(targetSteamId) + " is not in the host's native ban list."; return false; } string item = val.BanServerPlayers[num]; string text = val.BanServerPlayerNicks[num]; val.BanServerPlayers.RemoveAt(num); val.BanServerPlayerNicks.RemoveAt(num); SessionAuditController.ArmPersistedModerationAudit("ban-removed", "bluesage-sidunban", targetSteamId, text, SafePersona(targetSteamId), issuerSteamId, incidentId, "SAVED BAN"); try { i.SaveBanData(); message = "Native ban removed for Steam …" + Suffix(targetSteamId) + "."; return true; } catch (Exception ex) { SessionAuditController.CancelPersistedModerationAudit(); val.BanServerPlayers.Insert(num, item); val.BanServerPlayerNicks.Insert(num, text); message = "Native unban save failed and was rolled back: " + ex.GetType().Name + "."; return false; } } internal static bool TryGetOwnedHostLobby(out CSteamID lobby, out string ownerSteamId) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_001b: 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_004f: 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_0059: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) lobby = CSteamID.Nil; ownerSteamId = string.Empty; if (!Plugin.TryGetRescueLobby(out lobby) || lobby == CSteamID.Nil || !SteamManager.Initialized || (Object)(object)NetworkManager.main == (Object)null || !NetworkManager.main.isHost) { return false; } CSteamID lobbyOwner = SteamMatchmaking.GetLobbyOwner(lobby); CSteamID steamID = SteamUser.GetSteamID(); if (lobbyOwner == CSteamID.Nil || lobbyOwner != steamID) { return false; } ownerSteamId = ((ulong)lobbyOwner).ToString(); return true; } private static bool IsUniqueSteamLobbyMember(CSteamID lobby, string steamId) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) return CountSteamLobbyMembers(lobby, steamId) == 1; } private static int CountSteamLobbyMembers(CSteamID lobby, string steamId) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) int num = 0; int numLobbyMembers = SteamMatchmaking.GetNumLobbyMembers(lobby); for (int i = 0; i < numLobbyMembers; i++) { if (string.Equals(((ulong)SteamMatchmaking.GetLobbyMemberByIndex(lobby, i)).ToString(), steamId, StringComparison.Ordinal)) { num++; } } return num; } private static bool RememberNonce(string nonce) { if (!SeenNonces.Add(nonce)) { return false; } NonceOrder.Enqueue(nonce); while (NonceOrder.Count > 128) { SeenNonces.Remove(NonceOrder.Dequeue()); } return true; } private static string SafePersona(string steamId) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) if (!ulong.TryParse(steamId, out var result)) { return "Steam …" + Suffix(steamId); } string text = SteamFriends.GetFriendPersonaName(new CSteamID(result)) ?? string.Empty; text = text.Replace("\r", " ").Replace("\n", " ").Trim(); if (string.IsNullOrWhiteSpace(text)) { return "Steam …" + Suffix(steamId); } if (text.Length <= 48) { return text; } return text.Substring(0, 48); } private static string SafeOfflinePersona(string steamId) { string text = SafePersona(steamId) + " (offline)"; if (text.Length <= 48) { return text; } return text.Substring(0, 48); } private static string Suffix(string steamId) { string text; if (steamId == null || steamId.Length <= 6) { text = steamId; if (text == null) { return "unknown"; } } else { text = steamId.Substring(steamId.Length - 6); } return text; } } internal sealed class SteamRuntimeSnapshot { internal static readonly SteamRuntimeSnapshot Empty = new SteamRuntimeSnapshot(steamWasReady: false, CSteamID.Nil, CSteamID.Nil, localWasOwner: false); internal bool SteamWasReady { get; } internal CSteamID Lobby { get; } internal CSteamID LocalUser { get; } internal bool LocalWasOwner { get; } internal SteamRuntimeSnapshot(bool steamWasReady, CSteamID lobby, CSteamID localUser, bool localWasOwner) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) SteamWasReady = steamWasReady; Lobby = lobby; LocalUser = localUser; LocalWasOwner = localWasOwner; } } internal static class SteamRuntimeFacade { private static readonly object Sync = new object(); private static SteamRuntimeSnapshot _lastKnown = SteamRuntimeSnapshot.Empty; internal static void CaptureLive() { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) if (PluginShutdownController.IsShuttingDown) { return; } try { if (!SteamManager.Initialized || !Plugin.TryGetRescueLobby(out var lobbyId) || lobbyId == CSteamID.Nil) { return; } CSteamID steamID = SteamUser.GetSteamID(); if (steamID == CSteamID.Nil) { return; } lock (Sync) { _lastKnown = new SteamRuntimeSnapshot(steamWasReady: true, lobbyId, steamID, SteamMatchmaking.GetLobbyOwner(lobbyId) == steamID); } } catch { } } internal static SteamRuntimeSnapshot SnapshotForShutdown() { lock (Sync) { return _lastKnown; } } } internal interface IStewardPipeHost { bool TryStart(string keyPath, string serviceIdentity, Action accepted, out string boundedError); void Stop(); } internal sealed class StewardAnnouncementBridgeController : MonoBehaviour { private const string WindowsHostTypeName = "BlueSage.QoLTweaks.WindowsStewardPipeHost"; private const string StewardPipeName = "BlueSage.OnTogether.Steward.v1"; private const int MaximumQueuedAnnouncements = 8; private const int MaximumPerFrame = 2; private readonly object _queueSync = new object(); private readonly Queue _queue = new Queue(8); private readonly StewardAnnouncementReplayCache _replay = new StewardAnnouncementReplayCache(); private readonly StewardAnnouncementRateLimiter _rate = new StewardAnnouncementRateLimiter(); private IStewardPipeHost _host; private string _replayPath = string.Empty; private bool _started; private void Awake() { //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Invalid comparison between Unknown and I4 if (!string.Equals("BlueSage.OnTogether.Steward.v1", "BlueSage.OnTogether.Steward.v1", StringComparison.Ordinal) || Plugin.EnableStewardAnnouncementBridge == null || !Plugin.EnableStewardAnnouncementBridge.Value || (int)Application.platform != 2) { return; } Type type = Assembly.GetExecutingAssembly().GetType("BlueSage.QoLTweaks.WindowsStewardPipeHost", throwOnError: false); if (type == null || !typeof(IStewardPipeHost).IsAssignableFrom(type)) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)"Steward announcement bridge could not start: Windows pipe host is unavailable."); } return; } _replayPath = Path.Combine(Paths.ConfigPath, "BlueSageSteward", "steward-replay-cache.v1"); LoadReplay(); try { _host = (IStewardPipeHost)Activator.CreateInstance(type); _started = _host.TryStart(Plugin.StewardBridgeKeyPath?.Value ?? string.Empty, Plugin.StewardBridgeServiceIdentity?.Value ?? string.Empty, EnqueueAuthenticated, out var boundedError); if (!_started) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)("Steward announcement bridge stayed off: " + BoundError(boundedError))); } } } catch { _started = false; _host = null; ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogWarning((object)"Steward announcement bridge stayed off after local initialization failed."); } } } private void Update() { if (!_started || PluginShutdownController.IsShuttingDown) { return; } for (int i = 0; i < 2; i++) { StewardAnnouncementEnvelope envelope; lock (_queueSync) { if (_queue.Count == 0) { break; } envelope = _queue.Dequeue(); } TryPresent(envelope); } } private void OnDestroy() { try { _host?.Stop(); } catch { } _host = null; _started = false; lock (_queueSync) { _queue.Clear(); } } private void EnqueueAuthenticated(StewardAnnouncementEnvelope envelope) { if (envelope == null || PluginShutdownController.IsShuttingDown) { return; } lock (_queueSync) { if (_queue.Count < 8) { _queue.Enqueue(envelope); } } } private void TryPresent(StewardAnnouncementEnvelope envelope) { //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00ce: Invalid comparison between Unknown and I4 //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0041: 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_0048: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) long ticks = DateTime.UtcNow.Ticks; bool steamOwnerMatchesLocal = false; bool purrNetHost = false; string currentLobbyToken = string.Empty; TextChannelManager val = null; try { if (SteamManager.Initialized && Plugin.TryGetRescueLobby(out var lobbyId) && lobbyId != CSteamID.Nil) { CSteamID steamID = SteamUser.GetSteamID(); CSteamID lobbyOwner = SteamMatchmaking.GetLobbyOwner(lobbyId); steamOwnerMatchesLocal = steamID != CSteamID.Nil && lobbyOwner == steamID; purrNetHost = (Object)(object)NetworkManager.main != (Object)null && NetworkManager.main.isHost; currentLobbyToken = StewardAnnouncementPolicy.FingerprintLobby(((ulong)lobbyId).ToString(), "0.2.4+20260730.1-public-24414155-release"); val = NetworkSingleton.I; } } catch { steamOwnerMatchesLocal = false; purrNetHost = false; currentLobbyToken = string.Empty; val = null; } StewardAnnouncementContext context = new StewardAnnouncementContext(Plugin.EnableStewardAnnouncementBridge != null && Plugin.EnableStewardAnnouncementBridge.Value, (int)Application.platform == 2, PluginShutdownController.IsShuttingDown, steamOwnerMatchesLocal, purrNetHost, (Object)(object)val != (Object)null, "0.2.4+20260730.1-public-24414155-release", currentLobbyToken, ticks); if (StewardAnnouncementPolicy.TryAuthorize(envelope, context, _replay, _rate, out string safeText, out string _)) { val.AddNotification(safeText); PersistReplay(ticks); } } private void LoadReplay() { try { if (File.Exists(_replayPath)) { _replay.Import(File.ReadAllText(_replayPath), DateTime.UtcNow.Ticks); } } catch { } } private void PersistReplay(long nowUtcTicks) { try { AtomicReplace(_replayPath, _replay.Export(nowUtcTicks)); } catch { } } private static void AtomicReplace(string path, string content) { string directoryName = Path.GetDirectoryName(path); if (string.IsNullOrWhiteSpace(directoryName)) { return; } Directory.CreateDirectory(directoryName); string text = path + ".tmp"; string text2 = path + ".bak"; File.WriteAllText(text, content ?? string.Empty); if (File.Exists(path)) { try { File.Replace(text, path, text2, ignoreMetadataErrors: true); TryDelete(text2); return; } catch { TryDelete(text); return; } } File.Move(text, path); } private static void TryDelete(string path) { try { if (File.Exists(path)) { File.Delete(path); } } catch { } } private static string BoundError(string value) { string text = (value ?? string.Empty).Replace("\r", " ").Replace("\n", " ").Trim(); if (text.Length > 120) { return text.Substring(0, 120); } return text; } } internal sealed class StyleHelperWindow : MonoBehaviour { private struct StyleHelperSnapshot { public string Action; public string Text; public string ColorA; public string ColorB; public string ColorC; public string Status; public string StatusColorDraft; public string Preview; public string Message; public bool PreviewIsValid; public string DetectedNamePreview; public bool Bold; public bool Italic; public int Mode; public int Tab; public int ActiveColorSlot; public bool RestoreName; public bool RestoreStatus; public string PlayerName; public string PlayerStatus; public string PlayerStatusColor; public string SpoonCountText; public string SpoonLabelText; public string StateLabel; public string StateDetail; public bool RestoreSpoons; public bool SpoonsEnabled; public int SpoonCount; public string SavedSpoonLabel; public string SpoonsUpdatedUtc; } private const string PlaceholderName = "YOURNAMEHERE"; private const float MinWindowWidth = 920f; private const float MinWindowHeight = 640f; private const float ResizeGripSize = 42f; private const float FooterHeight = 156f; private const float CompactFooterHeight = 198f; private const float ContentChromeHeight = 78f; private const string DefaultColorA = "0E6EED"; private const string DefaultColorB = "A59"; private const string DefaultColorC = "D85"; private const string NameStylerDescription = "Build styled name/chat text with colors, bold, italic, and gradients."; private const string StatusStylerDescription = "Set your activity tag and Spoons together without overwriting your styled name."; private const string TemplatesDescription = "Save or load five local name-style templates. Status and Spoons stay separate."; private static readonly string[] PresetColors = new string[84] { "0E6EED", "1E90FF", "5AC8FA", "00C7BE", "70FFBD", "34C759", "FFD45D", "FFB347", "FF9500", "FF6B8A", "FF2D55", "FF3B30", "AF52DE", "9B59B6", "8833EE", "A550A5", "D88555", "C7A17A", "FFFFFF", "F7E7CE", "D8CAB8", "9B8E7E", "7B5B63", "2D1B2F", "FFFD82", "B8FF70", "70D6FF", "B388FF", "FFAFCC", "CDB4DB", "000000", "808080", "C0C0C0", "800000", "FF0000", "FFA500", "FFFF00", "808000", "008000", "00FF00", "008080", "00FFFF", "000080", "0000FF", "800080", "FF00FF", "4B0082", "F5DEB3", "CD853F", "D2691E", "8B4513", "2F4F4F", "4682B4", "DA70D6", "FF69B4", "FF1493", "DB7093", "E6E6FA", "DDA0DD", "BA55D3", "7FFFD4", "40E0D0", "48D1CC", "20B2AA", "66CDAA", "00FA9A", "ADFF2F", "7CFC00", "32CD32", "228B22", "556B2F", "6B8E23", "FFFACD", "FFE4B5", "FFDAB9", "F4A460", "DEB887", "A0522D", "B0E0E6", "87CEEB", "6495ED", "4169E1", "191970", "708090" }; private Rect _windowRect = new Rect(80f, 80f, 960f, 720f); private bool _visible; private bool _hasOpened; private string _text = "YOURNAMEHERE"; private string _colorA = "0E6EED"; private string _colorB = "A59"; private string _colorC = "D85"; private string _status = "BRB"; private string _statusColor = "FFD45D"; private string _spoonCountText = "5"; private string _spoonLabelText = "sp"; private string _preview = string.Empty; private string _message = "Pick a tab, choose colors, then Apply or Copy."; private bool _previewIsValid; private bool _bold; private bool _italic; private int _mode = 3; private int _tab; private int _activeColorSlot; private string _detectedNamePreview = string.Empty; private Vector2 _scroll; private Vector2 _contentScroll; private GUIStyle _windowStyle; private GUIStyle _headerBoxStyle; private GUIStyle _headerStyle; private GUIStyle _labelStyle; private GUIStyle _smallStyle; private GUIStyle _buttonStyle; private GUIStyle _activeButtonStyle; private GUIStyle _boxStyle; private GUIStyle _footerBoxStyle; private GUIStyle _footerTextStyle; private GUIStyle _textFieldStyle; private GUIStyle _generatedTextStyle; private GUIStyle _swatchButtonStyle; private GUIStyle _previewStyle; private Texture2D _windowBackgroundTexture; private bool _isResizing; private string _lastThemeKey = string.Empty; private int _renderedTextureGeneration; private BlueSageUiThemePalette _theme; private readonly Stack _undoHistory = new Stack(); private readonly Stack _redoHistory = new Stack(); private string _stateLabel = "Saved"; private string _stateDetail = "Detected current in-game values."; private string _lastOwnedFooterTooltip = string.Empty; internal bool IsVisible => _visible; private bool _hasUndo => _undoHistory.Count > 0; public void ToggleVisible() { //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0089: 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) _visible = !_visible; BlueSageWindowCoordinator.SetVisible(BlueSagePublicWindow.StyleHelper, _visible); if (!_visible) { BlueSageWindowHoverScope.UnregisterWindow(47057); } if (_visible) { _lastOwnedFooterTooltip = string.Empty; WindowFitResult windowFitResult = BlueSageWindowCoordinator.ResolveForOpen(BlueSagePublicWindow.StyleHelper, _hasOpened, _windowRect, 920f, 640f, Screen.width, Screen.height); _windowRect = new Rect(windowFitResult.X, windowFitResult.Y, windowFitResult.Width, windowFitResult.Height); _hasOpened = true; ResetTransientEditorStateForLiveSync(resetTab: true); DetectedDisplayNameState detectedDisplayNameState = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.GetCurrentDisplayNameStateForStyleUi() : null); HydrateCurrentName(force: true, detectedDisplayNameState); HydrateCurrentStatus(detectedDisplayNameState); HydrateSpoons(detectedDisplayNameState); RebuildPreview(); MarkSaved(detectedDisplayNameState?.DriftSummary ?? "Detected current in-game values. Edit a field to start a draft."); } } private void OnGUI() { //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Expected O, but got Unknown //IL_006b: 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) if (!_visible || Plugin.EnableStyleUi == null || !Plugin.EnableStyleUi.Value) { BlueSageWindowCoordinator.SetVisible(BlueSagePublicWindow.StyleHelper, visible: false); return; } BlueSageWindowCoordinator.SetVisible(BlueSagePublicWindow.StyleHelper, visible: true); EnsureStyles(); ClampWindowToScreen(); BlueSageWindowHoverScope.RegisterWindow(47057, _windowRect, 10); _windowRect = GUI.Window(47057, _windowRect, new WindowFunction(DrawWindow), string.Empty, _windowStyle); ClampWindowToScreen(); } private void DrawWindow(int id) { //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_008b: 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_003e: Unknown result type (might be due to invalid IL or missing references) //IL_00ee: Unknown result type (might be due to invalid IL or missing references) string tooltip = GUI.tooltip; GUI.tooltip = string.Empty; if ((Object)(object)_windowBackgroundTexture != (Object)null) { GUI.DrawTexture(new Rect(0f, 0f, ((Rect)(ref _windowRect)).width, ((Rect)(ref _windowRect)).height), (Texture)(object)_windowBackgroundTexture); } DrawHeader(); GUILayout.Space(10f); DrawTabs(); GUILayout.Space(8f); _contentScroll = GUILayout.BeginScrollView(_contentScroll, false, true, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(GetContentScrollHeight()) }); if (_tab == 0) { DrawStyleTab(); } else if (_tab == 1) { DrawStatusTab(); } else { DrawTemplatesTab(); } GUILayout.EndScrollView(); DrawFooter(tooltip); DrawResizeGrip(); GUI.DragWindow(new Rect(0f, 0f, ((Rect)(ref _windowRect)).width - 138f, 34f)); } private void DrawHeader() { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: Expected O, but got Unknown //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Unknown result type (might be due to invalid IL or missing references) //IL_00ee: Expected O, but got Unknown //IL_011a: Unknown result type (might be due to invalid IL or missing references) //IL_0129: Unknown result type (might be due to invalid IL or missing references) //IL_0139: Expected O, but got Unknown //IL_0165: Unknown result type (might be due to invalid IL or missing references) //IL_0174: Unknown result type (might be due to invalid IL or missing references) //IL_0184: Expected O, but got Unknown GUI.Box(new Rect(0f, 0f, ((Rect)(ref _windowRect)).width, 38f), GUIContent.none, _headerBoxStyle); GUI.Label(new Rect(18f, 7f, ((Rect)(ref _windowRect)).width - 330f, 28f), "▣ BlueSage Style Helper v0.2.4", _headerStyle); if (GUI.Button(new Rect(((Rect)(ref _windowRect)).width - 308f, 5f, 108f, 28f), new GUIContent("Thunderstore", "Open the BlueSage QoL Tweaks Thunderstore page."), _buttonStyle)) { Application.OpenURL("https://thunderstore.io/c/on-together/p/Blues/BlueSage_QoL_Tweaks_Beta/"); } if (GUI.Button(new Rect(((Rect)(ref _windowRect)).width - 192f, 5f, 58f, 28f), new GUIContent("Ko-fi", "Support Blue's community modding work on Ko-fi."), _buttonStyle)) { Application.OpenURL("https://ko-fi.com/Q5Q1JRPW"); } if (GUI.Button(new Rect(((Rect)(ref _windowRect)).width - 126f, 5f, 76f, 28f), new GUIContent("Discord", "Open Blue's On-Together community Discord."), _buttonStyle)) { Application.OpenURL("https://discord.gg/JujMEwtN3q"); } if (GUI.Button(new Rect(((Rect)(ref _windowRect)).width - 42f, 5f, 30f, 28f), new GUIContent("X", "Close this window"), _activeButtonStyle)) { _visible = false; BlueSageWindowCoordinator.SetVisible(BlueSagePublicWindow.StyleHelper, visible: false); BlueSageWindowHoverScope.UnregisterWindow(47057); } } private float GetContentScrollHeight() { //IL_0002: Unknown result type (might be due to invalid IL or missing references) return Mathf.Max(36f, BlueSageHelpFrame.Resolve(BlueSageHelpFrameKind.StyleHelper, _windowRect).ContentHeight); } private float GetFooterHeight() { //IL_0002: Unknown result type (might be due to invalid IL or missing references) return BlueSageHelpFrame.Resolve(BlueSageHelpFrameKind.StyleHelper, _windowRect).FooterHeight; } private void DrawTabs() { GUILayout.BeginHorizontal(Array.Empty()); DrawTabButton(0, "Name Styler", "Build styled name/chat text with colors, bold, italic, and gradients."); DrawTabButton(1, "Status Styler", "Set your activity tag and Spoons together without overwriting your styled name."); DrawTabButton(2, "Templates", "Save or load five local name-style templates. Status and Spoons stay separate."); GUILayout.EndHorizontal(); } private void DrawTabButton(int index, string label, string tooltip) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Expected O, but got Unknown //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) bool flag = _tab == index; if (GUILayout.Button(new GUIContent(label, tooltip), flag ? _activeButtonStyle : _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(32f) })) { if (_tab != index) { _tab = index; _contentScroll = Vector2.zero; } RebuildPreview(); } } private void DrawStyleTab() { GUILayout.BeginVertical(_boxStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandHeight(false) }); GUILayout.Label("NAME STYLER: type the name text players should see, choose color/bold/italic/gradient, then use Apply Name Styler or Copy Name Style Text.", _smallStyle, Array.Empty()); GUILayout.Label("Tip: choose Color A/B/C first, then tap a swatch. The highlighted slot is the one the shared color library will change.", _smallStyle, Array.Empty()); GUILayout.Label("Detected rich-text tags stay visible and editable here, including nested , , and . Undo/Redo keeps up to 50 steps.", _smallStyle, Array.Empty()); GUILayout.Space(4f); GUILayout.Label("Text", _labelStyle, Array.Empty()); string text = GUILayout.TextArea(_text ?? string.Empty, _textFieldStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.MinHeight(54f), GUILayout.MaxHeight(92f) }); if (text != _text) { CaptureUndo("Text edit"); _text = text; ClearDetectedNamePreview(); RebuildPreview(); MarkDraft("Name draft changed. Apply Name Styler or Copy Name Style Text when ready."); } GUILayout.Space(6f); DrawColorRow(); GUILayout.Space(6f); DrawModeRow(); if ((_text ?? string.Empty).IndexOf('<') >= 0) { GUILayout.Label("Custom Rich Text mode: the exact balanced markup is authoritative. BlueSage verifies tag structure; the game decides whether each tag/value renders. Presets do not rewrite custom markup unless you remove it first.", _smallStyle, Array.Empty()); } GUILayout.Space(6f); DrawPreview(); GUILayout.EndVertical(); } private void DrawStatusTab() { //IL_010b: Unknown result type (might be due to invalid IL or missing references) //IL_012e: Expected O, but got Unknown GUILayout.BeginVertical(_boxStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandHeight(false) }); GUILayout.Label("STATUS STYLER: set the short activity tag after your name, like Rowan (Testing). Spoons is the optional compact [3/5sp] tag.", _smallStyle, Array.Empty()); GUILayout.Label("Tip: Status Styler changes only the activity tag. Name Styler colors stay separate; the color library here updates the status color.", _smallStyle, Array.Empty()); GUILayout.Label("Clear Status removes the live status. Undo tries to restore the last Style Helper apply/clear/edit.", _smallStyle, Array.Empty()); GUILayout.Space(4f); GUILayout.Label("Status", _labelStyle, Array.Empty()); string text = GUILayout.TextArea(_status ?? string.Empty, _textFieldStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.MinHeight(54f), GUILayout.MaxHeight(92f) }); if (text != _status) { CaptureUndo("Status edit"); _status = text; RebuildPreview(); MarkDraft("Status draft changed. Apply Status Styler when ready."); } GUILayout.Space(6f); DrawStatusColorRow(); GUILayout.BeginHorizontal(Array.Empty()); if (GUILayout.Button(new GUIContent("Clear Status", "Remove only the activity/status text from your display name. Your styled base name and Spoons setting remain separate."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(32f) })) { CaptureUndo("Clear Status", restoreName: false, restoreStatus: true); bool applied = false; _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.TryClearStatusFromStyleUi(out applied) : "Style UI: plugin is not ready yet."); MarkApplyOutcome(applied, "Live status cleared. Name and Spoons settings were left alone.", "Status was not cleared; fix the reported problem and try again."); } GUILayout.EndHorizontal(); DrawSpoonsControls(); DrawPreview(); GUILayout.EndVertical(); } private void DrawSpoonsControls() { //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Expected O, but got Unknown //IL_00e3: Unknown result type (might be due to invalid IL or missing references) //IL_0106: Expected O, but got Unknown //IL_0126: Unknown result type (might be due to invalid IL or missing references) //IL_0149: Expected O, but got Unknown //IL_0194: Unknown result type (might be due to invalid IL or missing references) //IL_01b7: Expected O, but got Unknown //IL_0202: Unknown result type (might be due to invalid IL or missing references) //IL_0232: Expected O, but got Unknown GUILayout.Space(8f); GUILayout.Label("Spoons", _labelStyle, Array.Empty()); GUILayout.Label("Optional compact social-battery tag, like [3/5sp]. Default is hidden; Save Spoons or Show Tag opts in, Hide Tag removes it from your name.", _smallStyle, Array.Empty()); DrawSpoonCustomLabelRow(); GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label(new GUIContent("Spoons 0-5", "Set your optional spoon tag from 0 to 5. Lower numbers can mean lower social battery."), _labelStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(120f) }); string text = _spoonCountText ?? string.Empty; string text2 = GUILayout.TextField(text, 3, _textFieldStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(80f), GUILayout.Height(28f) }); if (!string.Equals(text2, text, StringComparison.Ordinal)) { CaptureUndo("Spoons edit"); _spoonCountText = text2; MarkDraft("Spoons draft changed. Save Spoons or Show Tag to apply it."); } if (GUILayout.Button(new GUIContent("Save Spoons", "Save the compact [0/5sp]-[5/5sp] tag and apply it to your display name."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(28f) })) { CaptureUndo("Save Spoons", restoreName: true, restoreStatus: false, restoreSpoons: true); SaveSpoonsFromStyleUi(); } if (GUILayout.Button(new GUIContent("Show Tag", "Show your saved [0/5sp]-[5/5sp] tag in your display name."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(28f) })) { CaptureUndo("Show Spoon Tag", restoreName: true, restoreStatus: false, restoreSpoons: true); if ((Object)(object)Plugin.Instance != (Object)null) { _message = Plugin.Instance.TrySetSpoonVisibilityFromMenu(enabled: true, out var applied); MarkApplyOutcome(applied, "Spoons tag is visible in your name.", "Spoons stayed unchanged; fix the reported problem and try again."); } } if (GUILayout.Button(new GUIContent("Hide Tag", "Hide the optional [spoons] tag from your display name. Your saved spoon number stays here for later."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(28f) })) { CaptureUndo("Hide Spoon Tag", restoreName: true, restoreStatus: false, restoreSpoons: true); if ((Object)(object)Plugin.Instance != (Object)null) { _message = Plugin.Instance.TrySetSpoonVisibilityFromMenu(enabled: false, out var applied2); MarkApplyOutcome(applied2, "Spoons tag is hidden. Saved spoon number remains here.", "Spoons stayed unchanged; fix the reported problem and try again."); } } if (GUILayout.Button(new GUIContent("Undo Spoons", "Undo only the last Spoons save/show/hide action. Name, status, and template undo still live in the main footer Undo."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(104f), GUILayout.Height(28f) })) { UndoSpoonsLastAction(); } GUILayout.EndHorizontal(); } private void DrawSpoonCustomLabelRow() { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Expected O, but got Unknown GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label(new GUIContent("Custom label", "Optional 1-12 character label after 0/5. Default 'sp' makes [3/5sp]; try energy or spoons."), _labelStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(120f) }); string text = GUILayout.TextField(_spoonLabelText ?? string.Empty, 12, _textFieldStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(140f), GUILayout.Height(28f) }); if (!string.Equals(text, _spoonLabelText, StringComparison.Ordinal)) { CaptureUndo("Mood label edit"); _spoonLabelText = text; MarkDraft("Mood label changed. Save Spoons applies it; /setmood does the same from chat."); } GUILayout.Label("Default is 'sp'. /setmood default restores it.", _smallStyle, Array.Empty()); GUILayout.EndHorizontal(); } private void DrawStatusColorRow() { DrawStatusColorPicker(); GUILayout.Label("Status color library: tap a swatch to color only the status tag.", _labelStyle, Array.Empty()); DrawPresetColorLibrary((string preset) => new GUIContent("#" + preset, "Use #" + preset + " for only your status tag. It will not recolor your styled name."), delegate(string preset) { CaptureUndo("Status color swatch"); _statusColor = preset; _message = "Status color set to #" + preset + "."; RebuildPreview(); MarkDraft("Status color draft changed. Apply Status Styler when ready."); }); } private void DrawStatusColorPicker() { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Expected O, but got Unknown //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00b0: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_00f5: Expected O, but got Unknown //IL_00f6: Unknown result type (might be due to invalid IL or missing references) GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label(new GUIContent("Status color", "This color belongs only to Status Styler. It will not change Name Styler Color A."), _labelStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(120f) }); string text = _statusColor ?? string.Empty; string text2 = GUILayout.TextField(text, 8, _textFieldStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(112f), GUILayout.Height(28f) }); if (text2 != text) { CaptureUndo("Status color edit"); _statusColor = text2; RebuildPreview(); MarkDraft("Status color draft changed. Apply Status Styler when ready."); } Color backgroundColor = GUI.backgroundColor; GUI.backgroundColor = HexToColor(_statusColor, backgroundColor); GUILayout.Button(new GUIContent(" ", "Current color preview for your status tag only."), _swatchButtonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(38f), GUILayout.Height(28f) }); GUI.backgroundColor = backgroundColor; GUILayout.EndHorizontal(); } private void DrawTemplatesTab() { //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Expected O, but got Unknown //IL_00bb: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Expected O, but got Unknown //IL_00fd: Unknown result type (might be due to invalid IL or missing references) //IL_0120: Expected O, but got Unknown GUILayout.BeginVertical(_boxStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandHeight(false) }); GUILayout.Label("Tip: Templates save name styling only: text, colors, style mode, bold, and italic. Load applies the saved name style right away; Status and Spoons stay separate.", _smallStyle, Array.Empty()); for (int i = 1; i <= 5; i++) { GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label("Template " + i, _labelStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(110f) }); if (GUILayout.Button(new GUIContent("Save " + i, "Save the current Style tab text/colors/mode into this local template slot."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) })) { SaveTemplate(i); } if (GUILayout.Button(new GUIContent("Undo", "Undo the last template save/load or other Style Helper action."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) })) { UndoLastAction(); } if (GUILayout.Button(new GUIContent("Load " + i, "Load and apply this saved name-style template now. Use Undo if it was not what you wanted."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) })) { LoadTemplate(i); } GUILayout.EndHorizontal(); } GUILayout.EndVertical(); } private void DrawColorRow() { DrawColorPicker("Color A", 0); DrawColorPicker("Color B", 1); DrawColorPicker("Color C", 2); GUILayout.Label("Color library: tap Color A/B/C first, then tap a swatch. Color mode uses A, Gradient uses A/B, Gradient 3 uses A/B/C.", _labelStyle, Array.Empty()); DrawPresetColorLibrary((string preset) => new GUIContent("#" + preset, "Apply #" + preset + " to Color " + (char)(65 + _activeColorSlot) + " in the editor preview."), delegate(string preset) { CaptureUndo("Color swatch"); SetColorSlot(_activeColorSlot, preset); RebuildPreview(); MarkDraft("Color draft changed. Apply Name Styler or Copy Name Style Text when ready."); }); } private void DrawPresetColorLibrary(Func buildContent, Action applyPreset) { //IL_0059: 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_0060: 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_0069: Unknown result type (might be due to invalid IL or missing references) //IL_006b: 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) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00ca: Unknown result type (might be due to invalid IL or missing references) int num = Mathf.Clamp(Mathf.FloorToInt((((Rect)(ref _windowRect)).width - 72f) / 96f), 4, 9); int num2 = Mathf.CeilToInt((float)PresetColors.Length / (float)num); for (int i = 0; i < num2; i++) { GUILayout.BeginHorizontal(Array.Empty()); for (int j = i * num; j < Math.Min(PresetColors.Length, i * num + num); j++) { string text = PresetColors[j]; Color backgroundColor = GUI.backgroundColor; Color contentColor = GUI.contentColor; GUI.contentColor = BlueSageUiTheme.BestTextColor(GUI.backgroundColor = HexToColor(text, backgroundColor), _theme); if (GUILayout.Button(buildContent(text), _swatchButtonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.MinWidth(84f), GUILayout.Height(28f) })) { applyPreset(text); } GUI.contentColor = contentColor; GUI.backgroundColor = backgroundColor; } GUILayout.EndHorizontal(); } } private void DrawColorPicker(string label, int slot) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Expected O, but got Unknown //IL_00d5: Unknown result type (might be due to invalid IL or missing references) //IL_00da: Unknown result type (might be due to invalid IL or missing references) //IL_00dc: Unknown result type (might be due to invalid IL or missing references) //IL_00dd: Unknown result type (might be due to invalid IL or missing references) //IL_00f1: Unknown result type (might be due to invalid IL or missing references) //IL_0121: Expected O, but got Unknown //IL_0122: Unknown result type (might be due to invalid IL or missing references) GUILayout.BeginHorizontal(Array.Empty()); if (GUILayout.Button(new GUIContent(label, "Choose which color slot the swatch library changes. A is used by every style mode."), (_activeColorSlot == slot) ? _activeButtonStyle : _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(92f), GUILayout.Height(28f) })) { _activeColorSlot = slot; } string colorSlot = GetColorSlot(slot); string text = GUILayout.TextField(colorSlot ?? string.Empty, 8, _textFieldStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(112f), GUILayout.Height(28f) }); if (text != colorSlot) { CaptureUndo(label + " edit"); SetColorSlot(slot, text); RebuildPreview(); MarkDraft(label + " draft changed. Apply when ready."); } Color backgroundColor = GUI.backgroundColor; GUI.backgroundColor = HexToColor(colorSlot, backgroundColor); GUILayout.Button(new GUIContent(" ", "Current color preview for this slot."), _swatchButtonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(38f), GUILayout.Height(28f) }); GUI.backgroundColor = backgroundColor; GUILayout.EndHorizontal(); } private void DrawModeRow() { //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Expected O, but got Unknown //IL_00ba: Unknown result type (might be due to invalid IL or missing references) //IL_00ed: Expected O, but got Unknown //IL_013d: Unknown result type (might be due to invalid IL or missing references) //IL_0171: Expected O, but got Unknown //IL_01b9: Unknown result type (might be due to invalid IL or missing references) //IL_01ed: Expected O, but got Unknown //IL_0235: Unknown result type (might be due to invalid IL or missing references) //IL_0269: Expected O, but got Unknown GUILayout.Label("Style toggles", _labelStyle, Array.Empty()); GUILayout.BeginHorizontal(Array.Empty()); if (GUILayout.Button(new GUIContent(_bold ? "Bold On" : "Bold Off", "Adds bold tags when using Color mode. Gradients ignore bold for cleaner output."), _bold ? _activeButtonStyle : _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) })) { CaptureUndo("Bold toggle"); _bold = !_bold; ClearDetectedNamePreview(); RebuildPreview(); MarkDraft("Bold style draft changed. Apply Name Styler when ready."); } if (GUILayout.Button(new GUIContent(_italic ? "Italic On" : "Italic Off", "Adds italic tags when using Color mode. Gradients ignore italic for cleaner output."), _italic ? _activeButtonStyle : _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) })) { CaptureUndo("Italic toggle"); _italic = !_italic; ClearDetectedNamePreview(); RebuildPreview(); MarkDraft("Italic style draft changed. Apply Name Styler when ready."); } if (GUILayout.Button(new GUIContent((_mode == 1) ? "Color On" : "Color", "Use Color A across the whole text. Best for simple readable names."), (_mode == 1) ? _activeButtonStyle : _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) })) { CaptureUndo("Color mode"); _mode = 1; ClearDetectedNamePreview(); RebuildPreview(); MarkDraft("Color mode draft changed. Apply Name Styler when ready."); } if (GUILayout.Button(new GUIContent((_mode == 2) ? "Gradient On" : "Gradient", "Blend from Color A to Color B across the text."), (_mode == 2) ? _activeButtonStyle : _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) })) { CaptureUndo("Gradient mode"); _mode = 2; ClearDetectedNamePreview(); RebuildPreview(); MarkDraft("Gradient mode draft changed. Apply Name Styler when ready."); } if (GUILayout.Button(new GUIContent((_mode == 3) ? "Gradient 3 On" : "Gradient 3", "Blend from Color A to B to C. Best for playful names, but can create longer hidden text."), (_mode == 3) ? _activeButtonStyle : _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) })) { CaptureUndo("Gradient 3 mode"); _mode = 3; ClearDetectedNamePreview(); RebuildPreview(); MarkDraft("Gradient 3 mode draft changed. Apply Name Styler when ready."); } GUILayout.EndHorizontal(); } private void DrawPreview() { GUILayout.Space(6f); int num = (_previewIsValid ? (_preview ?? string.Empty).Length : ((_tab == 1) ? (_status ?? string.Empty).Length : (_text ?? string.Empty).Length)); GUILayout.Label($"Rich-text length: {num}/{Plugin.LockedMaxIdCardCharacters} characters (tags count too)", _smallStyle, Array.Empty()); GUILayout.Label(_message ?? string.Empty, _smallStyle, Array.Empty()); GUILayout.Box(_previewIsValid ? _preview : "Build a style to preview it here.", _previewStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Height(70f), GUILayout.ExpandWidth(true) }); GUILayout.Label("Copy/paste text", _labelStyle, Array.Empty()); DrawGeneratedTextArea(); } private void DrawGeneratedTextArea() { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) _scroll = GUILayout.BeginScrollView(_scroll, true, true, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height((_mode == 3) ? 116f : 90f) }); GUILayout.TextArea(_preview ?? string.Empty, _generatedTextStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true) }); GUILayout.EndScrollView(); } private void DrawFooter(string inheritedTooltip) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Expected O, but got Unknown //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_00b5: Expected O, but got Unknown //IL_00dd: Unknown result type (might be due to invalid IL or missing references) //IL_0100: Expected O, but got Unknown //IL_0147: Unknown result type (might be due to invalid IL or missing references) //IL_016a: Expected O, but got Unknown //IL_0271: Unknown result type (might be due to invalid IL or missing references) //IL_0294: Expected O, but got Unknown //IL_02ff: Unknown result type (might be due to invalid IL or missing references) //IL_0322: Expected O, but got Unknown HelpFrameLayout layout = BlueSageHelpFrame.Resolve(BlueSageHelpFrameKind.StyleHelper, _windowRect); BlueSageHelpFrame.BeginFooter(_footerBoxStyle, layout); bool flag = ((Rect)(ref _windowRect)).width < 1100f; GUILayout.BeginHorizontal(Array.Empty()); if (GUILayout.Button(new GUIContent("Detect Current", "Re-detect your current in-game name, status, and Spoons into this helper. It does not apply anything by itself."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(34f) })) { CaptureUndo("Detect Current"); ResetToCurrentPlayerState(); } if (GUILayout.Button(new GUIContent("Undo", _hasUndo ? "Undo the last Style Helper apply, clear, template save/load, color pick, or local edit." : "No Style Helper action to undo yet."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(34f) })) { UndoLastAction(); } if (GUILayout.Button(new GUIContent("Redo", (_redoHistory.Count > 0) ? "Redo the last undone Style Helper action." : "No Style Helper action to redo yet."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(34f) })) { RedoLastAction(); } if (flag) { GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(Array.Empty()); } string obj = ((_tab == 1) ? "Apply Status Styler" : "Apply Name Styler"); string text = ((_tab == 1) ? "Apply only the activity/status tag after your current name. Use Save Spoons for the Spoons tag." : "Apply the styled preview to your display name. Status and Spoons stay separate."); if (GUILayout.Button(new GUIContent(obj, text), _activeButtonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(34f) })) { if (_tab == 1 && _previewIsValid) { CaptureUndo("Apply Status Styler", restoreName: false, restoreStatus: true); bool applied = false; _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.TryApplyStatusFromStyleUi(null, _status, CleanColor(_statusColor), out applied) : "Style UI: plugin is not ready yet."); MarkApplyOutcome(applied, "Status Styler applied to your live name.", "Status Styler did not apply; fix the reported problem and try again."); } else if (_tab != 1 && _previewIsValid) { CaptureUndo("Apply Name Styler", restoreName: true); bool applied2 = false; _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.TryApplyStyledNameFromStyleUi(_preview, out applied2) : "Style UI: plugin is not ready yet."); MarkApplyOutcome(applied2, "Name Styler applied to your live name.", "Name Styler did not apply; fix the reported problem and try again."); } else { _message = "Build a valid style before applying it to your name."; MarkDraft("Fix the draft before applying."); } } string text2 = ((_tab == 1) ? "Copy Status Style Text" : "Copy Name Style Text"); if (GUILayout.Button(new GUIContent(text2, "Copy the generated rich text to clipboard so you can paste it into chat, ID card, room, or profile fields."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(34f) })) { if (_previewIsValid) { CaptureUndo(text2); GUIUtility.systemCopyBuffer = _preview ?? string.Empty; _message = "Copied. Paste it into your name, ID card, room, status, or chat field."; Plugin.AddLocalNotification("Style UI: copied styled text."); MarkAction("Copied current style text to clipboard."); } else { _message = "Build a valid style before copying."; MarkDraft("Fix the draft before copying."); } } if (GUILayout.Button(new GUIContent("Clear Draft", "Clear only the current editor draft. It does not change your live name/status; use Apply Status Styler or Clear Status for live status changes."), _buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(34f) })) { CaptureUndo("Clear Draft"); if (_tab == 1) { _status = string.Empty; } else { _text = "YOURNAMEHERE"; ClearDetectedNamePreview(); } _preview = string.Empty; _previewIsValid = false; _message = ((_tab == 1) ? "Status draft cleared only. Use Clear Status to remove your live status." : "Draft cleared. Type your name or load a template."); MarkDraft("Draft cleared locally. Live name/status is unchanged until you apply."); } GUILayout.EndHorizontal(); string stateLine = "State: " + _stateLabel + " — " + _stateDetail; BlueSageHelpFrame.DrawStateAndHelp(_footerTextStyle, layout, stateLine, BuildActiveTabFooterDescription(inheritedTooltip)); BlueSageHelpFrame.EndFooter(); } private string BuildActiveTabFooterDescription(string inheritedTooltip) { string result = ((_tab == 1) ? "Set your activity tag and Spoons together without overwriting your styled name." : ((_tab == 2) ? "Save or load five local name-style templates. Status and Spoons stay separate." : "Build styled name/chat text with colors, bold, italic, and gradients.")); string tooltip = GUI.tooltip; bool flag = string.Equals(inheritedTooltip, _lastOwnedFooterTooltip, StringComparison.Ordinal); if (string.IsNullOrWhiteSpace(tooltip)) { _lastOwnedFooterTooltip = string.Empty; return result; } if (string.Equals(tooltip, inheritedTooltip, StringComparison.Ordinal) && !flag) { _lastOwnedFooterTooltip = string.Empty; return result; } _lastOwnedFooterTooltip = tooltip; return tooltip; } private void RebuildPreview() { if (_tab != 1 && UseDetectedNamePreviewIfAvailable()) { return; } string usableName = GetUsableName(); if (!RichTextDraftPolicy.TryValidate((_tab == 1) ? (_status ?? string.Empty) : usableName, out var message)) { _previewIsValid = false; _preview = message; _message = message; return; } if (_tab != 1 && usableName.IndexOf('<') >= 0) { _previewIsValid = usableName.Length <= Plugin.LockedMaxIdCardCharacters; _preview = usableName; _message = (_previewIsValid ? $"Ready: {_preview.Length}/{Plugin.LockedMaxIdCardCharacters} chars. Custom Rich Text mode preserves exact balanced markup; verify uncommon TMP tags in preview/game." : $"Styled text is too long ({usableName.Length}/{Plugin.LockedMaxIdCardCharacters})."); return; } if (_tab == 1) { _preview = StatusSuffixPolicy.Build(_status ?? string.Empty, CleanColor(_statusColor), Plugin.StatusBrackets?.Value ?? "()"); _previewIsValid = _preview.Length <= Plugin.LockedMaxIdCardCharacters; _message = (_previewIsValid ? $"Ready: {_preview.Length}/{Plugin.LockedMaxIdCardCharacters} chars. Preview, Copy, and Apply use your saved status brackets." : $"Styled status is too long ({_preview.Length}/{Plugin.LockedMaxIdCardCharacters})."); return; } string arguments; if (_mode != 1) { arguments = ((_mode != 2) ? ("gradient3 #" + CleanColor(_colorA) + " #" + CleanColor(_colorB) + " #" + CleanColor(_colorC) + " " + usableName) : ("gradient #" + CleanColor(_colorA) + " #" + CleanColor(_colorB) + " " + usableName)); } else { string text = ((_bold && _italic) ? "bolditalic" : (_bold ? "bold" : (_italic ? "italic" : "color"))); arguments = text + " #" + CleanColor(_colorA) + " " + usableName; } RichTextStyleResult richTextStyleResult = RichTextStyleBuilder.TryBuild(arguments, Plugin.LockedMaxIdCardCharacters); _previewIsValid = richTextStyleResult.Success; _preview = (richTextStyleResult.Success ? richTextStyleResult.GeneratedText : richTextStyleResult.Message); _message = (richTextStyleResult.Success ? $"Ready: {_preview.Length}/{Plugin.LockedMaxIdCardCharacters} chars. Apply Name Styler or Copy Name Style Text." : richTextStyleResult.Message); } private void HydrateCurrentName() { HydrateCurrentName(force: false); } private void HydrateCurrentName(bool force, DetectedDisplayNameState detectedState = null) { if (force || string.Equals(_text, "YOURNAMEHERE", StringComparison.Ordinal)) { string text = ((detectedState != null) ? RichTextStyleBuilder.ToPlainStyleEditorText(detectedState.BaseName) : (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.GetCurrentPlayerPlainNameForStyleUi() : string.Empty)); _detectedNamePreview = detectedState?.BaseName ?? (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.GetCurrentPlayerStyledNameForStyleUi() : string.Empty); if (!string.IsNullOrWhiteSpace(_detectedNamePreview)) { _text = _detectedNamePreview; HydrateDetectedStyleControls(_detectedNamePreview); } else if (!string.IsNullOrWhiteSpace(text)) { _text = text; } } } private void HydrateDetectedStyleControls(string styledName) { MatchCollection matchCollection = Regex.Matches(styledName ?? string.Empty, "<(?:color\\s*=\\s*#?|#)([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})\\s*>", RegexOptions.IgnoreCase); List list = new List(); foreach (Match item in matchCollection) { if (RichTextStyleBuilder.TryNormalizeHex(item.Groups[1].Value, out var hex) && !list.Contains(hex)) { list.Add(hex); } } if (list.Count > 0) { _colorA = list[0]; _colorB = list[Math.Min(list.Count - 1, Math.Max(1, list.Count / 2))]; _colorC = list[list.Count - 1]; _mode = DetectColorMode(list); } _bold = Regex.IsMatch(styledName ?? string.Empty, "<\\s*b(?:\\s|>)", RegexOptions.IgnoreCase); _italic = Regex.IsMatch(styledName ?? string.Empty, "<\\s*i(?:\\s|>)", RegexOptions.IgnoreCase); } private void ResetToCurrentPlayerState() { ResetTransientEditorStateForLiveSync(resetTab: false, clearHistory: false); DetectedDisplayNameState detectedDisplayNameState = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.GetCurrentDisplayNameStateForStyleUi() : null); HydrateCurrentName(force: true, detectedDisplayNameState); HydrateCurrentStatus(detectedDisplayNameState); HydrateSpoons(detectedDisplayNameState); _message = ((_tab == 1) ? ("Detected current status and Spoons. " + (detectedDisplayNameState?.DriftSummary ?? "No live player state was available yet.")) : ("Detected current name. " + (detectedDisplayNameState?.DriftSummary ?? "No live player state was available yet."))); RebuildPreview(); MarkSaved(detectedDisplayNameState?.DriftSummary ?? "Detected current in-game values. Edit a field to start a draft."); } private void ResetTransientEditorStateForLiveSync(bool resetTab, bool clearHistory = true) { //IL_0012: 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) //IL_0022: Unknown result type (might be due to invalid IL or missing references) if (resetTab) { _tab = 0; } _activeColorSlot = 0; _scroll = Vector2.zero; _contentScroll = Vector2.zero; if (clearHistory) { _undoHistory.Clear(); _redoHistory.Clear(); } _text = "YOURNAMEHERE"; _colorA = "0E6EED"; _colorB = "A59"; _colorC = "D85"; _bold = false; _italic = false; _mode = 3; _detectedNamePreview = string.Empty; _preview = string.Empty; _previewIsValid = false; } private void HydrateCurrentStatus(DetectedDisplayNameState detectedState = null) { _status = detectedState?.StatusMessage ?? Plugin.StatusMessage?.Value ?? string.Empty; _statusColor = detectedState?.StatusColor ?? Plugin.StatusColor?.Value ?? "FFD45D"; } private void HydrateSpoons(DetectedDisplayNameState detectedState = null) { _spoonCountText = ((detectedState != null) ? detectedState.SpoonCount.ToString() : (Plugin.SpoonCount?.Value.ToString() ?? "5")); _spoonLabelText = detectedState?.SpoonLabel ?? Plugin.SpoonLabel?.Value ?? "sp"; } private string GetUsableName() { string text = (_text ?? string.Empty).Trim(); if (!string.IsNullOrWhiteSpace(text)) { return text; } return "YOURNAMEHERE"; } private void SaveTemplate(int index) { CaptureUndo("Save Template " + index); string text = TemplatePrefix(index); PlayerPrefs.SetString(text + "Text", _text ?? string.Empty); PlayerPrefs.SetString(text + "ColorA", _colorA ?? string.Empty); PlayerPrefs.SetString(text + "ColorB", _colorB ?? string.Empty); PlayerPrefs.SetString(text + "ColorC", _colorC ?? string.Empty); PlayerPrefs.SetInt(text + "Mode", _mode); PlayerPrefs.SetInt(text + "Bold", _bold ? 1 : 0); PlayerPrefs.SetInt(text + "Italic", _italic ? 1 : 0); PlayerPrefs.Save(); _message = "Saved template " + index + "."; MarkSaved("Template " + index + " saved locally."); } private void LoadTemplate(int index) { CaptureUndo("Load Template " + index, restoreName: true); string text = TemplatePrefix(index); if (!PlayerPrefs.HasKey(text + "Text")) { _message = "Template " + index + " is empty. Detect Current loads live player data; Save " + index + " stores this slot."; MarkDraft("Template " + index + " is empty; live player data was left unchanged."); return; } _text = PlayerPrefs.GetString(text + "Text", "YOURNAMEHERE"); ClearDetectedNamePreview(); _colorA = PlayerPrefs.GetString(text + "ColorA", "0E6EED"); _colorB = PlayerPrefs.GetString(text + "ColorB", "A59"); _colorC = PlayerPrefs.GetString(text + "ColorC", "D85"); _mode = PlayerPrefs.GetInt(text + "Mode", 3); _bold = PlayerPrefs.GetInt(text + "Bold", 0) == 1; _italic = PlayerPrefs.GetInt(text + "Italic", 0) == 1; RebuildPreview(); if (!_previewIsValid) { _message = "Template " + index + " loaded, but it cannot apply yet: " + _message; MarkDraft("Template " + index + " loaded into a draft that needs fixing."); return; } bool applied = false; string text2 = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.TryApplyStyledNameFromStyleUi(_preview, out applied) : "Style UI: plugin is not ready yet."); _message = (applied ? "Loaded and applied template " : "Loaded template but could not apply it ") + index + ". " + text2; MarkApplyOutcome(applied, "Template " + index + " loaded and applied to your live name.", "Template " + index + " remains a draft because the live apply failed."); } private void SaveSpoonsFromStyleUi() { if (Plugin.SpoonCount == null || Plugin.EnableSpoons == null) { _message = "Spoons config is not ready yet."; MarkDraft("Spoons could not save because config is not ready."); return; } if (!int.TryParse((_spoonCountText ?? string.Empty).Trim(), out var result)) { _message = "Spoons must be a number from 0 to 5."; MarkDraft("Fix Spoons to a number from 0 to 5."); return; } int count = Mathf.Clamp(result, 0, 5); string text = new string((_spoonLabelText ?? string.Empty).Where((char c) => char.IsLetterOrDigit(c) || c == '-' || c == '_').Take(12).ToArray()); if (string.IsNullOrWhiteSpace(text)) { _message = "Custom label must use 1-12 letters/numbers, '-' or '_'. Use 'sp' for the default."; MarkDraft("Fix the custom Spoons/mood label before saving."); return; } bool applied = false; _message = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.TryApplySpoonsFromStyleUi(count, text, enabled: true, DateTime.UtcNow.ToString("O"), out applied) : "Spoons config is not ready yet."); if (applied) { _spoonCountText = count.ToString(); } MarkApplyOutcome(applied, "Spoons saved and applied to your live name.", "Spoons were not saved; fix the reported problem and try again."); } private void CaptureUndo(string action, bool restoreName = false, bool restoreStatus = false, bool restoreSpoons = false) { _undoHistory.Push(CaptureSnapshot(action, restoreName, restoreStatus, restoreSpoons)); while (_undoHistory.Count > 50) { StyleHelperSnapshot[] array = _undoHistory.ToArray(); _undoHistory.Clear(); for (int num = Math.Min(49, array.Length - 1); num >= 0; num--) { _undoHistory.Push(array[num]); } } _redoHistory.Clear(); } private StyleHelperSnapshot CaptureSnapshot(string action, bool restoreName = false, bool restoreStatus = false, bool restoreSpoons = false) { return new StyleHelperSnapshot { Action = (action ?? "Action"), Text = _text, ColorA = _colorA, ColorB = _colorB, ColorC = _colorC, Status = _status, StatusColorDraft = _statusColor, Preview = _preview, Message = _message, PreviewIsValid = _previewIsValid, DetectedNamePreview = _detectedNamePreview, Bold = _bold, Italic = _italic, Mode = _mode, Tab = _tab, ActiveColorSlot = _activeColorSlot, RestoreName = restoreName, RestoreStatus = restoreStatus, PlayerName = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.GetCurrentPlayerDisplayNameForStyleUi() : string.Empty), PlayerStatus = (Plugin.StatusMessage?.Value ?? string.Empty), PlayerStatusColor = (Plugin.StatusColor?.Value ?? "FFD45D"), SpoonCountText = _spoonCountText, SpoonLabelText = _spoonLabelText, StateLabel = _stateLabel, StateDetail = _stateDetail, RestoreSpoons = restoreSpoons, SpoonsEnabled = (Plugin.EnableSpoons != null && Plugin.EnableSpoons.Value), SpoonCount = (Plugin.SpoonCount?.Value ?? 5), SavedSpoonLabel = (Plugin.SpoonLabel?.Value ?? "sp"), SpoonsUpdatedUtc = (Plugin.SpoonsUpdatedUtc?.Value ?? string.Empty) }; } private bool UndoLastAction() { if (!_hasUndo) { _message = "Nothing to undo yet."; return false; } _redoHistory.Push(CaptureSnapshot("Redo " + _undoHistory.Peek().Action, _undoHistory.Peek().RestoreName, _undoHistory.Peek().RestoreStatus, _undoHistory.Peek().RestoreSpoons)); StyleHelperSnapshot snapshot = _undoHistory.Pop(); return RestoreSnapshot(snapshot, "Undid "); } private bool RedoLastAction() { if (_redoHistory.Count == 0) { _message = "Nothing to redo yet."; return false; } StyleHelperSnapshot styleHelperSnapshot = _redoHistory.Peek(); _undoHistory.Push(CaptureSnapshot("Undo " + styleHelperSnapshot.Action, styleHelperSnapshot.RestoreName, styleHelperSnapshot.RestoreStatus, styleHelperSnapshot.RestoreSpoons)); return RestoreSnapshot(_redoHistory.Pop(), "Redid "); } private bool RestoreSnapshot(StyleHelperSnapshot snapshot, string verb) { _text = snapshot.Text; _colorA = snapshot.ColorA; _colorB = snapshot.ColorB; _colorC = snapshot.ColorC; _status = snapshot.Status; _statusColor = snapshot.StatusColorDraft; _preview = snapshot.Preview; _detectedNamePreview = snapshot.DetectedNamePreview; _message = snapshot.Message; _previewIsValid = snapshot.PreviewIsValid; _bold = snapshot.Bold; _italic = snapshot.Italic; _mode = snapshot.Mode; _tab = snapshot.Tab; _activeColorSlot = snapshot.ActiveColorSlot; _spoonCountText = snapshot.SpoonCountText; _spoonLabelText = snapshot.SpoonLabelText; _stateLabel = snapshot.StateLabel; _stateDetail = snapshot.StateDetail; bool flag = true; string text = verb + snapshot.Action + "."; if (snapshot.RestoreSpoons && (Object)(object)Plugin.Instance != (Object)null) { bool applied; string text2 = Plugin.Instance.TryApplySpoonsFromStyleUi(snapshot.SpoonCount, snapshot.SavedSpoonLabel ?? "sp", snapshot.SpoonsEnabled, snapshot.SpoonsUpdatedUtc, out applied); text = text + " " + text2; flag = flag && applied; if (applied) { _spoonCountText = Mathf.Clamp(snapshot.SpoonCount, 0, 5).ToString(); _spoonLabelText = snapshot.SavedSpoonLabel ?? "sp"; } } if (snapshot.RestoreName && (Object)(object)Plugin.Instance != (Object)null && !string.IsNullOrWhiteSpace(snapshot.PlayerName)) { text = text + " " + Plugin.Instance.TryApplyStyledNameFromStyleUi(snapshot.PlayerName, out var applied2); flag = flag && applied2; } if (snapshot.RestoreStatus && (Object)(object)Plugin.Instance != (Object)null) { bool applied3; string text3 = (string.IsNullOrWhiteSpace(snapshot.PlayerStatus) ? Plugin.Instance.TryClearStatusFromStyleUi(out applied3) : Plugin.Instance.TryApplyStatusFromStyleUi(null, snapshot.PlayerStatus, snapshot.PlayerStatusColor, out applied3)); text = text + " " + text3; flag = flag && applied3; } RebuildPreview(); _message = text; MarkApplyOutcome(flag, verb.Trim() + " restored the saved/live state for that action.", verb.Trim() + " could not restore every live value; rejected values stayed unchanged."); return flag; } private void UndoSpoonsLastAction() { if (!_hasUndo || !_undoHistory.Peek().RestoreSpoons) { _message = "Nothing to undo for Spoons yet."; MarkDraft("Spoons undo needs a prior Save Spoons, Show Tag, or Hide Tag action."); } else { bool applied = UndoLastAction(); MarkApplyOutcome(applied, "Spoons undo restored only the last Spoons state.", "Spoons undo could not apply; saved/live values stayed unchanged."); } } private static string TemplatePrefix(int index) { return "BlueSageQoL.StyleTemplate." + index + "."; } private string GetColorSlot(int slot) { return slot switch { 1 => _colorB, 2 => _colorC, _ => _colorA, }; } private void MarkDraft(string detail) { _stateLabel = "Draft"; _stateDetail = detail ?? "Changes are local until you apply or save."; } private void MarkSaved(string detail) { _stateLabel = "Saved"; _stateDetail = detail ?? "Ready."; } private void MarkApplyOutcome(bool applied, string savedDetail, string failedDetail) { if (applied) { MarkSaved(savedDetail); } else { MarkDraft(failedDetail); } } private void MarkAction(string detail) { _stateLabel = "Done"; _stateDetail = detail ?? "Done."; } private void SetColorSlot(int slot, string value) { switch (slot) { case 1: ClearDetectedNamePreview(); _colorB = value; break; case 2: ClearDetectedNamePreview(); _colorC = value; break; default: ClearDetectedNamePreview(); _colorA = value; break; } } private bool UseDetectedNamePreviewIfAvailable() { if (string.IsNullOrWhiteSpace(_detectedNamePreview)) { return false; } string a = RichTextStyleBuilder.ToPlainStyleEditorText(_detectedNamePreview); if (!string.Equals(_detectedNamePreview, GetUsableName(), StringComparison.Ordinal) && !string.Equals(a, GetUsableName(), StringComparison.Ordinal)) { return false; } _preview = _detectedNamePreview; string message = string.Empty; _previewIsValid = _preview.Length <= Plugin.LockedMaxIdCardCharacters && RichTextDraftPolicy.TryValidate(_preview, out message); _message = (_previewIsValid ? "Detected current live name style." : ((_preview.Length > Plugin.LockedMaxIdCardCharacters) ? $"Detected current live name style is too long ({_preview.Length}/{Plugin.LockedMaxIdCardCharacters}). Shorten before applying." : message)); return true; } private static int DetectColorMode(IReadOnlyList colors) { if (colors == null || colors.Count <= 1) { return 1; } if (colors.Count == 2) { return 2; } if (!TryParseRgb(colors[0], out var red, out var green, out var blue) || !TryParseRgb(colors[colors.Count - 1], out var red2, out var green2, out var blue2)) { return 3; } for (int i = 1; i < colors.Count - 1; i++) { if (!TryParseRgb(colors[i], out var red3, out var green3, out var blue3)) { return 3; } double num = (double)i / (double)(colors.Count - 1); int num2 = (int)Math.Round((double)red + (double)(red2 - red) * num); int num3 = (int)Math.Round((double)green + (double)(green2 - green) * num); int num4 = (int)Math.Round((double)blue + (double)(blue2 - blue) * num); if (Math.Abs(red3 - num2) > 1 || Math.Abs(green3 - num3) > 1 || Math.Abs(blue3 - num4) > 1) { return 3; } } return 2; } private static bool TryParseRgb(string value, out int red, out int green, out int blue) { red = (green = (blue = 0)); if (!RichTextStyleBuilder.TryNormalizeHex(value, out var hex)) { return false; } red = Convert.ToInt32(hex.Substring(0, 2), 16); green = Convert.ToInt32(hex.Substring(2, 2), 16); blue = Convert.ToInt32(hex.Substring(4, 2), 16); return true; } private void ClearDetectedNamePreview() { _detectedNamePreview = string.Empty; } private static string CleanColor(string value) { string text = (value ?? string.Empty).Trim().TrimStart(new char[1] { '#' }); if (!string.IsNullOrWhiteSpace(text)) { return text; } return "0E6EED"; } private static Color HexToColor(string value, Color fallback) { //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) if (!RichTextStyleBuilder.TryNormalizeHex(value, out var hex)) { return fallback; } float num = (float)Convert.ToInt32(hex.Substring(0, 2), 16) / 255f; float num2 = (float)Convert.ToInt32(hex.Substring(2, 2), 16) / 255f; float num3 = (float)Convert.ToInt32(hex.Substring(4, 2), 16) / 255f; return new Color(num, num2, num3, 1f); } private void ClampWindowToScreen() { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) WindowFitResult windowFitResult = BlueSageWindowCoordinator.ResolveForFrame(BlueSagePublicWindow.StyleHelper, _windowRect, 920f, 640f, Screen.width, Screen.height); _windowRect = new Rect(windowFitResult.X, windowFitResult.Y, windowFitResult.Width, windowFitResult.Height); } private void OnDisable() { BlueSageWindowCoordinator.SetVisible(BlueSagePublicWindow.StyleHelper, visible: false); BlueSageWindowHoverScope.UnregisterWindow(47057); } private void DrawResizeGrip() { //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Expected O, but got Unknown //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Invalid comparison between Unknown and I4 //IL_00ea: Unknown result type (might be due to invalid IL or missing references) //IL_00f0: Invalid comparison between Unknown and I4 //IL_00a7: 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) Rect val = default(Rect); ((Rect)(ref val))..ctor(((Rect)(ref _windowRect)).width - 42f - 8f, ((Rect)(ref _windowRect)).height - 42f - 8f, 42f, 42f); GUI.Box(val, new GUIContent("↘", "Drag this larger corner handle to resize the Style Helper."), _buttonStyle); Event current = Event.current; if (current != null) { if ((int)current.type == 0 && ((Rect)(ref val)).Contains(current.mousePosition)) { _isResizing = true; current.Use(); } else if (_isResizing && (int)current.type == 3) { ref Rect windowRect = ref _windowRect; ((Rect)(ref windowRect)).width = ((Rect)(ref windowRect)).width + current.delta.x; ref Rect windowRect2 = ref _windowRect; ((Rect)(ref windowRect2)).height = ((Rect)(ref windowRect2)).height + current.delta.y; ClampWindowToScreen(); current.Use(); } else if (_isResizing && (int)current.rawType == 1) { _isResizing = false; current.Use(); } } } private void EnsureStyles() { //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_008b: 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_009e: Expected O, but got Unknown //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: Unknown result type (might be due to invalid IL or missing references) //IL_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Unknown result type (might be due to invalid IL or missing references) //IL_00cc: Unknown result type (might be due to invalid IL or missing references) //IL_00d8: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Expected O, but got Unknown //IL_00f2: Unknown result type (might be due to invalid IL or missing references) //IL_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_0103: Unknown result type (might be due to invalid IL or missing references) //IL_0117: Expected O, but got Unknown //IL_0122: Unknown result type (might be due to invalid IL or missing references) //IL_0127: Unknown result type (might be due to invalid IL or missing references) //IL_012f: Unknown result type (might be due to invalid IL or missing references) //IL_0136: Unknown result type (might be due to invalid IL or missing references) //IL_0142: Unknown result type (might be due to invalid IL or missing references) //IL_0151: Expected O, but got Unknown //IL_015c: Unknown result type (might be due to invalid IL or missing references) //IL_0161: Unknown result type (might be due to invalid IL or missing references) //IL_0169: Unknown result type (might be due to invalid IL or missing references) //IL_0170: Unknown result type (might be due to invalid IL or missing references) //IL_017c: Unknown result type (might be due to invalid IL or missing references) //IL_018b: Expected O, but got Unknown //IL_0196: Unknown result type (might be due to invalid IL or missing references) //IL_019b: Unknown result type (might be due to invalid IL or missing references) //IL_01a2: Unknown result type (might be due to invalid IL or missing references) //IL_01aa: Unknown result type (might be due to invalid IL or missing references) //IL_01b1: Unknown result type (might be due to invalid IL or missing references) //IL_01b8: Unknown result type (might be due to invalid IL or missing references) //IL_01c4: Unknown result type (might be due to invalid IL or missing references) //IL_01cf: Unknown result type (might be due to invalid IL or missing references) //IL_01d9: Unknown result type (might be due to invalid IL or missing references) //IL_01e8: Expected O, but got Unknown //IL_0212: Unknown result type (might be due to invalid IL or missing references) //IL_021d: Unknown result type (might be due to invalid IL or missing references) //IL_0228: Unknown result type (might be due to invalid IL or missing references) //IL_0244: Unknown result type (might be due to invalid IL or missing references) //IL_0249: Unknown result type (might be due to invalid IL or missing references) //IL_0250: Unknown result type (might be due to invalid IL or missing references) //IL_0259: Unknown result type (might be due to invalid IL or missing references) //IL_0263: Expected O, but got Unknown //IL_0263: Unknown result type (might be due to invalid IL or missing references) //IL_026f: Unknown result type (might be due to invalid IL or missing references) //IL_027b: Unknown result type (might be due to invalid IL or missing references) //IL_028a: Expected O, but got Unknown //IL_0295: Unknown result type (might be due to invalid IL or missing references) //IL_029a: Unknown result type (might be due to invalid IL or missing references) //IL_02a1: Unknown result type (might be due to invalid IL or missing references) //IL_02a8: Unknown result type (might be due to invalid IL or missing references) //IL_02b2: Expected O, but got Unknown //IL_02b2: Unknown result type (might be due to invalid IL or missing references) //IL_02be: Unknown result type (might be due to invalid IL or missing references) //IL_02cd: Unknown result type (might be due to invalid IL or missing references) //IL_02d9: Unknown result type (might be due to invalid IL or missing references) //IL_02e8: Expected O, but got Unknown //IL_02f3: Unknown result type (might be due to invalid IL or missing references) //IL_02f8: Unknown result type (might be due to invalid IL or missing references) //IL_02ff: Unknown result type (might be due to invalid IL or missing references) //IL_0307: Unknown result type (might be due to invalid IL or missing references) //IL_030e: Unknown result type (might be due to invalid IL or missing references) //IL_0315: Unknown result type (might be due to invalid IL or missing references) //IL_0321: Unknown result type (might be due to invalid IL or missing references) //IL_0330: Expected O, but got Unknown //IL_0355: Unknown result type (might be due to invalid IL or missing references) //IL_035a: Unknown result type (might be due to invalid IL or missing references) //IL_0366: Expected O, but got Unknown //IL_0371: Unknown result type (might be due to invalid IL or missing references) //IL_0376: Unknown result type (might be due to invalid IL or missing references) //IL_037d: Unknown result type (might be due to invalid IL or missing references) //IL_0384: Unknown result type (might be due to invalid IL or missing references) //IL_038b: Unknown result type (might be due to invalid IL or missing references) //IL_0393: Unknown result type (might be due to invalid IL or missing references) //IL_039f: Unknown result type (might be due to invalid IL or missing references) //IL_03ae: Unknown result type (might be due to invalid IL or missing references) //IL_03ba: Unknown result type (might be due to invalid IL or missing references) //IL_03c9: Expected O, but got Unknown _theme = BlueSageUiTheme.Current; if (_windowStyle == null || !string.Equals(_lastThemeKey, _theme.Key, StringComparison.Ordinal) || _renderedTextureGeneration != BlueSageUiTheme.RuntimeTextureGeneration || !BlueSageUiTheme.AreRuntimeTexturesAlive()) { _lastThemeKey = _theme.Key; Texture2D solidTexture = BlueSageUiTheme.GetSolidTexture(_theme.Window); Texture2D solidTexture2 = BlueSageUiTheme.GetSolidTexture(_theme.Panel); _windowBackgroundTexture = solidTexture; GUIStyle val = new GUIStyle(GUI.skin.window) { padding = new RectOffset(16, 16, 46, 14) }; val.normal.background = solidTexture; val.normal.textColor = _theme.LabelText; val.onNormal.background = solidTexture; val.onNormal.textColor = _theme.LabelText; _windowStyle = val; GUIStyle val2 = new GUIStyle(GUI.skin.box); val2.normal.background = BlueSageUiTheme.GetSolidTexture(_theme.Header); _headerBoxStyle = val2; GUIStyle val3 = new GUIStyle(GUI.skin.label) { fontSize = 18, fontStyle = (FontStyle)1 }; val3.normal.textColor = _theme.HeaderText; _headerStyle = val3; GUIStyle val4 = new GUIStyle(GUI.skin.label) { fontSize = 15, fontStyle = (FontStyle)1 }; val4.normal.textColor = _theme.LabelText; _labelStyle = val4; GUIStyle val5 = new GUIStyle(GUI.skin.label) { richText = false, fontSize = 15, fontStyle = (FontStyle)1, wordWrap = true }; val5.normal.textColor = Color.Lerp(_theme.SmallText, _theme.LabelText, 0.35f); _smallStyle = val5; _buttonStyle = BlueSageUiTheme.CreateNeutralButtonStyle(GUI.skin.button, _theme, 14); _activeButtonStyle = BlueSageUiTheme.CreateStateButtonStyle(_buttonStyle, _theme.ActiveAccent, _theme.FieldFocusedBorder, _theme.ActiveStateText, 14); GUIStyle val6 = new GUIStyle(GUI.skin.box) { stretchHeight = false, padding = new RectOffset(12, 12, 10, 10) }; val6.normal.background = solidTexture2; val6.normal.textColor = _theme.LabelText; _boxStyle = val6; GUIStyle val7 = new GUIStyle(GUI.skin.box) { stretchHeight = false, padding = new RectOffset(12, 12, 8, 8) }; val7.normal.background = BlueSageUiTheme.GetSolidTexture(_theme.FooterBackground); val7.normal.textColor = _theme.FooterText; _footerBoxStyle = val7; GUIStyle val8 = new GUIStyle(GUI.skin.label) { richText = false, fontSize = 14, fontStyle = (FontStyle)1, wordWrap = true }; val8.normal.textColor = _theme.FooterText; _footerTextStyle = val8; _textFieldStyle = BlueSageUiTheme.CreateTextInputStyle(GUI.skin.textField, _theme, 15, wordWrap: true); _generatedTextStyle = new GUIStyle(_textFieldStyle) { wordWrap = false }; GUIStyle val9 = new GUIStyle(GUI.skin.box) { richText = true, wordWrap = true, alignment = (TextAnchor)4, fontSize = 17 }; val9.normal.background = BlueSageUiTheme.GetSolidTexture(_theme.PreviewBackground); val9.normal.textColor = _theme.PreviewText; _previewStyle = val9; _swatchButtonStyle = BlueSageUiTheme.CreateSwatchButtonStyle(GUI.skin.button, 13); _renderedTextureGeneration = BlueSageUiTheme.RuntimeTextureGeneration; } } private static void SetAllStateTextColors(GUIStyle style, Color text) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_001e: 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_0036: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) style.normal.textColor = text; style.hover.textColor = text; style.active.textColor = text; style.focused.textColor = text; style.onNormal.textColor = text; style.onHover.textColor = text; style.onActive.textColor = text; style.onFocused.textColor = text; } } internal static class SweepResourceEvidenceCapture { internal const int MaximumAssetFacts = 50000; public static SweepResourceSnapshot Capture() { try { List list = new List(5); List list2 = new List(Math.Min(50000, 8192)); bool detailTruncated = false; CaptureResourceType(Resources.FindObjectsOfTypeAll(), "Texture2D", list, list2, ref detailTruncated); CaptureResourceType(Resources.FindObjectsOfTypeAll(), "RenderTexture", list, list2, ref detailTruncated); CaptureResourceType(Resources.FindObjectsOfTypeAll(), "Material", list, list2, ref detailTruncated); CaptureResourceType(Resources.FindObjectsOfTypeAll(), "Mesh", list, list2, ref detailTruncated); CaptureResourceType(Resources.FindObjectsOfTypeAll(), "AudioClip", list, list2, ref detailTruncated); return new SweepResourceSnapshot { Captured = true, Status = "ok", SceneObjectCount = Resources.FindObjectsOfTypeAll().Length, Categories = list.ToArray(), Assets = list2.ToArray(), DetailTruncated = detailTruncated }; } catch (Exception ex) { return SweepResourceSnapshot.Unavailable(ex.GetType().Name); } } private static void CaptureResourceType(T[] resources, string category, List categories, List assets, ref bool detailTruncated) where T : Object { int num = 0; long num2 = 0L; if (resources != null) { foreach (T val in resources) { if (!((Object)(object)val == (Object)null)) { num++; long num3 = Profiler.GetRuntimeMemorySizeLong((Object)(object)val); if (num3 < 0) { num3 = 0L; } num2 += num3; if (assets.Count < 50000) { assets.Add(new SweepResourceAssetFact(((Object)val).GetInstanceID(), category, num3)); } else { detailTruncated = true; } } } } categories.Add(new SweepResourceCategoryTotal(category, num, num2)); } } internal static class SweepSessionEvidenceCapture { internal static SweepSessionSnapshot Capture() { //IL_00b4: Unknown result type (might be due to invalid IL or missing references) try { long workingSet; long privateMemorySize; int handleCount; int count; using (Process process = Process.GetCurrentProcess()) { process.Refresh(); workingSet = process.WorkingSet64; privateMemorySize = process.PrivateMemorySize64; handleCount = process.HandleCount; count = process.Threads.Count; } int nativeRows = NetworkSingleton.I?.PlayerSteamIDs?.Count ?? (-1); int steamMembers = -1; MultiplayerManager i = MonoSingleton.I; if (SteamManager.Initialized && (Object)(object)i != (Object)null && ulong.TryParse(i.LobbyCode, out var result) && result != 0) { steamMembers = SteamMatchmaking.GetNumLobbyMembers(new CSteamID(result)); } VoiceManager i2 = MonoSingleton.I; return new SweepSessionSnapshot(captured: true, "ok", workingSet, privateMemorySize, handleCount, count, nativeRows, steamMembers, (Object)(object)i2 != (Object)null, i2?.IsVoiceLobbyOff ?? false, i2 != null && i2.IsJoined, CountQueuedVoiceParticipants(i2)); } catch (Exception ex) { return SweepSessionSnapshot.Unavailable(ex.GetType().Name); } } private static int CountQueuedVoiceParticipants(VoiceManager voiceManager) { if ((Object)(object)voiceManager == (Object)null) { return -1; } try { return (typeof(VoiceManager).GetField("QueuedParticipants", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(voiceManager) is ICollection collection) ? collection.Count : (-1); } catch { return -1; } } } internal sealed class WindowsStewardPipeHost : IStewardPipeHost { private static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); private readonly object _sync = new object(); private Thread _thread; private NamedPipeServerStream _activePipe; private volatile bool _stopping; private StewardAnnouncementKeyRing _keys = StewardAnnouncementKeyRing.Empty; private Action _accepted; private string _serviceIdentity = string.Empty; private SecurityIdentifier _serviceSid; private string _keyPath = string.Empty; private long _keyLastWriteUtcTicks; public bool TryStart(string keyPath, string serviceIdentity, Action accepted, out string boundedError) { boundedError = string.Empty; if (accepted == null) { boundedError = "Receiver callback is unavailable."; return false; } if (!Path.IsPathRooted(keyPath ?? string.Empty) || !IsPathOutsidePackage(keyPath) || !TryLoadKeyRing(keyPath, out _keys)) { boundedError = "Protected CurrentUser key bundle is missing, invalid, or inside the package."; return false; } _keyPath = Path.GetFullPath(keyPath); _keyLastWriteUtcTicks = File.GetLastWriteTimeUtc(_keyPath).Ticks; _serviceIdentity = serviceIdentity ?? string.Empty; if (!TryResolveServiceSid(_serviceIdentity, out _serviceSid)) { _keys.ClearSecrets(); _keys = StewardAnnouncementKeyRing.Empty; boundedError = "Configured Windows service identity could not be resolved."; return false; } _accepted = accepted; _stopping = false; _thread = new Thread(ListenLoop) { IsBackground = true, Name = "BlueSage-Steward-Pipe" }; _thread.Start(); return true; } public void Stop() { _stopping = true; lock (_sync) { try { _activePipe?.Dispose(); } catch { } _activePipe = null; } try { if (_thread != null && _thread.IsAlive) { _thread.Join(1000); } } catch { } _thread = null; _accepted = null; _keys.ClearSecrets(); _keys = StewardAnnouncementKeyRing.Empty; _keyPath = string.Empty; _keyLastWriteUtcTicks = 0L; _serviceSid = null; } private void ListenLoop() { while (!_stopping) { NamedPipeServerStream namedPipeServerStream = null; try { namedPipeServerStream = CreatePipe(); lock (_sync) { _activePipe = namedPipeServerStream; } namedPipeServerStream.WaitForConnection(); if (_stopping) { break; } if (TryReadFrame(namedPipeServerStream, out var payload) && RefreshKeyRingIfChanged() && StewardAnnouncementPolicy.TryAuthenticate(payload, "0.2.4+20260730.1-public-24414155-release", DateTime.UtcNow.Ticks, _keys, out StewardAnnouncementEnvelope envelope, out string _)) { _accepted?.Invoke(envelope); } } catch { if (_stopping) { break; } Thread.Sleep(250); } finally { lock (_sync) { if (_activePipe == namedPipeServerStream) { _activePipe = null; } } try { namedPipeServerStream?.Dispose(); } catch { } } } } private bool RefreshKeyRingIfChanged() { try { long ticks = File.GetLastWriteTimeUtc(_keyPath).Ticks; if (ticks == _keyLastWriteUtcTicks) { return _keys.HasCurrentKey; } _keyLastWriteUtcTicks = ticks; if (!TryLoadKeyRing(_keyPath, out var keys)) { _keys.ClearSecrets(); _keys = StewardAnnouncementKeyRing.Empty; return false; } _keys.ClearSecrets(); _keys = keys; return true; } catch { _keys.ClearSecrets(); _keys = StewardAnnouncementKeyRing.Empty; return false; } } private NamedPipeServerStream CreatePipe() { PipeSecurity pipeSecurity = new PipeSecurity(); pipeSecurity.SetAccessRuleProtection(isProtected: true, preserveInheritance: false); using (WindowsIdentity windowsIdentity = WindowsIdentity.GetCurrent()) { if (windowsIdentity?.User == null) { throw new InvalidOperationException("Current Windows identity has no SID."); } pipeSecurity.AddAccessRule(new PipeAccessRule(windowsIdentity.User, PipeAccessRights.ReadWrite | PipeAccessRights.CreateNewInstance, AccessControlType.Allow)); } pipeSecurity.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.NetworkSid, null), PipeAccessRights.FullControl, AccessControlType.Deny)); if (_serviceSid != null) { pipeSecurity.AddAccessRule(new PipeAccessRule(_serviceSid, PipeAccessRights.ReadWrite, AccessControlType.Allow)); } return new NamedPipeServerStream("BlueSage.OnTogether.Steward.v1", PipeDirection.In, 1, PipeTransmissionMode.Byte, PipeOptions.None, 2052, 2052, pipeSecurity); } private static bool TryReadFrame(Stream stream, out byte[] payload) { payload = Array.Empty(); byte[] array = new byte[4]; if (!ReadExactly(stream, array, 0, array.Length)) { return false; } int num = array[0] | (array[1] << 8) | (array[2] << 16) | (array[3] << 24); if (num <= 0 || num > 2048) { return false; } payload = new byte[num]; return ReadExactly(stream, payload, 0, num); } private static bool ReadExactly(Stream stream, byte[] buffer, int offset, int count) { int num; for (int i = 0; i < count; i += num) { num = stream.Read(buffer, offset + i, count - i); if (num <= 0) { return false; } } return true; } private static bool TryLoadKeyRing(string keyPath, out StewardAnnouncementKeyRing keys) { keys = StewardAnnouncementKeyRing.Empty; try { byte[] array = ProtectedData.Unprotect(File.ReadAllBytes(keyPath), (byte[])null, (DataProtectionScope)0); string text = StrictUtf8.GetString(array); Array.Clear(array, 0, array.Length); string[] array2 = text.Replace("\r\n", "\n").Split(new char[1] { '\n' }); if (array2.Length != 8 || array2[7].Length != 0 || !TryValue(array2[0], "schema", out var value) || value != "1" || !TryValue(array2[1], "rotatedUtcTicks", out var value2) || !TryValue(array2[2], "currentKeyId", out var value3) || !TryValue(array2[3], "currentKey", out var value4) || !TryValue(array2[4], "previousKeyId", out var value5) || !TryValue(array2[5], "previousKey", out var value6) || !TryValue(array2[6], "previousValidUntilUtcTicks", out var value7) || !long.TryParse(value2, NumberStyles.None, CultureInfo.InvariantCulture, out var result) || !long.TryParse(value7, NumberStyles.None, CultureInfo.InvariantCulture, out var result2)) { return false; } byte[] array3 = Convert.FromBase64String(value4); byte[] array4 = (string.IsNullOrEmpty(value6) ? null : Convert.FromBase64String(value6)); if (result >= 621355968000000000L) { long num = result; DateTime maxValue = DateTime.MaxValue; if (num <= maxValue.Ticks - TimeSpan.FromMinutes(5.0).Ticks && !string.IsNullOrWhiteSpace(value3)) { long num2 = result + TimeSpan.FromMinutes(5.0).Ticks; if (array3.Length < 32 || result2 > num2 || result2 < result || (array4 != null && array4.Length < 32) || array4 == null != string.IsNullOrEmpty(value5)) { Array.Clear(array3, 0, array3.Length); if (array4 != null) { Array.Clear(array4, 0, array4.Length); } return false; } keys = new StewardAnnouncementKeyRing(value3, array3, value5, array4, result, result2); Array.Clear(array3, 0, array3.Length); if (array4 != null) { Array.Clear(array4, 0, array4.Length); } return keys.HasCurrentKey; } } return false; } catch { keys = StewardAnnouncementKeyRing.Empty; return false; } } private static bool TryValue(string line, string name, out string value) { string text = name + "="; if (!(line ?? string.Empty).StartsWith(text, StringComparison.Ordinal)) { value = string.Empty; return false; } value = line.Substring(text.Length); return true; } private static bool TryResolveServiceSid(string serviceIdentity, out SecurityIdentifier serviceSid) { serviceSid = null; if (string.IsNullOrWhiteSpace(serviceIdentity)) { return true; } try { NTAccount nTAccount = new NTAccount(serviceIdentity.Trim()); serviceSid = (SecurityIdentifier)nTAccount.Translate(typeof(SecurityIdentifier)); return serviceSid != null; } catch { serviceSid = null; return false; } } internal static bool IsPathOutsidePackage(string path) { try { string fullPath = Path.GetFullPath(path); string text = Path.GetFullPath(Path.GetDirectoryName(typeof(Plugin).Assembly.Location)).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); char directorySeparatorChar = Path.DirectorySeparatorChar; string value = text + directorySeparatorChar; return !fullPath.StartsWith(value, StringComparison.OrdinalIgnoreCase) && !HasGitAncestor(Path.GetDirectoryName(fullPath)); } catch { return false; } } private static bool HasGitAncestor(string directory) { try { for (DirectoryInfo directoryInfo = new DirectoryInfo(directory); directoryInfo != null; directoryInfo = directoryInfo.Parent) { if (Directory.Exists(Path.Combine(directoryInfo.FullName, ".git")) || File.Exists(Path.Combine(directoryInfo.FullName, ".git"))) { return true; } } } catch { return true; } return false; } internal static void AtomicReplace(string path, string content) { string? directoryName = Path.GetDirectoryName(path); if (string.IsNullOrWhiteSpace(directoryName)) { throw new InvalidOperationException("State path has no directory."); } Directory.CreateDirectory(directoryName); string text = path + ".tmp"; string text2 = path + ".bak"; File.WriteAllText(text, content ?? string.Empty, StrictUtf8); if (File.Exists(path)) { try { File.Replace(text, path, text2, ignoreMetadataErrors: true); TryDelete(text2); return; } catch { TryDelete(text); return; } } File.Move(text, path); } private static void TryDelete(string path) { try { if (File.Exists(path)) { File.Delete(path); } } catch { } } } } namespace BlueSage.QoLTweaks.Patches { internal static class AvatarStyleSlotsBridge { internal const int NativeSlotCount = 3; internal const int TotalSlotCount = 9; private const string PayloadVersion = "BSAS1"; private const string RowMarkerPrefix = "BlueSage_StyleSlot_"; private static readonly FieldInfo StyleDropdownPanelField = AccessTools.Field(typeof(CustomizationUIController), "_styleDropdownPanel"); private static readonly FieldInfo StyleTextsField = AccessTools.Field(typeof(CustomizationUIController), "_styleTexts"); private static readonly FieldInfo StyleSelectedBgsField = AccessTools.Field(typeof(CustomizationUIController), "_styleSelectedBgs"); private static readonly FieldInfo StyleNameInputField = AccessTools.Field(typeof(CustomizationUIController), "_styleNameInputField"); private static readonly FieldInfo StyleNumTextField = AccessTools.Field(typeof(CustomizationUIController), "_styleNumText"); private static readonly FieldInfo CustomizationDataField = AccessTools.Field(typeof(CustomizationUIController), "_customizationData"); private static readonly FieldInfo IsStyleChangedField = AccessTools.Field(typeof(CustomizationUIController), "_isStyleChanged"); private static readonly FieldInfo CustomizationTypeField = AccessTools.Field(typeof(CustomizationUIController), "_customizationType"); private static readonly FieldInfo GroupIndexField = AccessTools.Field(typeof(CustomizationUIController), "_groupIndex"); private static readonly MethodInfo ResetUnsavedCustomizationMethod = AccessTools.Method(typeof(CustomizationUIController), "ResetUnsavedCustomization", (Type[])null, (Type[])null); private static readonly MethodInfo SetGroupUiMethod = AccessTools.Method(typeof(CustomizationUIController), "SetGroupUI", (Type[])null, (Type[])null); private static readonly MethodInfo PointerExitStyleNamingMethod = AccessTools.Method(typeof(CustomizationUIController), "PointerExitStyleNaming", (Type[])null, (Type[])null); private static readonly MethodInfo SetPartOptionUiMethod = AccessTools.Method(typeof(CustomizationUIController), "SetPartOptionUI", (Type[])null, (Type[])null); private static readonly MethodInfo SetCharacterMethod = AccessTools.Method(typeof(CustomizationUIController), "SetCharacter", (Type[])null, (Type[])null); private static readonly MethodInfo UpdateCustomizationCameraMethod = AccessTools.Method(typeof(CustomizationUIController), "UpdateCustomizationCamera", (Type[])null, (Type[])null); private static readonly MethodInfo WaitAndCaptureMethod = AccessTools.Method(typeof(CustomizationUIController), "WaitAndCapture", (Type[])null, (Type[])null); private static ConfigFile _config; private static ConfigEntry _selectedExtendedSlot; private static readonly ConfigEntry[] Names = new ConfigEntry[6]; private static readonly ConfigEntry[] Payloads = new ConfigEntry[6]; private static readonly Dictionary RowTexts = new Dictionary(); private static readonly Dictionary RowSelectedBgs = new Dictionary(); private static int _activeExtendedSlot = -1; private static bool _closingExtendedSlot; private static CustomizationUIController _lastController; private static CustomizationDataIDs3 _nativeSlotSnapshot; private static int _nativeSlotSnapshotIndex = -1; internal static bool IsExtendedActive { get { if (_activeExtendedSlot >= 3) { return _activeExtendedSlot < 9; } return false; } } private static void EnsureConfig() { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Expected O, but got Unknown if (_config == null) { _config = new ConfigFile(Path.Combine(Paths.ConfigPath, "com.bluesage.ontogether.qol-avatar-styles.cfg"), true); _selectedExtendedSlot = _config.Bind("State", "SelectedExtendedSlot", -1, "Last selected BlueSage avatar style slot. -1 means a vanilla slot is active."); for (int i = 3; i < 9; i++) { int num = i - 3; Names[num] = _config.Bind("Style " + (i + 1), "Name", string.Empty, "Display name for BlueSage avatar style slot " + (i + 1) + "."); Payloads[num] = _config.Bind("Style " + (i + 1), "Payload", string.Empty, "Versioned avatar customization payload. Managed by BlueSage QoL."); } } } internal static void ExtendDropdown(CustomizationUIController controller) { if ((Object)(object)controller == (Object)null || Plugin.EnableExtendedAvatarStyles == null || !Plugin.EnableExtendedAvatarStyles.Value) { return; } _lastController = controller; EnsureConfig(); List list = StyleTextsField?.GetValue(controller) as List; List list2 = StyleSelectedBgsField?.GetValue(controller) as List; object? obj = StyleDropdownPanelField?.GetValue(controller); GameObject val = (GameObject)((obj is GameObject) ? obj : null); if (list == null || list.Count < 3 || list2 == null || list2.Count < 3 || (Object)(object)val == (Object)null) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)"Avatar Styles 4-9 skipped: the vanilla three-slot dropdown surface was not complete."); } return; } GameObject val2 = FindSharedRow(((TMP_Text)list[2]).transform, list2[2].transform); if ((Object)(object)val2 == (Object)null || (Object)(object)val2.transform.parent == (Object)null) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)"Avatar Styles 4-9 skipped: no safe vanilla style row root was found."); } return; } Transform parent = val2.transform.parent; int num = 0; for (int i = 3; i < 9; i++) { string text = "BlueSage_StyleSlot_" + (i + 1); Transform val3 = parent.Find(text); GameObject val4 = (((Object)(object)val3 != (Object)null) ? ((Component)val3).gameObject : Object.Instantiate(val2, parent)); if ((Object)(object)val3 == (Object)null) { num++; } ((Object)val4).name = text; val4.transform.SetSiblingIndex(val2.transform.GetSiblingIndex() + (i - 3) + 1); TextMeshProUGUI val5 = FindMatchingText(val2, list[2], val4); GameObject val6 = FindMatchingObject(val2, list2[2], val4); if ((Object)(object)val5 == (Object)null || (Object)(object)val6 == (Object)null) { Object.Destroy((Object)(object)val4); ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogWarning((object)$"Avatar style slot {i + 1} skipped because its cloned row could not be mapped safely."); } } else { ConfigureRowNumber(val4, val5, i + 1); ((TMP_Text)val5).text = "- " + GetName(i); val6.SetActive(i == _activeExtendedSlot); ConfigureRowButton(val4, controller, i); RowTexts[i] = val5; RowSelectedBgs[i] = val6; } } ExpandPanelForRows(val, val2, num); try { RestoreExtendedSelectionIfNeeded(controller, list2); } catch (Exception ex) { _activeExtendedSlot = -1; if (_selectedExtendedSlot != null) { _selectedExtendedSlot.Value = -1; } ManualLogSource log4 = Plugin.Log; if (log4 != null) { log4.LogWarning((object)("Avatar Styles 4-9 deferred an invalid saved-slot restore without interrupting CustomizationUI startup: " + ex.GetType().Name + ": " + ex.Message)); } } } internal static void RefreshEnabledState() { CustomizationUIController val = _lastController; if ((Object)(object)val == (Object)null) { val = Object.FindFirstObjectByType(); } if (!((Object)(object)val == (Object)null)) { _lastController = val; if (Plugin.EnableExtendedAvatarStyles != null && Plugin.EnableExtendedAvatarStyles.Value) { ExtendDropdown(val); } else { DisableAndRestoreVanilla(val); } } } private static void DisableAndRestoreVanilla(CustomizationUIController controller) { //IL_01d7: Unknown result type (might be due to invalid IL or missing references) //IL_01dc: Unknown result type (might be due to invalid IL or missing references) //IL_01a6: Unknown result type (might be due to invalid IL or missing references) //IL_01ab: Unknown result type (might be due to invalid IL or missing references) if (IsExtendedActive) { int slot = Mathf.Clamp(MonoSingleton.I.PlayerDataZip.SelectedStyleIndex, 0, 2); SelectNativeFromExtended(controller, slot); } object? obj = StyleDropdownPanelField?.GetValue(controller); GameObject val = (GameObject)((obj is GameObject) ? obj : null); int num = 0; KeyValuePair[] array = RowTexts.ToArray(); for (int i = 0; i < array.Length; i++) { KeyValuePair keyValuePair = array[i]; if (!((Object)(object)keyValuePair.Value == (Object)null)) { Transform val2 = ((TMP_Text)keyValuePair.Value).transform; while ((Object)(object)val2.parent != (Object)null && !((Object)val2).name.StartsWith("BlueSage_StyleSlot_", StringComparison.Ordinal)) { val2 = val2.parent; } if (((Object)val2).name.StartsWith("BlueSage_StyleSlot_", StringComparison.Ordinal)) { Object.Destroy((Object)(object)((Component)val2).gameObject); num++; } } } RowTexts.Clear(); RowSelectedBgs.Clear(); if ((Object)(object)val != (Object)null && num > 0) { RectTransform component = val.GetComponent(); if ((Object)(object)component != (Object)null && (Object)(object)val.GetComponent() == (Object)null) { float num2 = 0f; Rect rect; if (StyleTextsField?.GetValue(controller) is List { Count: >=3 } list) { GameObject obj2 = FindSharedRow(((TMP_Text)list[2]).transform, ((List)StyleSelectedBgsField.GetValue(controller))[2].transform); float? obj3; if (obj2 == null) { obj3 = null; } else { RectTransform component2 = obj2.GetComponent(); if (component2 == null) { obj3 = null; } else { rect = component2.rect; obj3 = ((Rect)(ref rect)).height; } } float? num3 = obj3; num2 = Mathf.Abs(num3.GetValueOrDefault()); } if (num2 > 1f) { rect = component.rect; component.SetSizeWithCurrentAnchors((Axis)1, Mathf.Max(((Rect)(ref rect)).height - num2 * (float)num, num2)); } } } _activeExtendedSlot = -1; ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)"Avatar preset slots 4-9 disabled; vanilla slots 1-3 restored and QoL preset data retained."); } } internal static void SelectExtended(CustomizationUIController controller, int slot) { //IL_0056: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)controller == (Object)null || slot < 3 || slot >= 9) { return; } EnsureConfig(); try { bool num = !IsExtendedActive; SaveDepartingSlot(controller); if (num) { CaptureNativeSlotSnapshot(); } else { RestoreNativeSlotSnapshot(); } SetAllSelectionBgs(controller, -1); _activeExtendedSlot = slot; _selectedExtendedSlot.Value = slot; CustomizationDataIDs3 ids = (CustomizationDataIDs3)(((object)LoadPayload(slot)) ?? ((object)new CustomizationDataIDs3())); ApplyToEditor(controller, ids); SetAllSelectionBgs(controller, slot); UpdateHeader(controller, slot, GetName(slot)); ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)$"Avatar style slot {slot + 1} selected through the BlueSage compatibility bridge."); } } catch (Exception ex) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)$"Avatar style slot {slot + 1} selection failed safely: {ex.GetType().Name}: {ex.Message}"); } } } internal static bool SelectNativeFromExtended(CustomizationUIController controller, int slot) { //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Expected O, but got Unknown //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Expected O, but got Unknown //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Expected O, but got Unknown if (!IsExtendedActive || (Object)(object)controller == (Object)null || slot < 0 || slot >= 3) { return false; } try { SaveExtended(controller, _activeExtendedSlot); RestoreNativeSlotSnapshot(); SetAllSelectionBgs(controller, -1); _activeExtendedSlot = -1; _selectedExtendedSlot.Value = -1; PlayerDataZip playerDataZip = MonoSingleton.I.PlayerDataZip; int num = ((_nativeSlotSnapshotIndex >= 0) ? _nativeSlotSnapshotIndex : Mathf.Clamp(playerDataZip.SelectedStyleIndex, 0, 2)); CustomizationData val = new CustomizationData(playerDataZip.CurrentCustomizationDataIDs); MonoSingleton.I.CustomizationData = new CustomizationData(val); CustomizationDataField?.SetValue(controller, (object?)new CustomizationData(val)); IsStyleChangedField?.SetValue(controller, false); if (slot != num) { return false; } ApplyToEditor(controller, playerDataZip.CurrentCustomizationDataIDs); SetAllSelectionBgs(controller, num); UpdateHeader(controller, num, playerDataZip.StyleNames[num]); return true; } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)$"Returning to vanilla avatar style {slot + 1} failed safely: {ex.GetType().Name}: {ex.Message}"); } return true; } } internal static bool SaveExtendedClose(CustomizationUIController controller, bool notFromStyleChanged) { //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Expected O, but got Unknown //IL_00c4: Unknown result type (might be due to invalid IL or missing references) //IL_00ce: Expected O, but got Unknown if (!IsExtendedActive || _closingExtendedSlot || (Object)(object)controller == (Object)null) { return false; } _closingExtendedSlot = true; try { object? obj = CustomizationDataField?.GetValue(controller); CustomizationData val = (CustomizationData)((obj is CustomizationData) ? obj : null); if (val == null) { return true; } CustomizationData val2 = new CustomizationData(val); if (notFromStyleChanged) { PointerExitStyleNamingMethod?.Invoke(controller, null); } object obj2 = CustomizationTypeField?.GetValue(controller); object obj3 = GroupIndexField?.GetValue(controller); if (obj2 != null && obj3 != null) { ResetUnsavedCustomizationMethod?.Invoke(controller, new object[2] { obj2, obj3 }); } SetGroupUiMethod?.Invoke(controller, null); MonoSingleton.I.CustomizationData = new CustomizationData(val2); SaveExtendedData(val2, _activeExtendedSlot); RestoreNativeSlotSnapshot(); if (WaitAndCaptureMethod?.Invoke(controller, null) is IEnumerator enumerator) { ((MonoBehaviour)controller).StartCoroutine(enumerator); } if ((Object)(object)NetworkSingleton.I != (Object)null) { NetworkSingleton.I.MainCustomizationController.ApplyCustomization(val2); } controller.IsAnyCustomize = true; IsStyleChangedField?.SetValue(controller, false); return true; } finally { _closingExtendedSlot = false; } } internal static bool RenameExtended(CustomizationUIController controller, string styleName) { if (!IsExtendedActive) { return false; } EnsureConfig(); string text = styleName ?? string.Empty; Names[_activeExtendedSlot - 3].Value = text; if (RowTexts.TryGetValue(_activeExtendedSlot, out var value) && (Object)(object)value != (Object)null) { ((TMP_Text)value).text = "- " + text; } return true; } private static void RestoreExtendedSelectionIfNeeded(CustomizationUIController controller, List nativeBgs) { int num = _selectedExtendedSlot?.Value ?? (-1); if (num < 3 || num >= 9 || string.IsNullOrWhiteSpace(Payloads[num - 3].Value)) { _activeExtendedSlot = -1; return; } _activeExtendedSlot = num; foreach (GameObject nativeBg in nativeBgs) { if (nativeBg != null) { nativeBg.SetActive(false); } } CustomizationDataIDs3 val = LoadPayload(num); if (val != null) { ApplyToEditor(controller, val); SetAllSelectionBgs(controller, num); UpdateHeader(controller, num, GetName(num)); } } private static void SaveDepartingSlot(CustomizationUIController controller) { if (IsExtendedActive) { SaveExtended(controller, _activeExtendedSlot); return; } controller.CloseAvatarPanel(false); MonoSingleton.I.SavePlayerZipData(); } private static void CaptureNativeSlotSnapshot() { PlayerDataZip val = MonoSingleton.I?.PlayerDataZip; if (val == null) { _nativeSlotSnapshot = null; _nativeSlotSnapshotIndex = -1; } else { _nativeSlotSnapshotIndex = Mathf.Clamp(val.SelectedStyleIndex, 0, 2); _nativeSlotSnapshot = CloneIds(val.CurrentCustomizationDataIDs); } } private static void RestoreNativeSlotSnapshot() { PlayerDataZip val = MonoSingleton.I?.PlayerDataZip; if (val != null && _nativeSlotSnapshot != null && _nativeSlotSnapshotIndex >= 0) { val.SelectedStyleIndex = _nativeSlotSnapshotIndex; val.CurrentCustomizationDataIDs = CloneIds(_nativeSlotSnapshot); } } private static CustomizationDataIDs3 CloneIds(CustomizationDataIDs3 source) { //IL_0004: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Expected O, but got Unknown //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Expected O, but got Unknown if (source != null) { return new CustomizationDataIDs3(new CustomizationData(source)); } return null; } private static void SaveExtended(CustomizationUIController controller, int slot) { object? obj = CustomizationDataField?.GetValue(controller); CustomizationData val = (CustomizationData)((obj is CustomizationData) ? obj : null); if (val != null) { SaveExtendedData(val, slot); } } private static void SaveExtendedData(CustomizationData data, int slot) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Expected O, but got Unknown if (data != null && slot >= 3 && slot < 9) { Payloads[slot - 3].Value = Serialize(new CustomizationDataIDs3(data)); } } private static void ApplyToEditor(CustomizationUIController controller, CustomizationDataIDs3 ids) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Expected O, but got Unknown //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Expected O, but got Unknown //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Expected O, but got Unknown CustomizationData val = new CustomizationData(ids); MonoSingleton.I.CustomizationData = new CustomizationData(val); CustomizationDataField?.SetValue(controller, (object?)new CustomizationData(val)); IsStyleChangedField?.SetValue(controller, true); SetPartOptionUiMethod?.Invoke(controller, null); SetCharacterMethod?.Invoke(controller, null); UpdateCustomizationCameraMethod?.Invoke(controller, null); if ((Object)(object)NetworkSingleton.I != (Object)null) { NetworkSingleton.I.MainCustomizationController.ApplyCustomization(val); } controller.IsAnyCustomize = true; } private static void SetAllSelectionBgs(CustomizationUIController controller, int selectedSlot) { if (StyleSelectedBgsField?.GetValue(controller) is List list) { for (int i = 0; i < list.Count; i++) { GameObject obj = list[i]; if (obj != null) { obj.SetActive(i == selectedSlot); } } } foreach (KeyValuePair rowSelectedBg in RowSelectedBgs) { GameObject value = rowSelectedBg.Value; if (value != null) { value.SetActive(rowSelectedBg.Key == selectedSlot); } } } private static void UpdateHeader(CustomizationUIController controller, int slot, string name) { object? obj = StyleNumTextField?.GetValue(controller); TextMeshProUGUI val = (TextMeshProUGUI)((obj is TextMeshProUGUI) ? obj : null); if (val != null) { ((TMP_Text)val).text = (slot + 1).ToString(CultureInfo.InvariantCulture); } object? obj2 = StyleNameInputField?.GetValue(controller); TMP_InputField val2 = (TMP_InputField)((obj2 is TMP_InputField) ? obj2 : null); if (val2 != null) { val2.SetTextWithoutNotify(name ?? string.Empty); } } private static string GetName(int slot) { return Names[slot - 3]?.Value ?? string.Empty; } private static void ConfigureRowButton(GameObject row, CustomizationUIController controller, int slot) { //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Expected O, but got Unknown //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Expected O, but got Unknown Button val = row.GetComponent