Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
}
}
27 changes: 27 additions & 0 deletions VRCFaceTracking/Strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,33 @@
<data name="UsageInstructions.Text" xml:space="preserve">
<value>Usage Instructions</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
27 changes: 27 additions & 0 deletions VRCFaceTracking/Strings/es-ES/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -284,4 +284,31 @@
<data name="Shell_TrackingSettings.Content" xml:space="preserve">
<value>Configuración de Seguimiento</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>
27 changes: 27 additions & 0 deletions VRCFaceTracking/Strings/ja-JP/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -326,4 +326,31 @@
<data name="AutoStartSettings.Description" xml:space="preserve">
<value>SteamVRの起動時に自動で起動します</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>
27 changes: 27 additions & 0 deletions VRCFaceTracking/Strings/ko-KR/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -395,4 +395,31 @@
<data name="Shell_TrackingSettings.Content" 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