using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using SherpaOnnx;
using UnityEngine;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: IgnoresAccessChecksTo("Assembly-CSharp-firstpass")]
[assembly: IgnoresAccessChecksTo("Assembly-CSharp")]
[assembly: IgnoresAccessChecksTo("Unity.RenderPipelines.Universal.Runtime")]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("MHZ")]
[assembly: AssemblyConfiguration("Debug")]
[assembly: AssemblyCopyright("Copyright © 2026 Masaicker")]
[assembly: AssemblyDescription("Replace YAPYAP original Vosk engine with Fun-ASR (Sherpa-ONNX)")]
[assembly: AssemblyFileVersion("1.0.6")]
[assembly: AssemblyInformationalVersion("1.0.6+18d42f2c2f8c0f3d4faefb3a5ecfd3c76e10bc68")]
[assembly: AssemblyProduct("VoiceInputFix")]
[assembly: AssemblyTitle("VoiceInputFix")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.0.6.0")]
[module: UnverifiableCode]
namespace VoiceInputFix
{
[BepInPlugin("Mhz.voiceinputfix", "VoiceInputFix", "1.0.6")]
public class Plugin : BaseUnityPlugin
{
public static ManualLogSource LogSource;
private static string _initError;
private static string _pluginFolder;
private static string _apiFolder;
private static string _onnxFolder;
private static string _managedFolder;
private static ConfigEntry<bool> _enableDebugLog;
private static ConfigEntry<float> _speechThreshold;
private static ConfigEntry<string> _language;
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern bool SetDllDirectory(string lpPathName);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr LoadLibrary(string lpLibFileName);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type);
private void Awake()
{
//IL_00c0: Unknown result type (might be due to invalid IL or missing references)
LogSource = ((BaseUnityPlugin)this).Logger;
_pluginFolder = Path.GetDirectoryName(((BaseUnityPlugin)this).Info.Location);
AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;
bool flag = LocateDependencies();
CheckAndRepairModelEnvironment();
if (flag)
{
_enableDebugLog = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "EnableDebugLog", false, "Whether to show detailed recognition logs in the console. | 是否在控制台中显示详细的识别日志。");
_speechThreshold = ((BaseUnityPlugin)this).Config.Bind<float>("General", "SpeechThreshold", 0.015f, "Minimum amplitude threshold for speech detection (VAD). | 语音检测的最低振幅阈值(VAD)。建议范围 0.01-0.05。");
_language = ((BaseUnityPlugin)this).Config.Bind<string>("General", "Language", "auto", "Specified language for recognition. This PRIORITIZES the selected language to improve accuracy, but may still detect others if confidence is high. Options: auto, zh, en, ja, ko, yue. Invalid values will fallback to auto. | 指定识别语言。这会【优先】识别所选语言以提高准确率,但在置信度极高时仍可能识别出其他语言。可选:auto (自动), zh (中文), en (英文), ja (日文), ko (韩文), yue (粤语)。无效值将回退到自动。");
new Harmony("Mhz.voiceinputfix").PatchAll();
LogSource.LogInfo((object)"=== [VoiceInputFix] Initialized ===");
}
}
private bool LocateDependencies()
{
_apiFolder = FindFileRecursively(_pluginFolder, "sherpa-onnx-c-api.dll", 4);
_onnxFolder = FindFileRecursively(_pluginFolder, "onnxruntime.dll", 4);
_managedFolder = FindFileRecursively(_pluginFolder, "sherpa-onnx.dll", 4);
List<string> list = new List<string>();
if (string.IsNullOrEmpty(_apiFolder))
{
list.Add(" - sherpa-onnx-c-api.dll");
}
if (string.IsNullOrEmpty(_onnxFolder))
{
list.Add(" - onnxruntime.dll");
}
if (string.IsNullOrEmpty(_managedFolder))
{
list.Add(" - sherpa-onnx.dll");
}
if (list.Count > 0)
{
_initError = "Dependency Missing";
string text = "https://www.nexusmods.com/yapyap/mods/5?tab=files";
string text2 = "https://thunderstore.io/c/yapyap/p/Mhz/SherpaOnnxRuntime/versions/";
string text3 = "【语音识别运行库缺失】\n\n检测到以下核心组件缺失,插件将无法加载:\n" + string.Join("\n", list) + "\n\n点击【确定】将为您随机复制一个下载链接到剪贴板,您可以直接在浏览器地址栏粘贴(Ctrl+V)访问。\n\n--------------------------------------------------\n\n[Runtime Dependency Missing]\n\nThe following core components are missing, the plugin will not load:\n" + string.Join("\n", list) + "\n\nClick [OK] to copy a download link to your clipboard, then you can paste it into your browser.";
int num = MessageBox(IntPtr.Zero, text3, "VoiceInputFix Diagnostic", 327697u);
if (num == 1)
{
string[] array = new string[2] { text, text2 };
GUIUtility.systemCopyBuffer = array[new Random().Next(array.Length)];
}
return false;
}
SetDllDirectory(_onnxFolder);
LoadLibrary(Path.Combine(_onnxFolder, "onnxruntime.dll"));
LoadLibrary(Path.Combine(_apiFolder, "sherpa-onnx-c-api.dll"));
SetDllDirectory(null);
LogSource.LogInfo((object)("[VoiceInputFix] Dependencies loaded: API @ " + _apiFolder + ", ONNX @ " + _onnxFolder + ", Managed @ " + _managedFolder));
return true;
}
private string FindFileRecursively(string startDir, string fileName, int maxLevels)
{
string text = startDir;
for (int i = 0; i < maxLevels; i++)
{
if (string.IsNullOrEmpty(text))
{
break;
}
if (File.Exists(Path.Combine(text, fileName)))
{
return text;
}
try
{
string[] files = Directory.GetFiles(text, fileName, SearchOption.AllDirectories);
if (files.Length != 0)
{
return Path.GetDirectoryName(files[0]);
}
}
catch
{
}
text = Path.GetDirectoryName(text);
}
return null;
}
private Assembly OnAssemblyResolve(object sender, ResolveEventArgs args)
{
if (args.Name.Contains("sherpa-onnx") || args.Name.Contains("SherpaOnnx"))
{
string path = _managedFolder ?? _apiFolder ?? _pluginFolder;
string text = Path.Combine(path, "sherpa-onnx.dll");
if (File.Exists(text))
{
return Assembly.LoadFrom(text);
}
}
return null;
}
private void CheckAndRepairModelEnvironment()
{
string text = Path.Combine(_pluginFolder, "models");
if (!Directory.Exists(text))
{
Directory.CreateDirectory(text);
}
string path = Path.Combine(text, "model.onnx");
string path2 = Path.Combine(text, "tokens.txt");
string path3 = Path.Combine(text, "README_DOWNLOAD.md");
if (!File.Exists(path3))
{
string contents = "# VoiceInputFix - Model Download Guide / 模型下载指南\n\nPlease download the following files and place them in THIS directory (the 'models' folder):\n请下载以下文件并将它们放入【当前】目录(即 models 文件夹)中:\n\n1. **model.onnx** (1.03GB)\n URL: https://huggingface.co/csukuangfj/sherpa-onnx-sense-voice-funasr-nano-2025-12-17/resolve/main/model.onnx\n\n2. **tokens.txt** (940KB)\n URL: https://huggingface.co/csukuangfj/sherpa-onnx-sense-voice-funasr-nano-2025-12-17/resolve/main/tokens.txt\n\n--------------------------------------------------\n\n### Optional: Runtime Dependencies / 运行库依赖 (If mod fails to load)\n\nIf the mod fails to load due to missing native DLLs, please download from:\n如果模组因缺少运行库 DLL 无法加载,请从以下地址获取:\n\n- NexusMods: https://www.nexusmods.com/yapyap/mods/5?tab=files\n- Thunderstore: https://thunderstore.io/c/yapyap/p/Mhz/SherpaOnnxRuntime/versions/\n\nPlease start or restart the game after all files are placed correctly. / 所有文件放置正确后,请重新启动游戏。";
File.WriteAllText(path3, contents);
}
List<string> list = new List<string>();
if (!File.Exists(path))
{
list.Add(" - model.onnx");
}
if (!File.Exists(path2))
{
list.Add(" - tokens.txt");
}
if (list.Count <= 0)
{
return;
}
if (string.IsNullOrEmpty(_initError))
{
_initError = "Models Missing";
}
string text2 = string.Join("\n", list);
string text3 = "【检测到语音识别模型文件缺失】\n\n检测到以下模型权重文件缺失:\n" + text2 + "\n\n存放目录:\n" + text + "\n\n状态说明:\n由于模型文件缺失,本模组及原版语音识别功能将无法生效,但游戏仍可正常进行。\n点击确认后将为您打开目标目录,请查阅 README_DOWNLOAD.md 获取下载指引。\n\n--------------------------------------------------\n\n[Model Files Missing]\n\nMissing Files:\n" + text2 + "\n\nDirectory:\n" + text + "\n\nNotice:\nThe mod and original voice recognition will be disabled without these files, but the game will run normally. \nClick [OK] to open the folder and check README_DOWNLOAD.md.";
int num = MessageBox(IntPtr.Zero, text3, "VoiceInputFix Diagnostic", 327729u);
if (num == 1)
{
try
{
Process.Start(new ProcessStartInfo(text)
{
UseShellExecute = true
});
}
catch
{
}
}
LogError("[Diagnostic] Missing required files in " + text);
}
private void OnDestroy()
{
SherpaEngine.Cleanup();
}
public static void Log(string m)
{
if (_enableDebugLog != null && _enableDebugLog.Value)
{
ManualLogSource logSource = LogSource;
if (logSource != null)
{
logSource.LogInfo((object)m);
}
}
}
public static void LogError(string m)
{
ManualLogSource logSource = LogSource;
if (logSource != null)
{
logSource.LogError((object)m);
}
}
internal static void EnsureRecognizer()
{
SherpaEngine.Init(_pluginFolder, _initError, _language.Value);
}
public static string InternalDecode(float[] samples)
{
return SherpaEngine.Decode(samples);
}
public static async Task RecognitionLoop(VoskSpeechToText instance)
{
await SherpaEngine.RunLoop(instance, _speechThreshold.Value);
}
}
internal static class SherpaEngine
{
private static OfflineRecognizer _recognizer;
private static readonly object Lock = new object();
private static readonly Regex TagRegex = new Regex("<\\|.*?\\|>", RegexOptions.Compiled);
public static void Init(string folder, string initError, string language = "auto")
{
//IL_013b: Unknown result type (might be due to invalid IL or missing references)
//IL_013d: Unknown result type (might be due to invalid IL or missing references)
//IL_0147: Expected O, but got Unknown
if (_recognizer != null || initError != null)
{
return;
}
lock (Lock)
{
if (_recognizer != null)
{
return;
}
try
{
string path = Path.Combine(folder, "models");
OfflineRecognizerConfig val = default(OfflineRecognizerConfig);
((OfflineRecognizerConfig)(ref val))..ctor();
val.ModelConfig.SenseVoice.Model = Path.Combine(path, "model.onnx");
val.ModelConfig.SenseVoice.UseInverseTextNormalization = 1;
string text = language?.ToLower().Trim() ?? "";
switch (text)
{
default:
text = "";
break;
case "zh":
case "en":
case "ja":
case "ko":
case "yue":
break;
}
val.ModelConfig.SenseVoice.Language = text;
val.ModelConfig.Tokens = Path.Combine(path, "tokens.txt");
val.ModelConfig.NumThreads = 4;
val.DecodingMethod = "greedy_search";
_recognizer = new OfflineRecognizer(val);
Plugin.Log("Fun-ASR Engine Ready. Language: " + (string.IsNullOrEmpty(text) ? "auto" : text));
}
catch (Exception ex)
{
Plugin.LogError("Init Fail: " + ex.Message);
}
}
}
public static string Decode(float[] samples)
{
if (samples == null || samples.Length == 0 || _recognizer == null)
{
return string.Empty;
}
lock (Lock)
{
OfflineStream val = _recognizer.CreateStream();
try
{
val.AcceptWaveform(16000, samples);
_recognizer.Decode(val);
string text = val.Result.Text;
string text2 = TagRegex.Replace(text, "").Trim();
return text2.Replace("。", "").Replace(",", "").Replace("?", "")
.Replace("!", "");
}
finally
{
((IDisposable)val)?.Dispose();
}
}
}
public static void Cleanup()
{
lock (Lock)
{
OfflineRecognizer recognizer = _recognizer;
if (recognizer != null)
{
recognizer.Dispose();
}
_recognizer = null;
}
}
public static async Task RunLoop(VoskSpeechToText instance, float threshold)
{
if (_recognizer == null)
{
return;
}
List<float> audioAccumulator = new List<float>();
Stopwatch silenceTimer = new Stopwatch();
Stopwatch partialTimer = Stopwatch.StartNew();
string lastSentPartial = string.Empty;
try
{
ConcurrentQueue<short[]> bufferQueue = instance._threadedBufferQueue;
ConcurrentQueue<string> resultQueue = instance._threadedResultQueue;
while (instance._running)
{
bool isFrameLoud = false;
bool hasData = false;
short[] pcmData;
while (bufferQueue.TryDequeue(out pcmData))
{
hasData = true;
float[] frameSamples = new float[pcmData.Length];
float frameMax = 0f;
for (int i = 0; i < pcmData.Length; i++)
{
frameSamples[i] = (float)pcmData[i] / 32768f;
float abs = Math.Abs(frameSamples[i]);
if (abs > frameMax)
{
frameMax = abs;
}
}
if (frameMax > threshold)
{
isFrameLoud = true;
audioAccumulator.AddRange(frameSamples);
}
else if (audioAccumulator.Count > 0)
{
audioAccumulator.AddRange(frameSamples);
}
}
if (isFrameLoud)
{
silenceTimer.Restart();
}
if (hasData && audioAccumulator.Count > 0 && partialTimer.ElapsedMilliseconds > 120 && audioAccumulator.Count > 1600)
{
string text2 = Decode(audioAccumulator.ToArray());
if (!string.IsNullOrEmpty(text2) && text2 != lastSentPartial)
{
resultQueue.Enqueue("{\"partial\":\"" + text2.Replace("\"", "\\\"") + "\"}");
lastSentPartial = text2;
}
partialTimer.Restart();
}
if (audioAccumulator.Count > 0 && (silenceTimer.ElapsedMilliseconds > 350 || audioAccumulator.Count > 160000))
{
string text = Decode(audioAccumulator.ToArray());
if (!string.IsNullOrEmpty(text))
{
Plugin.Log("Final: " + text);
resultQueue.Enqueue("{\"alternatives\":[{\"conf\":1.0,\"text\":\"" + text.Replace("\"", "\\\"") + "\"}],\"partial\":false}");
}
resultQueue.Enqueue("{\"partial\":\"[unk]\"}");
audioAccumulator.Clear();
lastSentPartial = string.Empty;
silenceTimer.Reset();
await Task.Delay(100);
}
await Task.Delay(15);
}
}
catch (Exception ex)
{
Plugin.LogError("Loop Error: " + ex.Message);
}
}
}
[HarmonyPatch(typeof(VoskSpeechToText), "StartupRoutine")]
internal class PatchStartupRoutine
{
[CompilerGenerated]
private sealed class <EmptyEnumerator>d__1 : IEnumerator<object>, IEnumerator, IDisposable
{
private int <>1__state;
private object <>2__current;
object IEnumerator<object>.Current
{
[DebuggerHidden]
get
{
return <>2__current;
}
}
object IEnumerator.Current
{
[DebuggerHidden]
get
{
return <>2__current;
}
}
[DebuggerHidden]
public <EmptyEnumerator>d__1(int <>1__state)
{
this.<>1__state = <>1__state;
}
[DebuggerHidden]
void IDisposable.Dispose()
{
<>1__state = -2;
}
private bool MoveNext()
{
if (<>1__state != 0)
{
return false;
}
<>1__state = -1;
return false;
}
bool IEnumerator.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
return this.MoveNext();
}
[DebuggerHidden]
void IEnumerator.Reset()
{
throw new NotSupportedException();
}
}
private static bool Prefix(VoskSpeechToText __instance, ref IEnumerator __result, ref bool ____didInit)
{
____didInit = true;
if ((Object)(object)__instance.VoiceProcessor != (Object)null)
{
__instance.VoiceProcessor.OnFrameCaptured -= __instance.VoiceProcessorOnOnFrameCaptured;
__instance.VoiceProcessor.OnFrameCaptured += __instance.VoiceProcessorOnOnFrameCaptured;
}
__result = EmptyEnumerator();
return false;
}
[IteratorStateMachine(typeof(<EmptyEnumerator>d__1))]
private static IEnumerator EmptyEnumerator()
{
//yield-return decompiler failed: Unexpected instruction in Iterator.Dispose()
return new <EmptyEnumerator>d__1(0);
}
}
[HarmonyPatch(typeof(VoskSpeechToText), "StartRecording")]
internal class PatchStartRecording
{
private static bool Prefix(VoskSpeechToText __instance)
{
if (__instance._running)
{
return false;
}
Plugin.EnsureRecognizer();
__instance._running = true;
__instance._sampleRate = __instance.VoiceProcessor.StartRecording(__instance._sampleRate, 512, (bool?)null);
__instance._recognizerReady = true;
Traverse.Create((object)__instance).Field("_threadedWorkTask").SetValue((object)Task.Run(() => Plugin.RecognitionLoop(__instance)));
return false;
}
}
[HarmonyPatch(typeof(VoskSpeechToText), "StartVosk")]
internal class PatchStartVosk
{
private static bool Prefix(ref bool ____didInit)
{
if (____didInit)
{
return false;
}
return true;
}
}
}
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
internal sealed class IgnoresAccessChecksToAttribute : Attribute
{
public IgnoresAccessChecksToAttribute(string assemblyName)
{
}
}
}