diff --git a/VRCFTPicoModule/Assets/Locales/en-US.json b/VRCFTPicoModule/Assets/Locales/en-US.json new file mode 100644 index 0000000..ffa439d --- /dev/null +++ b/VRCFTPicoModule/Assets/Locales/en-US.json @@ -0,0 +1,14 @@ +{ + "start-init": "Starting Initialization...", + "initializing-udp-clients": "Initializing UDP Clients on ports: {0}", + "using-port": "Using port: {0}", + "eye-tracking-disabled": "Eye tracking is disabled", + "expression-tracking-disabled": "Expression tracking is disabled", + "legacy-protocol": "Legacy Protocol", + "full-face-tracking": "Full FaceTracking", + "eye-tracking": "Eye Tracking", + "expression-tracking": "Expression Tracking", + "init-failed": "Initialization failed, exception: {0}", + "update-timeout": "Receive data timed out", + "update-failed": "Update failed with exception: {0}" +} \ No newline at end of file diff --git a/VRCFTPicoModule/Assets/Locales/zh-CN.json b/VRCFTPicoModule/Assets/Locales/zh-CN.json new file mode 100644 index 0000000..fd85040 --- /dev/null +++ b/VRCFTPicoModule/Assets/Locales/zh-CN.json @@ -0,0 +1,14 @@ +{ + "start-init": "正在开始初始化...", + "initializing-udp-clients": "正在端口 {0} 上初始化 UDP 客户端", + "using-port": "使用端口:{0}", + "eye-tracking-disabled": "眼部追踪已停用", + "expression-tracking-disabled": "表情追踪已停用", + "legacy-protocol": "旧版协议", + "full-face-tracking": "完整面部追踪", + "eye-tracking": "眼部追踪", + "expression-tracking": "表情追踪", + "init-failed": "初始化失败,原因:{0}", + "update-timeout": "接收数据超时", + "update-failed": "更新数据失败,原因:{0}" +} \ No newline at end of file diff --git a/VRCFTPicoModule/Utils/DataPacketHelpers.cs b/VRCFTPicoModule/Utils/DataPacketHelpers.cs index f62147d..41a828e 100644 --- a/VRCFTPicoModule/Utils/DataPacketHelpers.cs +++ b/VRCFTPicoModule/Utils/DataPacketHelpers.cs @@ -6,8 +6,8 @@ public static class DataPacketHelpers { public static T ByteArrayToStructure(byte[] bytes, int offset = 0) where T : struct { - int size = Marshal.SizeOf(typeof(T)); - IntPtr ptr = Marshal.AllocHGlobal(size); + var size = Marshal.SizeOf(typeof(T)); + var ptr = Marshal.AllocHGlobal(size); try { diff --git a/VRCFTPicoModule/Utils/Localization.cs b/VRCFTPicoModule/Utils/Localization.cs new file mode 100644 index 0000000..26ddf43 --- /dev/null +++ b/VRCFTPicoModule/Utils/Localization.cs @@ -0,0 +1,54 @@ +using VRCFaceTracking.Core.Helpers; + +namespace VRCFTPicoModule.Utils; + +public class Localization +{ + private Dictionary? _translations; + + private Localization() { } + + private static Localization LocInstance { get; } = new(); + + public static void Initialize(string languageCode) + { + LocInstance.LoadLanguageAsync(languageCode).GetAwaiter().GetResult(); + } + + private async Task LoadLanguageAsync(string languageCode) + { + var jsonContent = await LoadResourceAsync($"VRCFTPicoModule.Assets.Locales.{languageCode}.json") + ?? await LoadResourceAsync("VRCFTPicoModule.Assets.Locales.en-US.json"); + + if (jsonContent != null) + { + _translations = await Json.ToObjectAsync>(jsonContent); + } + else + { + _translations = new Dictionary(); + } + } + + private async Task LoadResourceAsync(string resourceName) + { + await using var stream = GetType().Assembly.GetManifestResourceStream(resourceName); + if (stream == null) return null; + using var reader = new StreamReader(stream); + return await reader.ReadToEndAsync(); + } + + private string GetTranslation(string key) + { + if (_translations != null && _translations.TryGetValue(key, out var translation)) + { + return translation; + } + return key; + } + + public static string T(string key) + { + return LocInstance.GetTranslation(key); + } +} \ No newline at end of file diff --git a/VRCFTPicoModule/Utils/Updater.cs b/VRCFTPicoModule/Utils/Updater.cs index eb50d4f..65a4f2f 100644 --- a/VRCFTPicoModule/Utils/Updater.cs +++ b/VRCFTPicoModule/Utils/Updater.cs @@ -6,6 +6,7 @@ using VRCFaceTracking.Core.Library; using VRCFaceTracking.Core.Params.Expressions; using VRCFTPicoModule.Data; +using static VRCFTPicoModule.Utils.Localization; namespace VRCFTPicoModule.Utils { @@ -59,13 +60,13 @@ public void Update(ModuleState state) { if (++_timeOut > 600) { - _logger.LogWarning("Receive data timed out."); + _logger.LogWarning(T("update-timeout")); _timeOut = 0; } } catch (Exception ex) { - _logger.LogWarning("Update failed with exception: {0}", ex); + _logger.LogWarning(T("update-failed"), ex); } } @@ -74,13 +75,10 @@ private static float[] ParseData(byte[] data, bool isLegacy) if (isLegacy && data.Length >= Marshal.SizeOf()) return DataPacketHelpers.ByteArrayToStructure(data).blendShapeWeight; - if (data.Length >= Marshal.SizeOf() + Marshal.SizeOf()) - { - var header = DataPacketHelpers.ByteArrayToStructure(data); - if (header.trackingType == 2) - return DataPacketHelpers.ByteArrayToStructure(data, Marshal.SizeOf()).blendShapeWeight; - } - return []; + if (data.Length < + Marshal.SizeOf() + Marshal.SizeOf()) return []; + var header = DataPacketHelpers.ByteArrayToStructure(data); + return header.trackingType == 2 ? DataPacketHelpers.ByteArrayToStructure(data, Marshal.SizeOf()).blendShapeWeight : []; } private static void UpdateEye(float[] pShape) @@ -209,7 +207,7 @@ private void UpdateExpression(float[] pShape) #endregion #region Tongue - SetParam(pShape[(int)BlendShape.Index.TongueOut] > 0f ? 1f : 0f, UnifiedExpressions.TongueOut); + SetParam(pShape, BlendShape.Index.TongueOut, UnifiedExpressions.TongueOut); #endregion } diff --git a/VRCFTPicoModule/VRCFTPicoModule.cs b/VRCFTPicoModule/VRCFTPicoModule.cs index 3a6480d..633a910 100644 --- a/VRCFTPicoModule/VRCFTPicoModule.cs +++ b/VRCFTPicoModule/VRCFTPicoModule.cs @@ -1,7 +1,9 @@ -using Microsoft.Extensions.Logging; +using System.Globalization; +using Microsoft.Extensions.Logging; using System.Net.Sockets; using VRCFaceTracking; using VRCFTPicoModule.Utils; +using static VRCFTPicoModule.Utils.Localization; namespace VRCFTPicoModule; @@ -18,40 +20,47 @@ public class VRCFTPicoModule : ExtTrackingModule public override (bool eyeSuccess, bool expressionSuccess) Initialize(bool eyeAvailable, bool expressionAvailable) { - Logger.LogInformation("Starting initialization"); + Localization.Initialize(CultureInfo.CurrentUICulture.Name); + Logger.LogInformation(T("start-init")); _trackingAvailable = (eyeAvailable, expressionAvailable); var initializationResult = InitializeAsync().GetAwaiter().GetResult(); - - if (!initializationResult.Item1 || !initializationResult.Item2) - UpdateModuleInfo(); + UpdateModuleInfo(initializationResult); return initializationResult; } private async Task<(bool eyeSuccess, bool expressionSuccess)> InitializeAsync() { - Logger.LogDebug("Initializing UDP Clients on ports: {0}", string.Join(", ", Ports)); + Logger.LogDebug(T("initializing-udp-clients"), string.Join(", ", Ports)); var portIndex = await ListenOnPorts(); if (portIndex == -1) return (false, false); _port = Ports[portIndex]; _udpClient = new UdpClient(_port); - Logger.LogInformation("Using port: {0}", _port); + Logger.LogInformation(T("using-port"), _port); if (!_trackingAvailable.Item1) - Logger.LogInformation("Eye tracking is disabled"); + Logger.LogInformation(T("eye-tracking-disabled")); if (!_trackingAvailable.Item2) - Logger.LogInformation("Expression tracking is disabled"); + Logger.LogInformation(T("expression-tracking-disabled")); _updater = new Updater(_udpClient, Logger, _port == Ports[1], _trackingAvailable); return _trackingAvailable; } - private void UpdateModuleInfo() + private void UpdateModuleInfo((bool, bool) initializationResult) { - ModuleInformation.Name = "PICO Connect"; + var moduleProtocol = _port == Ports[1] ? $" [{T("legacy-protocol")}]" : ""; + var moduleTrackingStatus = initializationResult switch + { + { Item1: true, Item2: true } => T("full-face-tracking"), + { Item1: true, Item2: false } => T("eye-tracking"), + { Item1: false, Item2: true } => T("expression-tracking"), + _ => "" + }; + ModuleInformation.Name = "PICO / " + moduleTrackingStatus + moduleProtocol; var stream = GetType().Assembly.GetManifestResourceStream("VRCFTPicoModule.Assets.pico.png"); ModuleInformation.StaticImages = stream != null ? [stream] : ModuleInformation.StaticImages; } @@ -75,7 +84,7 @@ private async Task ListenOnPorts() } catch (Exception ex) { - Logger.LogError("Initialization failed, exception: {0}", ex); + Logger.LogError(T("init-failed"), ex); } return -1; @@ -83,10 +92,7 @@ private async Task ListenOnPorts() public override void Update() { - if (_updater == null) - return; - - _updater.Update(Status); + _updater?.Update(Status); } public override void Teardown() diff --git a/VRCFTPicoModule/VRCFTPicoModule.csproj b/VRCFTPicoModule/VRCFTPicoModule.csproj index 15be2d5..03161af 100644 --- a/VRCFTPicoModule/VRCFTPicoModule.csproj +++ b/VRCFTPicoModule/VRCFTPicoModule.csproj @@ -15,6 +15,16 @@ Always + + + Always + + + + + + Always +