Decompiled source of FusionVoiceLocalizer v1.5.1

Mods/FusionVoiceLocalizer.dll

Decompiled a day ago
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using BoneLib;
using BoneLib.BoneMenu;
using BoneLib.Notifications;
using FusionVoiceLocalizer;
using HarmonyLib;
using Il2CppTMPro;
using LabFusion.Audio;
using LabFusion.Network;
using LabFusion.Player;
using LabFusion.SDK.Metadata;
using LabFusion.Voice;
using LabFusion.Voice.Unity;
using MelonLoader;
using MelonLoader.Preferences;
using Microsoft.CodeAnalysis;
using UnityEngine;
using UnityEngine.UI;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: MelonInfo(typeof(Core), "FusionVoiceLocalizer", "1.0.0", "CAitStudio", null)]
[assembly: MelonGame("Stress Level Zero", "BONELAB")]
[assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")]
[assembly: AssemblyCompany("FusionVoiceLocalizer")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("FusionVoiceLocalizer")]
[assembly: AssemblyTitle("FusionVoiceLocalizer")]
[assembly: NeutralResourcesLanguage("en-US")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.0.0.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace FusionVoiceLocalizer
{
	public class Core : MelonMod
	{
		[HarmonyPatch(typeof(VoiceHelper), "OnVoiceDataReceived")]
		private static class VoiceHelperOnVoiceDataReceivedPatch
		{
			[HarmonyPrefix]
			private static void Prefix(PlayerID player, byte[] data)
			{
				OnFusionVoiceReceived(player, data);
			}
		}

		[HarmonyPatch(typeof(UnityVoiceSpeaker), "OnVoiceDataReceived")]
		private static class UnityVoiceSpeakerOnVoiceDataReceivedPatch
		{
			[HarmonyPrefix]
			private static void Prefix(UnityVoiceSpeaker __instance, byte[] data)
			{
				if (__instance != null)
				{
					_ = ((VoiceSpeaker)__instance).ID;
				}
				else
					_ = null;
			}
		}

		[HarmonyPatch(typeof(PlayerVoiceChatMessage), "OnHandleMessage")]
		private static class PlayerVoiceChatMessageOnHandleMessagePatch
		{
			[HarmonyPrefix]
			private static void Prefix(ReceivedMessage received)
			{
				try
				{
					PlayerVoiceChatData val = ((ReceivedMessage)(ref received)).ReadData<PlayerVoiceChatData>();
					byte? sender = ((ReceivedMessage)(ref received)).Sender;
					if (Time.realtimeSinceStartup - _lastVoiceDebugTime >= 0.5f)
					{
						Core instance = _instance;
						if (instance != null)
						{
							((MelonBase)instance).LoggerInstance.Msg($"PlayerVoiceChatMessage received: sender={(sender.HasValue ? sender.Value.ToString() : "null")}, bytes={((val?.Bytes != null) ? val.Bytes.Length : 0)}");
						}
					}
					if (sender.HasValue && sender.Value != PlayerIDManager.LocalSmallID && val?.Bytes != null && val.Bytes.Length != 0)
					{
						PlayerID playerID = PlayerIDManager.GetPlayerID(sender.Value);
						string playerName = ((playerID != null) ? GetPlayerName(playerID) : $"Player {sender.Value}");
						ProcessFusionVoice(sender.Value, playerName, val.Bytes);
					}
				}
				catch (Exception ex)
				{
					Core instance2 = _instance;
					if (instance2 != null)
					{
						((MelonBase)instance2).LoggerInstance.Warning("PlayerVoiceChatMessage debug read failed: " + ex.Message);
					}
				}
			}
		}

		private readonly struct TestResult
		{
			public readonly NotificationType Type;

			public readonly string Message;

			public TestResult(NotificationType type, string message)
			{
				//IL_0001: Unknown result type (might be due to invalid IL or missing references)
				//IL_0002: Unknown result type (might be due to invalid IL or missing references)
				Type = type;
				Message = message;
			}
		}

		private sealed class VoiceBuffer
		{
			public byte SmallId;

			public string PlayerName;

			public readonly List<short> Samples = new List<short>();

			public bool IsSubmitting;

			public float LastVoiceTime = -999f;

			public float LastSubtitleTime = -999f;

			public string SubtitleText = string.Empty;

			public int Order;

			public TextMeshPro Text;

			public int LastPacketHash;

			public float LastPacketTime = -999f;
		}

		private static readonly ConcurrentQueue<Action> MainThreadActions = new ConcurrentQueue<Action>();

		private static readonly HttpClient HttpClient = new HttpClient
		{
			Timeout = TimeSpan.FromSeconds(20.0)
		};

		private static MelonPreferences_Category _category;

		private static MelonPreferences_Entry<string> _apiKey;

		private static MelonPreferences_Entry<string> _chatBaseUrl;

		private static MelonPreferences_Entry<string> _chatModelName;

		private static MelonPreferences_Entry<string> _asrBaseUrl;

		private static MelonPreferences_Entry<string> _asrModelName;

		private static MelonPreferences_Entry<string> _targetLanguage;

		private static MelonPreferences_Entry<bool> _useWhisperCppServer;

		private static MelonPreferences_Entry<float> _subtitleSize;

		private static MelonPreferences_Entry<float> _subtitleDistance;

		private static MelonPreferences_Entry<float> _subtitleHorizontalOffset;

		private static MelonPreferences_Entry<float> _subtitleVerticalOffset;

		private static bool _isTestingAi;

		private static Core _instance;

		private static GameObject _subtitleRoot;

		private static readonly object VoiceLock = new object();

		private static readonly Dictionary<byte, VoiceBuffer> VoiceBuffers = new Dictionary<byte, VoiceBuffer>();

		private static readonly List<byte> SubtitleOrder = new List<byte>();

		private static int _nextSubtitleOrder;

		private static float _lastAnyVoiceTime = -999f;

		private static float _lastVoiceDebugTime = -999f;

		private const float SubtitleDistance = 0.35f;

		private const float SubtitleHideDelay = 2f;

		private const float VoiceChunkSeconds = 1.15f;

		private const float SubtitleRowSpacing = 0.025f;

		private const float DuplicatePacketWindow = 0.08f;

		public override void OnInitializeMelon()
		{
			_instance = this;
			CreatePreferences();
			CreateBoneMenu();
			((MelonBase)this).HarmonyInstance.PatchAll();
			((MelonBase)this).LoggerInstance.Msg("Initialized.");
		}

		public override void OnUpdate()
		{
			Action result;
			while (MainThreadActions.TryDequeue(out result))
			{
				result?.Invoke();
			}
			FlushSilentVoiceBuffers();
			UpdateSubtitleDisplay();
		}

		private static void CreatePreferences()
		{
			_category = MelonPreferences.CreateCategory("FusionVoiceLocalizer");
			_category.SetFilePath("UserData/FusionVoiceLocalizer.cfg", true);
			_apiKey = _category.CreateEntry<string>("ApiKey", string.Empty, (string)null, (string)null, false, false, (ValueValidator)null, (string)null);
			_chatBaseUrl = _category.CreateEntry<string>("BaseUrl", "https://api.z.ai/api/paas/v4", (string)null, (string)null, false, false, (ValueValidator)null, (string)null);
			_chatModelName = _category.CreateEntry<string>("ModelName", "glm-5.2", (string)null, (string)null, false, false, (ValueValidator)null, (string)null);
			_asrBaseUrl = _category.CreateEntry<string>("AsrBaseUrl", "https://api.z.ai/api/paas/v4", (string)null, (string)null, false, false, (ValueValidator)null, (string)null);
			_asrModelName = _category.CreateEntry<string>("AsrModelName", "glm-asr-2512", (string)null, (string)null, false, false, (ValueValidator)null, (string)null);
			_targetLanguage = _category.CreateEntry<string>("TargetLanguage", "English", (string)null, (string)null, false, false, (ValueValidator)null, (string)null);
			_useWhisperCppServer = _category.CreateEntry<bool>("UseWhisperCppServer", false, (string)null, (string)null, false, false, (ValueValidator)null, (string)null);
			_subtitleSize = _category.CreateEntry<float>("SubtitleSize", 0.13f, (string)null, (string)null, false, false, (ValueValidator)null, (string)null);
			_subtitleDistance = _category.CreateEntry<float>("SubtitleDistance", 0.35f, (string)null, (string)null, false, false, (ValueValidator)null, (string)null);
			_subtitleHorizontalOffset = _category.CreateEntry<float>("SubtitleHorizontalOffset", 0f, (string)null, (string)null, false, false, (ValueValidator)null, (string)null);
			_subtitleVerticalOffset = _category.CreateEntry<float>("SubtitleVerticalOffset", 0f, (string)null, (string)null, false, false, (ValueValidator)null, (string)null);
			MigrateLegacyAsrModelSetting();
		}

		private static void CreateBoneMenu()
		{
			//IL_000a: Unknown result type (might be due to invalid IL or missing references)
			//IL_001c: Unknown result type (might be due to invalid IL or missing references)
			//IL_002f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0041: Unknown result type (might be due to invalid IL or missing references)
			//IL_0053: Unknown result type (might be due to invalid IL or missing references)
			//IL_0092: Unknown result type (might be due to invalid IL or missing references)
			//IL_00cc: Unknown result type (might be due to invalid IL or missing references)
			//IL_0106: Unknown result type (might be due to invalid IL or missing references)
			//IL_0140: Unknown result type (might be due to invalid IL or missing references)
			//IL_017a: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b4: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ee: Unknown result type (might be due to invalid IL or missing references)
			//IL_021a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0263: Unknown result type (might be due to invalid IL or missing references)
			//IL_02ac: Unknown result type (might be due to invalid IL or missing references)
			//IL_02f5: Unknown result type (might be due to invalid IL or missing references)
			//IL_033d: Unknown result type (might be due to invalid IL or missing references)
			Page obj = Page.Root.CreatePage("Voice Localization", Color.cyan, 0, true);
			Page val = obj.CreatePage("AI Setting", Color.cyan, 0, true);
			Page val2 = obj.CreatePage("ASR Setting", Color.cyan, 0, true);
			Page obj2 = obj.CreatePage("Subtitle Settings", Color.white, 0, true);
			val.CreateString("API Key", Color.white, MaskSecret(_apiKey.Value), (Action<string>)delegate(string value)
			{
				_apiKey.Value = UnmaskIfUnchanged(value, _apiKey.Value);
				SavePreferences();
			});
			val.CreateString("Chat Base URL", Color.white, _chatBaseUrl.Value, (Action<string>)delegate(string value)
			{
				_chatBaseUrl.Value = value?.Trim() ?? string.Empty;
				SavePreferences();
			});
			val.CreateString("Chat Model Name", Color.white, _chatModelName.Value, (Action<string>)delegate(string value)
			{
				_chatModelName.Value = value?.Trim() ?? string.Empty;
				SavePreferences();
			});
			val2.CreateString("ASR Base URL", Color.white, _asrBaseUrl.Value, (Action<string>)delegate(string value)
			{
				_asrBaseUrl.Value = value?.Trim() ?? string.Empty;
				SavePreferences();
			});
			val2.CreateString("ASR Model Name", Color.white, _asrModelName.Value, (Action<string>)delegate(string value)
			{
				_asrModelName.Value = value?.Trim() ?? string.Empty;
				SavePreferences();
			});
			val2.CreateBool("Local ASR", Color.white, _useWhisperCppServer.Value, (Action<bool>)delegate(bool value)
			{
				_useWhisperCppServer.Value = value;
				SavePreferences();
			});
			val.CreateString("Target Language", Color.white, _targetLanguage.Value, (Action<string>)delegate(string value)
			{
				_targetLanguage.Value = (string.IsNullOrWhiteSpace(value) ? "English" : value.Trim());
				SavePreferences();
			});
			val.CreateFunction("Test AI Connection", Color.yellow, (Action)TestAiConnection);
			obj2.CreateFloat("Size", Color.white, _subtitleSize.Value, 0.01f, 0.03f, 0.5f, (Action<float>)delegate(float value)
			{
				_subtitleSize.Value = value;
				SavePreferences();
			});
			obj2.CreateFloat("Distance", Color.white, _subtitleDistance.Value, 0.05f, 0.1f, 2f, (Action<float>)delegate(float value)
			{
				_subtitleDistance.Value = value;
				SavePreferences();
			});
			obj2.CreateFloat("Horizontal Offset", Color.white, _subtitleHorizontalOffset.Value, 0.025f, -1f, 1f, (Action<float>)delegate(float value)
			{
				_subtitleHorizontalOffset.Value = value;
				SavePreferences();
			});
			obj2.CreateFloat("Vertical Offset", Color.white, _subtitleVerticalOffset.Value, 0.025f, -1f, 1f, (Action<float>)delegate(float value)
			{
				_subtitleVerticalOffset.Value = value;
				SavePreferences();
			});
			obj2.CreateFunction("TEST", Color.yellow, (Action)delegate
			{
				ShowSubtitlePreview(5f);
			});
		}

		private static void MigrateLegacyAsrModelSetting()
		{
			string text = _chatModelName.Value?.Trim() ?? string.Empty;
			if (string.IsNullOrWhiteSpace(_asrModelName.Value?.Trim() ?? string.Empty) && text.Contains("asr", StringComparison.OrdinalIgnoreCase))
			{
				_asrModelName.Value = text;
				_chatModelName.Value = "glm-5.2";
				SavePreferences();
			}
		}

		private static void SavePreferences()
		{
			_category.SaveToFile(true);
			MelonPreferences.Save();
		}

		private static void TestAiConnection()
		{
			ShowSubtitlePreview();
			if (_isTestingAi)
			{
				SendNotification((NotificationType)1, "AI Testing", "A connection test is already running.");
				return;
			}
			string apiKey = _apiKey.Value?.Trim() ?? string.Empty;
			string baseUrl = _chatBaseUrl.Value?.Trim() ?? string.Empty;
			string chatModelName = _chatModelName.Value?.Trim() ?? string.Empty;
			string baseUrl2 = _asrBaseUrl.Value?.Trim() ?? string.Empty;
			string asrModelName = _asrModelName.Value?.Trim() ?? string.Empty;
			bool useWhisperCppServer = _useWhisperCppServer.Value;
			string targetLanguage = (string.IsNullOrWhiteSpace(_targetLanguage.Value) ? "English" : _targetLanguage.Value.Trim());
			if (string.IsNullOrWhiteSpace(apiKey))
			{
				SendNotification((NotificationType)2, "AI Testing", "Failed: Invalid API key.");
				return;
			}
			if (!TryBuildEndpoint(baseUrl, "chat/completions", out var chatEndpoint, out var error))
			{
				SendNotification((NotificationType)2, "AI Testing", "Failed: Invalid chat URL. " + error);
				return;
			}
			string path = (useWhisperCppServer ? "inference" : "audio/transcriptions");
			if (!TryBuildEndpoint(baseUrl2, path, out var asrEndpoint, out var error2))
			{
				SendNotification((NotificationType)2, "AI Testing", "Failed: Invalid ASR URL. " + error2);
				return;
			}
			if (string.IsNullOrWhiteSpace(chatModelName))
			{
				SendNotification((NotificationType)2, "AI Testing", "Failed: Invalid chat model name.");
				return;
			}
			if (!useWhisperCppServer && string.IsNullOrWhiteSpace(asrModelName))
			{
				SendNotification((NotificationType)2, "AI Testing", "Failed: Invalid ASR model name.");
				return;
			}
			_isTestingAi = true;
			SendNotification((NotificationType)0, "AI Testing", "Testing AI connection...");
			Task.Run(async delegate
			{
				TestResult result = await RunAiConnectionTest(apiKey, chatEndpoint, chatModelName, asrEndpoint, asrModelName, useWhisperCppServer, targetLanguage);
				MainThreadActions.Enqueue(delegate
				{
					//IL_000c: Unknown result type (might be due to invalid IL or missing references)
					_isTestingAi = false;
					SendNotification(result.Type, "AI Testing", result.Message);
				});
			});
		}

		private static async Task<TestResult> RunAiConnectionTest(string apiKey, Uri chatEndpoint, string chatModelName, Uri asrEndpoint, string asrModelName, bool useWhisperCppServer, string targetLanguage)
		{
			_ = 2;
			try
			{
				using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, chatEndpoint);
				request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
				request.Content = new StringContent(CreateChatTestBody(chatModelName, targetLanguage), Encoding.UTF8, "application/json");
				using HttpResponseMessage response = await HttpClient.SendAsync(request);
				string responseBody = await response.Content.ReadAsStringAsync();
				if (!response.IsSuccessStatusCode)
				{
					return new TestResult((NotificationType)2, BuildFailureMessage(response.StatusCode, responseBody));
				}
				TestResult result = await RunVoiceSupportTest(apiKey, asrEndpoint, asrModelName, useWhisperCppServer);
				if ((int)result.Type != 3)
				{
					return result;
				}
				return new TestResult((NotificationType)3, "Success: AI connection and voice transcription are working.");
			}
			catch (TaskCanceledException)
			{
				return new TestResult((NotificationType)2, "Failed: Connection timed out.");
			}
			catch (HttpRequestException ex2)
			{
				return new TestResult((NotificationType)2, "Failed: Network error. " + ex2.Message);
			}
			catch (Exception ex3)
			{
				return new TestResult((NotificationType)2, "Failed: " + ex3.Message);
			}
		}

		private static async Task<TestResult> RunVoiceSupportTest(string apiKey, Uri audioEndpoint, string asrModelName, bool useWhisperCppServer)
		{
			_ = 1;
			try
			{
				byte[] content = CreateWav(new short[Mathf.RoundToInt((float)AudioInfo.OutputSampleRate * 0.2f)], AudioInfo.OutputSampleRate);
				using MultipartFormDataContent form = new MultipartFormDataContent();
				using ByteArrayContent file = new ByteArrayContent(content);
				file.Headers.ContentType = new MediaTypeHeaderValue("audio/wav");
				form.Add(file, "file", "voice_test.wav");
				form.Add(new StringContent("json", Encoding.UTF8), "response_format");
				if (!useWhisperCppServer)
				{
					form.Add(new StringContent(asrModelName, Encoding.UTF8), "model");
				}
				using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, audioEndpoint);
				request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
				request.Content = form;
				using HttpResponseMessage response = await HttpClient.SendAsync(request);
				string responseBody = await response.Content.ReadAsStringAsync();
				if (response.IsSuccessStatusCode)
				{
					return new TestResult((NotificationType)3, "Success: Voice transcription is supported.");
				}
				return new TestResult((NotificationType)1, "Warning: AI connection succeeded, but voice transcription is not supported. " + BuildFailureMessage(response.StatusCode, responseBody));
			}
			catch (TaskCanceledException)
			{
				return new TestResult((NotificationType)1, "Warning: AI connection succeeded, but voice transcription is not supported. Voice test timed out.");
			}
			catch (Exception ex2)
			{
				return new TestResult((NotificationType)1, "Warning: AI connection succeeded, but voice transcription is not supported. " + ex2.Message);
			}
		}

		private static string CreateChatTestBody(string modelName, string targetLanguage)
		{
			return JsonSerializer.Serialize(new
			{
				model = modelName,
				messages = new[]
				{
					new
					{
						role = "system",
						content = "Reply with only OK in " + targetLanguage + "."
					},
					new
					{
						role = "user",
						content = "Connection test."
					}
				},
				max_tokens = 8,
				temperature = 0
			});
		}

		private static string BuildFailureMessage(HttpStatusCode statusCode, string responseBody)
		{
			string text = responseBody?.ToLowerInvariant() ?? string.Empty;
			if (statusCode == HttpStatusCode.Unauthorized || text.Contains("invalid_api_key") || text.Contains("incorrect api key"))
			{
				return "Failed: Invalid API key.";
			}
			if (statusCode == HttpStatusCode.NotFound && (text.Contains("model") || text.Contains("not found")))
			{
				return "Failed: Invalid model name.";
			}
			if (statusCode == HttpStatusCode.BadRequest && text.Contains("model"))
			{
				return "Failed: Invalid model name.";
			}
			switch (statusCode)
			{
			case HttpStatusCode.Forbidden:
				return "Failed: API key does not have permission to use this model.";
			case HttpStatusCode.TooManyRequests:
				return "Failed: Rate limit or quota exceeded.";
			default:
			{
				string text2 = ExtractErrorMessage(responseBody);
				if (!string.IsNullOrWhiteSpace(text2))
				{
					return "Failed: " + text2;
				}
				return $"Failed: Server returned HTTP {statusCode} {statusCode}.";
			}
			}
		}

		private static string ExtractErrorMessage(string responseBody)
		{
			if (string.IsNullOrWhiteSpace(responseBody))
			{
				return string.Empty;
			}
			try
			{
				using JsonDocument jsonDocument = JsonDocument.Parse(responseBody);
				if (jsonDocument.RootElement.TryGetProperty("error", out var value) && value.TryGetProperty("message", out var value2))
				{
					return value2.GetString();
				}
			}
			catch
			{
			}
			if (responseBody.Length <= 180)
			{
				return responseBody;
			}
			return responseBody.Substring(0, 180);
		}

		private static bool TryBuildEndpoint(string baseUrl, string path, out Uri endpoint, out string error)
		{
			endpoint = null;
			error = null;
			if (string.IsNullOrWhiteSpace(baseUrl) || !Uri.TryCreate(baseUrl.Trim(), UriKind.Absolute, out Uri result) || (result.Scheme != Uri.UriSchemeHttp && result.Scheme != Uri.UriSchemeHttps))
			{
				error = "Invalid URL.";
				return false;
			}
			string text = result.ToString().TrimEnd('/') + "/";
			endpoint = new Uri(text + path.TrimStart('/'));
			return true;
		}

		private static void SendNotification(NotificationType type, string title, string message)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_0007: Unknown result type (might be due to invalid IL or missing references)
			//IL_000c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0011: Unknown result type (might be due to invalid IL or missing references)
			//IL_0013: Unknown result type (might be due to invalid IL or missing references)
			//IL_0018: Unknown result type (might be due to invalid IL or missing references)
			//IL_001d: 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_002f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0030: Unknown result type (might be due to invalid IL or missing references)
			//IL_0031: Unknown result type (might be due to invalid IL or missing references)
			//IL_003b: Expected O, but got Unknown
			Notifier.Send(new Notification
			{
				Title = NotificationText.op_Implicit(title),
				Message = NotificationText.op_Implicit(message),
				ShowTitleOnPopup = true,
				PopupLength = 4f,
				Type = type
			});
		}

		private static string MaskSecret(string value)
		{
			if (string.IsNullOrEmpty(value))
			{
				return string.Empty;
			}
			if (value.Length > 8)
			{
				string text = value.Substring(0, 4);
				int length = value.Length;
				int num = length - 4;
				return text + "..." + value.Substring(num, length - num);
			}
			return "********";
		}

		private static string UnmaskIfUnchanged(string value, string currentSecret)
		{
			string text = value?.Trim() ?? string.Empty;
			if (!(text == MaskSecret(currentSecret)))
			{
				return text;
			}
			return currentSecret;
		}

		private static void OnFusionVoiceReceived(PlayerID player, byte[] data)
		{
			if (NetworkInfo.HasServer && player != null && !player.IsMe && data != null && data.Length != 0)
			{
				ProcessFusionVoice(player.SmallID, GetPlayerName(player), data);
			}
		}

		private static void ProcessFusionVoice(byte smallId, string playerName, byte[] data)
		{
			if (!NetworkInfo.HasServer || data == null || data.Length == 0 || string.IsNullOrWhiteSpace(_apiKey?.Value) || string.IsNullOrWhiteSpace(_chatBaseUrl?.Value) || string.IsNullOrWhiteSpace(_chatModelName?.Value) || string.IsNullOrWhiteSpace(_asrBaseUrl?.Value) || (!_useWhisperCppServer.Value && string.IsNullOrWhiteSpace(_asrModelName?.Value)))
			{
				return;
			}
			short[] array;
			try
			{
				array = VoiceConverter.Decode(data);
			}
			catch (Exception ex)
			{
				Core instance = _instance;
				if (instance != null)
				{
					((MelonBase)instance).LoggerInstance.Warning("Failed to decode Fusion voice data: " + ex.Message);
				}
				return;
			}
			if (array == null || array.Length == 0)
			{
				return;
			}
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			short[] array2 = null;
			VoiceBuffer value;
			lock (VoiceLock)
			{
				_lastAnyVoiceTime = realtimeSinceStartup;
				if (!VoiceBuffers.TryGetValue(smallId, out value))
				{
					value = new VoiceBuffer
					{
						SmallId = smallId,
						PlayerName = playerName,
						Order = _nextSubtitleOrder++
					};
					VoiceBuffers[smallId] = value;
					SubtitleOrder.Add(smallId);
				}
				int packetHash = GetPacketHash(data);
				if (value.LastPacketHash == packetHash && realtimeSinceStartup - value.LastPacketTime <= 0.08f)
				{
					return;
				}
				value.LastPacketHash = packetHash;
				value.LastPacketTime = realtimeSinceStartup;
				value.PlayerName = playerName;
				value.LastVoiceTime = realtimeSinceStartup;
				value.Samples.AddRange(array);
				if (realtimeSinceStartup - _lastVoiceDebugTime >= 0.5f)
				{
					_lastVoiceDebugTime = realtimeSinceStartup;
					Core instance2 = _instance;
					if (instance2 != null)
					{
						((MelonBase)instance2).LoggerInstance.Msg($"Voice packet: {playerName} ({smallId}), samples={array.Length}, buffered={value.Samples.Count}");
					}
				}
				int num = Mathf.RoundToInt((float)AudioInfo.OutputSampleRate * 1.15f);
				if (!value.IsSubmitting && value.Samples.Count >= num)
				{
					array2 = value.Samples.ToArray();
					value.Samples.Clear();
					value.IsSubmitting = true;
					Core instance3 = _instance;
					if (instance3 != null)
					{
						((MelonBase)instance3).LoggerInstance.Msg($"Submitting voice chunk for {playerName} ({smallId}), samples={array2.Length}");
					}
				}
			}
			if (array2 != null)
			{
				SubmitVoiceChunk(value.SmallId, value.PlayerName, array2);
			}
		}

		private static void SubmitVoiceChunk(byte smallId, string playerName, short[] samples)
		{
			string apiKey = _apiKey.Value.Trim();
			string baseUrl = _chatBaseUrl.Value.Trim();
			string chatModelName = _chatModelName.Value.Trim();
			string baseUrl2 = _asrBaseUrl.Value.Trim();
			string asrModelName = _asrModelName.Value.Trim();
			bool useWhisperCppServer = _useWhisperCppServer.Value;
			string targetLanguage = (string.IsNullOrWhiteSpace(_targetLanguage.Value) ? "English" : _targetLanguage.Value.Trim());
			string path = (useWhisperCppServer ? "inference" : "audio/transcriptions");
			if (!TryBuildEndpoint(baseUrl2, path, out var asrEndpoint, out var error))
			{
				FinishVoiceSubmit(smallId);
				Core instance = _instance;
				if (instance != null)
				{
					((MelonBase)instance).LoggerInstance.Warning("Voice localization skipped: Invalid ASR URL. " + error);
				}
				return;
			}
			if (!TryBuildEndpoint(baseUrl, "chat/completions", out var chatEndpoint, out var error2))
			{
				FinishVoiceSubmit(smallId);
				Core instance2 = _instance;
				if (instance2 != null)
				{
					((MelonBase)instance2).LoggerInstance.Warning("Voice localization skipped: Invalid chat URL. " + error2);
				}
				return;
			}
			if (string.IsNullOrWhiteSpace(chatModelName) || (!useWhisperCppServer && string.IsNullOrWhiteSpace(asrModelName)))
			{
				FinishVoiceSubmit(smallId);
				Core instance3 = _instance;
				if (instance3 != null)
				{
					((MelonBase)instance3).LoggerInstance.Warning("Voice localization skipped: Missing ASR or chat model name.");
				}
				return;
			}
			Task.Run(async delegate
			{
				string translatedText = await TranscribeAndTranslate(apiKey, asrEndpoint, asrModelName, chatEndpoint, chatModelName, useWhisperCppServer, targetLanguage, samples);
				MainThreadActions.Enqueue(delegate
				{
					if (!string.IsNullOrWhiteSpace(translatedText))
					{
						SetSubtitleText(smallId, playerName, translatedText.Trim());
					}
					FinishVoiceSubmit(smallId);
					TrySubmitBufferedVoice(smallId);
				});
			});
		}

		private static async Task<string> TranscribeAndTranslate(string apiKey, Uri asrEndpoint, string asrModelName, Uri chatEndpoint, string chatModelName, bool useWhisperCppServer, string targetLanguage, short[] samples)
		{
			_ = 2;
			try
			{
				byte[] content = CreateWav(samples, AudioInfo.OutputSampleRate);
				using MultipartFormDataContent form = new MultipartFormDataContent();
				using ByteArrayContent file = new ByteArrayContent(content);
				file.Headers.ContentType = new MediaTypeHeaderValue("audio/wav");
				form.Add(file, "file", "fusion_voice.wav");
				form.Add(new StringContent("json", Encoding.UTF8), "response_format");
				if (!useWhisperCppServer)
				{
					form.Add(new StringContent(asrModelName, Encoding.UTF8), "model");
				}
				using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, asrEndpoint);
				request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
				request.Content = form;
				using HttpResponseMessage response = await HttpClient.SendAsync(request);
				string text = await response.Content.ReadAsStringAsync();
				if (!response.IsSuccessStatusCode)
				{
					Core instance = _instance;
					if (instance != null)
					{
						((MelonBase)instance).LoggerInstance.Warning("Voice localization AI request failed: " + BuildFailureMessage(response.StatusCode, text));
					}
					return string.Empty;
				}
				string text2 = ExtractTranscriptionText(text);
				if (string.IsNullOrWhiteSpace(text2))
				{
					return string.Empty;
				}
				return await TranslateText(apiKey, chatEndpoint, chatModelName, targetLanguage, text2);
			}
			catch (Exception ex)
			{
				Core instance2 = _instance;
				if (instance2 != null)
				{
					((MelonBase)instance2).LoggerInstance.Warning("Voice localization AI request failed: " + ex.Message);
				}
				return string.Empty;
			}
		}

		private static async Task<string> TranslateText(string apiKey, Uri chatEndpoint, string chatModelName, string targetLanguage, string transcript)
		{
			_ = 1;
			try
			{
				using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, chatEndpoint);
				request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
				request.Content = new StringContent(CreateTranslationBody(chatModelName, targetLanguage, transcript), Encoding.UTF8, "application/json");
				using HttpResponseMessage response = await HttpClient.SendAsync(request);
				string text = await response.Content.ReadAsStringAsync();
				if (!response.IsSuccessStatusCode)
				{
					Core instance = _instance;
					if (instance != null)
					{
						((MelonBase)instance).LoggerInstance.Warning("Translation request failed: " + BuildFailureMessage(response.StatusCode, text));
					}
					return string.Empty;
				}
				return ExtractChatText(text);
			}
			catch (Exception ex)
			{
				Core instance2 = _instance;
				if (instance2 != null)
				{
					((MelonBase)instance2).LoggerInstance.Warning("Translation request failed: " + ex.Message);
				}
				return string.Empty;
			}
		}

		private static void FinishVoiceSubmit(byte smallId)
		{
			lock (VoiceLock)
			{
				if (VoiceBuffers.TryGetValue(smallId, out var value))
				{
					value.IsSubmitting = false;
				}
			}
		}

		private static void TrySubmitBufferedVoice(byte smallId)
		{
			short[] array = null;
			VoiceBuffer value;
			lock (VoiceLock)
			{
				if (!VoiceBuffers.TryGetValue(smallId, out value) || value.IsSubmitting)
				{
					return;
				}
				int num = Mathf.RoundToInt((float)AudioInfo.OutputSampleRate * 1.15f);
				if (value.Samples.Count >= num)
				{
					array = value.Samples.ToArray();
					value.Samples.Clear();
					value.IsSubmitting = true;
					Core instance = _instance;
					if (instance != null)
					{
						((MelonBase)instance).LoggerInstance.Msg($"Submitting buffered voice chunk for {value.PlayerName} ({smallId}), samples={array.Length}");
					}
				}
			}
			if (array != null)
			{
				SubmitVoiceChunk(value.SmallId, value.PlayerName, array);
			}
		}

		private static void FlushSilentVoiceBuffers()
		{
			if (!NetworkInfo.HasServer)
			{
				return;
			}
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			List<(byte, string, short[])> list = null;
			lock (VoiceLock)
			{
				foreach (KeyValuePair<byte, VoiceBuffer> voiceBuffer in VoiceBuffers)
				{
					VoiceBuffer value = voiceBuffer.Value;
					if (!value.IsSubmitting && value.Samples.Count > 0 && realtimeSinceStartup - value.LastVoiceTime >= 0.4f)
					{
						if (list == null)
						{
							list = new List<(byte, string, short[])>();
						}
						list.Add((voiceBuffer.Key, value.PlayerName, value.Samples.ToArray()));
						value.Samples.Clear();
						value.IsSubmitting = true;
						Core instance = _instance;
						if (instance != null)
						{
							Instance loggerInstance = ((MelonBase)instance).LoggerInstance;
							DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(45, 3);
							defaultInterpolatedStringHandler.AppendLiteral("Flushing silent voice chunk for ");
							defaultInterpolatedStringHandler.AppendFormatted(value.PlayerName);
							defaultInterpolatedStringHandler.AppendLiteral(" (");
							defaultInterpolatedStringHandler.AppendFormatted(voiceBuffer.Key);
							defaultInterpolatedStringHandler.AppendLiteral("), samples=");
							List<(byte, string, short[])> list2 = list;
							defaultInterpolatedStringHandler.AppendFormatted(list2[list2.Count - 1].Item3.Length);
							loggerInstance.Msg(defaultInterpolatedStringHandler.ToStringAndClear());
						}
					}
				}
			}
			if (list == null)
			{
				return;
			}
			foreach (var item in list)
			{
				SubmitVoiceChunk(item.Item1, item.Item2, item.Item3);
			}
		}

		private static void SetSubtitleText(byte smallId, string playerName, string translatedText)
		{
			lock (VoiceLock)
			{
				if (!VoiceBuffers.TryGetValue(smallId, out var value))
				{
					value = new VoiceBuffer
					{
						SmallId = smallId,
						PlayerName = playerName,
						Order = _nextSubtitleOrder++,
						LastVoiceTime = Time.realtimeSinceStartup
					};
					VoiceBuffers[smallId] = value;
					SubtitleOrder.Add(smallId);
				}
				value.PlayerName = playerName;
				value.SubtitleText = playerName + " : " + translatedText;
				value.LastSubtitleTime = Time.realtimeSinceStartup;
			}
		}

		private static void ShowSubtitlePreview()
		{
			ShowSubtitlePreview(2f);
		}

		private static void ShowSubtitlePreview(float displaySeconds)
		{
			_lastAnyVoiceTime = Time.realtimeSinceStartup;
			SetSubtitleText(byte.MaxValue, "PlayerName", "Content");
			lock (VoiceLock)
			{
				if (VoiceBuffers.TryGetValue(byte.MaxValue, out var value))
				{
					value.LastSubtitleTime = Time.realtimeSinceStartup + Mathf.Max(0f, displaySeconds - 2f);
				}
			}
		}

		private static void UpdateSubtitleDisplay()
		{
			//IL_005d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0063: Unknown result type (might be due to invalid IL or missing references)
			//IL_007c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0081: Unknown result type (might be due to invalid IL or missing references)
			//IL_0087: Unknown result type (might be due to invalid IL or missing references)
			//IL_0096: Unknown result type (might be due to invalid IL or missing references)
			//IL_009b: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b5: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ca: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00de: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e3: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e8: Unknown result type (might be due to invalid IL or missing references)
			//IL_020b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0219: Unknown result type (might be due to invalid IL or missing references)
			//IL_0237: Unknown result type (might be due to invalid IL or missing references)
			EnsureSubtitleRoot();
			Transform viewerTransform = GetViewerTransform();
			if ((Object)(object)viewerTransform == (Object)null || (Object)(object)_subtitleRoot == (Object)null)
			{
				return;
			}
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			if (!(realtimeSinceStartup - _lastAnyVoiceTime <= 2f))
			{
				_subtitleRoot.SetActive(false);
				return;
			}
			_subtitleRoot.SetActive(true);
			_subtitleRoot.transform.position = viewerTransform.position + viewerTransform.forward * Mathf.Max(0.1f, _subtitleDistance.Value) + viewerTransform.right * _subtitleHorizontalOffset.Value + viewerTransform.up * _subtitleVerticalOffset.Value;
			_subtitleRoot.transform.rotation = Quaternion.LookRotation(viewerTransform.position - _subtitleRoot.transform.position, Vector3.up);
			List<VoiceBuffer> list = new List<VoiceBuffer>();
			lock (VoiceLock)
			{
				for (int num = SubtitleOrder.Count - 1; num >= 0; num--)
				{
					byte key = SubtitleOrder[num];
					if (!VoiceBuffers.TryGetValue(key, out var value))
					{
						SubtitleOrder.RemoveAt(num);
					}
					else if (realtimeSinceStartup - Mathf.Max(value.LastVoiceTime, value.LastSubtitleTime) <= 2f && !string.IsNullOrWhiteSpace(value.SubtitleText))
					{
						list.Add(value);
					}
				}
				list.Sort((VoiceBuffer a, VoiceBuffer b) => a.Order.CompareTo(b.Order));
			}
			for (int num2 = 0; num2 < list.Count; num2++)
			{
				VoiceBuffer voiceBuffer = list[num2];
				TextMeshPro obj = EnsureSubtitleText(voiceBuffer);
				((Component)obj).gameObject.SetActive(true);
				((TMP_Text)obj).text = voiceBuffer.SubtitleText;
				((TMP_Text)obj).fontSize = Mathf.Max(0.03f, _subtitleSize.Value);
				obj.transform.localPosition = Vector3.up * ((float)num2 * 0.025f);
				obj.transform.localRotation = Quaternion.Euler(0f, 180f, 0f);
			}
			HideUnusedSubtitleObjects(list);
		}

		private static void EnsureSubtitleRoot()
		{
			//IL_0013: Unknown result type (might be due to invalid IL or missing references)
			//IL_001d: Expected O, but got Unknown
			if (!((Object)(object)_subtitleRoot != (Object)null))
			{
				_subtitleRoot = new GameObject("Fusion Voice Localizer Subtitles");
				Object.DontDestroyOnLoad((Object)(object)_subtitleRoot);
				_subtitleRoot.SetActive(false);
			}
		}

		private static TextMeshPro EnsureSubtitleText(VoiceBuffer buffer)
		{
			//IL_003f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0044: Unknown result type (might be due to invalid IL or missing references)
			//IL_0086: Unknown result type (might be due to invalid IL or missing references)
			//IL_0098: Unknown result type (might be due to invalid IL or missing references)
			//IL_009d: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c2: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)buffer.Text != (Object)null)
			{
				return buffer.Text;
			}
			GameObject val = new GameObject($"Subtitle {buffer.SmallId}");
			val.transform.SetParent(_subtitleRoot.transform, false);
			TextMeshPro val2 = val.AddComponent<TextMeshPro>();
			((TMP_Text)val2).alignment = (TextAlignmentOptions)514;
			((TMP_Text)val2).fontSize = Mathf.Max(0.03f, _subtitleSize.Value);
			((Graphic)val2).color = Color.white;
			((TMP_Text)val2).enableWordWrapping = false;
			((TMP_Text)val2).outlineColor = Color32.op_Implicit(Color.black);
			((TMP_Text)val2).outlineWidth = 0.22f;
			((TMP_Text)val2).rectTransform.sizeDelta = new Vector2(2.4f, 0.35f);
			buffer.Text = val2;
			return val2;
		}

		private static void HideUnusedSubtitleObjects(List<VoiceBuffer> visible)
		{
			HashSet<byte> hashSet = new HashSet<byte>(visible.Select((VoiceBuffer v) => v.SmallId));
			lock (VoiceLock)
			{
				foreach (VoiceBuffer value in VoiceBuffers.Values)
				{
					if ((Object)(object)value.Text != (Object)null && !hashSet.Contains(value.SmallId))
					{
						((Component)value.Text).gameObject.SetActive(false);
					}
				}
			}
		}

		private static Transform GetViewerTransform()
		{
			if ((Object)(object)Player.Head != (Object)null)
			{
				return Player.Head;
			}
			if (!((Object)(object)Camera.main != (Object)null))
			{
				return null;
			}
			return ((Component)Camera.main).transform;
		}

		private static string GetPlayerName(PlayerID player)
		{
			PlayerMetadata metadata = player.Metadata;
			object obj;
			if (metadata == null)
			{
				obj = null;
			}
			else
			{
				MetadataVariable nickname = metadata.Nickname;
				obj = ((nickname != null) ? nickname.GetValueOrEmpty() : null);
			}
			string text = (string)obj;
			if (string.IsNullOrWhiteSpace(text))
			{
				PlayerMetadata metadata2 = player.Metadata;
				object obj2;
				if (metadata2 == null)
				{
					obj2 = null;
				}
				else
				{
					MetadataVariable username = metadata2.Username;
					obj2 = ((username != null) ? username.GetValueOrEmpty() : null);
				}
				text = (string)obj2;
			}
			if (!string.IsNullOrWhiteSpace(text))
			{
				return text.Trim();
			}
			return $"Player {player.SmallID}";
		}

		private static string ExtractTranscriptionText(string body)
		{
			if (string.IsNullOrWhiteSpace(body))
			{
				return string.Empty;
			}
			try
			{
				using JsonDocument jsonDocument = JsonDocument.Parse(body);
				if (jsonDocument.RootElement.TryGetProperty("text", out var value))
				{
					return value.GetString();
				}
			}
			catch
			{
			}
			return body;
		}

		private static string ExtractChatText(string body)
		{
			if (string.IsNullOrWhiteSpace(body))
			{
				return string.Empty;
			}
			try
			{
				using JsonDocument jsonDocument = JsonDocument.Parse(body);
				if (jsonDocument.RootElement.TryGetProperty("choices", out var value) && value.ValueKind == JsonValueKind.Array && value.GetArrayLength() > 0 && value[0].TryGetProperty("message", out var value2) && value2.TryGetProperty("content", out var value3))
				{
					return value3.GetString();
				}
			}
			catch
			{
			}
			return body;
		}

		private static string CreateTranslationBody(string chatModelName, string targetLanguage, string transcript)
		{
			return JsonSerializer.Serialize(new
			{
				model = chatModelName,
				messages = new[]
				{
					new
					{
						role = "system",
						content = "Translate the user's speech to " + targetLanguage + ". Return only the translated text."
					},
					new
					{
						role = "user",
						content = transcript
					}
				},
				temperature = 0
			});
		}

		private static byte[] CreateWav(short[] samples, int sampleRate)
		{
			int value = sampleRate * 16 / 8;
			short value2 = 2;
			int num = samples.Length * 2;
			using MemoryStream memoryStream = new MemoryStream();
			using BinaryWriter binaryWriter = new BinaryWriter(memoryStream, Encoding.UTF8, leaveOpen: true);
			binaryWriter.Write(Encoding.ASCII.GetBytes("RIFF"));
			binaryWriter.Write(36 + num);
			binaryWriter.Write(Encoding.ASCII.GetBytes("WAVE"));
			binaryWriter.Write(Encoding.ASCII.GetBytes("fmt "));
			binaryWriter.Write(16);
			binaryWriter.Write((short)1);
			binaryWriter.Write((short)1);
			binaryWriter.Write(sampleRate);
			binaryWriter.Write(value);
			binaryWriter.Write(value2);
			binaryWriter.Write((short)16);
			binaryWriter.Write(Encoding.ASCII.GetBytes("data"));
			binaryWriter.Write(num);
			for (int i = 0; i < samples.Length; i++)
			{
				binaryWriter.Write(samples[i]);
			}
			binaryWriter.Flush();
			return memoryStream.ToArray();
		}

		private static int GetPacketHash(byte[] data)
		{
			int num = 17;
			for (int i = 0; i < data.Length; i++)
			{
				num = num * 31 + data[i];
			}
			return num;
		}
	}
}