Skip to content
7 changes: 7 additions & 0 deletions VRCFaceTracking.Core/Contracts/Services/ILibManager.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.ObjectModel;
using VRCFaceTracking.Core.Models;

namespace VRCFaceTracking.Core.Contracts.Services;

Expand All @@ -7,4 +8,10 @@ public interface ILibManager
public ObservableCollection<ModuleMetadataInternal> LoadedModulesMetadata { get; set; }
public void Initialize();
void TeardownAllAndResetAsync();

/// <summary>
/// The module enabled states that were actually applied at the last initialize.
/// Used to detect state changes that still require a restart to take effect.
/// </summary>
IReadOnlyDictionary<string, ModuleEnabledState> AppliedModuleStates { get; }
}
41 changes: 37 additions & 4 deletions VRCFaceTracking.Core/Library/UnifiedLibManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -15,6 +16,7 @@ public class UnifiedLibManager : ILibManager
private readonly ILogger<UnifiedLibManager> _logger;
private readonly ILogger _moduleLogger;
private readonly ILoggerFactory _loggerFactory;
private readonly ILocalSettingsService _settingsService;
#endregion

#region Observables
Expand All @@ -36,20 +38,23 @@ public class UnifiedLibManager : ILibManager

private string _sandboxProcessPath { get; set; }
private List<ModuleRuntimeInfo> AvailableSandboxModules = new ();
private Dictionary<string, ModuleEnabledState> _moduleSettingsByPath = new ();
public IReadOnlyDictionary<string, ModuleEnabledState> AppliedModuleStates { get; private set; } = new Dictionary<string, ModuleEnabledState>();
#endregion

#region Thread
private Thread _initializeWorker;
private static VrcftSandboxServer _sandboxServer;
#endregion

public UnifiedLibManager(ILoggerFactory factory, IDispatcherService dispatcherService, IModuleDataService moduleDataService)
public UnifiedLibManager(ILoggerFactory factory, IDispatcherService dispatcherService, IModuleDataService moduleDataService, ILocalSettingsService settingsService)
{
_loggerFactory = factory;
_logger = factory.CreateLogger<UnifiedLibManager>();
_moduleLogger = factory.CreateLogger("\0VRCFT\0");
_dispatcherService = dispatcherService;
_moduleDataService = moduleDataService;
_settingsService = settingsService;

LoadedModulesMetadata = new ObservableCollection<ModuleMetadataInternal>();
_sandboxProcessPath = Path.GetFullPath(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "VRCFaceTracking.ModuleProcess.exe" : "VRCFaceTracking.ModuleProcess");
Expand Down Expand Up @@ -157,11 +162,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 state. A module may only claim a slot it has been allowed to use.
_moduleSettingsByPath.TryGetValue(AvailableSandboxModules[moduleIndex].SandboxModulePath, out var moduleState);
var allowEye = moduleState is ModuleEnabledState.Enabled or ModuleEnabledState.EyesOnly;
var allowExpression = moduleState is ModuleEnabledState.Enabled or ModuleEnabledState.FaceOnly;

// 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,
Expand Down Expand Up @@ -296,7 +306,30 @@ public void Initialize()

// Find all modules
var modules = _moduleDataService.GetInstalledModules().Concat(_moduleDataService.GetLegacyModules());
var modulePaths = modules.Select(m => m.AssemblyLoadPath);

// Load the per-module state settings. They are applied at startup, so changing a module's state only takes effect after a restart.
var allSettings = _settingsService.ReadSettingAsync(Utils.ModuleStateSettingsKey, new Dictionary<string, ModuleEnabledState>()).GetAwaiter().GetResult();
var modulesToLoad = new List<InstallableTrackingModule>();
var settingsByPath = new Dictionary<string, ModuleEnabledState>();
var appliedStates = new Dictionary<string, ModuleEnabledState>();
foreach ( var m in modules )
{
var state = allSettings.TryGetValue(m.ModuleKey, out var s) ? s : ModuleEnabledState.Enabled;
settingsByPath[m.AssemblyLoadPath] = state;
appliedStates[m.ModuleKey] = state;

// Skip modules that have been disabled by the user as a whole
if ( state == ModuleEnabledState.Disabled )
{
continue;
}

modulesToLoad.Add(m);
}

_moduleSettingsByPath = settingsByPath;
AppliedModuleStates = appliedStates;
var modulePaths = modulesToLoad.Select(m => m.AssemblyLoadPath);

// Load all modules
AvailableSandboxModules.Clear();
Expand Down
78 changes: 76 additions & 2 deletions VRCFaceTracking.Core/Models/InstallableTrackingModule.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Newtonsoft.Json;
using System.ComponentModel;
using Newtonsoft.Json;

namespace VRCFaceTracking.Core.Models;

Expand All @@ -10,8 +11,10 @@ public enum InstallState
AwaitingRestart
}

public class InstallableTrackingModule : TrackingModuleMetadata
public class InstallableTrackingModule : TrackingModuleMetadata, INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;

public InstallState InstallationState
{
get; set;
Expand All @@ -22,4 +25,75 @@ public string AssemblyLoadPath
{
get; set;
}

private ModuleEnabledState _state = ModuleEnabledState.Enabled;

/// <summary>
/// The enabled state of this module. Changes take effect after VRCFT is restarted.
/// The value is persisted separately and does not get written to module.json.
/// </summary>
[JsonIgnore]
public ModuleEnabledState State
{
get => _state;
set
{
if ( _state == value )
{
return;
}

_state = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(State)));
}
}

private string _stateBadgeText = string.Empty;

/// <summary>
/// Localized display text for the module's enabled state shown in the module list.
/// Shows "(old → new)" when the state has been changed and a restart is pending.
/// </summary>
[JsonIgnore]
public string StateBadgeText
{
get => _stateBadgeText;
set
{
if ( _stateBadgeText == value )
{
return;
}

_stateBadgeText = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StateBadgeText)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HasStateBadge)));
}
}

/// <summary>
/// Whether the module list should show a state badge (false for not-installed modules).
/// </summary>
[JsonIgnore]
public bool HasStateBadge => !string.IsNullOrEmpty(_stateBadgeText);

/// <summary>
/// Updates the list badge text. The state-to-string conversion is delegated to
/// <paramref name="localize"/> so this Core model stays free of UI dependencies.
/// Shows "(old → new)" when the state has been changed and a restart is pending.
/// </summary>
public void UpdateStateBadge(ModuleEnabledState? applied, Func<ModuleEnabledState, string> localize)
{
var current = localize(State);
StateBadgeText = applied.HasValue && applied.Value != State
? $"({localize(applied.Value)} → {current})"
: $"({current})";
}

/// <summary>
/// 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.
/// </summary>
[JsonIgnore]
public string ModuleKey => ModuleId != Guid.Empty ? ModuleId.ToString() : AssemblyLoadPath;
}
28 changes: 28 additions & 0 deletions VRCFaceTracking.Core/Models/ModuleEnabledState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace VRCFaceTracking.Core.Models;

/// <summary>
/// The enabled state of a module, controlling which tracking slots it may claim.
/// Persisted as its numeric value.
/// </summary>
public enum ModuleEnabledState : byte
{
/// <summary>
/// Module is loaded and may claim both the eye and facial tracking slots.
/// </summary>
Enabled = 0,

/// <summary>
/// Module is not loaded at all.
/// </summary>
Disabled = 1,

/// <summary>
/// Module is loaded but may only claim the eye tracking slot.
/// </summary>
EyesOnly = 2,

/// <summary>
/// Module is loaded but may only claim the facial / expression tracking slot.
/// </summary>
FaceOnly = 3
}
5 changes: 5 additions & 0 deletions VRCFaceTracking.Core/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ public static class Utils
public static readonly string UserAccessibleDataDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "VRCFaceTracking");
public static readonly string PersistentDataDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "VRCFaceTracking");
public static readonly string CustomLibsDirectory = Path.Combine(PersistentDataDirectory, "CustomLibs");

/// <summary>
/// Local settings key for the persisted per-module enabled-state dictionary.
/// </summary>
public const string ModuleStateSettingsKey = "ModuleEnabledState";

public static int GetRandomFreePort()
{
Expand Down
2 changes: 1 addition & 1 deletion VRCFaceTracking/Services/ModuleDataService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -198,5 +198,5 @@ public IEnumerable<InstallableTrackingModule> GetInstalledModules()
}

return installedModules;
}
}
}
30 changes: 30 additions & 0 deletions VRCFaceTracking/Strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,36 @@
<data name="UsageInstructions.Text" xml:space="preserve">
<value>Usage Instructions</value>
</data>
<data name="ModuleStateTitle.Text" xml:space="preserve">
<value>Module State</value>
</data>
<data name="ModuleState_Enabled.Content" xml:space="preserve">
<value>Enabled</value>
</data>
<data name="ModuleState_Disabled.Content" xml:space="preserve">
<value>Disabled</value>
</data>
<data name="ModuleState_EyesOnly.Content" xml:space="preserve">
<value>Eyes Only</value>
</data>
<data name="ModuleState_FaceOnly.Content" xml:space="preserve">
<value>Face Only</value>
</data>
<data name="ModuleStateText_Enabled" xml:space="preserve">
<value>Enabled</value>
</data>
<data name="ModuleStateText_Disabled" xml:space="preserve">
<value>Disabled</value>
</data>
<data name="ModuleStateText_EyesOnly" xml:space="preserve">
<value>Eyes Only</value>
</data>
<data name="ModuleStateText_FaceOnly" xml:space="preserve">
<value>Face Only</value>
</data>
<data name="ModuleEnabledHint.Text" xml:space="preserve">
<value>Changes take effect after restarting VRCFT</value>
</data>
<data name="Version.Text" xml:space="preserve">
<value>Version</value>
</data>
Expand Down
30 changes: 30 additions & 0 deletions VRCFaceTracking/Strings/es-ES/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -284,4 +284,34 @@
<data name="Shell_TrackingSettings.Content" xml:space="preserve">
<value>Configuración de Seguimiento</value>
</data>
<data name="ModuleStateTitle.Text" xml:space="preserve">
<value>Estado del Módulo</value>
</data>
<data name="ModuleState_Enabled.Content" xml:space="preserve">
<value>Habilitado</value>
</data>
<data name="ModuleState_Disabled.Content" xml:space="preserve">
<value>Deshabilitado</value>
</data>
<data name="ModuleState_EyesOnly.Content" xml:space="preserve">
<value>Solo ojos</value>
</data>
<data name="ModuleState_FaceOnly.Content" xml:space="preserve">
<value>Solo cara</value>
</data>
<data name="ModuleStateText_Enabled" xml:space="preserve">
<value>Habilitado</value>
</data>
<data name="ModuleStateText_Disabled" xml:space="preserve">
<value>Deshabilitado</value>
</data>
<data name="ModuleStateText_EyesOnly" xml:space="preserve">
<value>Solo ojos</value>
</data>
<data name="ModuleStateText_FaceOnly" xml:space="preserve">
<value>Solo cara</value>
</data>
<data name="ModuleEnabledHint.Text" xml:space="preserve">
<value>Los cambios se aplicarán al reiniciar VRCFT</value>
</data>
</root>
30 changes: 30 additions & 0 deletions VRCFaceTracking/Strings/ja-JP/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -326,4 +326,34 @@
<data name="AutoStartSettings.Description" xml:space="preserve">
<value>SteamVRの起動時に自動で起動します</value>
</data>
<data name="ModuleStateTitle.Text" xml:space="preserve">
<value>モジュール状態</value>
</data>
<data name="ModuleState_Enabled.Content" xml:space="preserve">
<value>有効</value>
</data>
<data name="ModuleState_Disabled.Content" xml:space="preserve">
<value>無効</value>
</data>
<data name="ModuleState_EyesOnly.Content" xml:space="preserve">
<value>目のみ</value>
</data>
<data name="ModuleState_FaceOnly.Content" xml:space="preserve">
<value>顔のみ</value>
</data>
<data name="ModuleStateText_Enabled" xml:space="preserve">
<value>有効</value>
</data>
<data name="ModuleStateText_Disabled" xml:space="preserve">
<value>無効</value>
</data>
<data name="ModuleStateText_EyesOnly" xml:space="preserve">
<value>目のみ</value>
</data>
<data name="ModuleStateText_FaceOnly" xml:space="preserve">
<value>顔のみ</value>
</data>
<data name="ModuleEnabledHint.Text" xml:space="preserve">
<value>変更はVRCFTの再起動後に有効になります</value>
</data>
</root>
Loading