Decompiled source of BetterPeakVoiceFix v1.1.4

BetterPeakVoiceFix.dll

Decompiled 6 days 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.3.0")]
[assembly: AssemblyInformationalVersion("1.1.3+cb9e9fef3439b35ff41e3965e32d4c347c234b90")]
[assembly: AssemblyProduct("BetterPeakVoiceFix")]
[assembly: AssemblyTitle("BetterPeakVoiceFix")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.1.3.0")]
[module: UnverifiableCode]
namespace PeakVoiceFix
{
	internal static class InviteHandshake
	{
		private const float TIMEOUT = 8f;

		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 < 8f)
				{
					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_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_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_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", "如果连接卡住,超过多少秒判定为断开。" },
			{ "cfg_retry_interval", "每次自动重连之间的冷却时间。" },
			{ "cfg_manual_reconnect", "允许按 Alt+K 强制断开或重连语音。" },
			{ "cfgn_voice_recovery", "自动恢复语音状态" },
			{ "cfg_voice_recovery", "自动恢复本机远端玩家失效的语音状态引用。\n原理解析:游戏为每个玩家保存一份“语音状态”(静音/屏蔽等),新旧角色交接时登记表可能丢项,而语音组件只在初始化时取一次状态——引用失效后对方在你这里被当作静音,即使语音连接正常也听不到声音。\n开启后每 0.5 秒检测:发现已初始化的远端语音组件引用失效时,先从状态登记表取回同一玩家的有效状态,登记表缺失再用该角色自身数据兜底,交还原版组件处理;不修改静音、屏蔽或通信权限。\n面板隐藏时照常检测,异常玩家行显示“语音状态异常”或“已恢复”提示。不依赖 CrossplayStutterFix(它预防登记丢失,本项负责事后修复引用),不要求主机或队友安装。关闭后仍保留异常检测与提示。恢复引用只修这一类失声原因,不保证一定听到声音。" },
			{ "line2_voice_state_missing", "语音状态异常·本机播放受阻" },
			{ "line2_voice_state_recovered", "语音状态已恢复" },
			{ "cfg_max_name_len", "显示名字的最大字符数。" },
			{ "cfg_latency_offset", "Ping值显示的水平像素偏移。" },
			{ "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_latency_offset", "延迟对齐偏移量" },
			{ "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", "未知:本机断开语音服且对方未安装本模组 1.2 以上版本" },
			{ "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", "游戏那张区服编码表漏收了 hk(香港),导致港服房的房间码首字符是 '-',而本体解码时会把它夹成表里第一项 us(美国)——结果客机被送去美国区找一个在香港的房间,必然失败。这里指定 '-' 应该解析成哪个区。留空 = 不猜(保持本体行为,房间码进不去港服房)。与 BetterRoomShare 的同类修复兼容:结果已经正确时本 mod 不会再插手。" },
			{ "cfgn_wait_master", "切区服后等主服务器就绪" },
			{ "cfg_wait_master", "本体切换区服时是「断开 → 连接新区 → 立刻加房」,此时还没连上主服务器,加房必然失败(本体自己写了等待协程却没接上)。开启后会先等连上再加房,最多等 20 秒。" },
			{ "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 邀请后,客机要向大厅索要房间号,而房主只在自己确实待在房间里时才回应。本体这个握手没有超时、不重发、也不弹任何提示,房主当时在菜单/加载中/刚掉线,你就会永久静默卡在主菜单。开启后每 8 秒重试一次、最多 4 次,全部失败会明确告知原因。" },
			{ "invite_requested", "[邀请] 已向大厅索要房间号(第 {0} 次)" },
			{ "invite_retry", "[邀请] 8 秒未收到房间号,重新索要({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_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-region" },
			{ "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_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", "Seconds before connection is deemed disconnected." },
			{ "cfg_retry_interval", "Cooldown between auto-reconnect attempts." },
			{ "cfg_manual_reconnect", "Allow Alt+K to force disconnect/reconnect voice." },
			{ "cfgn_voice_recovery", "Auto Recover Voice State" },
			{ "cfg_voice_recovery", "Automatically restores a remote player's invalid voice-state reference on your machine.\nHow it happens: the game stores one voice state (mute/block etc.) per player; during an old/new character handover the registry entry can be lost, and the voice component reads the state only once at init - afterwards that player is treated as muted even when the voice connection is fine.\nWhen enabled it scans every 0.5s: if an initialized remote voice component's state reference is dead, a valid CharacterData belonging to the same player is taken from the registry first (falling back to that character's own data) and handed back to the stock component; mute, block and permission flags are never modified.\nDetection keeps running while the panel is hidden, and affected players show an error/restored note. Does not require CrossplayStutterFix (it prevents the loss; this repairs the reference afterwards) nor host/teammate installs. Turning it off keeps detection and the warning visible. Restoring the reference fixes one known cause of silence, not all of them." },
			{ "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_latency_offset", "Horizontal pixel offset for ping display." },
			{ "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_latency_offset", "Latency Alignment Offset" },
			{ "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", "Unknown: local voice is disconnected and this player has no BetterPeakVoiceFix 1.2+" },
			{ "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", "The game's region-code table is missing hk (Hong Kong), so a Hong Kong room's code starts with '-' and the game's decoder clamps that to the first table entry, us. The result is that the client is sent to the US region to look for a room that lives in Hong Kong, which always fails. This setting decides what '-' should resolve to. Leave empty to not guess (vanilla behaviour). Compatible with BetterRoomShare's equivalent fix: if the result is already correct, this mod stays out of it." },
			{ "cfgn_wait_master", "Wait for master server after region swap" },
			{ "cfg_wait_master", "When the game switches region it does disconnect -> connect to new region -> join room immediately, while still connecting to the master server, so the join always fails (the game has a wait coroutine for this but never calls it). With this on, the join waits for the master server first, up to 20 seconds." },
			{ "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 8 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 8s, 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 bool reconnectPending = false;

		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 int MAX_FAIL_BEFORE_BACKOFF = 12;

		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>();

		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_00b1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ce: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ee: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f1: Invalid comparison between Unknown and I4
			//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)
			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>();
				}
				catch (Exception)
				{
					punVoice = null;
				}
			}
			if ((Object)(object)punVoice != (Object)null && ((VoiceConnection)punVoice).Client != null)
			{
				ClientState state = ((LoadBalancingClient)((VoiceConnection)punVoice).Client).State;
				if (state != lastClientState)
				{
					BroadcastLog(string.Format("{0}: {1} -> {2}", L.Get("log_state_change"), lastClientState, state));
					SendStateSync(state);
					if ((int)state == 9)
					{
						ConnectionFailCount = 0;
						TryInitNetworkVoice();
					}
					UpdateDataLayer(force: true);
					lastClientState = state;
				}
			}
			ProcessPendingReconnect();
			UpdateDataLayer();
			PublishPingEvent();
			TrackDiagnostics();
			HandleInputAndState();
			ManageSOSList();
			if ((Object)(object)punVoice != (Object)null && ((VoiceConnection)punVoice).Client != null && !reconnectPending && Time.unscaledTime >= nextRetryTime)
			{
				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.3"
			};
			RaiseEventOptions val = new RaiseEventOptions
			{
				Receivers = (ReceiverGroup)0
			};
			PhotonNetwork.RaiseEvent((byte)186, (object)array, val, SendOptions.SendReliable);
		}

		public static void BroadcastLog(string message)
		{
			//IL_0044: Unknown result type (might be due to invalid IL or missing references)
			//IL_0049: Unknown result type (might be due to invalid IL or missing references)
			//IL_004b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0051: Expected O, but got Unknown
			//IL_0054: Unknown result type (might be due to invalid IL or missing references)
			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.3");
				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 == 14)
			{
				BroadcastLog(L.Get("log_host_disconnected"));
				ConnectVoiceNow(L.Get("log_host"));
				nextRetryTime = Time.unscaledTime + 5f;
			}
		}

		private static void HandleClientLogic()
		{
			//IL_00aa: Unknown result type (might be due to invalid IL or missing references)
			//IL_00af: 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_00b3: Invalid comparison between Unknown and I4
			//IL_0070: Unknown result type (might be due to invalid IL or missing references)
			//IL_0077: Invalid comparison between Unknown and I4
			//IL_014c: Unknown result type (might be due to invalid IL or missing references)
			//IL_014f: Invalid comparison between Unknown and I4
			string mode;
			string text = DecideTargetIP(out mode);
			if (!string.IsNullOrEmpty(text) && ConnectionFailCount > 0 && ConnectionFailCount % 6 >= 3)
			{
				BroadcastLog(L.Get("log_loop_blind", ConnectionFailCount));
				text = null;
				mode += "->BlindLoop";
			}
			if (string.IsNullOrEmpty(text))
			{
				IsBlindConnect = true;
				TargetGameServer = null;
				if ((int)((LoadBalancingClient)((VoiceConnection)punVoice).Client).State == 14)
				{
					PerformReconnect("Blind");
				}
				return;
			}
			IsBlindConnect = false;
			TargetGameServer = text;
			string gameServerAddress = ((LoadBalancingClient)((VoiceConnection)punVoice).Client).GameServerAddress;
			ClientState state = ((LoadBalancingClient)((VoiceConnection)punVoice).Client).State;
			if ((int)state == 9)
			{
				if (gameServerAddress == TargetGameServer)
				{
					WrongIPCount = 0;
					ConnectionFailCount = 0;
					TotalRetryCount = 0;
					return;
				}
				WrongIPCount++;
				if (WrongIPCount <= 2)
				{
					BroadcastLog(L.Get("log_wrong_freq", gameServerAddress, TargetGameServer, WrongIPCount) ?? "");
					PerformReconnect(mode);
				}
				else if (WrongIPCount == 3)
				{
					BroadcastLog(L.Get("log_compromise", gameServerAddress));
				}
			}
			else if ((int)state == 14)
			{
				ConnectionFailCount++;
				if (ConnectionFailCount > 12)
				{
					nextRetryTime = Time.unscaledTime + Mathf.Min(VoiceFix.RetryInterval.Value * 5f, 60f);
				}
				else
				{
					PerformReconnect(mode);
				}
			}
		}

		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_0097: Unknown result type (might be due to invalid IL or missing references)
			//IL_009c: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ac: Unknown result type (might be due to invalid IL or missing references)
			//IL_0071: Unknown result type (might be due to invalid IL or missing references)
			//IL_007b: Expected O, but got Unknown
			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;
				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));
				return ((VoiceConnection)punVoice).ConnectUsingSettings(val);
			}
			catch (Exception ex)
			{
				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_003c: 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;
			}
			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;
				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_00a1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a8: Invalid comparison between Unknown and I4
			if (!((Object)(object)punVoice == (Object)null) && ((VoiceConnection)punVoice).Client != null)
			{
				TotalRetryCount++;
				nextRetryTime = Time.unscaledTime + VoiceFix.RetryInterval.Value;
				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"));
				}
				((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;
			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;
			reconnectPendingMode = "";
			reconnectPendingDeadline = 0f;
			nextDiagTime = 0f;
			lastDiagVoiceRegion = null;
			lastDiagVoiceRoom = null;
			lastDiagIsolated = false;
			lastDiagRoomVoice = null;
			lastDiagRoomVoiceSource = RoomVoiceSource.Unknown;
			lastDiagCross.Clear();
			nextPingEventTime = 0f;
		}
	}
	internal static class PanelText
	{
		internal static string Literal(string value)
		{
			return (value ?? "").Replace("<", "<noparse><</noparse>");
		}

		internal static bool? RoomNamesMatch(string gameRoom, string voiceRoom)
		{
			if (!string.IsNullOrEmpty(gameRoom) && !string.IsNullOrEmpty(voiceRoom))
			{
				return string.Equals(voiceRoom, gameRoom + "_voice_", StringComparison.Ordinal);
			}
			return null;
		}
	}
	[HarmonyPatch]
	public static class PhotonRPCFix
	{
		[HarmonyPatch(typeof(PhotonNetwork), "RPC", new Type[]
		{
			typeof(PhotonView),
			typeof(string),
			typeof(RpcTarget),
			typeof(Player),
			typeof(bool),
			typeof(object[])
		})]
		[HarmonyPrefix]
		public static void PrePhotonNetworkRPC(PhotonView view, string methodName, ref RpcTarget target)
		{
			if (methodName == "RemoveSkeletonRPC" && (int)target == 3)
			{
				target = (RpcTarget)0;
			}
		}
	}
	internal static class PhotonSettingsGuard
	{
		private static string realRealtime;

		private static string realVoice;

		private static bool captured;

		private static bool deviatedLogged;

		private static float nextCheckTime;

		private const float CHECK_INTERVAL = 1f;

		public static bool Enabled
		{
			get
			{
				if (VoiceFix.EnableAppIdGuard != null)
				{
					return VoiceFix.EnableAppIdGuard.Value;
				}
				return true;
			}
		}

		public static void Init()
		{
			TryCapture();
		}

		public static void Update()
		{
			if (Enabled && !(Time.unscaledTime < nextCheckTime))
			{
				nextCheckTime = Time.unscaledTime + 1f;
				if (!captured)
				{
					TryCapture();
				}
				else
				{
					CheckGlobal();
				}
			}
		}

		private static AppSettings Global()
		{
			ServerSettings photonServerSettings = PhotonNetwork.PhotonServerSettings;
			if ((Object)(object)photonServerSettings == (Object)null)
			{
				return null;
			}
			return photonServerSettings.AppSettings;
		}

		private static void TryCapture()
		{
			try
			{
				AppSettings val = Global();
				if (val != null)
				{
					if (realRealtime == null && !string.IsNullOrEmpty(val.AppIdRealtime))
					{
						realRealtime = val.AppIdRealtime;
					}
					if (realVoice == null && !string.IsNullOrEmpty(val.AppIdVoice))
					{
						realVoice = val.AppIdVoice;
					}
					if (!captured && realRealtime != null && realVoice != null)
					{
						captured = true;
						NetworkManager.DiagLog(L.Get("diag_appid_snapshot", realRealtime, realVoice));
					}
				}
			}
			catch (Exception)
			{
			}
		}

		private static void CheckGlobal()
		{
			try
			{
				AppSettings val = Global();
				if (val == null)
				{
					return;
				}
				bool flag = realRealtime != null && val.AppIdRealtime != realRealtime;
				bool flag2 = realVoice != null && val.AppIdVoice != realVoice;
				if (!flag && !flag2)
				{
					deviatedLogged = false;
					return;
				}
				if (!deviatedLogged)
				{
					deviatedLogged = true;
					string text = L.Get("diag_appid_restored", flag ? val.AppIdRealtime : "—", flag2 ? val.AppIdVoice : "—");
					NetworkManager.DiagLog(text);
					if (VoiceFix.logger != null)
					{
						VoiceFix.logger.LogWarning((object)text);
					}
				}
				if (flag)
				{
					val.AppIdRealtime = realRealtime;
				}
				if (flag2)
				{
					val.AppIdVoice = realVoice;
				}
			}
			catch (Exception)
			{
			}
		}

		public static void SanitizeForVoiceConnect(VoiceConnection conn, AppSettings overwrite)
		{
			if (!Enabled || !captured)
			{
				return;
			}
			try
			{
				AppSettings val = overwrite ?? (((Object)(object)conn != (Object)null) ? conn.Settings : null);
				if (val != null)
				{
					if (realVoice != null && val.AppIdVoice != realVoice)
					{
						val.AppIdVoice = realVoice;
					}
					if (realRealtime != null && val.AppIdRealtime != realRealtime)
					{
						val.AppIdRealtime = realRealtime;
					}
				}
				CheckGlobal();
			}
			catch (Exception)
			{
			}
		}
	}
	[HarmonyPatch(typeof(VoiceConnection), "ConnectUsingSettings")]
	internal static class VoiceConnectSettingsPatch
	{
		private static void Prefix(VoiceConnection __instance, AppSettings overwriteSettings)
		{
			PhotonSettingsGuard.SanitizeForVoiceConnect(__instance, overwriteSettings);
		}
	}
	internal static class RegionControl
	{
		public const string AUTO = "auto";

		public static readonly string[] KNOWN_REGIONS = new string[9] { "asia", "au", "eu", "hk", "jp", "sa", "us", "ussc", "usw" };

		private static readonly Dictionary<string, string> ZH_NAME = new Dictionary<string, string>
		{
			{ "asia", "亚洲/新加坡" },
			{ "au", "澳洲" },
			{ "eu", "欧洲" },
			{ "hk", "香港" },
			{ "jp", "日本" },
			{ "sa", "南美" },
			{ "us", "美东" },
			{ "ussc", "美中南" },
			{ "usw", "美西" }
		};

		private static string appliedRegion = null;

		private static float connectAttemptTime = -1f;

		private static bool warnedThisAttempt = false;

		private static LoadBalancingClient pingClient;

		private static RegionHandler borrowedHandler;

		private static bool pingBusy;

		private static bool pingRequested;

		private static bool pingDisconnecting;

		private static float pingStartTime;

		private static volatile bool pingDone;

		private static volatile string pingPending;

		private static bool qualityChecked = false;

		private static float qualityCheckTime = -1f;

		public static bool IsPinging => pingBusy;

		public static string Configured
		{
			get
			{
				string text = ((VoiceFix.ForcedRegion != null) ? VoiceFix.ForcedRegion.Value : "auto");
				if (string.IsNullOrEmpty(text))
				{
					return "auto";
				}
				text = text.Trim().ToLowerInvariant();
				if (text == "auto")
				{
					return "auto";
				}
				string[] kNOWN_REGIONS = KNOWN_REGIONS;
				for (int i = 0; i < kNOWN_REGIONS.Length; i++)
				{
					if (kNOWN_REGIONS[i] == text)
					{
						return text;
					}
				}
				return "auto";
			}
		}

		public static bool IsAuto => Configured == "auto";

		public static string Describe(string code)
		{
			if (string.IsNullOrEmpty(code))
			{
				return "—";
			}
			if (L.IsChinese && ZH_NAME.TryGetValue(code, out var value))
			{
				return code + " " + value;
			}
			return code;
		}

		public static void ApplyFixedRegion()
		{
			try
			{
				AppSettings val = (((Object)(object)PhotonNetwork.PhotonServerSettings != (Object)null) ? PhotonNetwork.PhotonServerSettings.AppSettings : null);
				if (val == null)
				{
					return;
				}
				connectAttemptTime = Time.unscaledTime;
				warnedThisAttempt = false;
				qualityChecked = false;
				qualityCheckTime = -1f;
				if (IsAuto)
				{
					if (appliedRegion != null && val.FixedRegion == appliedRegion)
					{
						val.FixedRegion = "";
						if (VoiceFix.logger != null)
						{
							VoiceFix.logger.LogInfo((object)"[区服] 已恢复自动选区");
						}
					}
					appliedRegion = null;
					return;
				}
				string configured = Configured;
				if (val.FixedRegion != configured)
				{
					val.FixedRegion = configured;
					if (VoiceFix.logger != null)
					{
						VoiceFix.logger.LogInfo((object)("[区服] 强制连接到 " + Describe(configured) + "(原设置: '" + appliedRegion + "')"));
					}
				}
				appliedRegion = configured;
			}
			catch (Exception ex)
			{
				if (VoiceFix.logger != null)
				{
					VoiceFix.logger.LogWarning((object)("[区服] 应用强制区服失败: " + ex.Message));
				}
			}
		}

		public static void Update()
		{
			CheckConnectTimeout();
			CheckForcedRegionQuality();
			PumpPing();
		}

		private static void CheckForcedRegionQuality()
		{
			//IL_000f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0014: Unknown result type (might be due to invalid IL or missing references)
			//IL_0015: Unknown result type (might be due to invalid IL or missing references)
			//IL_0018: Invalid comparison between Unknown and I4
			//IL_001a: Unknown result type (might be due to invalid IL or missing references)
			//IL_001c: Invalid comparison between Unknown and I4
			//IL_001e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0021: Invalid comparison between Unknown and I4
			if (qualityChecked || IsAuto)
			{
				return;
			}
			ClientState networkClientState = PhotonNetwork.NetworkClientState;
			if ((int)networkClientState != 15 && (int)networkClientState != 4 && (int)networkClientState != 9 && !PhotonNetwork.InRoom)
			{
				qualityCheckTime = -1f;
			}
			else if (qualityCheckTime < 0f)
			{
				qualityCheckTime = Time.unscaledTime;
			}
			else
			{
				if (Time.unscaledTime - qualityCheckTime < 8f)
				{
					return;
				}
				qualityChecked = true;
				int ping = PhotonNetwork.GetPing();
				if (ping <= 0)
				{
					return;
				}
				string code;
				int num = ParseCachedBestPing(out code);
				string text = null;
				if (num > 0 && ping > num + 60)
				{
					text = L.Get("region_worse", Describe(Configured), ping, Describe(code), num);
				}
				else if (ping >= 300)
				{
					text = L.Get("region_high", Describe(Configured), ping);
				}
				if (text != null)
				{
					if (VoiceFix.logger != null)
					{
						VoiceFix.logger.LogWarning((object)text);
					}
					if ((Object)(object)VoiceUIManager.Instance != (Object)null)
					{
						VoiceUIManager.Instance.SetRegionWarning(text);
						VoiceUIManager.Instance.AddLog("System", text, isLocal: true);
					}
				}
			}
		}

		private static int ParseCachedBestPing(out string code)
		{
			code = null;
			try
			{
				string bestRegionSummaryInPreferences = PhotonNetwork.BestRegionSummaryInPreferences;
				if (string.IsNullOrEmpty(bestRegionSummaryInPreferences))
				{
					return -1;
				}
				string[] array = bestRegionSummaryInPreferences.Split(';');
				if (array.Length < 2)
				{
					return -1;
				}
				code = array[0];
				int result;
				return int.TryParse(array[1], out result) ? result : (-1);
			}
			catch (Exception)
			{
				return -1;
			}
		}

		private static void CheckConnectTimeout()
		{
			//IL_0026: 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_002c: Unknown result type (might be due to invalid IL or missing references)
			//IL_002f: Invalid comparison between Unknown and I4
			//IL_0031: Unknown result type (might be due to invalid IL or missing references)
			//IL_0033: Invalid comparison between Unknown and I4
			//IL_0035: Unknown result type (might be due to invalid IL or missing references)
			//IL_0038: Invalid comparison between Unknown and I4
			if (connectAttemptTime < 0f || warnedThisAttempt)
			{
				return;
			}
			if (IsAuto)
			{
				connectAttemptTime = -1f;
				return;
			}
			ClientState networkClient