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(); 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 Samples = new List(); 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 MainThreadActions = new ConcurrentQueue(); private static readonly HttpClient HttpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(20.0) }; private static MelonPreferences_Category _category; private static MelonPreferences_Entry _apiKey; private static MelonPreferences_Entry _chatBaseUrl; private static MelonPreferences_Entry _chatModelName; private static MelonPreferences_Entry _asrBaseUrl; private static MelonPreferences_Entry _asrModelName; private static MelonPreferences_Entry _targetLanguage; private static MelonPreferences_Entry _useWhisperCppServer; private static MelonPreferences_Entry _subtitleSize; private static MelonPreferences_Entry _subtitleDistance; private static MelonPreferences_Entry _subtitleHorizontalOffset; private static MelonPreferences_Entry _subtitleVerticalOffset; private static bool _isTestingAi; private static Core _instance; private static GameObject _subtitleRoot; private static readonly object VoiceLock = new object(); private static readonly Dictionary VoiceBuffers = new Dictionary(); private static readonly List SubtitleOrder = new List(); 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("ApiKey", string.Empty, (string)null, (string)null, false, false, (ValueValidator)null, (string)null); _chatBaseUrl = _category.CreateEntry("BaseUrl", "https://api.z.ai/api/paas/v4", (string)null, (string)null, false, false, (ValueValidator)null, (string)null); _chatModelName = _category.CreateEntry("ModelName", "glm-5.2", (string)null, (string)null, false, false, (ValueValidator)null, (string)null); _asrBaseUrl = _category.CreateEntry("AsrBaseUrl", "https://api.z.ai/api/paas/v4", (string)null, (string)null, false, false, (ValueValidator)null, (string)null); _asrModelName = _category.CreateEntry("AsrModelName", "glm-asr-2512", (string)null, (string)null, false, false, (ValueValidator)null, (string)null); _targetLanguage = _category.CreateEntry("TargetLanguage", "English", (string)null, (string)null, false, false, (ValueValidator)null, (string)null); _useWhisperCppServer = _category.CreateEntry("UseWhisperCppServer", false, (string)null, (string)null, false, false, (ValueValidator)null, (string)null); _subtitleSize = _category.CreateEntry("SubtitleSize", 0.13f, (string)null, (string)null, false, false, (ValueValidator)null, (string)null); _subtitleDistance = _category.CreateEntry("SubtitleDistance", 0.35f, (string)null, (string)null, false, false, (ValueValidator)null, (string)null); _subtitleHorizontalOffset = _category.CreateEntry("SubtitleHorizontalOffset", 0f, (string)null, (string)null, false, false, (ValueValidator)null, (string)null); _subtitleVerticalOffset = _category.CreateEntry("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)delegate(string value) { _apiKey.Value = UnmaskIfUnchanged(value, _apiKey.Value); SavePreferences(); }); val.CreateString("Chat Base URL", Color.white, _chatBaseUrl.Value, (Action)delegate(string value) { _chatBaseUrl.Value = value?.Trim() ?? string.Empty; SavePreferences(); }); val.CreateString("Chat Model Name", Color.white, _chatModelName.Value, (Action)delegate(string value) { _chatModelName.Value = value?.Trim() ?? string.Empty; SavePreferences(); }); val2.CreateString("ASR Base URL", Color.white, _asrBaseUrl.Value, (Action)delegate(string value) { _asrBaseUrl.Value = value?.Trim() ?? string.Empty; SavePreferences(); }); val2.CreateString("ASR Model Name", Color.white, _asrModelName.Value, (Action)delegate(string value) { _asrModelName.Value = value?.Trim() ?? string.Empty; SavePreferences(); }); val2.CreateBool("Local ASR", Color.white, _useWhisperCppServer.Value, (Action)delegate(bool value) { _useWhisperCppServer.Value = value; SavePreferences(); }); val.CreateString("Target Language", Color.white, _targetLanguage.Value, (Action)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)delegate(float value) { _subtitleSize.Value = value; SavePreferences(); }); obj2.CreateFloat("Distance", Color.white, _subtitleDistance.Value, 0.05f, 0.1f, 2f, (Action)delegate(float value) { _subtitleDistance.Value = value; SavePreferences(); }); obj2.CreateFloat("Horizontal Offset", Color.white, _subtitleHorizontalOffset.Value, 0.025f, -1f, 1f, (Action)delegate(float value) { _subtitleHorizontalOffset.Value = value; SavePreferences(); }); obj2.CreateFloat("Vertical Offset", Color.white, _subtitleVerticalOffset.Value, 0.025f, -1f, 1f, (Action)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 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 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 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 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 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 list = new List(); 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(); ((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 visible) { HashSet hashSet = new HashSet(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; } } }