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;
}
}
}