From 97a4b0ccff51af390bfc57dac13b9acb044e55d0 Mon Sep 17 00:00:00 2001 From: moewhite19 Date: Sun, 6 Sep 2026 03:00:38 +0800 Subject: [PATCH 1/9] Add per-module enable/disable toggles for eye and facial tracking - Each installed module can be enabled/disabled as a whole in the Module Registry - Independent Eye Tracking / Facial Tracking sub-toggles per module - Sub-toggles are gated by the module's main enable toggle - Disabled modules are skipped on load; changes take effect after restart - Show a 'Disabled' badge for disabled modules in the module list - Refresh toggle state immediately after a module install --- .../Contracts/Services/IModuleDataService.cs | 3 + .../Library/UnifiedLibManager.cs | 33 +++++++- .../Models/InstallableTrackingModule.cs | 35 +++++++++ .../Models/ModuleEnabledSettings.cs | 22 ++++++ VRCFaceTracking/Services/ModuleDataService.cs | 28 ++++++- VRCFaceTracking/Strings/en-US/Resources.resw | 21 +++++ VRCFaceTracking/Strings/zh-CN/Resources.resw | 21 +++++ .../ViewModels/ModuleRegistryViewModel.cs | 11 +++ .../Views/ModuleRegistryDetailControl.xaml | 30 +++++++- .../Views/ModuleRegistryDetailControl.xaml.cs | 76 +++++++++++++++++++ VRCFaceTracking/Views/ModuleRegistryPage.xaml | 6 ++ 11 files changed, 280 insertions(+), 6 deletions(-) create mode 100644 VRCFaceTracking.Core/Models/ModuleEnabledSettings.cs diff --git a/VRCFaceTracking.Core/Contracts/Services/IModuleDataService.cs b/VRCFaceTracking.Core/Contracts/Services/IModuleDataService.cs index e1470574..cb2fc286 100644 --- a/VRCFaceTracking.Core/Contracts/Services/IModuleDataService.cs +++ b/VRCFaceTracking.Core/Contracts/Services/IModuleDataService.cs @@ -10,4 +10,7 @@ public interface IModuleDataService IEnumerable GetInstalledModules(); Task IncrementDownloadsAsync(TrackingModuleMetadata moduleMetadata); IEnumerable GetLegacyModules(); + + Task> GetModuleSettingsAsync(); + Task SaveModuleSettingsAsync(InstallableTrackingModule module, ModuleEnabledSettings settings); } \ No newline at end of file diff --git a/VRCFaceTracking.Core/Library/UnifiedLibManager.cs b/VRCFaceTracking.Core/Library/UnifiedLibManager.cs index 87417bfe..ee61bf00 100644 --- a/VRCFaceTracking.Core/Library/UnifiedLibManager.cs +++ b/VRCFaceTracking.Core/Library/UnifiedLibManager.cs @@ -4,6 +4,7 @@ using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; using VRCFaceTracking.Core.Contracts.Services; +using VRCFaceTracking.Core.Models; using VRCFaceTracking.Core.Sandboxing; using VRCFaceTracking.Core.Sandboxing.IPC; @@ -36,6 +37,7 @@ public class UnifiedLibManager : ILibManager private string _sandboxProcessPath { get; set; } private List AvailableSandboxModules = new (); + private Dictionary _moduleSettingsByPath = new (); #endregion #region Thread @@ -157,11 +159,16 @@ public void Initialize() AvailableSandboxModules[moduleIndex].SupportsEyeTracking = AvailableSandboxModules[moduleIndex].SupportsEyeTracking && replySupportedPacket.eyeAvailable; AvailableSandboxModules[moduleIndex].SupportsExpressionTracking = AvailableSandboxModules[moduleIndex].SupportsExpressionTracking && replySupportedPacket.expressionAvailable; + // Respect the user's per-module eye / facial toggles. A module may only claim a slot it has been allowed to use. + _moduleSettingsByPath.TryGetValue(AvailableSandboxModules[moduleIndex].SandboxModulePath, out var moduleSettings); + var allowEye = moduleSettings?.EnableEye ?? true; + var allowExpression = moduleSettings?.EnableExpression ?? true; + // Now tell it to initialise EventInitPacket eventInitPacket = new EventInitPacket() { - expressionAvailable = ExpressionStatus == ModuleState.Uninitialized, - eyeAvailable = EyeStatus == ModuleState.Uninitialized, + expressionAvailable = ExpressionStatus == ModuleState.Uninitialized && allowExpression, + eyeAvailable = EyeStatus == ModuleState.Uninitialized && allowEye, }; _logger.LogInformation("Got supported for module {module}. Expr: {} Eye: {}...", AvailableSandboxModules[moduleIndex].ModuleClassName, @@ -296,7 +303,27 @@ public void Initialize() // Find all modules var modules = _moduleDataService.GetInstalledModules().Concat(_moduleDataService.GetLegacyModules()); - var modulePaths = modules.Select(m => m.AssemblyLoadPath); + + // Load the per-module toggle settings. They are applied at startup, so toggling a module only takes effect after a restart. + var allSettings = _moduleDataService.GetModuleSettingsAsync().GetAwaiter().GetResult(); + var modulesToLoad = new List(); + var settingsByPath = new Dictionary(); + foreach ( var m in modules ) + { + var settings = allSettings.TryGetValue(m.ModuleKey, out var s) ? s : new ModuleEnabledSettings(); + settingsByPath[m.AssemblyLoadPath] = settings; + + // Skip modules that have been disabled by the user as a whole + if ( !settings.Enabled ) + { + continue; + } + + modulesToLoad.Add(m); + } + + _moduleSettingsByPath = settingsByPath; + var modulePaths = modulesToLoad.Select(m => m.AssemblyLoadPath); // Load all modules AvailableSandboxModules.Clear(); diff --git a/VRCFaceTracking.Core/Models/InstallableTrackingModule.cs b/VRCFaceTracking.Core/Models/InstallableTrackingModule.cs index e3b5b28a..27ede635 100644 --- a/VRCFaceTracking.Core/Models/InstallableTrackingModule.cs +++ b/VRCFaceTracking.Core/Models/InstallableTrackingModule.cs @@ -22,4 +22,39 @@ public string AssemblyLoadPath { get; set; } + + /// + /// Whether this module is enabled. Disabled modules are not loaded until VRCFT is restarted. + /// The value is persisted separately and does not get written to module.json. + /// + [JsonIgnore] + public bool IsEnabled + { + get; set; + } = true; + + /// + /// Whether this module is allowed to claim the eye tracking slot. + /// + [JsonIgnore] + public bool EnableEye + { + get; set; + } = true; + + /// + /// Whether this module is allowed to claim the facial / expression tracking slot. + /// + [JsonIgnore] + public bool EnableExpression + { + get; set; + } = true; + + /// + /// A stable identifier used to persist the enabled/disabled state of a module. + /// Registry modules are keyed by their module id, legacy modules by their assembly path. + /// + [JsonIgnore] + public string ModuleKey => ModuleId != Guid.Empty ? ModuleId.ToString() : AssemblyLoadPath; } \ No newline at end of file diff --git a/VRCFaceTracking.Core/Models/ModuleEnabledSettings.cs b/VRCFaceTracking.Core/Models/ModuleEnabledSettings.cs new file mode 100644 index 00000000..8ad65a48 --- /dev/null +++ b/VRCFaceTracking.Core/Models/ModuleEnabledSettings.cs @@ -0,0 +1,22 @@ +namespace VRCFaceTracking.Core.Models; + +/// +/// Persisted per-module toggles controlling which parts of a module VRCFT may load. +/// +public class ModuleEnabledSettings +{ + /// + /// Whether the module as a whole is allowed to load. + /// + public bool Enabled { get; set; } = true; + + /// + /// Whether the module is allowed to claim the eye tracking slot. + /// + public bool EnableEye { get; set; } = true; + + /// + /// Whether the module is allowed to claim the facial / expression tracking slot. + /// + public bool EnableExpression { get; set; } = true; +} \ No newline at end of file diff --git a/VRCFaceTracking/Services/ModuleDataService.cs b/VRCFaceTracking/Services/ModuleDataService.cs index 4b9d6972..992f00fb 100644 --- a/VRCFaceTracking/Services/ModuleDataService.cs +++ b/VRCFaceTracking/Services/ModuleDataService.cs @@ -16,14 +16,17 @@ public class ModuleDataService : IModuleDataService private readonly IIdentityService _identityService; private readonly ILogger _logger; + private readonly ILocalSettingsService _settingsService; private readonly HttpClient _httpClient; private const string BaseUrl = "https://registry.vrcft.io/"; + private const string ModuleSettingsKey = "ModuleEnabledSettings"; - public ModuleDataService(IIdentityService identityService, ILogger logger) + public ModuleDataService(IIdentityService identityService, ILogger logger, ILocalSettingsService settingsService) { _identityService = identityService; _logger = logger; + _settingsService = settingsService; _httpClient = HappyEyeballsHttp.CreateHttpClient(); _httpClient.BaseAddress = new Uri(BaseUrl); } @@ -198,5 +201,26 @@ public IEnumerable GetInstalledModules() } return installedModules; - } + } + + public async Task> GetModuleSettingsAsync() + { + return await _settingsService.ReadSettingAsync(ModuleSettingsKey, new Dictionary()); + } + + public async Task SaveModuleSettingsAsync(InstallableTrackingModule module, ModuleEnabledSettings settings) + { + if (string.IsNullOrEmpty(module.ModuleKey)) + { + _logger.LogWarning("Could not save settings for module {module} as it has no stable identifier.", module.ModuleName); + return; + } + + var allSettings = await GetModuleSettingsAsync(); + allSettings[module.ModuleKey] = settings; + + await _settingsService.SaveSettingAsync(ModuleSettingsKey, allSettings); + _logger.LogInformation("Module {module} settings saved: Enabled={enabled}, Eye={eye}, Expression={expression}. Restart VRCFT to apply changes.", + module.ModuleName, settings.Enabled, settings.EnableEye, settings.EnableExpression); + } } \ No newline at end of file diff --git a/VRCFaceTracking/Strings/en-US/Resources.resw b/VRCFaceTracking/Strings/en-US/Resources.resw index 1f93581c..b6ad39a2 100644 --- a/VRCFaceTracking/Strings/en-US/Resources.resw +++ b/VRCFaceTracking/Strings/en-US/Resources.resw @@ -299,6 +299,27 @@ Usage Instructions + + Enabled + + + Disabled + + + Enabled + + + Eye Tracking + + + Facial Tracking + + + Changes take effect after restarting VRCFT + + + Disabled + Version diff --git a/VRCFaceTracking/Strings/zh-CN/Resources.resw b/VRCFaceTracking/Strings/zh-CN/Resources.resw index 125928cc..ff75f536 100644 --- a/VRCFaceTracking/Strings/zh-CN/Resources.resw +++ b/VRCFaceTracking/Strings/zh-CN/Resources.resw @@ -272,6 +272,27 @@ 使用说明 + + 启用 + + + 已禁用 + + + 启用 + + + 眼部追踪 + + + 面部追踪 + + + 更改将在重启 VRCFT 后生效 + + + 已禁用 + 版本号 diff --git a/VRCFaceTracking/ViewModels/ModuleRegistryViewModel.cs b/VRCFaceTracking/ViewModels/ModuleRegistryViewModel.cs index 8efebde6..76e04329 100644 --- a/VRCFaceTracking/ViewModels/ModuleRegistryViewModel.cs +++ b/VRCFaceTracking/ViewModels/ModuleRegistryViewModel.cs @@ -30,10 +30,18 @@ public async void OnNavigatedTo(object parameter) // If any of the IDs match a remote module and the other data contained within does not match, // then we need to set the local module install state to outdated. If everything matches then we need to set the install state to installed. var installedModules = _moduleDataService.GetInstalledModules().Concat(_moduleDataService.GetLegacyModules()); + var moduleSettings = await _moduleDataService.GetModuleSettingsAsync(); var localModules = new List(); // dw about it foreach (var installedModule in installedModules) { installedModule.InstallationState = InstallState.Installed; + if (moduleSettings.TryGetValue(installedModule.ModuleKey, out var settings)) + { + installedModule.IsEnabled = settings.Enabled; + installedModule.EnableEye = settings.EnableEye; + installedModule.EnableExpression = settings.EnableExpression; + } + var remoteModule = data.FirstOrDefault(x => x.ModuleId == installedModule.ModuleId); if (remoteModule == null) // If this module is completely missing from the remote list, then we need to add it to the list. { @@ -44,6 +52,9 @@ public async void OnNavigatedTo(object parameter) { // This module is installed and in the remote list, so we need to update the remote module's install state. remoteModule.InstallationState = remoteModule.Version != installedModule.Version ? InstallState.Outdated : InstallState.Installed; + remoteModule.IsEnabled = installedModule.IsEnabled; + remoteModule.EnableEye = installedModule.EnableEye; + remoteModule.EnableExpression = installedModule.EnableExpression; } } diff --git a/VRCFaceTracking/Views/ModuleRegistryDetailControl.xaml b/VRCFaceTracking/Views/ModuleRegistryDetailControl.xaml index 3fddce80..9b316821 100644 --- a/VRCFaceTracking/Views/ModuleRegistryDetailControl.xaml +++ b/VRCFaceTracking/Views/ModuleRegistryDetailControl.xaml @@ -23,7 +23,35 @@