Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of LCMicRecovery v0.3.7
BepInEx/plugins/LCMicRecovery/LCMicRecovery.dll
Decompiled 12 hours agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using Dissonance; using Dissonance.Audio.Capture; using HarmonyLib; using Microsoft.CodeAnalysis; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; using UnityEngine.SceneManagement; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("LCMicRecovery")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("LCMicRecovery")] [assembly: AssemblyCopyright("")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("a1b2c3d4-e5f6-4789-abcd-1234567890ab")] [assembly: AssemblyFileVersion("0.3.7.0")] [assembly: IgnoresAccessChecksTo("Assembly-CSharp")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.3.7.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.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 LCMicRecovery { [BepInPlugin("com.yourname.lcmicrecovery", "LC Mic Recovery", "0.3.7")] public class Plugin : BaseUnityPlugin { internal static Plugin Instance; internal static Harmony HarmonyInstance; internal static ManualLogSource Log; private void Awake() { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Expected O, but got Unknown //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_0084: Expected O, but got Unknown //IL_0084: Unknown result type (might be due to invalid IL or missing references) Instance = this; Log = ((BaseUnityPlugin)this).Logger; PluginConfig.Bind(((BaseUnityPlugin)this).Config); HarmonyInstance = new Harmony("com.yourname.lcmicrecovery"); HarmonyInstance.PatchAll(); Log.LogInfo((object)MicRecoveryText.T("LC Mic Recovery 已加载。", "LC Mic Recovery loaded.")); Log.LogInfo((object)MicRecoveryText.T("LC Mic Recovery version: 0.3.7", "LC Mic Recovery version: 0.3.7")); if ((Object)(object)Object.FindObjectOfType<MicRecoveryWatcher>() == (Object)null) { GameObject val = new GameObject("LCMicRecovery_Watcher"); Object.DontDestroyOnLoad((Object)val); ((Object)val).hideFlags = (HideFlags)61; val.AddComponent<MicRecoveryWatcher>(); Log.LogInfo((object)MicRecoveryText.T("LCMicRecovery_Watcher 已创建。", "LCMicRecovery_Watcher created successfully.")); } else { Log.LogInfo((object)MicRecoveryText.T("LCMicRecovery_Watcher 已存在,跳过重复创建。", "LCMicRecovery_Watcher already exists, skipping duplicate creation.")); } Log.LogInfo((object)MicRecoveryText.T("配置项已绑定。可在游戏内 LethalConfig 检查是否显示。", "Config entries bound. Check LethalConfig in-game to see if they appear.")); } } internal static class PluginConfig { internal static ConfigEntry<bool> EnableMod; internal static ConfigEntry<string> LanguageMode; internal static ConfigEntry<bool> EnableDebugLog; internal static ConfigEntry<bool> EnableStateLogs; internal static ConfigEntry<bool> EnableHeartbeatLog; internal static ConfigEntry<bool> ShowFiveStepRecoveryLogs; internal static ConfigEntry<bool> EnablePreRoundSkipLog; internal static ConfigEntry<bool> LogPreRoundSkipOnlyOnce; internal static ConfigEntry<bool> SuspendAutoRecoveryWhenNoDevices; internal static ConfigEntry<bool> EnableNoDeviceSuspendLog; internal static ConfigEntry<bool> LogNoDeviceSuspendOnlyOnce; internal static ConfigEntry<bool> AllowManualRecoveryWhenNoDevices; internal static ConfigEntry<bool> SuspendAutoRecoveryDuringMenuOrTeardown; internal static ConfigEntry<bool> AllowLocalRecoveryWhenGameSideUnsafe; internal static ConfigEntry<float> LobbyExitSuspendSeconds; internal static ConfigEntry<bool> EnableTeardownSuspendLog; internal static ConfigEntry<bool> LogTeardownSuspendOnlyOnce; internal static ConfigEntry<bool> EnableManualRecoveryKey; internal static ConfigEntry<Key> ManualRecoveryKey; internal static ConfigEntry<bool> EnableAutoRecovery; internal static ConfigEntry<bool> EnablePreRoundRecovery; internal static ConfigEntry<float> AutoCheckIntervalSeconds; internal static ConfigEntry<float> RecoveryCooldownSeconds; internal static ConfigEntry<float> PostRecoveryGraceSeconds; internal static ConfigEntry<string> PreferredDeviceKeywords; internal static ConfigEntry<bool> PreferCurrentDeviceIfStillExists; internal static ConfigEntry<bool> RecoverWhenMicNameEmpty; internal static ConfigEntry<bool> RecoverWhenDeviceMissing; internal static ConfigEntry<bool> RecoverWhenUnityNotRecording; internal static bool DebugEnabled { get { if (EnableDebugLog != null) { return EnableDebugLog.Value; } return false; } } internal static bool StateLogsEnabled { get { if (EnableStateLogs != null) { return EnableStateLogs.Value; } return false; } } internal static bool HeartbeatEnabled { get { if (EnableHeartbeatLog != null) { return EnableHeartbeatLog.Value; } return false; } } internal static void Bind(ConfigFile config) { EnableMod = config.Bind<bool>("General", "EnableMod", true, "总开关。关闭后模组不执行任何恢复逻辑。"); LanguageMode = config.Bind<string>("Localization", "LanguageMode", "Auto", "User-facing language. Auto uses Chinese only when LC-Chinese-Project / V81TestChn is detected; English forces English; Chinese forces Chinese logs but falls HUD back to English when Chinese HUD font support is not detected. / 用户可见语言。Auto 检测到 LC-Chinese-Project / V81TestChn 时使用中文,否则英文;English 强制英文;Chinese 强制中文日志,但未检测到中文 HUD 字体支持时游戏内提示回退英文。"); EnableDebugLog = config.Bind<bool>("Logging", "EnableDebugLog", false, "是否输出调试日志。正常游玩建议关闭。"); EnableStateLogs = config.Bind<bool>("Logging", "EnableStateLogs", false, "是否输出状态检测日志(当前麦克风、IsRecording 等,容易刷屏)。正常游玩建议关闭。"); EnableHeartbeatLog = config.Bind<bool>("Logging", "EnableHeartbeatLog", false, "是否输出 watcher 心跳日志。正常游玩建议关闭。"); ShowFiveStepRecoveryLogs = config.Bind<bool>("Logging", "ShowFiveStepRecoveryLogs", true, "恢复时是否输出 1/5 到 5/5 的明显日志。正常游玩建议开启。"); EnablePreRoundSkipLog = config.Bind<bool>("Logging", "EnablePreRoundSkipLog", false, "在 StartOfRound 尚未就绪时,是否输出“跳过自动恢复检测”的日志。正常游玩建议关闭。"); LogPreRoundSkipOnlyOnce = config.Bind<bool>("Logging", "LogPreRoundSkipOnlyOnce", true, "是否只在每次进入局内前打印一次“StartOfRound 尚未就绪”的日志。"); SuspendAutoRecoveryWhenNoDevices = config.Bind<bool>("No Device Handling", "SuspendAutoRecoveryWhenNoDevices", true, "当没有检测到任何录音设备时,暂停自动恢复,避免反复刷恢复日志。"); EnableNoDeviceSuspendLog = config.Bind<bool>("No Device Handling", "EnableNoDeviceSuspendLog", false, "当没有录音设备时,是否输出“已暂停自动恢复”的提示日志。"); LogNoDeviceSuspendOnlyOnce = config.Bind<bool>("No Device Handling", "LogNoDeviceSuspendOnlyOnce", true, "没有录音设备时,是否只打印一次“已暂停自动恢复”的提示。"); AllowManualRecoveryWhenNoDevices = config.Bind<bool>("No Device Handling", "AllowManualRecoveryWhenNoDevices", false, "当没有录音设备时,是否允许手动按键继续强制恢复。"); SuspendAutoRecoveryDuringMenuOrTeardown = config.Bind<bool>("Teardown Handling", "SuspendAutoRecoveryDuringMenuOrTeardown", true, "当处于主菜单/大厅,或房间正在退出销毁时,暂停自动恢复。建议开启。"); AllowLocalRecoveryWhenGameSideUnsafe = config.Bind<bool>("Teardown Handling", "AllowLocalRecoveryWhenGameSideUnsafe", true, "当游戏侧 ResetDissonanceCommsComponent 不安全时,是否仍允许本地 ResetMicrophoneCapture。建议开启。"); LobbyExitSuspendSeconds = config.Bind<float>("Teardown Handling", "LobbyExitSuspendSeconds", 8f, "退出房间或切换到大厅后,暂停自动恢复的秒数。建议 8。"); EnableTeardownSuspendLog = config.Bind<bool>("Teardown Handling", "EnableTeardownSuspendLog", false, "当因为大厅/退出阶段而暂停自动恢复时,是否输出提示日志。"); LogTeardownSuspendOnlyOnce = config.Bind<bool>("Teardown Handling", "LogTeardownSuspendOnlyOnce", true, "是否只打印一次大厅/退出阶段挂起日志。"); EnableManualRecoveryKey = config.Bind<bool>("Manual Recovery", "EnableManualRecoveryKey", true, "是否启用手动按键恢复。"); ManualRecoveryKey = config.Bind<Key>("Manual Recovery", "ManualRecoveryKey", (Key)101, "手动触发恢复的按键。"); EnableAutoRecovery = config.Bind<bool>("Auto Recovery", "EnableAutoRecovery", true, "是否启用自动恢复。"); EnablePreRoundRecovery = config.Bind<bool>("Auto Recovery", "EnablePreRoundRecovery", false, "是否允许在 StartOfRound 出现前就触发恢复。建议关闭。"); AutoCheckIntervalSeconds = config.Bind<float>("Auto Recovery", "AutoCheckIntervalSeconds", 3f, "自动检测间隔(秒)。"); RecoveryCooldownSeconds = config.Bind<float>("Auto Recovery", "RecoveryCooldownSeconds", 10f, "两次恢复之间的冷却时间(秒)。"); PostRecoveryGraceSeconds = config.Bind<float>("Auto Recovery", "PostRecoveryGraceSeconds", 4f, "恢复完成后的宽限时间(秒)。"); PreferredDeviceKeywords = config.Bind<string>("Device", "PreferredDeviceKeywords", "Maxwell,Audeze", "优先匹配的设备关键词,多个用英文逗号分隔。"); PreferCurrentDeviceIfStillExists = config.Bind<bool>("Device", "PreferCurrentDeviceIfStillExists", true, "如果当前设备仍存在,恢复时优先保持当前设备。"); RecoverWhenMicNameEmpty = config.Bind<bool>("Recovery Conditions", "RecoverWhenMicNameEmpty", true, "当前麦克风名称为空时是否触发恢复。"); RecoverWhenDeviceMissing = config.Bind<bool>("Recovery Conditions", "RecoverWhenDeviceMissing", true, "当前麦克风不在设备列表中时是否触发恢复。"); RecoverWhenUnityNotRecording = config.Bind<bool>("Recovery Conditions", "RecoverWhenUnityNotRecording", true, "Unity 报告当前麦克风未在录音时是否触发恢复。"); } } internal static class MicRecoveryText { private const string ChineseProjectGuid = "cn.codex.v81testchn"; private const string ChineseProjectAssemblyName = "V81TestChn"; private static bool _hudFallbackWarningLogged; internal static string RecoveryCompleteTipTitle { get { if (!ShouldUseChineseHudText()) { return "Mic recovery triggered"; } return "麦克风修复已触发"; } } internal static string RecoveryCompleteTipBody { get { if (!ShouldUseChineseHudText()) { return "Test your voice. If it still fails, check logs."; } return "请测试语音。若仍异常,请查看日志。"; } } internal static string T(string chinese, string english) { if (!ShouldUseChineseText()) { return english; } return chinese; } internal static string Format(string chinese, string english, params object[] args) { return string.Format(T(chinese, english), args); } internal static void WarnHudFallbackIfNeeded() { if (!_hudFallbackWarningLogged && IsForcedChineseMode() && !IsChineseHudSupported()) { _hudFallbackWarningLogged = true; ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)"[MicRecovery] LanguageMode=Chinese,但未检测到 LC-Chinese-Project / V81TestChn;游戏内 HUD 提示已回退英文,以避免中文方块乱码。BepInEx 日志仍使用中文。"); } } } private static bool ShouldUseChineseHudText() { if (ShouldUseChineseText()) { return IsChineseHudSupported(); } return false; } private static bool ShouldUseChineseText() { string languageMode = GetLanguageMode(); if (IsEnglishMode(languageMode)) { return false; } if (IsForcedChineseMode(languageMode)) { return true; } return IsChineseProjectLoaded(); } private static bool IsForcedChineseMode() { return IsForcedChineseMode(GetLanguageMode()); } private static bool IsForcedChineseMode(string mode) { if (!string.Equals(mode, "Chinese", StringComparison.OrdinalIgnoreCase) && !string.Equals(mode, "zh", StringComparison.OrdinalIgnoreCase) && !string.Equals(mode, "zh-CN", StringComparison.OrdinalIgnoreCase)) { return string.Equals(mode, "zh-Hans", StringComparison.OrdinalIgnoreCase); } return true; } private static bool IsEnglishMode(string mode) { if (!string.Equals(mode, "English", StringComparison.OrdinalIgnoreCase) && !string.Equals(mode, "en", StringComparison.OrdinalIgnoreCase)) { return string.Equals(mode, "en-US", StringComparison.OrdinalIgnoreCase); } return true; } private static string GetLanguageMode() { string text = ((PluginConfig.LanguageMode != null) ? PluginConfig.LanguageMode.Value : null); if (!string.IsNullOrWhiteSpace(text)) { return text.Trim(); } return "Auto"; } private static bool IsChineseHudSupported() { return IsChineseProjectLoaded(); } private static bool IsChineseProjectLoaded() { try { if (Chainloader.PluginInfos != null) { if (Chainloader.PluginInfos.ContainsKey("cn.codex.v81testchn")) { return true; } foreach (PluginInfo value in Chainloader.PluginInfos.Values) { BepInPlugin val = ((value != null) ? value.Metadata : null); if (val != null && (string.Equals(val.GUID, "cn.codex.v81testchn", StringComparison.OrdinalIgnoreCase) || string.Equals(val.Name, "V81TestChn", StringComparison.OrdinalIgnoreCase))) { return true; } } } } catch { } try { return AppDomain.CurrentDomain.GetAssemblies().Any((Assembly assembly) => string.Equals(assembly.GetName().Name, "V81TestChn", StringComparison.OrdinalIgnoreCase)); } catch { return false; } } } public class MicRecoveryWatcher : MonoBehaviour { private float _checkTimer; private float _heartbeatTimer; private float _nextCommsSearchTime; private float _lastManualRecoveryTime = -999f; private float _suspendRecoveryUntil = -999f; private bool _preRoundSkipLogged; private bool _noDeviceSuspendLogged; private bool _teardownSuspendLogged; private int _autoDeviceListFailureCount; private DissonanceComms _cachedComms; private readonly List<string> _deviceBuffer = new List<string>(8); private void OnEnable() { SceneManager.activeSceneChanged += OnActiveSceneChanged; } private void OnDisable() { SceneManager.activeSceneChanged -= OnActiveSceneChanged; } private void OnActiveSceneChanged(Scene oldScene, Scene newScene) { //IL_0041: Unknown result type (might be due to invalid IL or missing references) _cachedComms = null; _nextCommsSearchTime = 0f; _preRoundSkipLogged = false; _noDeviceSuspendLogged = false; _teardownSuspendLogged = false; _autoDeviceListFailureCount = 0; if (PluginConfig.SuspendAutoRecoveryDuringMenuOrTeardown != null && PluginConfig.SuspendAutoRecoveryDuringMenuOrTeardown.Value && IsMenuScene(newScene)) { SuspendRecoveryForTeardown(MicRecoveryText.T("切换到主菜单/大厅场景,已暂停自动恢复。", "Switched to the main menu/lobby scene; automatic recovery is suspended.")); } } private static bool IsMenuScene(Scene scene) { if (!((Scene)(ref scene)).IsValid()) { return true; } return (((Scene)(ref scene)).name ?? string.Empty).IndexOf("menu", StringComparison.OrdinalIgnoreCase) >= 0; } private bool IsRecoveryTemporarilySuspended() { return Time.unscaledTime < _suspendRecoveryUntil; } private void SuspendRecoveryForTeardown(string reason) { float num = ((PluginConfig.LobbyExitSuspendSeconds != null) ? Mathf.Max(0f, PluginConfig.LobbyExitSuspendSeconds.Value) : 8f); float num2 = Time.unscaledTime + num; if (num2 > _suspendRecoveryUntil) { _suspendRecoveryUntil = num2; } if (PluginConfig.EnableTeardownSuspendLog != null && PluginConfig.EnableTeardownSuspendLog.Value && (PluginConfig.LogTeardownSuspendOnlyOnce == null || !PluginConfig.LogTeardownSuspendOnlyOnce.Value || !_teardownSuspendLogged)) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)("[MicRecovery] " + reason)); } _teardownSuspendLogged = true; } } private void Update() { if (PluginConfig.EnableMod != null && !PluginConfig.EnableMod.Value) { return; } HandleManualRecoveryKey(); HandleHeartbeat(); if (PluginConfig.EnableAutoRecovery == null || PluginConfig.EnableAutoRecovery.Value) { float num = ((PluginConfig.AutoCheckIntervalSeconds != null) ? Mathf.Max(0.5f, PluginConfig.AutoCheckIntervalSeconds.Value) : 3f); _checkTimer += Time.unscaledDeltaTime; if (!(_checkTimer < num)) { _checkTimer = 0f; AutoCheckMic(); } } } private void HandleManualRecoveryKey() { //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_002f: Unknown result type (might be due to invalid IL or missing references) if (PluginConfig.EnableManualRecoveryKey == null || !PluginConfig.EnableManualRecoveryKey.Value) { return; } try { Keyboard current = Keyboard.current; if (current == null) { return; } Key value = PluginConfig.ManualRecoveryKey.Value; if (!((ButtonControl)current[value]).wasPressedThisFrame || Time.unscaledTime - _lastManualRecoveryTime < 2f) { return; } bool flag = MicRecoveryCore.IsRecoveryBlockedByScene(); bool flag2 = MicRecoveryCore.IsGameSideResetSafe(); bool flag3 = PluginConfig.ShowFiveStepRecoveryLogs != null && PluginConfig.ShowFiveStepRecoveryLogs.Value; if (PluginConfig.DebugEnabled || flag3) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)MicRecoveryText.Format("[MicRecovery] 手动恢复请求:menuScene={0}, gameSideResetSafe={1}", "[MicRecovery] Manual recovery requested: menuScene={0}, gameSideResetSafe={1}", flag, flag2)); } } if (flag) { if (PluginConfig.DebugEnabled || flag3) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogInfo((object)MicRecoveryText.T("[MicRecovery] 当前位于主菜单/大厅,已跳过手动恢复。", "[MicRecovery] Current scene is the main menu/lobby; manual recovery was skipped.")); } } return; } if (PluginConfig.SuspendAutoRecoveryDuringMenuOrTeardown != null && PluginConfig.SuspendAutoRecoveryDuringMenuOrTeardown.Value && IsRecoveryTemporarilySuspended()) { if (PluginConfig.DebugEnabled || flag3) { ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogInfo((object)MicRecoveryText.T("[MicRecovery] 当前处于退出/切场景暂停窗口,已跳过手动恢复。", "[MicRecovery] Currently in the exit/scene-switch suspend window; manual recovery was skipped.")); } } return; } if (!flag2 && PluginConfig.DebugEnabled) { ManualLogSource log4 = Plugin.Log; if (log4 != null) { log4.LogInfo((object)MicRecoveryText.T("[MicRecovery] 游戏侧重置当前不安全,手动恢复仍将尝试本地 ResetMicrophoneCapture。", "[MicRecovery] Game-side reset is currently unsafe; manual recovery will still try local ResetMicrophoneCapture.")); } } DissonanceComms comms = GetComms(); if ((Object)(object)comms == (Object)null) { if (PluginConfig.DebugEnabled || flag3) { ManualLogSource log5 = Plugin.Log; if (log5 != null) { log5.LogWarning((object)MicRecoveryText.T("[MicRecovery] 手动恢复未找到 DissonanceComms,无法执行本地恢复。", "[MicRecovery] Manual recovery could not find DissonanceComms; local recovery cannot run.")); } } return; } if (PluginConfig.DebugEnabled || flag3) { ManualLogSource log6 = Plugin.Log; if (log6 != null) { log6.LogInfo((object)MicRecoveryText.T("[MicRecovery] 手动恢复已找到 DissonanceComms。", "[MicRecovery] Manual recovery found DissonanceComms.")); } } _deviceBuffer.Clear(); bool flag4 = false; try { comms.GetMicrophoneDevices(_deviceBuffer); } catch (Exception ex) { flag4 = true; if (PluginConfig.DebugEnabled) { ManualLogSource log7 = Plugin.Log; if (log7 != null) { log7.LogWarning((object)MicRecoveryText.Format("[MicRecovery] 手动恢复获取录音设备列表失败,将继续尝试强制恢复:{0}", "[MicRecovery] Manual recovery failed to get the recording device list; continuing forced recovery: {0}", ex.Message)); } } } if (!flag4 && _deviceBuffer.Count == 0 && PluginConfig.AllowManualRecoveryWhenNoDevices != null && !PluginConfig.AllowManualRecoveryWhenNoDevices.Value) { if (PluginConfig.DebugEnabled) { ManualLogSource log8 = Plugin.Log; if (log8 != null) { log8.LogInfo((object)MicRecoveryText.T("[MicRecovery] 当前没有录音设备,已跳过手动恢复。", "[MicRecovery] No recording devices are currently available; manual recovery was skipped.")); } } } else { _lastManualRecoveryTime = Time.unscaledTime; MicRecoveryCore.TryRecover(MicRecoveryText.T("手动按键触发恢复", "Manual recovery key pressed"), ignoreCooldown: true); } } catch (Exception ex2) { if (PluginConfig.DebugEnabled) { ManualLogSource log9 = Plugin.Log; if (log9 != null) { log9.LogWarning((object)MicRecoveryText.Format("[MicRecovery] 手动按键检测失败:{0}", "[MicRecovery] Manual recovery key check failed: {0}", ex2.Message)); } } } } private void HandleHeartbeat() { if (!PluginConfig.HeartbeatEnabled) { return; } _heartbeatTimer += Time.unscaledDeltaTime; if (!(_heartbeatTimer < 15f)) { _heartbeatTimer = 0f; ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)MicRecoveryText.T("[MicRecovery] Watcher 正在运行。", "[MicRecovery] Watcher is running.")); } } } private DissonanceComms GetComms() { Object cachedComms = (Object)(object)_cachedComms; if (cachedComms != null && cachedComms == (Object)null) { _cachedComms = null; } if ((Object)(object)_cachedComms != (Object)null) { return _cachedComms; } if (Time.unscaledTime < _nextCommsSearchTime) { return null; } _nextCommsSearchTime = Time.unscaledTime + 1f; _cachedComms = Object.FindObjectOfType<DissonanceComms>(); return _cachedComms; } private void AutoCheckMic() { try { if (PluginConfig.SuspendAutoRecoveryDuringMenuOrTeardown != null && PluginConfig.SuspendAutoRecoveryDuringMenuOrTeardown.Value) { if (MicRecoveryCore.IsMenuSceneActive()) { SuspendRecoveryForTeardown(MicRecoveryText.T("当前处于主菜单/大厅场景,已暂停自动恢复。", "Current scene is the main menu/lobby; automatic recovery is suspended.")); return; } if (IsRecoveryTemporarilySuspended()) { return; } } DissonanceComms comms = GetComms(); if ((Object)(object)comms == (Object)null) { if (PluginConfig.DebugEnabled) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)MicRecoveryText.T("[MicRecovery] 当前未找到 DissonanceComms,跳过检测。", "[MicRecovery] DissonanceComms is not currently available; skipping detection.")); } } return; } bool flag = PluginConfig.AllowLocalRecoveryWhenGameSideUnsafe == null || PluginConfig.AllowLocalRecoveryWhenGameSideUnsafe.Value; bool flag2 = MicRecoveryCore.IsStartOfRoundReady(); bool flag3 = MicRecoveryCore.IsGameSideResetSafe(); if (!PluginConfig.EnablePreRoundRecovery.Value && !flag2 && !flag) { if (PluginConfig.EnablePreRoundSkipLog.Value && (!PluginConfig.LogPreRoundSkipOnlyOnce.Value || !_preRoundSkipLogged)) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogInfo((object)MicRecoveryText.T("[MicRecovery] StartOfRound 尚未就绪,跳过自动恢复检测。", "[MicRecovery] StartOfRound is not ready; skipping automatic recovery detection.")); } _preRoundSkipLogged = true; } return; } if (!flag2 && PluginConfig.EnablePreRoundSkipLog.Value && flag && (!PluginConfig.LogPreRoundSkipOnlyOnce.Value || !_preRoundSkipLogged)) { ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogInfo((object)MicRecoveryText.T("[MicRecovery] StartOfRound 尚未就绪,继续执行本地 Dissonance 检测,游戏侧重置将由恢复流程自行跳过。", "[MicRecovery] StartOfRound is not ready; continuing local Dissonance detection. Game-side reset will be skipped by recovery flow.")); } _preRoundSkipLogged = true; } if (flag2) { _preRoundSkipLogged = false; } if (PluginConfig.SuspendAutoRecoveryDuringMenuOrTeardown.Value && !flag3) { if (!flag) { SuspendRecoveryForTeardown(MicRecoveryText.T("检测到房间正在退出或语音上下文正在销毁,已暂停自动恢复。", "Room exit or voice context teardown detected; automatic recovery is suspended.")); return; } if ((PluginConfig.DebugEnabled || PluginConfig.StateLogsEnabled) && !_teardownSuspendLogged) { ManualLogSource log4 = Plugin.Log; if (log4 != null) { log4.LogInfo((object)MicRecoveryText.T("[MicRecovery] 游戏侧重置当前不安全,自动检测仍将允许本地 Dissonance 恢复。", "[MicRecovery] Game-side reset is currently unsafe; automatic detection will still allow local Dissonance recovery.")); } _teardownSuspendLogged = true; } } else { _teardownSuspendLogged = false; } _deviceBuffer.Clear(); try { comms.GetMicrophoneDevices(_deviceBuffer); } catch (Exception ex) { _autoDeviceListFailureCount++; if (PluginConfig.DebugEnabled) { ManualLogSource log5 = Plugin.Log; if (log5 != null) { log5.LogWarning((object)MicRecoveryText.Format("[MicRecovery] 自动检测获取录音设备列表失败({0}/2):{1}", "[MicRecovery] Automatic detection failed to get the recording device list ({0}/2): {1}", _autoDeviceListFailureCount, ex.Message)); } } if (_autoDeviceListFailureCount >= 2) { _autoDeviceListFailureCount = 0; MicRecoveryCore.TryRecover(MicRecoveryText.T("自动检测连续获取录音设备列表失败", "Automatic detection failed to get the recording device list repeatedly")); } return; } _autoDeviceListFailureCount = 0; if (PluginConfig.SuspendAutoRecoveryWhenNoDevices.Value && _deviceBuffer.Count == 0) { if (PluginConfig.EnableNoDeviceSuspendLog.Value && (!PluginConfig.LogNoDeviceSuspendOnlyOnce.Value || !_noDeviceSuspendLogged)) { ManualLogSource log6 = Plugin.Log; if (log6 != null) { log6.LogInfo((object)MicRecoveryText.T("[MicRecovery] 未检测到任何录音设备,已暂停自动恢复与相关状态日志。", "[MicRecovery] No recording devices were detected; automatic recovery and related state logs are suspended.")); } _noDeviceSuspendLogged = true; } return; } _noDeviceSuspendLogged = false; string currentMicName; try { currentMicName = comms.MicrophoneName; } catch (Exception ex2) { if (PluginConfig.DebugEnabled) { ManualLogSource log7 = Plugin.Log; if (log7 != null) { log7.LogWarning((object)MicRecoveryText.Format("[MicRecovery] 自动检测读取当前麦克风名称失败:{0}", "[MicRecovery] Automatic detection failed to read the current microphone name: {0}", ex2.Message)); } } MicRecoveryCore.TryRecover(MicRecoveryText.T("自动检测读取当前麦克风名称失败", "Automatic detection failed to read the current microphone name")); return; } if (PluginConfig.RecoverWhenMicNameEmpty.Value && string.IsNullOrWhiteSpace(currentMicName)) { MicRecoveryCore.TryRecover(MicRecoveryText.T("当前 Dissonance 麦克风名称为空", "Current Dissonance microphone name is empty")); return; } bool isInPostRecoveryGracePeriod = MicRecoveryCore.IsInPostRecoveryGracePeriod; if (!isInPostRecoveryGracePeriod) { try { IMicrophoneCapture microphoneCapture = comms.MicrophoneCapture; if (microphoneCapture == null) { MicRecoveryCore.TryRecover(MicRecoveryText.T("Dissonance 麦克风采集管线为空", "Dissonance microphone capture pipeline is null")); return; } Object val = (Object)(object)((microphoneCapture is Object) ? microphoneCapture : null); if (val != null && val == (Object)null) { _cachedComms = null; _nextCommsSearchTime = 0f; MicRecoveryCore.TryRecover(MicRecoveryText.T("Dissonance 麦克风采集管线已被销毁", "Dissonance microphone capture pipeline has been destroyed")); return; } if (!microphoneCapture.IsRecording) { MicRecoveryCore.TryRecover(MicRecoveryText.T("Dissonance 麦克风采集管线未在录音", "Dissonance microphone capture pipeline is not recording")); return; } } catch (Exception ex3) { if (PluginConfig.DebugEnabled) { ManualLogSource log8 = Plugin.Log; if (log8 != null) { log8.LogWarning((object)MicRecoveryText.Format("[MicRecovery] 读取 Dissonance 麦克风采集管线失败:{0}", "[MicRecovery] Failed to read the Dissonance microphone capture pipeline: {0}", ex3.Message)); } } } } else if (PluginConfig.DebugEnabled) { ManualLogSource log9 = Plugin.Log; if (log9 != null) { log9.LogInfo((object)MicRecoveryText.T("[MicRecovery] 当前处于恢复宽限期,跳过 Dissonance 麦克风采集管线判定。", "[MicRecovery] Currently in the recovery grace period; skipping Dissonance microphone capture pipeline checks.")); } } bool flag4 = _deviceBuffer.Any((string d) => string.Equals(d, currentMicName, StringComparison.Ordinal)); if (PluginConfig.StateLogsEnabled) { ManualLogSource log10 = Plugin.Log; if (log10 != null) { log10.LogInfo((object)MicRecoveryText.Format("[MicRecovery] 当前麦克风:{0} | 设备数:{1} | 设备仍存在:{2}", "[MicRecovery] Current microphone: {0} | Device count: {1} | Device still exists: {2}", currentMicName, _deviceBuffer.Count, flag4)); } } if (PluginConfig.RecoverWhenDeviceMissing.Value && !flag4) { MicRecoveryCore.TryRecover(MicRecoveryText.Format("当前麦克风已不在设备列表中:{0}", "Current microphone is no longer in the device list: {0}", currentMicName)); return; } if (isInPostRecoveryGracePeriod) { if (PluginConfig.DebugEnabled) { ManualLogSource log11 = Plugin.Log; if (log11 != null) { log11.LogInfo((object)MicRecoveryText.T("[MicRecovery] 当前处于恢复宽限期,跳过 Unity.IsRecording 判定。", "[MicRecovery] Currently in the recovery grace period; skipping Unity.IsRecording check.")); } } return; } bool flag5 = false; try { if (!string.IsNullOrWhiteSpace(currentMicName)) { flag5 = Microphone.IsRecording(currentMicName); } } catch (Exception ex4) { if (PluginConfig.DebugEnabled) { ManualLogSource log12 = Plugin.Log; if (log12 != null) { log12.LogWarning((object)MicRecoveryText.Format("[MicRecovery] 调用 Microphone.IsRecording 失败:{0}", "[MicRecovery] Microphone.IsRecording call failed: {0}", ex4.Message)); } } } if (PluginConfig.StateLogsEnabled) { ManualLogSource log13 = Plugin.Log; if (log13 != null) { log13.LogInfo((object)$"[MicRecovery] Unity.IsRecording({currentMicName}) = {flag5}"); } } if (PluginConfig.RecoverWhenUnityNotRecording.Value && !flag5) { MicRecoveryCore.TryRecover(MicRecoveryText.Format("Unity 报告麦克风未在录音:{0}", "Unity reports that the microphone is not recording: {0}", currentMicName)); } } catch (Exception ex5) { ManualLogSource log14 = Plugin.Log; if (log14 != null) { log14.LogWarning((object)MicRecoveryText.Format("[MicRecovery] AutoCheckMic 异常:{0}", "[MicRecovery] AutoCheckMic exception: {0}", ex5)); } _cachedComms = null; _nextCommsSearchTime = 0f; } } } internal static class MicRecoveryCore { private enum GameSideResetResult { Invoked, SkippedInFlight, SkippedUnsafe, MethodNotFound, Failed } [CompilerGenerated] private sealed class <SafeRunGameSideResetCoroutine>d__29 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public IEnumerator routine; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <SafeRunGameSideResetCoroutine>d__29(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if (num == -3 || num == 1) { try { } finally { <>m__Finally1(); } } <>1__state = -2; } private bool MoveNext() { bool result; try { int num = <>1__state; if (num == 0) { <>1__state = -1; <>1__state = -3; goto IL_0027; } if (num == 1) { <>1__state = -3; goto IL_0027; } result = false; goto end_IL_0000; IL_0027: bool flag = false; bool flag2 = false; object obj; try { flag = routine.MoveNext(); obj = (flag ? routine.Current : null); } catch (Exception ex) { flag2 = true; if (!_gameSideResetCoroutineWarningLogged) { _gameSideResetCoroutineWarningLogged = true; ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)MicRecoveryText.Format("[MicRecovery] 游戏侧 ResetDissonanceCommsComponent 协程执行失败,已停止该协程并保留本地 ResetMicrophoneCapture 结果:{0}", "[MicRecovery] Game-side ResetDissonanceCommsComponent coroutine failed; stopped it and kept the local ResetMicrophoneCapture result: {0}", ex.Message)); } } else if (PluginConfig.DebugEnabled) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)MicRecoveryText.Format("[MicRecovery] 游戏侧 ResetDissonanceCommsComponent 协程再次失败:{0}", "[MicRecovery] Game-side ResetDissonanceCommsComponent coroutine failed again: {0}", ex.Message)); } } obj = null; } if (flag2 || !flag) { result = false; <>m__Finally1(); } else { <>2__current = obj; <>1__state = 1; result = true; } end_IL_0000:; } catch { //try-fault ((IDisposable)this).Dispose(); throw; } return result; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; _gameSideResetInFlight = false; } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } private const float FailedRecoveryBackoffSeconds = 3f; private const float FailedRecoveryLogBackoffSeconds = 10f; private const float GameSideResetGraceSeconds = 3f; private static float _lastRecoveryTime = -999f; private static float _lastFailedRecoveryTime = -999f; private static float _lastFailureLogTime = -999f; private static float _postRecoveryGraceUntil = -999f; private static bool _gameSideResetCompatibilityWarningLogged; private static bool _gameSideResetCoroutineWarningLogged; private static bool _gameSideResetInFlight; internal static bool IsInPostRecoveryGracePeriod => Time.unscaledTime < _postRecoveryGraceUntil; internal static bool TryRecover(string reason) { return TryRecover(reason, ignoreCooldown: false); } internal static bool TryRecover(string reason, bool ignoreCooldown) { float num = ((PluginConfig.RecoveryCooldownSeconds != null) ? Mathf.Max(0f, PluginConfig.RecoveryCooldownSeconds.Value) : 10f); if (!ignoreCooldown && Time.unscaledTime - _lastRecoveryTime < num) { if (PluginConfig.DebugEnabled) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)MicRecoveryText.Format("[MicRecovery] 恢复冷却中,跳过本次恢复。reason={0}", "[MicRecovery] Recovery cooldown active; skipping this recovery. reason={0}", reason)); } } return false; } if (!ignoreCooldown && Time.unscaledTime - _lastFailedRecoveryTime < 3f) { LogFailedRecoveryBackoff(reason); return false; } LogRecoveryStep(1, MicRecoveryText.T("检测到疑似麦克风失效,开始执行恢复", "Possible microphone failure detected; starting recovery")); LogRecoveryStep(2, MicRecoveryText.Format("触发原因:{0}", "Trigger reason: {0}", reason)); try { DissonanceComms val = Object.FindObjectOfType<DissonanceComms>(); if ((Object)(object)val == (Object)null) { MarkRecoveryFailed(reason, MicRecoveryText.T("未找到 DissonanceComms", "DissonanceComms not found"), delegate { ManualLogSource log12 = Plugin.Log; if (log12 != null) { log12.LogError((object)MicRecoveryText.T("========== [麦克风修复 3/5] 未找到 DissonanceComms,无法执行恢复 ==========", "========== [Mic Recovery 3/5] DissonanceComms was not found; recovery cannot run ==========")); } ManualLogSource log13 = Plugin.Log; if (log13 != null) { log13.LogError((object)MicRecoveryText.T("========== [麦克风修复 4/5] 本次恢复失败 ==========", "========== [Mic Recovery 4/5] This recovery attempt failed ==========")); } ManualLogSource log14 = Plugin.Log; if (log14 != null) { log14.LogError((object)MicRecoveryText.T("========== [麦克风修复 5/5] 请检查当前场景是否已进入可语音状态 ==========", "========== [Mic Recovery 5/5] Check whether the current scene is ready for voice chat ==========")); } }); return false; } string text = null; try { text = val.MicrophoneName; } catch (Exception ex2) { if (PluginConfig.DebugEnabled) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)MicRecoveryText.Format("[MicRecovery] 读取当前麦克风名称失败:{0}", "[MicRecovery] Failed to read current microphone name: {0}", ex2.Message)); } } } List<string> list = new List<string>(); try { val.GetMicrophoneDevices(list); } catch (Exception ex3) { if (PluginConfig.DebugEnabled) { ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogWarning((object)MicRecoveryText.Format("[MicRecovery] 获取麦克风列表失败:{0}", "[MicRecovery] Failed to get microphone device list: {0}", ex3.Message)); } } } string[] array = list.ToArray(); LogRecoveryStep(3, MicRecoveryText.Format("当前麦克风:{0} | 设备数:{1}", "Current microphone: {0} | Device count: {1}", string.IsNullOrEmpty(text) ? MicRecoveryText.T("<空>", "<empty>") : text, array.Length)); string text2 = PickPreferredDevice(array, text); if (!string.IsNullOrEmpty(text2)) { if (!string.Equals(text, text2, StringComparison.Ordinal)) { val.MicrophoneName = text2; if (PluginConfig.DebugEnabled) { ManualLogSource log4 = Plugin.Log; if (log4 != null) { log4.LogInfo((object)MicRecoveryText.Format("[MicRecovery] 已切换麦克风到首选设备:{0}", "[MicRecovery] Switched microphone to preferred device: {0}", text2)); } } } else if (PluginConfig.DebugEnabled) { ManualLogSource log5 = Plugin.Log; if (log5 != null) { log5.LogInfo((object)MicRecoveryText.Format("[MicRecovery] 当前已是首选设备:{0}", "[MicRecovery] Current microphone is already the preferred device: {0}", text2)); } } } else { ManualLogSource log6 = Plugin.Log; if (log6 != null) { log6.LogWarning((object)MicRecoveryText.T("[MicRecovery] 没找到可用的首选设备,将沿用当前设备继续恢复。", "[MicRecovery] No preferred device was found; continuing recovery with the current device.")); } } val.ResetMicrophoneCapture(); if (PluginConfig.DebugEnabled || PluginConfig.StateLogsEnabled) { ManualLogSource log7 = Plugin.Log; if (log7 != null) { log7.LogInfo((object)MicRecoveryText.T("[MicRecovery] 本地 ResetMicrophoneCapture 已成功执行。", "[MicRecovery] Local ResetMicrophoneCapture completed successfully.")); } } _lastRecoveryTime = Time.unscaledTime; _lastFailedRecoveryTime = -999f; GameSideResetResult gameSideResetResult = TryInvokeGameSideReset(); if (PluginConfig.DebugEnabled || PluginConfig.StateLogsEnabled) { ManualLogSource log8 = Plugin.Log; if (log8 != null) { log8.LogInfo((object)$"[MicRecovery] Game side reset result: {gameSideResetResult}"); } } float num2 = ((PluginConfig.PostRecoveryGraceSeconds != null) ? Mathf.Max(0f, PluginConfig.PostRecoveryGraceSeconds.Value) : 4f); if (gameSideResetResult == GameSideResetResult.Invoked) { num2 += 3f; } _postRecoveryGraceUntil = Mathf.Max(_postRecoveryGraceUntil, Time.unscaledTime + num2); float num3 = Mathf.Max(0f, _postRecoveryGraceUntil - Time.unscaledTime); switch (gameSideResetResult) { case GameSideResetResult.Invoked: LogRecoveryStep(4, MicRecoveryText.T("已执行 ResetMicrophoneCapture,已启动游戏侧重置", "ResetMicrophoneCapture completed; game-side reset started")); break; case GameSideResetResult.SkippedInFlight: LogRecoveryStep(4, MicRecoveryText.T("已执行 ResetMicrophoneCapture,已跳过游戏侧重置(已有重置正在执行)", "ResetMicrophoneCapture completed; skipped game-side reset because one is already running")); break; case GameSideResetResult.SkippedUnsafe: LogRecoveryStep(4, MicRecoveryText.T("已执行 ResetMicrophoneCapture,已跳过游戏侧重置(当前阶段不安全)", "ResetMicrophoneCapture completed; skipped game-side reset because the current phase is unsafe")); break; case GameSideResetResult.MethodNotFound: LogRecoveryStep(4, MicRecoveryText.T("已执行 ResetMicrophoneCapture,未执行游戏侧重置(未找到重置方法)", "ResetMicrophoneCapture completed; game-side reset was not run because the reset method was not found")); break; default: LogRecoveryStep(4, MicRecoveryText.T("已执行 ResetMicrophoneCapture,未执行游戏侧重置(调用失败)", "ResetMicrophoneCapture completed; game-side reset was not run because the call failed")); break; } LogRecoveryStep(5, MicRecoveryText.Format("麦克风恢复流程已触发,进入 {0:0.##} 秒恢复宽限期,请立刻测试语音是否恢复", "Microphone recovery has been triggered; entering a {0:0.##} second grace period. Test voice chat now.", num3)); ShowRecoveryCompleteTip(); LogRecoveryCompletionDetail(); return true; } catch (Exception ex4) { Exception ex5 = ex4; Exception ex = ex5; MarkRecoveryFailed(reason, MicRecoveryText.T("恢复过程中出现异常", "An exception occurred during recovery"), delegate { ManualLogSource log9 = Plugin.Log; if (log9 != null) { log9.LogError((object)MicRecoveryText.T("========== [麦克风修复 3/5] 恢复过程中出现异常 ==========", "========== [Mic Recovery 3/5] An exception occurred during recovery ==========")); } ManualLogSource log10 = Plugin.Log; if (log10 != null) { log10.LogError((object)MicRecoveryText.Format("========== [麦克风修复 4/5] 异常:{0} ==========", "========== [Mic Recovery 4/5] Exception: {0} ==========", ex)); } ManualLogSource log11 = Plugin.Log; if (log11 != null) { log11.LogError((object)MicRecoveryText.T("========== [麦克风修复 5/5] 本次恢复失败,请把日志贴回来 ==========", "========== [Mic Recovery 5/5] This recovery attempt failed; please provide the log ==========")); } }); return false; } } internal static bool IsMenuSceneActive() { //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(); if (!((Scene)(ref activeScene)).IsValid()) { return true; } return (((Scene)(ref activeScene)).name ?? string.Empty).IndexOf("menu", StringComparison.OrdinalIgnoreCase) >= 0; } catch { return true; } } internal static bool IsRecoveryBlockedByScene() { return IsMenuSceneActive(); } private static bool TryGetStartOfRoundInstance(out StartOfRound startOfRoundInstance) { startOfRoundInstance = null; try { startOfRoundInstance = StartOfRound.Instance; } catch { startOfRoundInstance = null; return false; } return (Object)(object)startOfRoundInstance != (Object)null; } internal static bool IsStartOfRoundReady() { StartOfRound startOfRoundInstance; return TryGetStartOfRoundInstance(out startOfRoundInstance); } internal static bool IsGameSideResetSafe() { try { if (IsMenuSceneActive()) { return false; } if (!TryGetStartOfRoundInstance(out var startOfRoundInstance)) { return false; } if ((Object)(object)startOfRoundInstance.localPlayerController == (Object)null) { return false; } if ((Object)(object)startOfRoundInstance.voiceChatModule == (Object)null) { return false; } return true; } catch { return false; } } private static void LogRecoveryStep(int step, string message) { if (PluginConfig.ShowFiveStepRecoveryLogs != null && PluginConfig.ShowFiveStepRecoveryLogs.Value) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)MicRecoveryText.Format("========== [麦克风修复 {0}/5] {1} ==========", "========== [Mic Recovery {0}/5] {1} ==========", step, message)); } } else if (PluginConfig.DebugEnabled) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogInfo((object)$"[MicRecovery] Step {step}/5: {message}"); } } } private static void ShowRecoveryCompleteTip() { try { if (!((Object)(object)HUDManager.Instance == (Object)null)) { HUDManager.Instance.DisplayTip(MicRecoveryText.RecoveryCompleteTipTitle, MicRecoveryText.RecoveryCompleteTipBody, false, false, "LCMicRecovery_RecoveryComplete"); MicRecoveryText.WarnHudFallbackIfNeeded(); } } catch (Exception ex) { if (PluginConfig.DebugEnabled) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)MicRecoveryText.Format("[MicRecovery] 显示恢复完成提示失败:{0}", "[MicRecovery] Failed to show recovery completion tip: {0}", ex.Message)); } } } } private static void LogRecoveryCompletionDetail() { if ((PluginConfig.ShowFiveStepRecoveryLogs != null && PluginConfig.ShowFiveStepRecoveryLogs.Value) || PluginConfig.DebugEnabled || PluginConfig.StateLogsEnabled) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)MicRecoveryText.T("[MicRecovery] 修复完成说明:修复完成以当前可检测到的实际修复效果为准。若修复后问题仍未解决,可能存在其他影响因素,需结合具体场景进一步排查。", "[MicRecovery] Recovery completion note: Completion is based on the currently detectable recovery result. If the issue remains, other factors may be involved and the specific situation should be checked.")); } } } private static void MarkRecoveryFailed(string reason, string summary, Action writeFailureLog) { _lastFailedRecoveryTime = Time.unscaledTime; if (ShouldWriteFailureLog()) { writeFailureLog?.Invoke(); } else if (PluginConfig.DebugEnabled) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)MicRecoveryText.Format("[MicRecovery] 本次恢复失败,失败日志退避中:{0}。reason={1}", "[MicRecovery] This recovery attempt failed; failure log backoff is active: {0}. reason={1}", summary, reason)); } } } private static bool ShouldWriteFailureLog() { if (Time.unscaledTime - _lastFailureLogTime < 10f) { return false; } _lastFailureLogTime = Time.unscaledTime; return true; } private static void LogFailedRecoveryBackoff(string reason) { if (ShouldWriteFailureLog()) { float num = Mathf.Max(0f, 3f - (Time.unscaledTime - _lastFailedRecoveryTime)); ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)MicRecoveryText.Format("[MicRecovery] 最近一次恢复失败,进入失败退避,{0:0.##} 秒后再尝试恢复。reason={1}", "[MicRecovery] The previous recovery attempt failed; backing off for {0:0.##} more seconds. reason={1}", num, reason)); } } } private static string PickPreferredDevice(string[] devices, string currentMicName) { if (devices == null || devices.Length == 0) { return null; } if (PluginConfig.PreferCurrentDeviceIfStillExists != null && PluginConfig.PreferCurrentDeviceIfStillExists.Value && !string.IsNullOrWhiteSpace(currentMicName) && devices.Any((string d) => string.Equals(d, currentMicName, StringComparison.Ordinal))) { return currentMicName; } string[] array = (from x in ((PluginConfig.PreferredDeviceKeywords != null) ? PluginConfig.PreferredDeviceKeywords.Value : "Maxwell,Audeze").Split(new char[1] { ',' }, StringSplitOptions.RemoveEmptyEntries) select x.Trim() into x where !string.IsNullOrWhiteSpace(x) select x).ToArray(); foreach (string keyword in array) { string text = devices.FirstOrDefault((string d) => !string.IsNullOrEmpty(d) && d.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0); if (!string.IsNullOrEmpty(text)) { return text; } } return devices.FirstOrDefault((string d) => !string.IsNullOrWhiteSpace(d)); } private static GameSideResetResult TryInvokeGameSideReset() { try { if (!IsGameSideResetSafe()) { if (PluginConfig.DebugEnabled) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)MicRecoveryText.T("[MicRecovery] 当前不适合调用游戏侧重置入口,已跳过 ResetDissonanceCommsComponent。", "[MicRecovery] Current state is unsafe for the game-side reset entry; skipped ResetDissonanceCommsComponent.")); } } return GameSideResetResult.SkippedUnsafe; } if (!TryGetStartOfRoundInstance(out var startOfRoundInstance)) { LogGameSideResetCompatibilityWarningOnce(MicRecoveryText.T("无法获取 StartOfRound 实例", "StartOfRound instance is unavailable")); return GameSideResetResult.SkippedUnsafe; } if ((Object)(object)startOfRoundInstance.voiceChatModule == (Object)null) { LogGameSideResetCompatibilityWarningOnce(MicRecoveryText.T("StartOfRound.voiceChatModule 不可用", "StartOfRound.voiceChatModule is unavailable")); if (PluginConfig.DebugEnabled) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)MicRecoveryText.T("[MicRecovery] StartOfRound.voiceChatModule 不可用。", "[MicRecovery] StartOfRound.voiceChatModule is unavailable.")); } } return GameSideResetResult.SkippedUnsafe; } if ((Object)(object)Plugin.Instance == (Object)null) { if (PluginConfig.DebugEnabled) { ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogWarning((object)MicRecoveryText.T("[MicRecovery] Plugin.Instance 不可用,无法启动游戏侧重置协程。", "[MicRecovery] Plugin.Instance is unavailable; cannot start the game-side reset coroutine.")); } } return GameSideResetResult.Failed; } if (_gameSideResetInFlight) { if (PluginConfig.DebugEnabled) { ManualLogSource log4 = Plugin.Log; if (log4 != null) { log4.LogInfo((object)MicRecoveryText.T("[MicRecovery] 游戏侧 ResetDissonanceCommsComponent 已在执行中,跳过新的游戏侧重置。", "[MicRecovery] Game-side ResetDissonanceCommsComponent is already running; skipped a new game-side reset.")); } } return GameSideResetResult.SkippedInFlight; } IEnumerator enumerator = startOfRoundInstance.ResetDissonanceCommsComponent(); if (enumerator == null) { if (PluginConfig.DebugEnabled) { ManualLogSource log5 = Plugin.Log; if (log5 != null) { log5.LogWarning((object)MicRecoveryText.T("[MicRecovery] 游戏侧 ResetDissonanceCommsComponent 未返回可执行协程。", "[MicRecovery] Game-side ResetDissonanceCommsComponent did not return a runnable coroutine.")); } } return GameSideResetResult.Failed; } _gameSideResetInFlight = true; try { ((MonoBehaviour)Plugin.Instance).StartCoroutine(SafeRunGameSideResetCoroutine(enumerator)); } catch { _gameSideResetInFlight = false; throw; } return GameSideResetResult.Invoked; } catch (MissingMethodException ex) { LogGameSideResetCompatibilityWarningOnce(MicRecoveryText.T("ResetDissonanceCommsComponent 运行时不可用", "ResetDissonanceCommsComponent is unavailable at runtime")); if (PluginConfig.DebugEnabled) { ManualLogSource log6 = Plugin.Log; if (log6 != null) { log6.LogWarning((object)MicRecoveryText.Format("[MicRecovery] 游戏侧重置入口运行时不可用:{0}", "[MicRecovery] Game-side reset entry is unavailable at runtime: {0}", ex.Message)); } } return GameSideResetResult.MethodNotFound; } catch (Exception ex2) { if (PluginConfig.DebugEnabled) { ManualLogSource log7 = Plugin.Log; if (log7 != null) { log7.LogWarning((object)MicRecoveryText.Format("[MicRecovery] 调用游戏侧重置入口失败:{0}", "[MicRecovery] Failed to call the game-side reset entry: {0}", ex2.Message)); } } return GameSideResetResult.Failed; } } private static void LogGameSideResetCompatibilityWarningOnce(string reason) { if (!_gameSideResetCompatibilityWarningLogged) { _gameSideResetCompatibilityWarningLogged = true; ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)MicRecoveryText.Format("[MicRecovery] 游戏侧重置入口不可用({0}),已降级为仅执行本地 ResetMicrophoneCapture。", "[MicRecovery] Game-side reset entry is unavailable ({0}); degraded to local ResetMicrophoneCapture only.", reason)); } } } [IteratorStateMachine(typeof(<SafeRunGameSideResetCoroutine>d__29))] private static IEnumerator SafeRunGameSideResetCoroutine(IEnumerator routine) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <SafeRunGameSideResetCoroutine>d__29(0) { routine = routine }; } } } namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] internal sealed class IgnoresAccessChecksToAttribute : Attribute { public IgnoresAccessChecksToAttribute(string assemblyName) { } } }