Decompiled source of BetterPeakVoiceFix v1.1.8

BetterPeakVoiceFix.dll

Decompiled 2 weeks ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using System.Threading;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using ExitGames.Client.Photon;
using HarmonyLib;
using Peak.Network;
using PeakVoiceFix.Patches;
using Photon.Pun;
using Photon.Realtime;
using Photon.Voice;
using Photon.Voice.PUN;
using Photon.Voice.Unity;
using Steamworks;
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("BetterPeakVoiceFix")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.1.8.0")]
[assembly: AssemblyInformationalVersion("1.1.8+b19e269f1f360419d67f13fcd2f66356ce0eb3ba")]
[assembly: AssemblyProduct("BetterPeakVoiceFix")]
[assembly: AssemblyTitle("BetterPeakVoiceFix")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.1.8.0")]
[module: UnverifiableCode]
namespace PeakVoiceFix
{
	internal static class InviteHandshake
	{
		private const float TIMEOUT = 15f;

		private const int MAX_ATTEMPTS = 4;

		private static readonly FieldInfo fRequesting = AccessTools.Field(typeof(SteamLobbyHandler), "m_currentlyRequestingRoomID");

		private static readonly FieldInfo fWaiting = AccessTools.Field(typeof(SteamLobbyHandler), "m_currentlyWaitingForRoomID");

		private static float sentAt = -1f;

		private static int attempts = 0;

		public static bool Available
		{
			get
			{
				if (fRequesting != null)
				{
					return fWaiting != null;
				}
				return false;
			}
		}

		private static SteamLobbyHandler Handler
		{
			get
			{
				try
				{
					return GameHandler.GetService<SteamLobbyHandler>();
				}
				catch (Exception)
				{
					return null;
				}
			}
		}

		public static void NoteRequestSent()
		{
			sentAt = Time.unscaledTime;
			if (attempts == 0)
			{
				attempts = 1;
			}
			NetworkManager.DiagLog(L.Get("invite_requested", attempts));
		}

		public static void Update()
		{
			if (VoiceFix.EnableInviteRetry == null || !VoiceFix.EnableInviteRetry.Value || sentAt < 0f)
			{
				return;
			}
			if (PhotonNetwork.InRoom || !IsWaiting())
			{
				Reset();
			}
			else
			{
				if (Time.unscaledTime - sentAt < 15f)
				{
					return;
				}
				attempts++;
				if (attempts > 4)
				{
					Fail();
					return;
				}
				sentAt = Time.unscaledTime;
				if (ClearRequestingFlag())
				{
					NetworkManager.DiagLog(L.Get("invite_retry", attempts, 4));
				}
				else
				{
					Fail();
				}
			}
		}

		private static void Fail()
		{
			string text = L.Get("invite_failed", 4);
			if (VoiceFix.logger != null)
			{
				VoiceFix.logger.LogWarning((object)text);
			}
			Debug.LogWarning((object)("[PVF] " + text));
			if ((Object)(object)VoiceUIManager.Instance != (Object)null)
			{
				VoiceUIManager.Instance.SetRegionWarning(text);
				VoiceUIManager.Instance.AddLog("System", text, isLocal: true);
			}
			Reset();
		}

		public static void Reset()
		{
			sentAt = -1f;
			attempts = 0;
		}

		private static bool IsWaiting()
		{
			try
			{
				SteamLobbyHandler handler = Handler;
				if (handler == null || fWaiting == null)
				{
					return false;
				}
				object value = fWaiting.GetValue(handler);
				if (value == null)
				{
					return false;
				}
				PropertyInfo property = value.GetType().GetProperty("IsSome", BindingFlags.Instance | BindingFlags.Public);
				return property != null && (bool)property.GetValue(value);
			}
			catch (Exception)
			{
				return false;
			}
		}

		private static bool ClearRequestingFlag()
		{
			try
			{
				SteamLobbyHandler handler = Handler;
				if (handler == null || fRequesting == null)
				{
					return false;
				}
				Type fieldType = fRequesting.FieldType;
				object obj = null;
				PropertyInfo property = fieldType.GetProperty("None", BindingFlags.Static | BindingFlags.Public);
				if (property != null)
				{
					obj = property.GetValue(null);
				}
				if (obj == null)
				{
					FieldInfo field = fieldType.GetField("None", BindingFlags.Static | BindingFlags.Public);
					if (field != null)
					{
						obj = field.GetValue(null);
					}
				}
				if (obj == null)
				{
					return false;
				}
				fRequesting.SetValue(handler, obj);
				return true;
			}
			catch (Exception)
			{
				return false;
			}
		}
	}
	[HarmonyPatch(typeof(SteamLobbyHandler), "RequestPhotonRoomID")]
	internal static class RequestRoomIDPatch
	{
		private static void Postfix()
		{
			InviteHandshake.NoteRequestSent();
		}
	}
	[HarmonyPatch]
	internal static class InviteRoomNameEncodingPatch
	{
		private const string DeserializerType = "Zorro.Core.Serizalization.BinaryDeserializer";

		private static int warned;

		private static readonly FieldInfo LabelsField = typeof(CodeInstruction).GetField("labels");

		private static MethodBase TargetMethod()
		{
			MethodInfo[] array = (from m in typeof(SteamLobbyHandler).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic)
				where m.Name == "HandleMessage"
				select m).ToArray();
			if (array == null || array.Length != 1)
			{
				throw new MissingMethodException("SteamLobbyHandler.HandleMessage is not unique");
			}
			MethodInfo obj = array[0];
			ParameterInfo[] parameters = obj.GetParameters();
			if (obj.ReturnType != typeof(void) || parameters.Length != 3 || parameters[0].ParameterType.FullName != "SteamLobbyHandler+MessageType" || parameters[1].ParameterType.FullName != "Zorro.Core.Serizalization.BinaryDeserializer" || parameters[2].ParameterType.FullName != "Steamworks.CSteamID")
			{
				throw new MissingMethodException("SteamLobbyHandler.HandleMessage signature changed");
			}
			return obj;
		}

		[HarmonyPriority(0)]
		[HarmonyAfter(new string[] { "com.github.LengSword.BetterRoomShare" })]
		internal static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
		{
			//IL_034a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0351: Expected O, but got Unknown
			List<CodeInstruction> list = instructions.ToList();
			try
			{
				int num = -1;
				int num2 = -1;
				for (int i = 0; i < list.Count; i++)
				{
					if (list[i].operand is MethodInfo { Name: "ReadString" } methodInfo && methodInfo.DeclaringType?.FullName == "Zorro.Core.Serizalization.BinaryDeserializer")
					{
						if (num >= 0 || methodInfo.IsStatic || methodInfo.ReturnType != typeof(string) || methodInfo.GetParameters().Length != 1 || methodInfo.GetParameters()[0].ParameterType != typeof(Encoding))
						{
							return Skip(list, "room-name ReadString is ambiguous or changed");
						}
						num = i;
					}
					if (list[i].opcode == OpCodes.Stfld && list[i].operand is FieldInfo fieldInfo && fieldInfo.DeclaringType?.FullName == "JoinSpecificRoomState" && fieldInfo.Name == "RoomName")
					{
						if (num2 >= 0 || fieldInfo.FieldType != typeof(string))
						{
							return Skip(list, "RoomName destination is ambiguous or changed");
						}
						num2 = i;
					}
				}
				if (num < 2 || num + 3 >= list.Count || num2 <= num + 3 || list[num - 2].opcode != OpCodes.Ldarg_2 || list[num - 1].opcode != OpCodes.Call || list[num].opcode != OpCodes.Callvirt || list[num + 2].opcode != OpCodes.Ldarg_0 || !IsField(list[num + 3], OpCodes.Ldflda, "m_currentlyWaitingForRoomID") || !list.Take(num - 2).Any((CodeInstruction c) => IsField(c, OpCodes.Ldflda, "m_currentlyRequestingRoomID")) || LocalIndex(list[num + 1], store: true) < 0 || LocalIndex(list[num + 1], store: true) != LocalIndex(list[num2 - 1], store: false) || HasIncomingLabel(list[num - 1]) || HasIncomingLabel(list[num]) || list[num - 1].blocks.Count != 0 || list[num].blocks.Count != 0)
				{
					return Skip(list, "room-name read/destination structure changed");
				}
				MethodInfo method = typeof(Encoding).GetMethod("get_ASCII");
				MethodInfo method2 = typeof(Encoding).GetMethod("get_UTF8");
				if (object.Equals(list[num - 1].operand, method2))
				{
					return list;
				}
				if (!object.Equals(list[num - 1].operand, method))
				{
					return Skip(list, "unknown room-name encoding; left unchanged");
				}
				CodeInstruction val = new CodeInstruction(list[num - 1]);
				val.operand = method2;
				list[num - 1] = val;
				return list;
			}
			catch (Exception ex)
			{
				return Skip(list, "inspection failed: " + ex);
			}
		}

		private static bool HasIncomingLabel(CodeInstruction c)
		{
			if (LabelsField?.GetValue(c) is ICollection collection)
			{
				return collection.Count != 0;
			}
			return true;
		}

		private static bool IsField(CodeInstruction c, OpCode opcode, string name)
		{
			if (c.opcode == opcode && c.operand is FieldInfo fieldInfo && fieldInfo.DeclaringType?.FullName == "SteamLobbyHandler")
			{
				return fieldInfo.Name == name;
			}
			return false;
		}

		private static int LocalIndex(CodeInstruction c, bool store)
		{
			if (c.opcode == (store ? OpCodes.Stloc_0 : OpCodes.Ldloc_0))
			{
				return 0;
			}
			if (c.opcode == (store ? OpCodes.Stloc_1 : OpCodes.Ldloc_1))
			{
				return 1;
			}
			if (c.opcode == (store ? OpCodes.Stloc_2 : OpCodes.Ldloc_2))
			{
				return 2;
			}
			if (c.opcode == (store ? OpCodes.Stloc_3 : OpCodes.Ldloc_3))
			{
				return 3;
			}
			if (c.opcode == (store ? OpCodes.Stloc : OpCodes.Ldloc) || c.opcode == (store ? OpCodes.Stloc_S : OpCodes.Ldloc_S))
			{
				if (c.operand is LocalBuilder localBuilder)
				{
					return localBuilder.LocalIndex;
				}
				object operand = c.operand;
				if (operand is int)
				{
					return (int)operand;
				}
				operand = c.operand;
				if (operand is byte)
				{
					return (byte)operand;
				}
			}
			return -1;
		}

		private static IEnumerable<CodeInstruction> Skip(List<CodeInstruction> code, string reason)
		{
			if (Interlocked.Exchange(ref warned, 1) == 0)
			{
				ManualLogSource logger = VoiceFix.logger;
				if (logger != null)
				{
					logger.LogWarning((object)("[邀请编码] " + reason + "; UTF-8 adaptation skipped, BVF core remains active."));
				}
			}
			return code;
		}
	}
	public static class L
	{
		private static string _lang = "中文";

		private static Dictionary<string, string> _current;

		private static readonly Dictionary<string, string> ZH = new Dictionary<string, string>
		{
			{ "ui_title", "语音详细状态" },
			{ "ui_debug_title", "语音修复调试控制台 (Alt+J) | EvCode:186" },
			{ "btn_copy_all", "复制全部" },
			{ "btn_export", "导出文件" },
			{ "btn_clear", "清空" },
			{ "btn_dump", "打印语音底层名单" },
			{ "filter_all", "全部" },
			{ "filter_local", "本机" },
			{ "client_not_connected", "客户端未连接" },
			{ "voice_player_list", "语音底层名单" },
			{ "ghost_tag", "[幽灵]" },
			{ "exported_to", "已导出" },
			{ "export_failed", "失败" },
			{ "copied", "已复制" },
			{ "label_local", "本机" },
			{ "label_sync", "同步:" },
			{ "label_connected", "已连:" },
			{ "label_pending", "待确认:" },
			{ "label_abnormal", "异常:" },
			{ "warn_majority", "⚠ [警告] 多数玩家({0}人)在另一频道!" },
			{ "virtual_player", "虚拟玩家1" },
			{ "sos_snapshot", "[SOS快照]" },
			{ "sos_majority", "多数派" },
			{ "sos_person", "人" },
			{ "sos_detected", "检测到 {0} 掉线" },
			{ "sos_target", "目标" },
			{ "sos_last", "上次" },
			{ "unknown", "未知" },
			{ "region_not_reported", "区服未上报" },
			{ "cache_snapshot", "[缓存快照]" },
			{ "seconds_ago", "秒前" },
			{ "not_connected", "未连接" },
			{ "history", "[历史]" },
			{ "notification_disconnected", "连接断开" },
			{ "manual_operation", "[系统] 手动操作..." },
			{ "state_synced", "同步" },
			{ "state_connecting", "连接中" },
			{ "state_disconnected", "断开" },
			{ "state_connected", "已连接" },
			{ "state_mismatch", "错位" },
			{ "state_abnormal", "异服" },
			{ "state_cross_region", "跨区" },
			{ "state_reported", "自报在线" },
			{ "state_unknown", "未知" },
			{ "cs_initializing", "初始化中..." },
			{ "cs_authenticating", "验证中..." },
			{ "cs_authenticated", "已验证" },
			{ "cs_joining", "加入中..." },
			{ "cs_joined", "已连接" },
			{ "cs_disconnecting", "断开中..." },
			{ "cs_disconnected", "断开" },
			{ "cs_connecting_game", "连接到游戏服务器中..." },
			{ "cs_connecting_master", "连接到主服务器中..." },
			{ "cs_connecting_name", "连接到名称服务器中..." },
			{ "detail_connecting_local", "连接中..." },
			{ "voice_offline", "离线模式(语音未启用)" },
			{ "log_state_change", "状态变更" },
			{ "log_host_ip_change", "房主连接服务器变动" },
			{ "log_host_disconnected", "[Host] 房主意外断开,正在自动恢复..." },
			{ "log_loop_blind", "[循环] 已失败{0}次,暂时切换为盲连..." },
			{ "log_wrong_freq", "[异频] 当前:{0} 目标:{1} | 纠正({2}/2)" },
			{ "log_compromise", "[妥协] 纠正失败,驻留当前IP: {0}" },
			{ "log_reconnect_go", "[重连] {0} | 区服:{1}" },
			{ "log_reconnect_timeout", "[重连] 等待断开完成超时,本轮放弃" },
			{ "log_wrong_destination", "[语音目的地] 当前 {0}/{1},游戏 {2}/{3};纠正({4}/2)" },
			{ "log_destination_repair", "纠正语音区服/房间" },
			{ "log_connection_stalled", "[语音卡住] {0} 状态持续 {1} 秒,准备恢复" },
			{ "log_stalled_repair", "连接超时恢复" },
			{ "log_disconnect_cause", "[语音断开] 原因:{0} | 游戏区服:{1} | 语音区服:{2} | 重连来源:{3}" },
			{ "log_connect_rejected", "[语音连接] 连接请求未接受 | 状态:{0} | 上次断开原因:{1}" },
			{ "log_auth_rejected", "[语音认证被拒] 操作:{0} | 错误码:{1} | 服务端原话:{2} | 服务器:{3} | 沿用游戏认证:{4}" },
			{ "log_auth_config_error", "[语音认证] 错误码 {0} 属于配置类拒绝,照原参数重连不会好转,重试间隔已拉长到 {1} 秒" },
			{ "log_auth_aligned", "[语音认证] 对象 '{0}' 的「沿用游戏认证」开关为开(与游戏预制体不同,多见于被代码默认创建),已对齐为关,避免语音应用返回 32755" },
			{ "log_native_recovery", "[语音重连] Photon 已开始恢复({0}),等待本轮完成" },
			{ "join_auth_start", "[房间码] 正在为加入 {0} 区服刷新认证" },
			{ "join_auth_failed", "跨区认证失败,请重试加入房间。" },
			{ "log_decision", "[决策] 目标变更" },
			{ "log_majority", "多数派({0}人)" },
			{ "log_host", "房主" },
			{ "log_auto_blind", "自动(盲连)" },
			{ "log_sos_send", "[SOS] 发送求救" },
			{ "log_sos_target", "目标" },
			{ "log_sos_local", "本机" },
			{ "log_sos_manual", "手动断开 (Manual)" },
			{ "log_sos_received", "收到 {0} SOS (目标:{1})" },
			{ "log_alt_k_disconnect", "[系统] Alt+K 手动断开" },
			{ "log_alt_k_reconnect", "[系统] Alt+K 强制重连" },
			{ "log_cache_snapshot", "[缓存快照] 记录数" },
			{ "cfg_cat_ui", "UI设置" },
			{ "cfg_cat_adv", "高级与调试" },
			{ "cfg_ui_position", "选择UI面板显示在屏幕的哪一侧。" },
			{ "cfg_show_pro", "是否在面板中显示具体的已连接语音服务器IP地址和调试信息。" },
			{ "cfg_offset_x_r", "距离屏幕右边缘的水平距离。" },
			{ "cfg_offset_y_r", "距离屏幕上边缘的垂直距离。" },
			{ "cfg_offset_x_l", "距离屏幕左边缘的水平距离。" },
			{ "cfg_offset_y_l", "距离屏幕上边缘的垂直距离。" },
			{ "cfg_font_size", "面板文字的基础大小。" },
			{ "cfg_host_symbol", "显示在房主名字前的特殊符号。" },
			{ "cfg_timeout", "连接在同一步骤卡住超过此秒数时自动恢复,最少按 5 秒处理;同时用于判断新玩家的连接宽限期。" },
			{ "cfg_retry_interval", "语音断线后每次自动重连之间的冷却时间。只管语音重连,不影响 Steam 邀请索要房间号的重试(那个固定 15 秒一次、最多 4 次)。" },
			{ "cfg_manual_reconnect", "允许按 Alt+K 强制断开或重连语音。" },
			{ "line2_voice_state_missing", "语音状态异常·本机播放受阻" },
			{ "line2_voice_state_recovered", "语音状态已恢复" },
			{ "cfg_max_name_len", "显示名字的最大字符数。" },
			{ "cfg_auto_hide", "在机场常驻显示简易UI。关闭后简易UI只在出现异常或有通知时才冒出来;非机场场景一律只在异常时显示。" },
			{ "cfg_show_ping", "在简易模式下方显示本机延迟。" },
			{ "cfg_hide_menu", "打开ESC菜单时隐藏UI。" },
			{ "cfg_virtual_player", "添加一个假玩家用于测试UI布局。" },
			{ "cfg_virtual_name", "假玩家的名字。" },
			{ "cfg_language", "语言(重启游戏生效) | Language(Need restart)" },
			{ "cfgn_ui_position", "UI位置" },
			{ "cfgn_toggle_key", "详细UI切换键" },
			{ "cfgn_show_pro", "显示连接到的语音服务器IP和详细信息" },
			{ "cfgn_offset_x_r", "右侧边距" },
			{ "cfgn_offset_y_r", "顶部边距(右)" },
			{ "cfgn_offset_x_l", "左侧边距" },
			{ "cfgn_offset_y_l", "顶部边距(左)" },
			{ "cfgn_font_size", "字体大小" },
			{ "cfgn_host_symbol", "房主标记符号" },
			{ "cfgn_timeout", "重连超时时间 (s)" },
			{ "cfgn_retry_interval", "重试间隔 (s)" },
			{ "cfgn_manual_reconnect", "启用手动重置 (Alt+K)" },
			{ "cfgn_max_name_len", "最大名字长度" },
			{ "cfgn_auto_hide", "机场常驻简易UI" },
			{ "cfgn_show_ping", "简易模式显示Ping" },
			{ "cfgn_hide_menu", "ESC菜单界面时隐藏" },
			{ "cfgn_virtual_player", "启用虚拟玩家" },
			{ "cfgn_virtual_name", "虚拟玩家名字" },
			{ "cfgn_forced_region", "强制区服-重启生效" },
			{ "cfg_forced_region", "强制游戏连接到指定 Photon 区服。auto = 由游戏自己测速选择。\n⚠ 注意:强制区服不等于更快,地理上更近的区服路由未必更优——无加速器直连时强制 hk 甚至延迟可能翻一倍(200ms→400ms+);该功能仅作为与区服延迟测试(Alt+J 面板中找到)功能联合调试使用。\n只影响你自己开房和主菜单连接;加入别人的房时游戏会自动切到房主所在区服,这是游戏本身的行为。可选 auto / asia / au / eu / hk / jp / sa / us / ussc / usw。改完需重启游戏。" },
			{ "region_worse", "[区服] 强制 {0} 实测 {1}ms,而自动选区({2}) 只有 {3}ms —— 强制反而更慢,建议改回 auto。" },
			{ "region_high", "[区服] 强制 {0} 实测 {1}ms,延迟偏高。没有加速器时强制区服通常不会更快。" },
			{ "region_ping_partial", "已测到的部分结果:" },
			{ "region_timeout", "[区服] 已强制 {0},但 20 秒仍未连上主服务器。检查加速器,或把「强制区服」改回 auto。" },
			{ "region_status_title", "区服状态" },
			{ "region_game", "游戏区服" },
			{ "region_voice", "语音区服" },
			{ "region_room_code", "房间码" },
			{ "region_from_code", "房间码解出" },
			{ "region_forced", "强制设置" },
			{ "region_auto", "auto(自动测速)" },
			{ "region_cache_pun", "游戏测速缓存" },
			{ "region_cache_voice", "语音测速缓存" },
			{ "region_unlisted", "未收录(多为港服)" },
			{ "region_ping_title", "各区延迟" },
			{ "region_ping_started", "[区服] 开始测速,约需 3-10 秒…" },
			{ "region_ping_busy", "[区服] 正在测速中,请稍候。" },
			{ "region_ping_failed", "[区服] 测速客户端连接失败。" },
			{ "region_ping_nosettings", "[区服] 读不到 PhotonServerSettings,无法测速。" },
			{ "region_ping_timeout", "[区服] 测速超时(30 秒)。" },
			{ "region_ping_error", "[区服] 测速出错: {0}" },
			{ "region_ping_empty", " 没有拿到任何区服,可能是网络不通。" },
			{ "region_ping_hint", " ★ = Photon 判定的最佳区;— = 未测到;>3s = 不可达" },
			{ "btn_region_ping", "测各区延迟" },
			{ "btn_region_status", "区服状态" },
			{ "mod_outdated", "{0}" },
			{ "majority_region", "多数派语音区服: {0} ({1},{2}人)" },
			{ "cfg_toggle_key_v2", "切换语音面板的按键,循环顺序:关闭 → 简易 → 详细。" },
			{ "hdr_room_region", "房间区服:" },
			{ "hdr_room_voice", "房间语音服:" },
			{ "hdr_local_state", "房间/语音地区:" },
			{ "hdr_room_status", "房间情况:" },
			{ "hdr_local_ping", "本机延迟:" },
			{ "col_status", "[状态]" },
			{ "col_name", "玩家名" },
			{ "col_ping", "房间 - 语音" },
			{ "line2_voice_server", "连接的语音服:" },
			{ "line2_cannot_judge", "本机语音未连接,暂时无法确认对方状态" },
			{ "line2_no_report", "等待对方语音状态或连接信息" },
			{ "line2_reported_connected", "对方自报已连接 {0}" },
			{ "local_voice_unavailable", "本机未连接,等待恢复" },
			{ "line2_not_in_voice", "未连入语音服" },
			{ "src_local_guess", "本机推定" },
			{ "src_host", "房主" },
			{ "src_single", "1人上报" },
			{ "rv_unknown", "未知(无人上报)" },
			{ "local_isolated", "本机孤立" },
			{ "voice_only_local", "语音房内只有本机" },
			{ "err_voice_connect", "语音服连接错误" },
			{ "snap_decision", "决策:" },
			{ "snap_retry", "重连:" },
			{ "snap_retry_fmt", "{0} 次 | 失败 {1} | IP不符 {2}" },
			{ "snap_voiceroom", "语音房:" },
			{ "snap_voiceroom_fmt", "{0} 人在线 | 判定 {1}/{2} | 幽灵 {3}" },
			{ "snap_roomcode", "房间码:" },
			{ "snap_voicename", "语音房名:" },
			{ "room_name_mismatch", "语音房名与游戏房名 + _voice_ 不一致,请检查语音连接。" },
			{ "room_name_pending", "房名信息缺失,暂时无法确认是否匹配。" },
			{ "snap_forced", "强制区服:" },
			{ "snap_no_decision", "尚未介入(语音正常,无需决策)" },
			{ "snap_blind", "盲连" },
			{ "newjoin", "新加入" },
			{ "newjoin_connecting", "正在连接…" },
			{ "newjoin_failed", "连接失败" },
			{ "diag_vregion_change", "[诊断] 本机语音区服: {0} → {1}" },
			{ "diag_vroom_change", "[诊断] 本机语音房: {0} → {1} (AppId {2})" },
			{ "diag_appid_snapshot", "[诊断] Photon AppId 快照: realtime {0} / voice {1}" },
			{ "diag_appid_restored", "[AppId守卫] 检测到 AppId 被改写: realtime {0} / voice {1} → 已还原真值" },
			{ "diag_isolated", "[诊断] 本机孤立:语音房内只有自己,游戏房有 {0} 人" },
			{ "diag_isolated_clear", "[诊断] 孤立解除,语音房内已有 {0} 人" },
			{ "diag_roomvoice", "[诊断] 房间语音服判定为 {0}(来源: {1})" },
			{ "diag_roomvoice_lost", "[诊断] 房间语音服失去可信来源,转为未知" },
			{ "diag_cross", "[诊断] {0} 跨区: 语音在 {1},房间在 {2}" },
			{ "diag_cross_clear", "[诊断] {0} 已回到房间区服 {1}" },
			{ "diag_newjoin_ok", "[诊断] {0} 入房后 {1} 秒接入语音" },
			{ "diag_newjoin_fail", "[诊断] {0} 入房 {1} 秒仍未接入语音" },
			{ "diag_mode_change", "[诊断] 面板模式: {0} → {1}" },
			{ "diag_cfg_change", "[诊断] 配置变更: {0} = {1}" },
			{ "mode_off", "关闭" },
			{ "mode_simple", "简易" },
			{ "mode_detail", "详细" },
			{ "cfgn_unknown_region", "未收录区服按此解析" },
			{ "cfg_unknown_region", "兼容以 '-' 开头的旧版或自定义房间码,指定它应解析到哪个区服。PEAK 3.0.0 的新港服房间码已使用 P,正常按游戏规则处理。留空保留原有解析;与 BetterRoomShare 的同类修复兼容。" },
			{ "cfgn_long_room_code", "兼容超长房间码(BetterRoomShare)" },
			{ "cfg_long_room_code", "本体手动输入房间码只接受 6 位。房主装了 BetterRoomShare 并开了自定义房名后,房间码会变长(如 -AYUANSHENNB666),没装 BRS 的客机会被「房间码长度不对」拦下。开启后输入框不限长度、校验按实际长度放行,规则与 BetterRoomShare 的「EnableVariableRoomCodeLength」一致,两者同时开也不冲突。只影响手动输入房间码,不影响 Steam 邀请/右键加入。默认关闭。" },
			{ "cfgn_wait_master", "切区服后等主服务器就绪" },
			{ "cfg_wait_master", "加入房间前先确认主服务器连接就绪,最多等待 20 秒。跨区所需的身份认证由本模组或已安装的 FixJoinRoomCode 处理。" },
			{ "cfgn_appid_guard", "Photon AppId 守卫" },
			{ "cfg_appid_guard", "本机 Photon AppId 防改写:插件加载时快照真 AppIdRealtime/AppIdVoice,之后发现被改写即还原,语音每次连接前也会兜底拨回。\n已知改写来源:LocalMultiplayer 会在每次 NetworkConnector.Start 把全局 AppId 换成它配置里的自建 Photon 应用——语音首连撞上就连进另一个应用的同名房,同区同房名却互不可见、双向静音。\n仅当你有意使用自建 Photon 应用时才需要关闭。" },
			{ "joinwait_timeout", "[房间码] 等待主服务器超时(状态: {0},区服: {1}),加房已取消。" },
			{ "joinwait_ready", "[房间码] 主服务器已就绪(区服 {0}),继续加房" },
			{ "cfgn_invite_retry", "邀请握手超时重试" },
			{ "cfg_invite_retry", "接受 Steam 邀请后,客机要向大厅索要房间号,而房主只在自己确实待在房间里时才回应。本体这个握手没有超时、不重发、也不弹任何提示,房主当时在菜单/加载中/刚掉线,你就会永久静默卡在主菜单。开启后每 15 秒重试一次、最多 4 次,全部失败会明确告知原因。" },
			{ "invite_requested", "[邀请] 已向大厅索要房间号(第 {0} 次)" },
			{ "invite_retry", "[邀请] 15 秒未收到房间号,重新索要({0}/{1})" },
			{ "invite_failed", "[邀请] 重试 {0} 次仍未收到房间号。房主可能不在房间里(在菜单、加载中或已掉线),也可能你连不上房主所在区服。让房主先进入房间再邀请,或改用房间码。" }
		};

		private static readonly Dictionary<string, string> EN = new Dictionary<string, string>
		{
			{ "ui_title", "Voice Detail Status" },
			{ "ui_debug_title", "Voice Fix Debug Console (Alt+J) | EvCode:186" },
			{ "btn_copy_all", "Copy All" },
			{ "btn_export", "Export" },
			{ "btn_clear", "Clear" },
			{ "btn_dump", "Dump Photon Player List" },
			{ "filter_all", "All" },
			{ "filter_local", "Local" },
			{ "client_not_connected", "Client not connected" },
			{ "voice_player_list", "Photon Player List" },
			{ "ghost_tag", "[Ghost]" },
			{ "exported_to", "Exported to" },
			{ "export_failed", "Failed" },
			{ "copied", "Copied" },
			{ "label_local", "Local" },
			{ "label_sync", "Sync:" },
			{ "label_connected", "Online:" },
			{ "label_pending", "Pending:" },
			{ "label_abnormal", "Abnormal:" },
			{ "warn_majority", "⚠ [WARN] Majority ({0}) on another channel!" },
			{ "virtual_player", "Virtual Player 1" },
			{ "sos_snapshot", "[SOS Snapshot]" },
			{ "sos_majority", "Majority" },
			{ "sos_person", "" },
			{ "sos_detected", "Detected {0} disconnected" },
			{ "sos_target", "Target" },
			{ "sos_last", "Last" },
			{ "unknown", "Unknown" },
			{ "region_not_reported", "Region not reported" },
			{ "cache_snapshot", "[Cache Snapshot]" },
			{ "seconds_ago", "sec ago" },
			{ "not_connected", "Not Connected" },
			{ "history", "[History]" },
			{ "notification_disconnected", "Disconnected" },
			{ "manual_operation", "[System] Manual operation..." },
			{ "state_synced", "Synced" },
			{ "state_connecting", "Connecting" },
			{ "state_disconnected", "Disconnected" },
			{ "state_connected", "Connected" },
			{ "state_mismatch", "Mismatched" },
			{ "state_abnormal", "Cross-server" },
			{ "state_cross_region", "Cross-region" },
			{ "state_reported", "Reported online" },
			{ "state_unknown", "Unknown" },
			{ "cs_initializing", "Initializing..." },
			{ "cs_authenticating", "Authenticating..." },
			{ "cs_authenticated", "Authenticated" },
			{ "cs_joining", "Joining..." },
			{ "cs_joined", "Joined" },
			{ "cs_disconnecting", "Disconnecting..." },
			{ "cs_disconnected", "Disconnected" },
			{ "cs_connecting_game", "Connecting to game server..." },
			{ "cs_connecting_master", "Connecting to master server..." },
			{ "cs_connecting_name", "Connecting to name server..." },
			{ "detail_connecting_local", "Connecting..." },
			{ "voice_offline", "Offline mode (voice disabled)" },
			{ "log_state_change", "State change" },
			{ "log_host_ip_change", "Host server connection change" },
			{ "log_host_disconnected", "[Host] Unexpected disconnect, recovering..." },
			{ "log_loop_blind", "[Loop] Failed {0} times, switching to blind..." },
			{ "log_wrong_freq", "[WrongIP] Current:{0} Target:{1} | Fix({2}/2)" },
			{ "log_compromise", "[Compromise] Fix failed, staying on: {0}" },
			{ "log_reconnect_go", "[Reconnect] {0} | Region:{1}" },
			{ "log_reconnect_timeout", "[Reconnect] Timed out waiting for disconnect, skipping this round" },
			{ "log_wrong_destination", "[Voice destination] Current {0}/{1}, game {2}/{3}; repair({4}/2)" },
			{ "log_destination_repair", "Voice region/room repair" },
			{ "log_connection_stalled", "[Voice stalled] State {0} has not progressed for {1}s; recovering" },
			{ "log_stalled_repair", "Connection timeout recovery" },
			{ "log_disconnect_cause", "[Voice disconnected] Cause:{0} | Game region:{1} | Voice region:{2} | Recovery source:{3}" },
			{ "log_connect_rejected", "[Voice connect] Request rejected | State:{0} | Last disconnect cause:{1}" },
			{ "log_auth_rejected", "[Voice auth rejected] Op:{0} | Code:{1} | Server message:{2} | Server:{3} | Reuse game auth:{4}" },
			{ "log_auth_config_error", "[Voice auth] Code {0} is a configuration rejection; retrying with the same parameters cannot succeed, retry interval raised to {1}s" },
			{ "log_auth_aligned", "[Voice auth] Object '{0}' had reuse-game-auth ON (unlike the game prefab; typical when created by code defaults). Aligned to OFF to avoid code 32755 from the voice app" },
			{ "log_native_recovery", "[Voice reconnect] Photon has started recovery ({0}); waiting for it to finish" },
			{ "join_auth_start", "[Room code] Refreshing authentication before joining region {0}" },
			{ "join_auth_failed", "Region authentication failed. Please try joining the room again." },
			{ "log_decision", "[Decision] Target changed" },
			{ "log_majority", "Majority ({0})" },
			{ "log_host", "Host" },
			{ "log_auto_blind", "Auto (Blind)" },
			{ "log_sos_send", "[SOS] Sending SOS" },
			{ "log_sos_target", "Target" },
			{ "log_sos_local", "Local" },
			{ "log_sos_manual", "Manual Disconnect" },
			{ "log_sos_received", "Received {0} SOS (Target:{1})" },
			{ "log_alt_k_disconnect", "[System] Alt+K Manual disconnect" },
			{ "log_alt_k_reconnect", "[System] Alt+K Force reconnect" },
			{ "log_cache_snapshot", "[Cache Snapshot] Count" },
			{ "cfg_cat_ui", "UI Settings" },
			{ "cfg_cat_adv", "Advanced & Debug" },
			{ "cfg_ui_position", "Which side of the screen to display the UI panel." },
			{ "cfg_show_pro", "Show connected voice server IP and debug info in the panel." },
			{ "cfg_offset_x_r", "Horizontal offset from right edge." },
			{ "cfg_offset_y_r", "Vertical offset from top edge." },
			{ "cfg_offset_x_l", "Horizontal offset from left edge." },
			{ "cfg_offset_y_l", "Vertical offset from top edge." },
			{ "cfg_font_size", "Base font size of the panel." },
			{ "cfg_host_symbol", "Symbol displayed before host's name." },
			{ "cfg_timeout", "Recover when a connection step stalls for this long (minimum 5s). Also used as the grace period for newly joined players." },
			{ "cfg_retry_interval", "Cooldown between voice auto-reconnect attempts. Voice only - the Steam invite room-ID retry is fixed at every 15s, up to 4 times." },
			{ "cfg_manual_reconnect", "Allow Alt+K to force disconnect/reconnect voice." },
			{ "line2_voice_state_missing", "Voice state error · local playback blocked" },
			{ "line2_voice_state_recovered", "Voice state restored" },
			{ "cfg_max_name_len", "Max display characters for names." },
			{ "cfg_auto_hide", "Keep the simple overlay visible while in the airport. When off, it only appears on a problem or a notification. Outside the airport it always only appears on a problem." },
			{ "cfg_show_ping", "Show local ping below simple mode UI." },
			{ "cfg_hide_menu", "Hide UI when ESC menu is open." },
			{ "cfg_virtual_player", "Add a fake player for UI layout testing." },
			{ "cfg_virtual_name", "Name of the fake player." },
			{ "cfg_language", "语言(重启游戏生效) | Language(Need restart)" },
			{ "cfgn_ui_position", "UI Position" },
			{ "cfgn_toggle_key", "Toggle Key" },
			{ "cfgn_show_pro", "Show Server IP & Details" },
			{ "cfgn_offset_x_r", "Right Margin" },
			{ "cfgn_offset_y_r", "Top Margin (Right)" },
			{ "cfgn_offset_x_l", "Left Margin" },
			{ "cfgn_offset_y_l", "Top Margin (Left)" },
			{ "cfgn_font_size", "Font Size" },
			{ "cfgn_host_symbol", "Host Symbol" },
			{ "cfgn_timeout", "Reconnect Timeout (s)" },
			{ "cfgn_retry_interval", "Retry Interval (s)" },
			{ "cfgn_manual_reconnect", "Enable Manual Reset (Alt+K)" },
			{ "cfgn_max_name_len", "Max Name Length" },
			{ "cfgn_auto_hide", "Persistent Simple UI in Airport" },
			{ "cfgn_show_ping", "Show Ping in Simple Mode" },
			{ "cfgn_hide_menu", "Hide on ESC Menu" },
			{ "cfgn_virtual_player", "Enable Virtual Player" },
			{ "cfgn_virtual_name", "Virtual Player Name" },
			{ "cfgn_forced_region", "Forced Region - needs restart" },
			{ "cfg_forced_region", "Force the game to connect to a specific Photon region. auto = let the game pick by ping.\nWARNING: forcing a region does not mean lower latency; a geographically closer region may still route worse - connecting to hk without a VPN/proxy can even double latency (200ms -> 400ms+). Use this only together with the region latency test (found in the Alt+J panel).\nOnly affects hosting your own room and the main-menu connection; joining someone else's room follows the host's region, which is the game's own behaviour. Accepts auto / asia / au / eu / hk / jp / sa / us / ussc / usw. Restart required." },
			{ "region_worse", "[Region] Forced {0} measures {1}ms, but auto ({2}) was only {3}ms - forcing made it worse. Consider setting it back to auto." },
			{ "region_high", "[Region] Forced {0} measures {1}ms, which is high. Without a proxy, forcing a region usually does not help." },
			{ "region_ping_partial", "Partial results measured so far:" },
			{ "region_timeout", "[Region] Forced {0}, but still not connected to a master server after 20s. Check your connection, or set Forced Region back to auto." },
			{ "region_status_title", "Region Status" },
			{ "region_game", "Game region" },
			{ "region_voice", "Voice region" },
			{ "region_room_code", "Room code" },
			{ "region_from_code", "decoded" },
			{ "region_forced", "Forced setting" },
			{ "region_auto", "auto (ping-based)" },
			{ "region_cache_pun", "Game ping cache" },
			{ "region_cache_voice", "Voice ping cache" },
			{ "region_unlisted", "unlisted (usually hk)" },
			{ "region_ping_title", "Region Latency" },
			{ "region_ping_started", "[Region] Pinging regions, takes 3-10s..." },
			{ "region_ping_busy", "[Region] A ping run is already in progress." },
			{ "region_ping_failed", "[Region] Ping client failed to connect." },
			{ "region_ping_nosettings", "[Region] PhotonServerSettings unavailable, cannot ping." },
			{ "region_ping_timeout", "[Region] Ping run timed out (30s)." },
			{ "region_ping_error", "[Region] Ping error: {0}" },
			{ "region_ping_empty", " No regions returned - network may be unreachable." },
			{ "region_ping_hint", " * = Photon's best region; - = not measured; >3s = unreachable" },
			{ "btn_region_ping", "Ping Regions" },
			{ "btn_region_status", "Region Status" },
			{ "mod_outdated", "{0}" },
			{ "majority_region", "Majority voice region: {0} ({1}, {2} people)" },
			{ "cfg_toggle_key_v2", "Key that cycles the voice panel: off -> simple -> detailed." },
			{ "hdr_room_region", "Room region:" },
			{ "hdr_room_voice", "Room voice server:" },
			{ "hdr_local_state", "Local game/voice:" },
			{ "hdr_room_status", "Room:" },
			{ "hdr_local_ping", "Local ping:" },
			{ "col_status", "[Status]" },
			{ "col_name", "Player" },
			{ "col_ping", "room - voice" },
			{ "line2_voice_server", "voice server:" },
			{ "line2_cannot_judge", "Local voice is disconnected; this player's status is unknown" },
			{ "line2_no_report", "Waiting for this player's voice status or connection details" },
			{ "line2_reported_connected", "Player reports a voice connection on {0}" },
			{ "local_voice_unavailable", "Local voice disconnected; waiting to recover" },
			{ "line2_not_in_voice", "not in the voice room" },
			{ "src_local_guess", "local guess" },
			{ "src_host", "host" },
			{ "src_single", "1 report" },
			{ "rv_unknown", "unknown (nobody reported)" },
			{ "local_isolated", "local isolated" },
			{ "voice_only_local", "only this client in the voice room" },
			{ "err_voice_connect", "wrong voice server" },
			{ "snap_decision", "Decision:" },
			{ "snap_retry", "Retries:" },
			{ "snap_retry_fmt", "{0} | failed {1} | wrong IP {2}" },
			{ "snap_voiceroom", "Voice room:" },
			{ "snap_voiceroom_fmt", "{0} online | classified {1}/{2} | ghosts {3}" },
			{ "snap_roomcode", "Room code:" },
			{ "snap_voicename", "Voice room name:" },
			{ "room_name_mismatch", "Voice room name does not equal the game room name + _voice_. Check the voice connection." },
			{ "room_name_pending", "Room name information is missing; matching cannot be confirmed yet." },
			{ "snap_forced", "Forced region:" },
			{ "snap_no_decision", "not engaged (voice is fine, nothing to decide)" },
			{ "snap_blind", "blind connect" },
			{ "newjoin", "Joined" },
			{ "newjoin_connecting", "connecting..." },
			{ "newjoin_failed", "connection failed" },
			{ "diag_vregion_change", "[Diag] Local voice region: {0} -> {1}" },
			{ "diag_vroom_change", "[Diag] Local voice room: {0} -> {1} (AppId {2})" },
			{ "diag_appid_snapshot", "[Diag] Photon AppId snapshot: realtime {0} / voice {1}" },
			{ "diag_appid_restored", "[AppIdGuard] AppId rewritten (realtime {0} / voice {1}), restored real values" },
			{ "diag_isolated", "[Diag] Local isolated: alone in the voice room while {0} players are in the game room" },
			{ "diag_isolated_clear", "[Diag] Isolation cleared, {0} clients in the voice room" },
			{ "diag_roomvoice", "[Diag] Room voice server resolved to {0} (source: {1})" },
			{ "diag_roomvoice_lost", "[Diag] Room voice server lost every trusted source, now unknown" },
			{ "diag_cross", "[Diag] {0} is cross-region: voice on {1}, room on {2}" },
			{ "diag_cross_clear", "[Diag] {0} returned to the room region {1}" },
			{ "diag_newjoin_ok", "[Diag] {0} reached voice {1}s after joining" },
			{ "diag_newjoin_fail", "[Diag] {0} still not in voice {1}s after joining" },
			{ "diag_mode_change", "[Diag] Panel mode: {0} -> {1}" },
			{ "diag_cfg_change", "[Diag] Config changed: {0} = {1}" },
			{ "mode_off", "off" },
			{ "mode_simple", "simple" },
			{ "mode_detail", "detailed" },
			{ "cfgn_unknown_region", "Unlisted region resolves to" },
			{ "cfg_unknown_region", "Choose a region for legacy or custom room codes starting with '-'. PEAK 3.0.0 HK room codes use P and follow the game's normal mapping. Leave empty to retain the original decoding. Compatible with BetterRoomShare's equivalent fix." },
			{ "cfgn_long_room_code", "Allow long room codes (BetterRoomShare)" },
			{ "cfg_long_room_code", "The game only accepts 6-character room codes on the manual join page. Hosts running BetterRoomShare with custom room names produce longer codes (e.g. -AYUANSHENNB666), which clients without BRS reject as the wrong length. When on, the input box has no length limit and the check uses the actual length - same rule as BetterRoomShare's EnableVariableRoomCodeLength; both can be on together. Only affects manual room-code entry, not Steam invites / Join Game. Off by default." },
			{ "cfgn_wait_master", "Wait for master server after region swap" },
			{ "cfg_wait_master", "Wait up to 20 seconds for the master server before joining a room. Cross-region authentication is handled by BVF or an installed FixJoinRoomCode." },
			{ "cfgn_appid_guard", "Photon AppId Guard" },
			{ "cfg_appid_guard", "Protects this machine's Photon AppIds: snapshots the real AppIdRealtime/AppIdVoice at plugin load, restores them whenever the global settings get rewritten, and re-checks right before every voice connect.\nKnown source: LocalMultiplayer swaps both AppIds for its own Photon app on every NetworkConnector.Start - a voice first-connect that hits the rewrite lands in the other app's same-named room: same region and name, invisible to everyone, muted both ways.\nDisable only if you intentionally run a custom Photon app." },
			{ "joinwait_timeout", "[RoomCode] Timed out waiting for the master server (state: {0}, region: {1}); join cancelled." },
			{ "joinwait_ready", "[RoomCode] Master server ready (region {0}), continuing to join" },
			{ "cfgn_invite_retry", "Retry the invite handshake on timeout" },
			{ "cfg_invite_retry", "After accepting a Steam invite the client asks the lobby for the room ID, and the host only answers while actually sitting in a room. The game's handshake has no timeout, never retries and shows nothing at all, so if the host is in a menu, still loading or has just dropped, you stay silently stuck on the main menu forever. With this on the request is retried every 15 seconds, up to 4 times, and the failure message names both likely causes." },
			{ "invite_requested", "[Invite] Asked the lobby for the room ID (attempt {0})" },
			{ "invite_retry", "[Invite] No room ID after 15s, asking again ({0}/{1})" },
			{ "invite_failed", "[Invite] Still no room ID after {0} attempts. The host may not be in a room (in a menu, loading, or just dropped), or you may not be able to reach the host's region. Ask the host to enter the room before inviting you, or use the room code instead." }
		};

		public static bool IsChinese => _lang == "中文";

		public static string DetectDefault()
		{
			if (!CultureInfo.CurrentUICulture.Name.StartsWith("zh"))
			{
				return "English";
			}
			return "中文";
		}

		public static void Init(string lang)
		{
			_lang = lang;
			_current = ((_lang == "中文") ? ZH : EN);
		}

		public static string Get(string key)
		{
			if (_current != null && _current.TryGetValue(key, out var value))
			{
				return value;
			}
			if (ZH.TryGetValue(key, out var value2))
			{
				return value2;
			}
			return key;
		}

		public static string Get(string key, params object[] args)
		{
			return string.Format(Get(key), args);
		}
	}
	public class CacheEntry
	{
		public string IP;

		public float LastSeenTime;

		public string PlayerName;

		public byte RemoteState;

		public string ModVersion;

		public string VoiceRegion;

		public int VoicePing = -1;

		public int GamePing = -1;
	}
	public class SOSData
	{
		public int ActorNumber;

		public string PlayerName;

		public string TargetIP;

		public string OriginIP;

		public float ReceiveTime;
	}
	public static class NetworkManager
	{
		public enum RoomVoiceSource
		{
			Majority,
			Host,
			SingleReport,
			LocalGuess,
			Unknown
		}

		public static Dictionary<int, CacheEntry> PlayerCache = new Dictionary<int, CacheEntry>();

		public static List<SOSData> ActiveSOSList = new List<SOSData>();

		public static List<string> HostHistory = new List<string>();

		private static string LastKnownHostIP = "";

		private static string LastDecisionLog = "";

		private static ClientState lastClientState = (ClientState)14;

		public static PunVoiceClient punVoice;

		private static float nextRetryTime = 0f;

		private static float lastVoiceProgressTime;

		private static float voiceJoinedAt = -1f;

		private const float STABLE_VOICE_SECONDS = 10f;

		private static bool reconnectPending = false;

		private static bool manualDisconnectRequested;

		private static string reconnectPendingMode = "";

		private static float reconnectPendingDeadline = 0f;

		private const float RECONNECT_WAIT_TIMEOUT = 5f;

		private static MethodInfo initNetworkVoiceMethod;

		private static bool initNetworkVoiceResolved = false;

		private static float lastPingPublishTime = 0f;

		private static float lastSOSTime = 0f;

		private static float nextSummaryLogTime = 0f;

		private static bool wasInRoom = false;

		private static float nextVoiceClientFindTime = 0f;

		private static float nextSOSManageTime = 0f;

		private static int lastPlayerCount = 0;

		private static readonly Dictionary<int, float> scavengeFailTime = new Dictionary<int, float>();

		private static readonly Dictionary<int, float> firstSeenTime = new Dictionary<int, float>();

		private static readonly Dictionary<int, int> voiceToGameActor = new Dictionary<int, int>();

		private static readonly Dictionary<int, int> gameToVoiceActor = new Dictionary<int, int>();

		private static float nextVoiceMapRefreshTime = 0f;

		private const float VOICE_MAP_REFRESH_INTERVAL = 1f;

		private const float SCAN_INTERVAL = 30f;

		private const float CACHE_TTL = 180f;

		private const float PING_PUBLISH_INTERVAL = 30f;

		private const float VOICE_CLIENT_FIND_INTERVAL = 1f;

		private const float SOS_MANAGE_INTERVAL = 0.5f;

		private const float SCAVENGE_RETRY_INTERVAL = 10f;

		private const string PROP_IP = "PVF_IP";

		private const string PROP_PING = "PVF_Ping";

		public const string PROP_VREGION = "PVF_VR";

		public const string PROP_VPING = "PVF_VP";

		private const byte TYPE_SOS = 0;

		private const byte TYPE_LOG = 1;

		private const byte TYPE_STATE = 2;

		private const byte TYPE_PING = 3;

		private const float PING_EVENT_INTERVAL = 3f;

		private static float nextPingEventTime = 0f;

		private static float nextDiagTime = 0f;

		private static string lastDiagVoiceRegion = null;

		private static string lastDiagVoiceRoom = null;

		private static bool lastDiagIsolated = false;

		private static string lastDiagRoomVoice = null;

		private static RoomVoiceSource lastDiagRoomVoiceSource = RoomVoiceSource.Unknown;

		private static readonly Dictionary<int, string> lastDiagCross = new Dictionary<int, string>();

		private static bool authAlignedLogged;

		private static bool configErrorLogged;

		public static string TargetGameServer { get; private set; }

		public static bool ConnectedUsingHost { get; private set; } = true;

		public static bool IsBlindConnect { get; private set; } = false;

		public static int WrongIPCount { get; private set; } = 0;

		public static int ConnectionFailCount { get; private set; } = 0;

		public static int TotalRetryCount { get; private set; } = 0;

		public static string LastErrorMessage { get; private set; } = "";

		public static float LastScanTime { get; private set; } = 0f;

		public static float LastHostUpdateTime { get; private set; } = 0f;

		public static string LocalVoiceRegion
		{
			get
			{
				if ((Object)(object)punVoice == (Object)null || ((VoiceConnection)punVoice).Client == null)
				{
					return null;
				}
				string cloudRegion = ((LoadBalancingClient)((VoiceConnection)punVoice).Client).CloudRegion;
				if (!string.IsNullOrEmpty(cloudRegion))
				{
					return cloudRegion;
				}
				return null;
			}
		}

		public static int LocalVoicePing
		{
			get
			{
				//IL_0027: Unknown result type (might be due to invalid IL or missing references)
				//IL_002e: Invalid comparison between Unknown and I4
				try
				{
					if ((Object)(object)punVoice == (Object)null || ((VoiceConnection)punVoice).Client == null)
					{
						return -1;
					}
					if ((int)((LoadBalancingClient)((VoiceConnection)punVoice).Client).State != 9)
					{
						return -1;
					}
					LoadBalancingPeer loadBalancingPeer = ((LoadBalancingClient)((VoiceConnection)punVoice).Client).LoadBalancingPeer;
					return (loadBalancingPeer != null) ? ((PhotonPeer)loadBalancingPeer).RoundTripTime : (-1);
				}
				catch (Exception)
				{
					return -1;
				}
			}
		}

		public static string LocalVoiceRoomName
		{
			get
			{
				if ((Object)(object)punVoice == (Object)null || ((VoiceConnection)punVoice).Client == null || ((LoadBalancingClient)((VoiceConnection)punVoice).Client).CurrentRoom == null)
				{
					return null;
				}
				return ((LoadBalancingClient)((VoiceConnection)punVoice).Client).CurrentRoom.Name;
			}
		}

		public static string CurrentDecisionMode
		{
			get
			{
				if (IsBlindConnect)
				{
					return L.Get("log_auto_blind");
				}
				if (ConnectedUsingHost)
				{
					return L.Get("log_host");
				}
				GetRoomVoiceServer(out var _, out var reporters);
				return L.Get("log_majority", reporters);
			}
		}

		public static string GetPlayerName(int actorNumber)
		{
			//IL_00b5: Unknown result type (might be due to invalid IL or missing references)
			string text = "Unknown";
			Player val = null;
			if (PhotonNetwork.CurrentRoom != null)
			{
				val = PhotonNetwork.CurrentRoom.GetPlayer(actorNumber, false);
			}
			if (val != null && !string.IsNullOrEmpty(val.NickName))
			{
				text = val.NickName;
				UpdatePlayerCache(actorNumber, text);
				return text;
			}
			if (PlayerCache.ContainsKey(actorNumber))
			{
				string playerName = PlayerCache[actorNumber].PlayerName;
				if (!string.IsNullOrEmpty(playerName) && playerName != "Unknown" && !playerName.StartsWith("Player "))
				{
					return playerName;
				}
			}
			if ((text == "Unknown" || string.IsNullOrEmpty(text)) && val != null && !string.IsNullOrEmpty(val.UserId))
			{
				try
				{
					if (ulong.TryParse(val.UserId, out var result))
					{
						string friendPersonaName = SteamFriends.GetFriendPersonaName(new CSteamID(result));
						if (!string.IsNullOrEmpty(friendPersonaName) && friendPersonaName != "[unknown]")
						{
							text = friendPersonaName;
							UpdatePlayerCache(actorNumber, text);
							return text;
						}
					}
				}
				catch (Exception)
				{
				}
			}
			if ((text == "Unknown" || string.IsNullOrEmpty(text)) && (!scavengeFailTime.TryGetValue(actorNumber, out var value) || Time.unscaledTime - value > 10f))
			{
				string text2 = ScavengeNameFromScene(actorNumber);
				if (!string.IsNullOrEmpty(text2))
				{
					scavengeFailTime.Remove(actorNumber);
					UpdatePlayerCache(actorNumber, text2);
					return text2;
				}
				scavengeFailTime[actorNumber] = Time.unscaledTime;
			}
			if (text == "Unknown")
			{
				return $"Player {actorNumber}";
			}
			return text;
		}

		public static string ScavengeNameFromScene(int actorNumber)
		{
			try
			{
				PhotonView[] array = Object.FindObjectsOfType<PhotonView>();
				foreach (PhotonView val in array)
				{
					if (!((Object)(object)val == (Object)null) && val.OwnerActorNr == actorNumber)
					{
						if (val.Owner != null && !string.IsNullOrEmpty(val.Owner.NickName))
						{
							return val.Owner.NickName;
						}
						TextMeshProUGUI componentInChildren = ((Component)val).GetComponentInChildren<TextMeshProUGUI>(true);
						if ((Object)(object)componentInChildren != (Object)null && !string.IsNullOrEmpty(((TMP_Text)componentInChildren).text))
						{
							return ((TMP_Text)componentInChildren).text;
						}
						TextMeshPro componentInChildren2 = ((Component)val).GetComponentInChildren<TextMeshPro>(true);
						if ((Object)(object)componentInChildren2 != (Object)null && !string.IsNullOrEmpty(((TMP_Text)componentInChildren2).text))
						{
							return ((TMP_Text)componentInChildren2).text;
						}
						Text componentInChildren3 = ((Component)val).GetComponentInChildren<Text>(true);
						if ((Object)(object)componentInChildren3 != (Object)null && !string.IsNullOrEmpty(componentInChildren3.text))
						{
							return componentInChildren3.text;
						}
					}
				}
			}
			catch (Exception)
			{
			}
			return null;
		}

		public static void UpdatePlayerCache(int actorNumber, string name, string ip = null, string version = null)
		{
			if (!PlayerCache.ContainsKey(actorNumber))
			{
				PlayerCache[actorNumber] = new CacheEntry();
			}
			if (!string.IsNullOrEmpty(name) && name != "Unknown")
			{
				PlayerCache[actorNumber].PlayerName = name;
			}
			if (!string.IsNullOrEmpty(ip))
			{
				PlayerCache[actorNumber].IP = ip;
			}
			if (!string.IsNullOrEmpty(version))
			{
				PlayerCache[actorNumber].ModVersion = version;
			}
			PlayerCache[actorNumber].LastSeenTime = Time.unscaledTime;
		}

		private static void RefreshVoiceActorMap()
		{
			if (Time.unscaledTime < nextVoiceMapRefreshTime)
			{
				return;
			}
			nextVoiceMapRefreshTime = Time.unscaledTime + 1f;
			voiceToGameActor.Clear();
			gameToVoiceActor.Clear();
			try
			{
				PhotonVoiceView[] array = Object.FindObjectsOfType<PhotonVoiceView>();
				foreach (PhotonVoiceView val in array)
				{
					if ((Object)(object)val == (Object)null || (Object)(object)val.SpeakerInUse == (Object)null || !val.SpeakerInUse.IsLinked)
					{
						continue;
					}
					RemoteVoiceLink remoteVoice = val.SpeakerInUse.RemoteVoice;
					if (remoteVoice != null)
					{
						PhotonView component = ((Component)val).GetComponent<PhotonView>();
						int num = (((Object)(object)component != (Object)null) ? component.OwnerActorNr : 0);
						if (num > 0)
						{
							voiceToGameActor[remoteVoice.PlayerId] = num;
							gameToVoiceActor[num] = remoteVoice.PlayerId;
						}
					}
				}
			}
			catch (Exception)
			{
			}
		}

		public static bool IsGhost(int voiceActorNumber)
		{
			if (PhotonNetwork.CurrentRoom == null)
			{
				return true;
			}
			if ((Object)(object)punVoice == (Object)null || ((VoiceConnection)punVoice).Client == null || ((LoadBalancingClient)((VoiceConnection)punVoice).Client).CurrentRoom == null)
			{
				return true;
			}
			if (!((LoadBalancingClient)((VoiceConnection)punVoice).Client).CurrentRoom.Players.TryGetValue(voiceActorNumber, out var value))
			{
				return true;
			}
			if (value.IsLocal)
			{
				return false;
			}
			RefreshVoiceActorMap();
			if (voiceToGameActor.TryGetValue(voiceActorNumber, out var value2))
			{
				return PhotonNetwork.CurrentRoom.GetPlayer(value2, false) == null;
			}
			string userId = value.UserId;
			if (!string.IsNullOrEmpty(userId))
			{
				Player[] playerList = PhotonNetwork.PlayerList;
				foreach (Player val in playerList)
				{
					if (!string.IsNullOrEmpty(val.UserId) && val.UserId == userId)
					{
						return false;
					}
				}
				return true;
			}
			if (PhotonNetwork.CurrentRoom.GetPlayer(voiceActorNumber, false) != null)
			{
				if (gameToVoiceActor.TryGetValue(voiceActorNumber, out var value3) && value3 != voiceActorNumber)
				{
					return true;
				}
				return false;
			}
			return true;
		}

		public static bool IsPlayerInVoiceRoom(int gameActorNumber)
		{
			//IL_0059: Unknown result type (might be due to invalid IL or missing references)
			//IL_0060: Invalid comparison between Unknown and I4
			if ((Object)(object)punVoice == (Object)null || ((VoiceConnection)punVoice).Client == null || ((LoadBalancingClient)((VoiceConnection)punVoice).Client).CurrentRoom == null)
			{
				return false;
			}
			if (PhotonNetwork.CurrentRoom == null)
			{
				return false;
			}
			Player player = PhotonNetwork.CurrentRoom.GetPlayer(gameActorNumber, false);
			if (player == null)
			{
				return false;
			}
			if (player.IsLocal)
			{
				return (int)((LoadBalancingClient)((VoiceConnection)punVoice).Client).State == 9;
			}
			RefreshVoiceActorMap();
			if (gameToVoiceActor.TryGetValue(gameActorNumber, out var value))
			{
				return ((LoadBalancingClient)((VoiceConnection)punVoice).Client).CurrentRoom.Players.ContainsKey(value);
			}
			string userId = player.UserId;
			if (!string.IsNullOrEmpty(userId))
			{
				foreach (KeyValuePair<int, Player> player2 in ((LoadBalancingClient)((VoiceConnection)punVoice).Client).CurrentRoom.Players)
				{
					if (!string.IsNullOrEmpty(player2.Value.UserId) && player2.Value.UserId == userId)
					{
						return true;
					}
				}
			}
			if (((LoadBalancingClient)((VoiceConnection)punVoice).Client).CurrentRoom.Players.TryGetValue(gameActorNumber, out var value2))
			{
				if (voiceToGameActor.TryGetValue(gameActorNumber, out var value3) && value3 != gameActorNumber)
				{
					return false;
				}
				if (!string.IsNullOrEmpty(value2.UserId) && value2.UserId != userId)
				{
					return false;
				}
				return true;
			}
			return false;
		}

		public static int GetGhostCount()
		{
			if ((Object)(object)punVoice == (Object)null || ((VoiceConnection)punVoice).Client == null || ((LoadBalancingClient)((VoiceConnection)punVoice).Client).CurrentRoom == null)
			{
				return 0;
			}
			if (PhotonNetwork.CurrentRoom != null && PhotonNetwork.CurrentRoom.PlayerCount <= 1)
			{
				return 0;
			}
			int num = 0;
			foreach (int key in ((LoadBalancingClient)((VoiceConnection)punVoice).Client).CurrentRoom.Players.Keys)
			{
				if (IsGhost(key))
				{
					num++;
				}
			}
			return num;
		}

		public static bool IsModUser(Player p)
		{
			if (p == null)
			{
				return false;
			}
			if (p.IsLocal)
			{
				return true;
			}
			if (p.CustomProperties != null && ((Dictionary<object, object>)(object)p.CustomProperties).ContainsKey((object)"PVF_Ping"))
			{
				return true;
			}
			if (PlayerCache.TryGetValue(p.ActorNumber, out var value) && !string.IsNullOrEmpty(value.ModVersion))
			{
				return true;
			}
			return false;
		}

		public static bool IsModUser(int actorNumber)
		{
			Player val = ((PhotonNetwork.CurrentRoom != null) ? PhotonNetwork.CurrentRoom.GetPlayer(actorNumber, false) : null);
			if (val != null)
			{
				return IsModUser(val);
			}
			if (PlayerCache.TryGetValue(actorNumber, out var value) && !string.IsNullOrEmpty(value.ModVersion))
			{
				return true;
			}
			return false;
		}

		private static void PurgeDepartedActors()
		{
			if (PhotonNetwork.CurrentRoom == null)
			{
				return;
			}
			List<int> list = new List<int>();
			foreach (int key in PlayerCache.Keys)
			{
				if (PhotonNetwork.CurrentRoom.GetPlayer(key, false) == null)
				{
					list.Add(key);
				}
			}
			foreach (int item in list)
			{
				PlayerCache.Remove(item);
				scavengeFailTime.Remove(item);
				firstSeenTime.Remove(item);
			}
			ActiveSOSList.RemoveAll((SOSData s) => PhotonNetwork.CurrentRoom.GetPlayer(s.ActorNumber, false) == null);
		}

		private static void TrackFirstSeen()
		{
			if (PhotonNetwork.PlayerList == null)
			{
				return;
			}
			Player[] playerList = PhotonNetwork.PlayerList;
			foreach (Player val in playerList)
			{
				if (val != null && !val.IsLocal && !firstSeenTime.ContainsKey(val.ActorNumber))
				{
					firstSeenTime[val.ActorNumber] = Time.unscaledTime;
				}
			}
		}

		public static void SystemUpdate()
		{
			//IL_00fe: 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_0104: 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_0165: Unknown result type (might be due to invalid IL or missing references)
			//IL_0168: Invalid comparison between Unknown and I4
			//IL_0125: 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_013f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0145: Unknown result type (might be due to invalid IL or missing references)
			//IL_0148: Invalid comparison between Unknown and I4
			//IL_015f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0160: 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: Invalid comparison between Unknown and I4
			if (!PhotonNetwork.InRoom)
			{
				if (wasInRoom)
				{
					ResetRoomScopedState();
				}
				wasInRoom = false;
				return;
			}
			wasInRoom = true;
			if (PhotonNetwork.OfflineMode)
			{
				return;
			}
			int num = ((PhotonNetwork.PlayerList != null) ? PhotonNetwork.PlayerList.Length : 0);
			if (num < lastPlayerCount)
			{
				PurgeDepartedActors();
			}
			lastPlayerCount = num;
			TrackFirstSeen();
			if ((Object)(object)punVoice == (Object)null && Time.unscaledTime >= nextVoiceClientFindTime)
			{
				nextVoiceClientFindTime = Time.unscaledTime + 1f;
				try
				{
					punVoice = Object.FindFirstObjectByType<PunVoiceClient>();
					lastVoiceProgressTime = Time.unscaledTime;
					voiceJoinedAt = (((Object)(object)punVoice != (Object)null && ((VoiceConnection)punVoice).Client != null && (int)((LoadBalancingClient)((VoiceConnection)punVoice).Client).State == 9) ? Time.unscaledTime : (-1f));
				}
				catch (Exception)
				{
					punVoice = null;
				}
			}
			if ((Object)(object)punVoice != (Object)null && ((VoiceConnection)punVoice).Client != null)
			{
				ClientState state = ((LoadBalancingClient)((VoiceConnection)punVoice).Client).State;
				if (state != lastClientState)
				{
					lastVoiceProgressTime = Time.unscaledTime;
					BroadcastLog(string.Format("{0}: {1} -> {2}", L.Get("log_state_change"), lastClientState, state));
					SendStateSync(state);
					if ((int)state == 9)
					{
						voiceJoinedAt = Time.unscaledTime;
						TryInitNetworkVoice();
					}
					UpdateDataLayer(force: true);
					lastClientState = state;
				}
				if ((int)state == 9 && voiceJoinedAt >= 0f && Time.unscaledTime - voiceJoinedAt >= 10f)
				{
					ConnectionFailCount = 0;
					TotalRetryCount = 0;
					LastErrorMessage = "";
					configErrorLogged = false;
				}
			}
			ProcessPendingReconnect();
			UpdateDataLayer();
			PublishPingEvent();
			TrackDiagnostics();
			HandleInputAndState();
			ManageSOSList();
			if ((Object)(object)punVoice != (Object)null && ((VoiceConnection)punVoice).Client != null && !reconnectPending && PhotonNetwork.IsConnectedAndReady && Time.unscaledTime >= nextRetryTime && !RecoverStalledVoiceConnection())
			{
				if (PhotonNetwork.IsMasterClient)
				{
					HandleHostLogic();
				}
				else
				{
					HandleClientLogic();
				}
			}
		}

		private static void PublishPingEvent()
		{
			//IL_0075: 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_007c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0082: Expected O, but got Unknown
			//IL_0089: Unknown result type (might be due to invalid IL or missing references)
			if (!(Time.unscaledTime < nextPingEventTime))
			{
				nextPingEventTime = Time.unscaledTime + 3f;
				if (PhotonNetwork.IsConnectedAndReady && PhotonNetwork.CurrentRoom != null && PhotonNetwork.CurrentRoom.PlayerCount > 1)
				{
					object[] array = new object[4]
					{
						(byte)3,
						PhotonNetwork.GetPing(),
						LocalVoicePing,
						LocalVoiceRegion ?? ""
					};
					RaiseEventOptions val = new RaiseEventOptions
					{
						Receivers = (ReceiverGroup)0
					};
					PhotonNetwork.RaiseEvent((byte)186, (object)array, val, SendOptions.SendUnreliable);
				}
			}
		}

		public static void SendStateSync(ClientState state)
		{
			//IL_0000: 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_0029: 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_0031: Expected O, but got Unknown
			//IL_0038: Unknown result type (might be due to invalid IL or missing references)
			byte b = (byte)state;
			object[] array = new object[3]
			{
				(byte)2,
				b,
				"v1.1.8"
			};
			RaiseEventOptions val = new RaiseEventOptions
			{
				Receivers = (ReceiverGroup)0
			};
			PhotonNetwork.RaiseEvent((byte)186, (object)array, val, SendOptions.SendReliable);
		}

		public static void BroadcastLog(string message)
		{
			//IL_0055: 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_005c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0062: Expected O, but got Unknown
			//IL_0065: Unknown result type (might be due to invalid IL or missing references)
			ManualLogSource logger = VoiceFix.logger;
			if (logger != null)
			{
				logger.LogInfo((object)message);
			}
			string playerName = GetPlayerName(PhotonNetwork.LocalPlayer.ActorNumber);
			if ((Object)(object)VoiceUIManager.Instance != (Object)null)
			{
				VoiceUIManager.Instance.AddLog(playerName, message, isLocal: true);
			}
			byte b = 186;
			object[] array = new object[2]
			{
				(byte)1,
				message
			};
			RaiseEventOptions val = new RaiseEventOptions
			{
				Receivers = (ReceiverGroup)0
			};
			PhotonNetwork.RaiseEvent(b, (object)array, val, SendOptions.SendReliable);
		}

		private static void UpdateDataLayer(bool force = false)
		{
			//IL_0025: Unknown result type (might be due to invalid IL or missing references)
			//IL_002b: Expected O, but got Unknown
			//IL_007e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0085: Invalid comparison between Unknown and I4
			bool flag = Time.unscaledTime - lastPingPublishTime > 30f;
			if (force || flag)
			{
				lastPingPublishTime = Time.unscaledTime;
				Hashtable val = new Hashtable();
				int actorNumber = PhotonNetwork.LocalPlayer.ActorNumber;
				int ping = PhotonNetwork.GetPing();
				string localVoiceRegion = LocalVoiceRegion;
				int localVoicePing = LocalVoicePing;
				val[(object)"PVF_Ping"] = ping;
				string ip = (string)(val[(object)"PVF_IP"] = (((Object)(object)punVoice != (Object)null && ((VoiceConnection)punVoice).Client != null && (int)((LoadBalancingClient)((VoiceConnection)punVoice).Client).State == 9) ? ((LoadBalancingClient)((VoiceConnection)punVoice).Client).GameServerAddress : ""));
				val[(object)"PVF_VR"] = localVoiceRegion ?? "";
				val[(object)"PVF_VP"] = localVoicePing;
				PhotonNetwork.LocalPlayer.SetCustomProperties(val, (Hashtable)null, (WebFlags)null);
				UpdatePlayerCache(actorNumber, PhotonNetwork.LocalPlayer.NickName, ip, "v1.1.8");
				CacheEntry cacheEntry = PlayerCache[actorNumber];
				cacheEntry.VoiceRegion = localVoiceRegion;
				cacheEntry.VoicePing = localVoicePing;
				cacheEntry.GamePing = ping;
			}
			if (!force)
			{
				if (Time.unscaledTime - LastScanTime > 30f)
				{
					LastScanTime = Time.unscaledTime;
					ScanPlayers();
				}
				if (Time.unscaledTime > nextSummaryLogTime)
				{
					PrintSummaryLog();
					nextSummaryLogTime = Time.unscaledTime + 60f;
				}
			}
		}

		private static void PrintSummaryLog()
		{
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.AppendLine(string.Format("{0}:{1}", L.Get("log_cache_snapshot"), PlayerCache.Count));
			foreach (KeyValuePair<int, CacheEntry> item in PlayerCache)
			{
				string arg = (string.IsNullOrEmpty(item.Value.IP) ? "N/A" : item.Value.IP);
				stringBuilder.AppendLine($" - {item.Value.PlayerName}: {arg} (St:{item.Value.RemoteState})");
			}
			if ((Object)(object)VoiceUIManager.Instance != (Object)null)
			{
				VoiceUIManager.Instance.AddLog("System", stringBuilder.ToString(), isLocal: true);
			}
		}

		public static void CacheVoiceInfo(Player p)
		{
			if (p != null && p.CustomProperties != null)
			{
				if (!PlayerCache.TryGetValue(p.ActorNumber, out var value))
				{
					value = new CacheEntry
					{
						LastSeenTime = Time.unscaledTime
					};
					PlayerCache[p.ActorNumber] = value;
				}
				if (((Dictionary<object, object>)(object)p.CustomProperties).TryGetValue((object)"PVF_VR", out object value2) && value2 is string text)
				{
					value.VoiceRegion = (string.IsNullOrEmpty(text) ? null : text);
				}
				if (((Dictionary<object, object>)(object)p.CustomProperties).TryGetValue((object)"PVF_VP", out value2) && value2 is int voicePing)
				{
					value.VoicePing = voicePing;
				}
			}
		}

		private static void ScanPlayers()
		{
			Player[] playerListOthers = PhotonNetwork.PlayerListOthers;
			foreach (Player val in playerListOthers)
			{
				CacheVoiceInfo(val);
				object value = null;
				if (!((Dictionary<object, object>)(object)val.CustomProperties).TryGetValue((object)"PVF_IP", out value) || !(value is string text))
				{
					continue;
				}
				string playerName = GetPlayerName(val.ActorNumber);
				UpdatePlayerCache(val.ActorNumber, playerName, text);
				if (!val.IsMasterClient)
				{
					continue;
				}
				if (!string.IsNullOrEmpty(text))
				{
					LastHostUpdateTime = Time.unscaledTime;
				}
				if (!string.IsNullOrEmpty(LastKnownHostIP) && LastKnownHostIP != text && !string.IsNullOrEmpty(text))
				{
					BroadcastLog(L.Get("log_host_ip_change") + ": " + LastKnownHostIP + " -> " + text);
					if (HostHistory.Count > 5)
					{
						HostHistory.RemoveAt(0);
					}
					HostHistory.Add($"[{DateTime.Now:HH:mm:ss}] {text}");
				}
				LastKnownHostIP = text;
			}
			foreach (int item in (from x in PlayerCache
				where Time.unscaledTime - x.Value.LastSeenTime > 180f
				select x.Key).ToList())
			{
				PlayerCache.Remove(item);
			}
		}

		private static void ManageSOSList()
		{
			if (ActiveSOSList.Count == 0 || Time.unscaledTime < nextSOSManageTime)
			{
				return;
			}
			nextSOSManageTime = Time.unscaledTime + 0.5f;
			for (int num = ActiveSOSList.Count - 1; num >= 0; num--)
			{
				SOSData sOSData = ActiveSOSList[num];
				if (Time.unscaledTime - sOSData.ReceiveTime > 60f)
				{
					ActiveSOSList.RemoveAt(num);
				}
				else
				{
					string playerName = GetPlayerName(sOSData.ActorNumber);
					if ((string.IsNullOrEmpty(playerName) || playerName == "Unknown" || playerName.StartsWith("Player ")) && (PhotonNetwork.CurrentRoom == null || PhotonNetwork.CurrentRoom.GetPlayer(sOSData.ActorNumber, false) == null))
					{
						ActiveSOSList.RemoveAt(num);
					}
					else if (PhotonNetwork.CurrentRoom != null)
					{
						Player player = PhotonNetwork.CurrentRoom.GetPlayer(sOSData.ActorNumber, false);
						object value = null;
						if (player != null && ((Dictionary<object, object>)(object)player.CustomProperties).TryGetValue((object)"PVF_IP", out value) && value is string value2 && !string.IsNullOrEmpty(value2))
						{
							ActiveSOSList.RemoveAt(num);
						}
					}
				}
			}
		}

		private static void HandleHostLogic()
		{
			//IL_000a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0011: Invalid comparison between Unknown and I4
			if ((int)((LoadBalancingClient)((VoiceConnection)punVoice).Client).State == 9)
			{
				CorrectVoiceDestination();
			}
			else if (CanStartVoiceConnection())
			{
				RetryDisconnectedVoice(L.Get("log_host"));
			}
		}

		private static void HandleClientLogic()
		{
			//IL_0021: Unknown result type (might be due to invalid IL or missing references)
			//IL_0028: Invalid comparison between Unknown and I4
			string mode;
			string text = DecideTargetIP(out mode);
			IsBlindConnect = string.IsNullOrEmpty(text);
			TargetGameServer = text;
			if ((int)((LoadBalancingClient)((VoiceConnection)punVoice).Client).State == 9)
			{
				CorrectVoiceDestination();
			}
			else if (CanStartVoiceConnection())
			{
				RetryDisconnectedVoice(mode);
			}
		}

		private static bool CanStartVoiceConnection()
		{
			//IL_000a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0011: Invalid comparison between Unknown and I4
			//IL_001d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0023: Invalid comparison between Unknown and I4
			if ((int)((LoadBalancingClient)((VoiceConnection)punVoice).Client).State != 14)
			{
				return (int)((LoadBalancingClient)((VoiceConnection)punVoice).Client).State == 0;
			}
			return true;
		}

		private static void RetryDisconnectedVoice(string mode)
		{
			PerformReconnect(mode);
		}

		private static void CorrectVoiceDestination()
		{
			string cloudRegion = PhotonNetwork.CloudRegion;
			string cloudRegion2 = ((LoadBalancingClient)((VoiceConnection)punVoice).Client).CloudRegion;
			Room currentRoom = PhotonNetwork.CurrentRoom;
			string text = ((currentRoom != null) ? currentRoom.Name : null);
			Room currentRoom2 = ((LoadBalancingClient)((VoiceConnection)punVoice).Client).CurrentRoom;
			string text2 = ((currentRoom2 != null) ? currentRoom2.Name : null);
			bool num = !string.IsNullOrEmpty(cloudRegion) && !string.IsNullOrEmpty(cloudRegion2) && !string.Equals(cloudRegion, cloudRegion2, StringComparison.OrdinalIgnoreCase);
			bool flag = PanelText.RoomNamesMatch(text, text2) == false;
			if (!num && !flag)
			{
				WrongIPCount = 0;
			}
			else if (WrongIPCount < 2)
			{
				WrongIPCount++;
				BroadcastLog(L.Get("log_wrong_destination", cloudRegion2 ?? "?", text2 ?? "?", cloudRegion ?? "?", text ?? "?", WrongIPCount));
				PerformReconnect(L.Get("log_destination_repair"));
			}
		}

		private static bool RecoverStalledVoiceConnection()
		{
			//IL_000a: 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_0010: Unknown result type (might be due to invalid IL or missing references)
			//IL_0013: Invalid comparison between Unknown and I4
			//IL_0050: Unknown result type (might be due to invalid IL or missing references)
			ClientState state = ((LoadBalancingClient)((VoiceConnection)punVoice).Client).State;
			if ((int)state == 9 || CanStartVoiceConnection())
			{
				return false;
			}
			float num = Mathf.Max(5f, VoiceFix.ConnectTimeout.Value);
			if (Time.unscaledTime - lastVoiceProgressTime < num)
			{
				return false;
			}
			LastErrorMessage = L.Get("log_connection_stalled", state, num);
			BroadcastLog(LastErrorMessage);
			ConnectionFailCount++;
			PerformReconnect(L.Get("log_stalled_repair"));
			lastVoiceProgressTime = Time.unscaledTime;
			return true;
		}

		internal unsafe static void NoteVoiceStateChange(PunVoiceClient source, ClientState fromState, ClientState toState)
		{
			//IL_0026: Unknown result type (might be due to invalid IL or missing references)
			//IL_0029: Invalid comparison between Unknown and I4
			//IL_003c: Unknown result type (might be due to invalid IL or missing references)
			//IL_003f: Invalid comparison between Unknown and I4
			//IL_0048: 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)
			if ((Object)(object)source != (Object)(object)punVoice || !PhotonNetwork.InRoom || PhotonNetwork.OfflineMode)
			{
				return;
			}
			lastVoiceProgressTime = Time.unscaledTime;
			voiceJoinedAt = (((int)toState == 9) ? Time.unscaledTime : (-1f));
			if ((int)toState == 14)
			{
				DisconnectCause disconnectedCause = ((LoadBalancingClient)((VoiceConnection)source).Client).DisconnectedCause;
				bool num = reconnectPending || manualDisconnectRequested;
				manualDisconnectRequested = false;
				if (!num)
				{
					ConnectionFailCount++;
				}
				nextRetryTime = Mathf.Max(nextRetryTime, Time.unscaledTime + VoiceRetryRules.Delay(VoiceFix.RetryInterval.Value, ConnectionFailCount));
				string text = (LastErrorMessage = ((object)(*(DisconnectCause*)(&disconnectedCause))/*cast due to .constrained prefix*/).ToString());
				DiagLog(L.Get("log_disconnect_cause", text, PhotonNetwork.CloudRegion ?? "?", ((LoadBalancingClient)((VoiceConnection)source).Client).CloudRegion ?? "?", reconnectPending ? reconnectPendingMode : "Photon"));
			}
		}

		internal static void AlignVoiceAuthWithPrefab(PunVoiceClient voice)
		{
			if (!((Object)(object)voice == (Object)null) && voice.UsePunAuthValues)
			{
				voice.UsePunAuthValues = false;
				if (!authAlignedLogged)
				{
					authAlignedLogged = true;
					DiagLog(L.Get("log_auth_aligned", ((Object)(object)((Component)voice).gameObject != (Object)null) ? ((Object)((Component)voice).gameObject).name : "?"));
				}
			}
		}

		internal static void NoteVoiceServerResponse(PunVoiceClient source, OperationResponse response)
		{
			if ((Object)(object)source == (Object)null || response == null || response.ReturnCode == 0 || ((Object)(object)punVoice != (Object)null && (Object)(object)source != (Object)(object)punVoice) || !VoiceAuthErrorRules.IsAuthOperation(response.OperationCode))
			{
				return;
			}
			string text = "?";
			try
			{
				text = ((LoadBalancingClient)((VoiceConnection)source).Client).CurrentServerAddress ?? "?";
			}
			catch (Exception)
			{
			}
			DiagLog(LastErrorMessage = L.Get("log_auth_rejected", response.OperationCode, response.ReturnCode, response.DebugMessage ?? "", text, source.UsePunAuthValues));
			if (VoiceAuthErrorRules.IsConfigurationError(response.ReturnCode))
			{
				nextRetryTime = Mathf.Max(nextRetryTime, Time.unscaledTime + 120f);
				if (!configErrorLogged)
				{
					configErrorLogged = true;
					DiagLog(L.Get("log_auth_config_error", response.ReturnCode, 120));
				}
			}
		}

		internal static bool DelayNativeVoiceConnect(PunVoiceClient source)
		{
			if ((Object)(object)source == (Object)(object)punVoice && (Object)(object)source != (Object)null && PhotonNetwork.InRoom && !PhotonNetwork.OfflineMode)
			{
				return VoiceRetryRules.IsCoolingDown(Time.unscaledTime, nextRetryTime);
			}
			return false;
		}

		internal static bool DelayNativeVoiceRejoin(LoadBalancingClient client)
		{
			if ((Object)(object)punVoice != (Object)null && (object)client == ((VoiceConnection)punVoice).Client)
			{
				return DelayNativeVoiceConnect(punVoice);
			}
			return false;
		}

		private static bool ConnectVoiceNow(string mode)
		{
			//IL_0031: Unknown result type (might be due to invalid IL or missing references)
			//IL_003b: Expected O, but got Unknown
			//IL_0057: 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_00b2: 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_00c2: 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_0091: Expected O, but got Unknown
			//IL_014a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0158: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)punVoice == (Object)null || ((VoiceConnection)punVoice).Client == null)
			{
				return false;
			}
			try
			{
				LoadBalancingTransport client = ((VoiceConnection)punVoice).Client;
				AppSettings val = PhotonNetwork.PhotonServerSettings.AppSettings.CopyTo(new AppSettings());
				string cloudRegion = PhotonNetwork.CloudRegion;
				if (!string.IsNullOrEmpty(cloudRegion))
				{
					val.FixedRegion = cloudRegion;
				}
				((LoadBalancingClient)client).SerializationProtocol = PhotonNetwork.NetworkingClient.SerializationProtocol;
				AlignVoiceAuthWithPrefab(punVoice);
				if (punVoice.UsePunAuthValues)
				{
					if (PhotonNetwork.AuthValues != null)
					{
						if (((LoadBalancingClient)client).AuthValues == null)
						{
							((LoadBalancingClient)client).AuthValues = new AuthenticationValues();
						}
						((LoadBalancingClient)client).AuthValues = PhotonNetwork.AuthValues.CopyTo(((LoadBalancingClient)client).AuthValues);
					}
					((LoadBalancingClient)client).AuthMode = PhotonNetwork.NetworkingClient.AuthMode;
					((LoadBalancingClient)client).EncryptionMode = PhotonNetwork.NetworkingClient.EncryptionMode;
				}
				BroadcastLog(L.Get("log_reconnect_go", string.IsNullOrEmpty(mode) ? "Auto" : mode, string.IsNullOrEmpty(cloudRegion) ? "?" : cloudRegion));
				bool flag = ((VoiceConnection)punVoice).ConnectUsingSettings(val);
				if (!flag)
				{
					ConnectionFailCount++;
					nextRetryTime = Time.unscaledTime + VoiceRetryRules.Delay(VoiceFix.RetryInterval.Value, ConnectionFailCount);
					LastErrorMessage = L.Get("log_connect_rejected", ((LoadBalancingClient)client).State, ((LoadBalancingClient)client).DisconnectedCause);
					DiagLog(LastErrorMessage);
				}
				return flag;
			}
			catch (Exception ex)
			{
				ConnectionFailCount++;
				nextRetryTime = Time.unscaledTime + VoiceRetryRules.Delay(VoiceFix.RetryInterval.Value, ConnectionFailCount);
				LastErrorMessage = ex.Message;
				if (VoiceFix.logger != null)
				{
					VoiceFix.logger.LogError((object)$"[PVF] ConnectVoiceNow: {ex}");
				}
				return false;
			}
		}

		private static void TryInitNetworkVoice()
		{
			try
			{
				if (!initNetworkVoiceResolved)
				{
					initNetworkVoiceResolved = true;
					Type type = AccessTools.TypeByName("VoiceClientHandler");
					if (type != null)
					{
						initNetworkVoiceMethod = AccessTools.Method(type, "InitNetworkVoice", (Type[])null, (Type[])null);
					}
				}
				initNetworkVoiceMethod?.Invoke(null, null);
			}
			catch (Exception)
			{
			}
		}

		private static void ProcessPendingReconnect()
		{
			//IL_0039: Unknown result type (might be due to invalid IL or missing references)
			//IL_0040: Invalid comparison between Unknown and I4
			//IL_0089: 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)
			if (!reconnectPending)
			{
				return;
			}
			if ((Object)(object)punVoice == (Object)null || ((VoiceConnection)punVoice).Client == null)
			{
				reconnectPending = false;
				return;
			}
			if (!CanStartVoiceConnection() && (int)((LoadBalancingClient)((VoiceConnection)punVoice).Client).State != 13)
			{
				reconnectPending = false;
				DiagLog(L.Get("log_native_recovery", ((LoadBalancingClient)((VoiceConnection)punVoice).Client).State));
				return;
			}
			LoadBalancingPeer loadBalancingPeer = ((LoadBalancingClient)((VoiceConnection)punVoice).Client).LoadBalancingPeer;
			if (loadBalancingPeer != null && (int)((PhotonPeer)loadBalancingPeer).PeerState != 0)
			{
				if (Time.unscaledTime > reconnectPendingDeadline)
				{
					reconnectPending = false;
					BroadcastLog(L.Get("log_reconnect_timeout"));
				}
			}
			else
			{
				reconnectPending = false;
				if (CanStartVoiceConnection())
				{
					ConnectVoiceNow(reconnectPendingMode);
				}
			}
		}

		private static string DecideTargetIP(out string mode)
		{
			RoomVoiceSource source;
			int reporters;
			string roomVoiceServer = GetRoomVoiceServer(out source, out reporters);
			if (source == RoomVoiceSource.Majority && !string.IsNullOrEmpty(roomVoiceServer))
			{
				mode = L.Get("log_majority", reporters);
				ConnectedUsingHost = false;
				LogDecision(mode, roomVoiceServer);
				return roomVoiceServer;
			}
			if (source == RoomVoiceSource.Host && !string.IsNullOrEmpty(roomVoiceServer))
			{
				mode = L.Get("log_host");
				ConnectedUsingHost = true;
				LogDecision(mode, roomVoiceServer);
				return roomVoiceServer;
			}
			mode = L.Get("log_auto_blind");
			ConnectedUsingHost = false;
			LogDecision(mode, "Auto");
			return null;
		}

		private static void LogDecision(string mode, string target)
		{
			string text = mode + "->" + target;
			if (text != LastDecisionLog)
			{
				BroadcastLog(L.Get("log_decision") + ": " + text);
				LastDecisionLog = text;
			}
		}

		public static string GetMajorityIP(out int maxCount)
		{
			maxCount = 0;
			if (PlayerCache.Count == 0)
			{
				return null;
			}
			Dictionary<string, int> dictionary = new Dictionary<string, int>();
			foreach (KeyValuePair<int, CacheEntry> item in PlayerCache)
			{
				if (!string.IsNullOrEmpty(item.Value.IP))
				{
					if (!dictionary.ContainsKey(item.Value.IP))
					{
						dictionary[item.Value.IP] = 0;
					}
					dictionary[item.Value.IP]++;
				}
			}
			string result = null;
			foreach (KeyValuePair<string, int> item2 in dictionary)
			{
				if (item2.Value > maxCount)
				{
					maxCount = item2.Value;
					result = item2.Key;
				}
			}
			return result;
		}

		public static string GetMajorityRegion(out int maxCount)
		{
			maxCount = 0;
			if (PlayerCache.Count == 0)
			{
				return null;
			}
			Dictionary<string, int> dictionary = new Dictionary<string, int>();
			foreach (KeyValuePair<int, CacheEntry> item in PlayerCache)
			{
				string voiceRegion = item.Value.VoiceRegion;
				if (!string.IsNullOrEmpty(voiceRegion))
				{
					dictionary.TryGetValue(voiceRegion, out var value);
					dictionary[voiceRegion] = value + 1;
				}
			}
			string result = null;
			foreach (KeyValuePair<string, int> item2 in dictionary)
			{
				if (item2.Value > maxCount)
				{
					maxCount = item2.Value;
					result = item2.Key;
				}
			}
			return result;
		}

		public static string GetRoomVoiceServer(out RoomVoiceSource source, out int reporters)
		{
			//IL_0135: Unknown result type (might be due to invalid IL or missing references)
			//IL_013c: Invalid comparison between Unknown and I4
			source = RoomVoiceSource.Unknown;
			reporters = 0;
			if (PhotonNetwork.CurrentRoom == null)
			{
				return null;
			}
			Dictionary<string, int> dictionary = new Dictionary<string, int>();
			string text = null;
			string text2 = null;
			Player[] playerListOthers = PhotonNetwork.PlayerListOthers;
			foreach (Player val in playerListOthers)
			{
				if (val == null || val.CustomProperties == null || !((Dictionary<object, object>)(object)val.CustomProperties).TryGetValue((object)"PVF_IP", out object value))
				{
					continue;
				}
				string text3 = value as string;
				if (!string.IsNullOrEmpty(text3))
				{
					dictionary.TryGetValue(text3, out var value2);
					dictionary[text3] = value2 + 1;
					if (val.IsMasterClient)
					{
						text = text3;
					}
					if (text2 == null)
					{
						text2 = text3;
					}
				}
			}
			string result = null;
			int num = 0;
			foreach (KeyValuePair<string, int> item in dictionary)
			{
				if (item.Value > num)
				{
					num = item.Value;
					result = item.Key;
				}
			}
			if (num >= 2)
			{
				source = RoomVoiceSource.Majority;
				reporters = num;
				return result;
			}
			if (!string.IsNullOrEmpty(text))
			{
				source = RoomVoiceSource.Host;
				reporters = 1;
				return text;
			}
			if (!string.IsNullOrEmpty(text2))
			{
				source = RoomVoiceSource.SingleReport;
				reporters = 1;
				return text2;
			}
			if ((Object)(object)punVoice != (Object)null && ((VoiceConnection)punVoice).Client != null && (int)((LoadBalancingClient)((VoiceConnection)punVoice).Client).State == 9)
			{
				string gameServerAddress = ((LoadBalancingClient)((VoiceConnection)punVoice).Client).GameServerAddress;
				if (!string.IsNullOrEmpty(gameServerAddress))
				{
					source = RoomVoiceSource.LocalGuess;
					return gameServerAddress;
				}
			}
			return null;
		}

		public static bool IsLocalIsolated()
		{
			//IL_0025: Unknown result type (might be due to invalid IL or missing references)
			//IL_002c: Invalid comparison between Unknown and I4
			if ((Object)(object)punVoice == (Object)null || ((VoiceConnection)punVoice).Client == null)
			{
				return false;
			}
			if ((int)((LoadBalancingClient)((VoiceConnection)punVoice).Client).State != 9)
			{
				return false;
			}
			if (((LoadBalancingClient)((VoiceConnection)punVoice).Client).CurrentRoom == null)
			{
				return false;
			}
			if (PhotonNetwork.CurrentRoom == null)
			{
				return false;
			}
			if (PhotonNetwork.CurrentRoom.PlayerCount <= 1)
			{
				return false;
			}
			if (((LoadBalancingClient)((VoiceConnection)punVoice).Client).CurrentRoom.Players.Count > 1)
			{
				return false;
			}
			float num = ((VoiceFix.ConnectTimeout != null) ? VoiceFix.ConnectTimeout.Value : 25f);
			Player[] playerListOthers = PhotonNetwork.PlayerListOthers;
			foreach (Player val in playerListOthers)
			{
				if (val != null)
				{
					if (val.CustomProperties != null && ((Dictionary<object, object>)(object)val.CustomProperties).TryGetValue((object)"PVF_IP", out object value) && value is string value2 && !string.IsNullOrEmpty(value2))
					{
						return true;
					}
					if (firstSeenTime.TryGetValue(val.ActorNumber, out var value3) && Time.unscaledTime - value3 > num)
					{
						return true;
					}
				}
			}
			return false;
		}

		public static void GetRetryStats(out int total, out int fail, out int wrongIP)
		{
			total = TotalRetryCount;
			fail = ConnectionFailCount;
			wrongIP = WrongIPCount;
		}

		private static void TrackDiagnostics()
		{
			if (Time.unscaledTime < nextDiagTime)
			{
				return;
			}
			nextDiagTime = Time.unscaledTime + 1f;
			string localVoiceRegion = LocalVoiceRegion;
			if (localVoiceRegion != lastDiagVoiceRegion)
			{
				DiagLog(L.Get("diag_vregion_change", lastDiagVoiceRegion ?? "—", localVoiceRegion ?? "—"));
				lastDiagVoiceRegion = localVoiceRegion;
			}
			string localVoiceRoomName = LocalVoiceRoomName;
			if (localVoiceRoomName != lastDiagVoiceRoom)
			{
				string text = "—";
				try
				{
					if ((Object)(object)punVoice != (Object)null && ((VoiceConnection)punVoice).Client != null && !string.IsNullOrEmpty(((LoadBalancingClient)((VoiceConnection)punVoice).Client).AppId))
					{
						text = ((LoadBalancingClient)((VoiceConnection)punVoice).Client).AppId;
					}
				}
				catch (Exception)
				{
				}
				DiagLog(L.Get("diag_vroom_change", lastDiagVoiceRoom ?? "—", localVoiceRoomName ?? "—", text));
				lastDiagVoiceRoom = localVoiceRoomName;
			}
			bool flag = IsLocalIsolated();
			if (flag != lastDiagIsolated)
			{
				int num = ((PhotonNetwork.CurrentRoom != null) ? PhotonNetwork.CurrentRoom.PlayerCount : 0);
				int num2 = (((Object)(object)punVoice != (Object)null && ((VoiceConnection)punVoice).Client != null && ((LoadBalancingClient)((VoiceConnection)punVoice).Client).CurrentRoom != null) ? ((LoadBalancingClient)((VoiceConnection)punVoice).Client).CurrentRoom.Players.Count : 0);
				DiagLog(flag ? L.Get("diag_isolated", num) : L.Get("diag_isolated_clear", num2));
				lastDiagIsolated = flag;
			}
			RoomVoiceSource source;
			int reporters;
			string roomVoiceServer = GetRoomVoiceServer(out source, out reporters);
			if (roomVoiceServer != lastDiagRoomVoice || source != lastDiagRoomVoiceSource)
			{
				if (string.IsNullOrEmpty(roomVoiceServer))
				{
					DiagLog(L.Get("diag_roomvoice_lost"));
				}
				else
				{
					DiagLog(L.Get("diag_roomvoice", roomVoiceServer, source.ToString()));
				}
				lastDiagRoomVoice = roomVoiceServer;
				lastDiagRoomVoiceSource = source;
			}
			string cloudRegion = PhotonNetwork.CloudRegion;
			if (PhotonNetwork.CurrentRoom == null || string.IsNullOrEmpty(cloudRegion))
			{
				return;
			}
			Player[] playerListOthers = PhotonNetwork.PlayerListOthers;
			foreach (Player val in playerListOthers)
			{
				if (val == null)
				{
					continue;
				}
				CacheEntry value;
				string text2 = (PlayerCache.TryGetValue(val.ActorNumber, out value) ? value.VoiceRegion : null);
				lastDiagCross.TryGetValue(val.ActorNumber, out var value2);
				bool flag2 = !string.IsNullOrEmpty(text2) && text2 != cloudRegion;
				string text3 = (flag2 ? text2 : null);
				if (!(text3 == value2))
				{
					if (flag2)
					{
						DiagLog(L.Get("diag_cross", GetPlayerName(val.ActorNumber), text2, cloudRegion));
					}
					else if (!string.IsNullOrEmpty(value2))
					{
						DiagLog(L.Get("diag_cross_clear", GetPlayerName(val.ActorNumber), cloudRegion));
					}
					lastDiagCross[val.ActorNumber] = text3;
				}
			}
		}

		public static void DiagLog(string msg)
		{
			if (!string.IsNullOrEmpty(msg))
			{
				if (VoiceFix.logger != null)
				{
					VoiceFix.logger.LogInfo((object)msg);
				}
				if ((Object)(object)VoiceUIManager.Instance != (Object)null)
				{
					VoiceUIManager.Instance.AddLog("System", msg, isLocal: true);
				}
			}
		}

		private static void PerformReconnect(string mode)
		{
			//IL_00ad: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b4: Invalid comparison between Unknown and I4
			if (!((Object)(object)punVoice == (Object)null) && ((VoiceConnection)punVoice).Client != null)
			{
				TotalRetryCount++;
				float num = VoiceRetryRules.Delay(VoiceFix.RetryInterval.Value, ConnectionFailCount);
				nextRetryTime = Time.unscaledTime + num;
				if (Time.unscaledTime - lastSOSTime > 20f && PhotonNetwork.IsConnectedAndReady)
				{
					lastSOSTime = Time.unscaledTime;
					SendSOS(string.IsNullOrEmpty(TargetGameServer) ? "Unknown" : TargetGameServer);
				}
				reconnectPending = true;
				reconnectPendingMode = mode;
				reconnectPendingDeadline = Time.unscaledTime + 5f;
				if ((int)((LoadBalancingClient)((VoiceConnection)punVoice).Client).State != 14)
				{
					((LoadBalancingClient)((VoiceConnection)punVoice).Client).Disconnect();
				}
			}
		}

		private static void SendSOS(string targetInfo)
		{
			//IL_00b5: 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_00bc: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c2: Expected O, but got Unknown
			//IL_00c9: Unknown result type (might be due to invalid IL or missing references)
			string text = "Unknown";
			if ((Object)(object)punVoice != (Object)null && ((VoiceConnection)punVoice).Client != null)
			{
				text = ((LoadBalancingClient)((VoiceConnection)punVoice).Client).GameServerAddress;
			}
			if (string.IsNullOrEmpty(text))
			{
				text = "Disconnected";
			}
			BroadcastLog(L.Get("log_sos_send") + " -> " + L.Get("log_sos_target") + ":" + targetInfo + " | " + L.Get("log_sos_local") + ":" + text);
			object[] array = new object[3]
			{
				(byte)0,
				targetInfo,
				text
			};
			RaiseEventOptions val = new RaiseEventOptions
			{
				Receivers = (ReceiverGroup)0
			};
			PhotonNetwork.RaiseEvent((byte)186, (object)array, val, SendOptions.SendReliable);
		}

		public static void OnEvent(EventData photonEvent)
		{
			if (photonEvent.Code != 186)
			{
				return;
			}
			int senderActor = photonEvent.Sender;
			string playerName = GetPlayerName(senderActor);
			if (!(photonEvent.CustomData is object[] array) || array.Length < 2)
			{
				return;
			}
			byte b = 0;
			if (array[0] is byte b2)
			{
				b = b2;
			}
			else if (array[0] is int num)
			{
				b = (byte)num;
			}
			switch (b)
			{
			case 1:
			{
				string msg = array[1] as string;
				if ((Object)(object)VoiceUIManager.Instance != (Object)null)
				{
					VoiceUIManager.Instance.AddLog(playerName, msg, isLocal: false);
				}
				break;
			}
			case 0:
			{
				string text4 = array[1] as string;
				string originIP = "Unknown(old)";
				if (array.Length >= 3 && array[2] is string text5)
				{
					originIP = text5;
				}
				else if (PlayerCache.ContainsKey(senderActor))
				{
					originIP = PlayerCache[senderActor].IP;
				}
				ActiveSOSList.RemoveAll((SOSData x) => x.ActorNumber == senderActor);
				ActiveSOSList.Add(new SOSData
				{
					ActorNumber = senderActor,
					PlayerName = playerName,
					TargetIP = text4,
					OriginIP = originIP,
					ReceiveTime = Time.unscaledTime
				});
				if ((Object)(object)VoiceUIManager.Instance != (Object)null)
				{
					VoiceUIManager.Instance.AddLog("System", L.Get("log_sos_received", playerName, text4), isLocal: true);
					VoiceUIManager.Instance.TriggerNotification(playerName);
				}
				break;
			}
			case 2:
			{
				byte remoteState = 0;
				if (array[1] is byte b3)
				{
					remoteState = b3;
				}
				else if (array[1] is int num2)
				{
					remoteState = (byte)num2;
				}
				string text2 = "";
				if (array.Length >= 3 && array[2] is string text3)
				{
					text2 = text3;
				}
				if (PlayerCache.ContainsKey(senderActor))
				{
					PlayerCache[senderActor].RemoteState = remoteState;
					if (!string.IsNullOrEmpty(text2))
					{
						PlayerCache[senderActor].ModVersion = text2;
					}
					PlayerCache[senderActor].LastSeenTime = Time.unscaledTime;
				}
				else
				{
					CacheEntry value2 = new CacheEntry
					{
						PlayerName = playerName,
						LastSeenTime = Time.unscaledTime,
						RemoteState = remoteState,
						ModVersion = text2
					};
					PlayerCache[senderActor] = value2;
				}
				break;
			}
			case 3:
			{
				if (!PlayerCache.TryGetValue(senderActor, out var value))
				{
					value = new CacheEntry
					{
						PlayerName = playerName
					};
					PlayerCache[senderActor] = value;
				}
				if (array.Length >= 2 && array[1] is int gamePing)
				{
					value.GamePing = gamePing;
				}
				if (array.Length >= 3 && array[2] is int voicePing)
				{
					value.VoicePing = voicePing;
				}
				if (array.Length >= 4 && array[3] is string text)
				{
					value.VoiceRegion = (string.IsNullOrEmpty(text) ? null : text);
				}
				value.LastSeenTime = Time.unscaledTime;
				break;
			}
			}
		}

		private static void HandleInputAndState()
		{
			//IL_006e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0075: Invalid comparison between Unknown and I4
			//IL_0081: Unknown result type (might be due to invalid IL or missing references)
			//IL_0087: Invalid comparison between Unknown and I4
			//IL_0093: Unknown result type (might be due to invalid IL or missing references)
			//IL_0099: Invalid comparison between Unknown and I4
			if (VoiceFix.EnableManualReconnect == null || !VoiceFix.EnableManualReconnect.Value || (!Input.GetKey((KeyCode)308) && !Input.GetKey((KeyCode)307)) || !Input.GetKeyDown((KeyCode)107))
			{
				return;
			}
			if ((Object)(object)punVoice == (Object)null || ((VoiceConnection)punVoice).Client == null)
			{
				BroadcastLog("[System] Alt+K ignored: Voice client not ready.");
				return;
			}
			if ((int)((LoadBalancingClient)((VoiceConnection)punVoice).Client).State == 9 || (int)((LoadBalancingClient)((VoiceConnection)punVoice).Client).State == 6 || (int)((LoadBalancingClient)((VoiceConnection)punVoice).Client).State == 1)
			{
				BroadcastLog(L.Get("log_alt_k_disconnect"));
				if (PhotonNetwork.IsConnectedAndReady)
				{
					SendSOS(L.Get("log_sos_manual"));
				}
				manualDisconnectRequested = true;
				((LoadBalancingClient)((VoiceConnection)punVoice).Client).Disconnect();
				if ((Object)(object)VoiceUIManager.Instance != (Object)null)
				{
					VoiceUIManager.Instance.ShowStatsTemporary();
				}
			}
			else
			{
				BroadcastLog(L.Get("log_alt_k_reconnect"));
				TargetGameServer = DecideTargetIP(out var _);
				ConnectionFailCount = 0;
				PerformReconnect(L.Get("log_sos_manual"));
			}
			WrongIPCount = 0;
			TotalRetryCount = 0;
		}

		private static void ResetRoomScopedState()
		{
			//IL_0076: Unknown result type (might be due to invalid IL or missing references)
			PlayerCache.Clear();
			ActiveSOSList.Clear();
			HostHistory.Clear();
			TargetGameServer = null;
			ConnectedUsingHost = true;
			IsBlindConnect = false;
			WrongIPCount = 0;
			ConnectionFailCount = 0;
			TotalRetryCount = 0;
			LastErrorMessage = "";
			LastKnownHostIP = "";
			LastHostUpdateTime = 0f;
			LastScanTime = 0f;
			LastDecisionLog = "";
			lastClientState = (ClientState)14;
			lastVoiceProgressTime = 0f;
			voiceJoinedAt = -1f;
			nextRetryTime = 0f;
			lastPingPublishTime = 0f;
			lastSOSTime = 0f;
			nextSummaryLogTime = 0f;
			nextVoiceClientFindTime = 0f;
			nextSOSManageTime = 0f;
			lastPlayerCount = 0;
			scavengeFailTime.Clear();
			firstSeenTime.Clear();
			voiceToGameActor.Clear();
			gameToVoiceActor.Clear();
			nextVoiceMapRefreshTime = 0f;
			punVoice = null;
			reconnectPending = false;
			manualDisconnectRequested = false;
			reconnectPendingMode = "";
			reconnectPendingDeadline = 0f;
			nextDiagTime = 0f;
			lastDiagVoiceRegion = null;
			lastDiagVoiceRoom = null;
			lastDiagIsolated = false;
			lastDiagRoomVoice = null;
			lastDiagRoomVoiceSource = RoomVoiceSource.Unknown;
			lastDiagCross.Clear();
			nextPingEventTime = 0f;
		}
	}
	[HarmonyPatch(typeof(VoiceFollowClient), "OnVoiceStateChanged")]
	internal static class VoiceConnectionStatePatch
	{
		private static void Prefix(VoiceFollowClient __instance, ClientState fromState, ClientState toState)
		{
			//IL_000b: Unknown result type (might be due to invalid IL or missing references)
			//IL_000c: Unknown result type (might be due to invalid IL or missing references)
			PunVoiceClient val = (PunVoiceClient)(object)((__instance is PunVoiceClient) ? __instance : null);
			if (val != null)
			{
				NetworkManager.NoteVoiceStateChange(val, fromState, toState);
			}
		}
	}
	[HarmonyPatch(typeof(PunVoiceClient), "ConnectVoice")]
	internal static class VoiceAuthAlignPatch
	{
		private static void Prefix(PunVoiceClient __instance)
		{
			NetworkManager.AlignVoiceAuthWithPrefab(__instance);
		}
	}
	[HarmonyPatch(typeof(VoiceFollowClient), "OnOperationResponseReceived")]
	internal static class VoiceServerResponsePatch
	{
		private static void Prefix(VoiceFollowClient __instance, OperationResponse operationResponse)
		{
			PunVoiceClient val = (PunVoiceClient)(object)((__instance is PunVoiceClient) ? __instance : null);
			if (val != null)
			{
				NetworkManager.NoteVoiceServerResponse(val, operationResponse);
			}
		}
	}
	[HarmonyPatch(typeof(VoiceFollowClient), "ConnectOrJoinVoice")]
	internal static class VoiceNativeConnectPatch
	{
		private static bool Prefix(VoiceFollowClient __instance)
		{
			//IL_0010: Unknown result type (might be due to invalid IL or missing references)
			//IL