diff --git a/bHapticsManager/BHapticsConnection.cs b/bHapticsManager/BHapticsConnection.cs index d6aba46..cfe246c 100644 --- a/bHapticsManager/BHapticsConnection.cs +++ b/bHapticsManager/BHapticsConnection.cs @@ -1,37 +1,54 @@ -// BHapticsConnection.cs -// Handles connection initialization to bHaptics Player - +using System; +using System.Collections.Generic; +using System.Linq; using Elements.Core; using FrooxEngine; using ResoniteModLoader; using ModernBHaptics = bHapticsLib; +using LegacyBHaptics = Bhaptics.Tact; namespace bHapticsManager { - public static class BHapticsConnection { + public class BHapticsConnection { + private static BHapticsConnection _instance = null!; + public static BHapticsConnection Instance => _instance ??= new BHapticsConnection(); + public static readonly Dictionary DeviceCache = new(); - private static ModernBHapticsWorkerThread _workerThread; + private static ModernBHapticsWorkerThread _workerThread = null!; private static bool _isInitialized = false; + private static readonly object _shutdownLock = new(); + private static bool _isShuttingDown = false; + + // Events for device connection/disconnection - used by DeviceEventHandler + public event Action? DeviceConnected; + public event Action? DeviceDisconnected; + + // Event raising methods + internal void RaiseDeviceConnected(LegacyBHaptics.PositionType position) { + DeviceConnected?.Invoke(position); + } + + internal void RaiseDeviceDisconnected(LegacyBHaptics.PositionType position) { + DeviceDisconnected?.Invoke(position); + } /// Initializes connection to bHaptics Player and subscribes to events. /// Called once during mod initialization. - public static void Initialize() { + public static bool Initialize() { if (_isInitialized) { ResoniteMod.Warn("Already initialized - skipping duplicate connection"); - return; + return true; } - DeviceEventHandler.Subscribe(); - // Connect to bHaptics Player bool connected = ModernBHaptics.bHapticsManager.Connect("Resonite", "Resonite", true, 10); if (!connected) { ResoniteMod.Error("Failed to connect to bHaptics Player!"); ResoniteMod.Error("Make sure bHaptics Player is running and try restarting Resonite."); - return; + return false; } _isInitialized = true; @@ -43,11 +60,28 @@ public static void Initialize() { foreach (ModernBHaptics.PositionID pos in Enum.GetValues(typeof(ModernBHaptics.PositionID))) { if (ModernBHaptics.bHapticsManager.IsDeviceConnected(pos)) { - ResoniteMod.Msg($" - {pos} device ready"); + ResoniteMod.Debug($"Device {pos} ready"); // Add to cache DeviceCache[pos] = (true, DateTime.Now); } } + + return true; + } + + public List GetConnectedDevices() { + var connectedDevices = new List(); + + foreach (ModernBHaptics.PositionID pos in Enum.GetValues(typeof(ModernBHaptics.PositionID))) { + if (ModernBHaptics.bHapticsManager.IsDeviceConnected(pos)) { + var legacyPos = PositionMapper.MapModernToLegacy(pos); + if (!connectedDevices.Contains(legacyPos)) { + connectedDevices.Add(legacyPos); + } + } + } + + return connectedDevices; } @@ -80,7 +114,7 @@ public static void StartWorkerThread() { _workerThread = new ModernBHapticsWorkerThread(inputInterface); _workerThread.Start(); - ResoniteMod.Msg("Worker thread started successfully"); + ResoniteMod.Debug("Worker thread started successfully"); } catch (Exception ex) { ResoniteMod.Error($"Failed to start worker thread: {ex.Message}"); @@ -90,25 +124,77 @@ public static void StartWorkerThread() { /// Shuts down the connection to bHaptics Player, stopping all patterns and clearing the device cache. public static void Shutdown() { + lock (_shutdownLock) { + if (_isShuttingDown) { + ResoniteMod.Warn("Shutdown already in progress"); + return; + } + _isShuttingDown = true; + } + try { - _workerThread?.Stop(); - _workerThread = null; + ResoniteMod.Debug("Starting bHaptics connection shutdown..."); + + // Stop worker thread first + if (_workerThread != null) { + try { + ResoniteMod.Debug("Stopping worker thread..."); + _workerThread.Stop(); + _workerThread = null!; + ResoniteMod.Debug("Worker thread stopped"); + } + catch (Exception ex) { + ResoniteMod.Error($"Error stopping worker thread: {ex}"); + } + } - ModernBHaptics.bHapticsManager.StopPlayingAll(); + // Stop all haptic playback + try { + ResoniteMod.Debug("Stopping all haptic playback..."); + ModernBHaptics.bHapticsManager.StopPlayingAll(); + ResoniteMod.Debug("Haptic playback stopped"); + } + catch (Exception ex) { + ResoniteMod.Error($"Error stopping haptic playback: {ex}"); + } - bool disconnected = ModernBHaptics.bHapticsManager.Disconnect(); + // Small delay to ensure all patterns are stopped + System.Threading.Thread.Sleep(100); + + // Disconnect from bHaptics Player + try { + ResoniteMod.Debug("Disconnecting from bHaptics Player..."); + bool disconnected = ModernBHaptics.bHapticsManager.Disconnect(); + + if (disconnected) { + ResoniteMod.Debug("Disconnected from bHaptics Player successfully"); + } else { + ResoniteMod.Warn("Disconnect returned false - may already be disconnected"); + } + } + catch (Exception ex) { + ResoniteMod.Error($"Error disconnecting from bHaptics Player: {ex}"); + } - if (disconnected) { - ResoniteMod.Msg("Disconnected successfully"); - } else { - ResoniteMod.Warn("Disconnect returned false"); + // Clear caches + try { + DeviceCache.Clear(); + ResoniteMod.Debug("Device cache cleared"); + } + catch (Exception ex) { + ResoniteMod.Error($"Error clearing device cache: {ex}"); } - DeviceCache.Clear(); _isInitialized = false; + ResoniteMod.Msg("bHaptics connection shutdown complete"); } catch (Exception ex) { - ResoniteMod.Error($"Error during shutdown: {ex.Message}"); + ResoniteMod.Error($"Error during bHaptics connection shutdown: {ex}"); + } + finally { + lock (_shutdownLock) { + _isShuttingDown = false; + } } } diff --git a/bHapticsManager/DeviceEventHandler.cs b/bHapticsManager/DeviceEventHandler.cs index 2200f25..b31450f 100644 --- a/bHapticsManager/DeviceEventHandler.cs +++ b/bHapticsManager/DeviceEventHandler.cs @@ -1,130 +1,141 @@ -// DeviceEventHandler.cs -// Handles all bHaptics device events (connect, disconnect, battery changes) - +using System; using Elements.Core; using FrooxEngine; using ResoniteModLoader; +using HarmonyLib; using ModernBHaptics = bHapticsLib; +using LegacyBHaptics = Bhaptics.Tact; namespace bHapticsManager { - public static class DeviceEventHandler { - - public static void Subscribe() { - ModernBHaptics.bHapticsManager.DeviceStatusChanged += OnDeviceStatusChanged; - ModernBHaptics.bHapticsManager.ConnectionEstablished += OnConnectionEstablished; - ModernBHaptics.bHapticsManager.ConnectionLost += OnConnectionLost; - ModernBHaptics.bHapticsManager.StatusChanged += OnStatusChanged; - - ResoniteMod.Msg("Event handlers subscribed successfully"); - } + public class DeviceEventHandler : IDisposable + { + private volatile bool _disposed; + private ModernBHapticsWorkerThread _workerThread = null!; + private readonly object _lock = new object(); + + public void Initialize(ModernBHapticsWorkerThread workerThread) + { + lock (_lock) + { + _workerThread = workerThread; + } + + try + { + BHapticsConnection.Instance.DeviceConnected += OnDeviceConnected; + BHapticsConnection.Instance.DeviceDisconnected += OnDeviceDisconnected; + + ResoniteMod.Debug("Event handlers subscribed successfully"); + } + catch (Exception ex) + { + bHapticsManager.Error($"Failed to subscribe event handlers: {ex}"); + } + } + + private void OnDeviceConnected(LegacyBHaptics.PositionType position) + { + if (_disposed) return; + + try + { + bHapticsManager.Msg($"Device {position} connected"); + + if (Engine.Current != null && Engine.Current.WorldManager != null) + { + var focusedWorld = Engine.Current.WorldManager.FocusedWorld; + if (focusedWorld != null) + { + focusedWorld.RunSynchronously(() => + { + try + { + DeviceRegistration.RegisterDevice(position); + } + catch (Exception ex) + { + bHapticsManager.Error($"Failed to register device {position}: {ex}"); + } + }); + } + } + + lock (_lock) + { + if (_disposed) return; + _workerThread?.OnDeviceConnected(position); + } + } + catch (Exception ex) + { + bHapticsManager.Error($"Error handling device connection for {position}: {ex}"); + } + } + + private void OnDeviceDisconnected(LegacyBHaptics.PositionType position) + { + if (_disposed) return; - public static void OnDeviceStatusChanged(object sender, ModernBHaptics.DeviceStatusChangedEventArgs e) { - try { - string status = e.IsConnected ? "CONNECTED" : "DISCONNECTED"; - ResoniteMod.Msg($"[Event] Device {e.Position} {status}"); - - BHapticsConnection.InvalidateDeviceCache(e.Position, e.IsConnected); - - var engine = FrooxEngine.Engine.Current; - if (engine == null) { - ResoniteMod.Warn("Engine not ready for device registration - will retry on next connection"); - return; - } - - var config = bHapticsManager.Config; - if (config == null) { - ResoniteMod.Warn("Config not ready - skipping event handling"); - return; - } - - if (e.IsConnected) { - var legacyPosition = PositionMapper.MapModernToLegacy(e.Position); - LegacyCompatibilityLayer.ResetDevice(legacyPosition); - - if (config.GetValue(bHapticsManager.ENABLE_HOTPLUG)) { - _ = Task.Run(async () => { - try { - bool success = await DeviceRegistration.TryRegisterDeviceAsync(e.Position); - if (success) { - ResoniteMod.Msg($"Device {e.Position} registered and ready"); - } else { - ResoniteMod.Warn($"Device {e.Position} registration failed"); - } - } - catch (Exception ex) { - ResoniteMod.Error($"Error registering device {e.Position}: {ex.Message}"); - } - }); - } - else { - ResoniteMod.Warn($"Device {e.Position} connected, but hot-plug is disabled in config"); - } - } - else { - _ = Task.Run(async () => { - try { - await DeviceRegistration.UnregisterDeviceAsync(e.Position); - - try { - ModernBHaptics.bHapticsManager.StopPlayingAll(); - } catch { } - - var legacyPosition = PositionMapper.MapModernToLegacy(e.Position); - LegacyCompatibilityLayer.CleanupDevice(legacyPosition); - - ResoniteMod.Msg($"Device {e.Position} disconnected and cleaned up"); - } - catch (Exception ex) { - ResoniteMod.Error($"Error during device {e.Position} cleanup: {ex.Message}"); - } - }); - } - } - catch (Exception ex) { - ResoniteMod.Error($"Error in OnDeviceStatusChanged: {ex.Message}"); - } - } + try + { + bHapticsManager.Msg($"Device {position} disconnected"); + + if (Engine.Current != null && Engine.Current.WorldManager != null) + { + var focusedWorld = Engine.Current.WorldManager.FocusedWorld; + if (focusedWorld != null) + { + focusedWorld.RunSynchronously(() => + { + try + { + DeviceRegistration.UnregisterDevice(position); + } + catch (Exception ex) + { + bHapticsManager.Error($"Failed to unregister device {position}: {ex}"); + } + }); + } + } - public static void OnConnectionEstablished(object sender, EventArgs e) { - try { - ResoniteMod.Msg("Connection to bHaptics Player ESTABLISHED"); - var deviceCount = ModernBHaptics.bHapticsManager.GetConnectedDeviceCount(); - ResoniteMod.Msg($"Detected {deviceCount} device(s)"); - - foreach (ModernBHaptics.PositionID pos in Enum.GetValues(typeof(ModernBHaptics.PositionID))) { - if (ModernBHaptics.bHapticsManager.IsDeviceConnected(pos)) { - ResoniteMod.Msg($" - {pos}"); - } - } - } - catch (Exception ex) { - ResoniteMod.Error($"Error in OnConnectionEstablished: {ex.Message}"); - } - } + lock (_lock) + { + if (_disposed) return; + _workerThread?.OnDeviceDisconnected(position); + } + } + catch (Exception ex) + { + bHapticsManager.Error($"Error handling device disconnection for {position}: {ex}"); + } + } - public static void OnConnectionLost(object sender, EventArgs e) { - try { - ResoniteMod.Warn("Connection to bHaptics Player LOST"); - ResoniteMod.Warn("Haptics will not work until connection is re-established"); - if (bHapticsManager.AUTO_RECONNECT) { - ResoniteMod.Warn("Auto-reconnect is enabled - waiting for reconnection..."); - } - } - catch (Exception ex) { - ResoniteMod.Error($"Error in OnConnectionLost: {ex.Message}"); - } - } + public void Dispose() + { + if (_disposed) return; + + _disposed = true; + + try + { + if (BHapticsConnection.Instance != null) + { + BHapticsConnection.Instance.DeviceConnected -= OnDeviceConnected; + BHapticsConnection.Instance.DeviceDisconnected -= OnDeviceDisconnected; + } + ResoniteMod.Debug("Event handlers unsubscribed"); + } + catch (Exception ex) + { + bHapticsManager.Error($"Error disposing event handlers: {ex}"); + } - public static void OnStatusChanged(object sender, ModernBHaptics.ConnectionStatusChangedEventArgs e) { - try { - if (bHapticsManager.Config?.GetValue(bHapticsManager.ENABLE_DIAGNOSTIC_LOGGING) ?? false) { - ResoniteMod.Msg($"Status changed: {e.PreviousStatus} -> {e.NewStatus}"); - } - } - catch (Exception ex) { - ResoniteMod.Error($"Error in OnStatusChanged: {ex.Message}"); - } - } - } + lock (_lock) + { + _workerThread = null!; + } + } + } } diff --git a/bHapticsManager/DeviceRegistration.cs b/bHapticsManager/DeviceRegistration.cs index ff9645a..e145c48 100644 --- a/bHapticsManager/DeviceRegistration.cs +++ b/bHapticsManager/DeviceRegistration.cs @@ -1,7 +1,8 @@ -// Handles dynamic registration of haptic points for hot-plugged devices - using Elements.Core; using FrooxEngine; +using System; +using System.Collections.Generic; +using System.Linq; using System.Reflection; using System.Threading.Tasks; using ResoniteModLoader; @@ -11,7 +12,7 @@ namespace bHapticsManager { public static class DeviceRegistration { - private static readonly HashSet _registeredDevices = new(); + private static readonly HashSet _registeredDevices = new HashSet(); private static readonly Dictionary _pendingRegistrations = new(); private static InputInterface? _inputInterface; @@ -19,6 +20,23 @@ public static class DeviceRegistration { private static readonly object _registrationLock = new object(); + public static void RegisterDevice(LegacyBHaptics.PositionType position) { + var modernPos = PositionMapper.MapLegacyToModern(position); + TryRegisterDevice(modernPos); + } + + public static void UnregisterDevice(LegacyBHaptics.PositionType position) { + var modernPos = PositionMapper.MapLegacyToModern(position); + UnregisterDevice(modernPos); + } + + public static void ClearAllRegistrations() { + lock (_registrationLock) { + _registeredDevices.Clear(); + _pendingRegistrations.Clear(); + } + } + public static Task TryRegisterDeviceAsync(ModernBHaptics.PositionID position) { lock (_registrationLock) { if (_registeredDevices.Contains(position)) { @@ -65,7 +83,7 @@ private static async Task RegisterDeviceInternalAsync(ModernBHaptics.Posit var legacyPosition = PositionMapper.MapModernToLegacy(position); - ResoniteMod.Msg($"Registering haptic points for device: {position}"); + ResoniteMod.Debug($"Registering haptic points for device: {position}"); await Task.Delay(100); @@ -75,7 +93,7 @@ private static async Task RegisterDeviceInternalAsync(ModernBHaptics.Posit lock (_registrationLock) { _registeredDevices.Add(position); } - ResoniteMod.Msg($"Registered {position} successfully"); + ResoniteMod.Debug($"Registered {position} successfully"); await RefreshHapticPointSamplersAsync(); } @@ -259,7 +277,7 @@ private static async Task RefreshHapticPointSamplersAsync() { totalRefreshed = results.Sum(); if (totalRefreshed > 0) { - ResoniteMod.Msg($"Refreshed {totalRefreshed} haptic sampler(s)"); + ResoniteMod.Debug($"Refreshed {totalRefreshed} haptic sampler(s)"); } } catch (Exception ex) { diff --git a/bHapticsManager/DiagnosticPatches.cs b/bHapticsManager/DiagnosticPatches.cs index 811d9f6..285a6a2 100644 --- a/bHapticsManager/DiagnosticPatches.cs +++ b/bHapticsManager/DiagnosticPatches.cs @@ -1,127 +1,109 @@ -// DiagnosticPatches.cs -// Diagnostic patches to help debug VestBack and haptic triggering issues -// These patches are ONLY active if enable_diagnostic_logging is true in config - +using System; +using System.Collections.Generic; using HarmonyLib; -using ResoniteModLoader; using FrooxEngine; -using LegacyBHaptics = Bhaptics.Tact; +using ResoniteModLoader; using ModernBHaptics = bHapticsLib; +using LegacyBHaptics = Bhaptics.Tact; namespace bHapticsManager { - /// - /// Patch BHapticsDriver.InitializeBhaptics to log detailed device detection info - /// - [HarmonyPatch(typeof(BHapticsDriver), "InitializeBhaptics")] - public class BHapticsDriverInitPatch { - static void Postfix(BHapticsDriver __instance) { - // Only log if diagnostic logging is enabled - if (!bHapticsManager.Config?.GetValue(bHapticsManager.ENABLE_DIAGNOSTIC_LOGGING) ?? false) { - return; - } - - try { - // Log all detected devices with their PositionID mapping - ResoniteMod.Msg("=== bHaptics Device Detection ==="); + [HarmonyPatch] + public static class DiagnosticPatches { + + [HarmonyPatch(typeof(BHapticsDriver), "InitializeBhaptics")] + public class BHapticsDriverInitPatch { + static void Postfix(BHapticsDriver __instance) { + if (!bHapticsManager.Config?.GetValue(bHapticsManager.ENABLE_DIAGNOSTIC_LOGGING) ?? false) { + return; + } - foreach (ModernBHaptics.PositionID pos in Enum.GetValues(typeof(ModernBHaptics.PositionID))) { - bool connected = ModernBHaptics.bHapticsManager.IsDeviceConnected(pos); - if (connected) { - var legacy = PositionMapper.MapModernToLegacy(pos); - ResoniteMod.Msg($"? {pos} (Legacy: {legacy}) - CONNECTED"); + try { + ResoniteMod.Debug("=== bHaptics Device Detection ==="); + + foreach (ModernBHaptics.PositionID pos in Enum.GetValues(typeof(ModernBHaptics.PositionID))) { + bool connected = ModernBHaptics.bHapticsManager.IsDeviceConnected(pos); + if (connected) { + var legacy = PositionMapper.MapModernToLegacy(pos); + ResoniteMod.Debug($"[OK] {pos} (Legacy: {legacy}) - CONNECTED"); + } } + + bool vestBackModern = ModernBHaptics.bHapticsManager.IsDeviceConnected(ModernBHaptics.PositionID.VestBack); + bool vestBackLegacy = ModernBHaptics.bHapticsManager.IsDeviceConnected( + PositionMapper.MapLegacyToModern(LegacyBHaptics.PositionType.VestBack) + ); + + ResoniteMod.Debug($"VestBack check: Modern={vestBackModern}, Legacy mapped={vestBackLegacy}"); + ResoniteMod.Debug("================================="); + } + catch (Exception ex) { + ResoniteMod.Error($"Error in device detection diagnostic: {ex.Message}"); } - - // Specifically check VestBack mapping - bool vestBackModern = ModernBHaptics.bHapticsManager.IsDeviceConnected(ModernBHaptics.PositionID.VestBack); - bool vestBackLegacy = ModernBHaptics.bHapticsManager.IsDeviceConnected( - PositionMapper.MapLegacyToModern(LegacyBHaptics.PositionType.VestBack) - ); - - ResoniteMod.Msg($"VestBack check: Modern={vestBackModern}, Legacy mapped={vestBackLegacy}"); - ResoniteMod.Msg("================================="); - } - catch (Exception ex) { - ResoniteMod.Error($"Error in device detection diagnostic: {ex.Message}"); } } - } - - /// - /// Patch HapticPoint.SampleSources to log when sources are sampled (for debugging weird triggers) - /// - [HarmonyPatch(typeof(HapticPoint), "SampleSources")] - public class HapticPointSampleDiagnosticPatch { - private static DateTime _lastLog = DateTime.MinValue; - private static readonly Dictionary _lastValues = new(); - static void Postfix(HapticPoint __instance) { - // Only log if diagnostic logging is enabled - if (!bHapticsManager.Config?.GetValue(bHapticsManager.ENABLE_DIAGNOSTIC_LOGGING) ?? false) { - return; - } + [HarmonyPatch(typeof(HapticPoint), "SampleSources")] + public class HapticPointSampleDiagnosticPatch { + private static DateTime _lastLog = DateTime.MinValue; + private static readonly Dictionary _lastValues = new(); - try { - // Only log when values change significantly (to avoid spam) - int index = __instance.Index; - float force = __instance.Force; - float temp = __instance.Temperature; - float pain = __instance.Pain; - float vib = __instance.Vibration; - - // Check if values changed - bool changed = false; - if (_lastValues.TryGetValue(index, out var last)) { - float deltaF = Math.Abs(force - last.force); - float deltaT = Math.Abs(temp - last.temp); - float deltaP = Math.Abs(pain - last.pain); - float deltaV = Math.Abs(vib - last.vib); - - // Log if any value changed by more than 0.1 - changed = deltaF > 0.1f || deltaT > 0.1f || deltaP > 0.1f || deltaV > 0.1f; - } else { - changed = force > 0 || temp != 0 || pain > 0 || vib > 0; + static void Postfix(HapticPoint __instance) { + if (!bHapticsManager.Config?.GetValue(bHapticsManager.ENABLE_DIAGNOSTIC_LOGGING) ?? false) { + return; } - if (changed) { - _lastValues[index] = (force, temp, pain, vib); + try { + int index = __instance.Index; + float force = __instance.Force; + float temp = __instance.Temperature; + float pain = __instance.Pain; + float vib = __instance.Vibration; - // Only log occasionally to prevent spam - if ((DateTime.Now - _lastLog).TotalSeconds > 2) { - ResoniteMod.Msg($"[Haptic#{index}] F={force:F2} T={temp:F2} P={pain:F2} V={vib:F2} | Position={__instance.Position}"); - _lastLog = DateTime.Now; + bool changed = false; + if (_lastValues.TryGetValue(index, out var last)) { + float deltaF = Math.Abs(force - last.force); + float deltaT = Math.Abs(temp - last.temp); + float deltaP = Math.Abs(pain - last.pain); + float deltaV = Math.Abs(vib - last.vib); + + changed = deltaF > 0.1f || deltaT > 0.1f || deltaP > 0.1f || deltaV > 0.1f; + } else { + changed = force > 0 || temp != 0 || pain > 0 || vib > 0; + } + + if (changed) { + _lastValues[index] = (force, temp, pain, vib); + + if ((DateTime.Now - _lastLog).TotalSeconds > 2) { + ResoniteMod.Debug($"[Haptic#{index}] F={force:F2} T={temp:F2} P={pain:F2} V={vib:F2} | Position={__instance.Position}"); + _lastLog = DateTime.Now; + } } } - } - catch { - // Silent fail - this is just diagnostics + catch (Exception ex) { + ResoniteMod.Error($"Error in HapticPoint sample diagnostic: {ex.Message}"); + } } } - } - - /// - /// Patch DirectTagHapticSource.GetIntensity to log when it provides haptic data - /// This helps debug TipTouchSource issues - /// - [HarmonyPatch(typeof(DirectTagHapticSource), "GetIntensity")] - public class DirectTagHapticSourceDiagnosticPatch { - private static DateTime _lastLog = DateTime.MinValue; - static void Postfix(DirectTagHapticSource __instance, SensationClass sensation, float __result) { - // Only log if diagnostic logging is enabled - if (!bHapticsManager.Config?.GetValue(bHapticsManager.ENABLE_DIAGNOSTIC_LOGGING) ?? false) { - return; - } + [HarmonyPatch(typeof(DirectTagHapticSource), "GetIntensity")] + public class DirectTagHapticSourceDiagnosticPatch { + private static DateTime _lastLog = DateTime.MinValue; - try { - // Only log when there's actual intensity and occasionally - if (__result > 0f && (DateTime.Now - _lastLog).TotalSeconds > 2) { - ResoniteMod.Msg($"[DirectTag] Tag='{__instance.HapticTag.Value}' Sensation={sensation} Intensity={__result:F2}"); - _lastLog = DateTime.Now; + static void Postfix(DirectTagHapticSource __instance, SensationClass sensation, float __result) { + if (!bHapticsManager.Config?.GetValue(bHapticsManager.ENABLE_DIAGNOSTIC_LOGGING) ?? false) { + return; + } + + try { + if (__result > 0f && (DateTime.Now - _lastLog).TotalSeconds > 2) { + ResoniteMod.Debug($"[DirectTag] Tag='{__instance.HapticTag.Value}' Sensation={sensation} Intensity={__result:F2}"); + _lastLog = DateTime.Now; + } + } + catch (Exception ex) { + ResoniteMod.Debug($"[DirectTag Diagnostic] Exception: {ex}"); } - } - catch { - // Silent fail } } } diff --git a/bHapticsManager/HapticMethodPatches.cs b/bHapticsManager/HapticMethodPatches.cs index 602fa6a..9016264 100644 --- a/bHapticsManager/HapticMethodPatches.cs +++ b/bHapticsManager/HapticMethodPatches.cs @@ -1,7 +1,5 @@ -// HapticMethodPatches.cs -// Harmony patches that intercept HapticPlayer methods (IsActive, Submit) -// and properly handle remote haptic data synchronization - +using System; +using System.Collections.Generic; using Elements.Core; using HarmonyLib; using ResoniteModLoader; @@ -12,6 +10,18 @@ namespace bHapticsManager { + public static class HapticMethodPatches { + public static void ApplyPatches(Harmony harmony) { + try { + harmony.PatchAll(typeof(HapticMethodPatches)); + ResoniteMod.Debug("HapticMethodPatches applied successfully"); + } + catch (Exception ex) { + ResoniteMod.Error($"Failed to apply HapticMethodPatches: {ex}"); + } + } + } + [HarmonyPatch(typeof(LegacyBHaptics.HapticPlayer), "IsActive")] public class IsActivePatch { static bool Prefix(LegacyBHaptics.PositionType type, ref bool __result) { @@ -85,7 +95,7 @@ static bool Prefix(HapticPointData __instance) { _updateCount++; if ((DateTime.Now - _lastDiagnostic).TotalSeconds >= 10 || (hasActivity && _updateCount % 100 == 0)) { - ResoniteMod.Msg($"[HapticData#{index}] user={user?.UserName ?? "null"} isLocal={isLocalUser} isRemote={isRemoteUser} unowned={isUnownedData} selfEnabled={enableSelfHaptics} activity={hasActivity}"); + ResoniteMod.Debug($"[HapticData#{index}] user={user?.UserName ?? "null"} isLocal={isLocalUser} isRemote={isRemoteUser} unowned={isUnownedData} selfEnabled={enableSelfHaptics} activity={hasActivity}"); _lastDiagnostic = DateTime.Now; } } @@ -106,7 +116,7 @@ static bool Prefix(HapticPointData __instance) { _registeredRemoteSources.Add(index); if (isDiagnosticEnabled) { - ResoniteMod.Msg($"Registered RemoteHapticSource for point {index}"); + ResoniteMod.Debug($"Registered RemoteHapticSource for point {index}"); } } } else { @@ -124,7 +134,7 @@ static bool Prefix(HapticPointData __instance) { __instance.TotalActivationIntensity.Value = point.TotalActivationIntensity; if (isDiagnosticEnabled && (point.Force > 0f || point.Pain > 0f)) { - ResoniteMod.Msg($"Self-haptics active for point {index}: F={point.Force:F2} P={point.Pain:F2}"); + ResoniteMod.Debug($"Self-haptics active for point {index}: F={point.Force:F2} P={point.Pain:F2}"); } return false; diff --git a/bHapticsManager/HapticPlayerPatches.cs b/bHapticsManager/HapticPlayerPatches.cs index 9bb0d84..3a04c2b 100644 --- a/bHapticsManager/HapticPlayerPatches.cs +++ b/bHapticsManager/HapticPlayerPatches.cs @@ -1,7 +1,5 @@ -// HapticPlayerPatches.cs -// Harmony patches that allow the legacy HapticPlayer to initialize -// but prevent it from connecting to bHaptics Player (we handle connection ourselves) - +using System; +using System.Linq; using Elements.Core; using HarmonyLib; using ResoniteModLoader; @@ -13,34 +11,38 @@ namespace bHapticsManager { - /// - /// CRITICAL: Blocks WebSocketSender creation to prevent duplicate connection! - /// FrooxEngine's HapticPlayer would create its own connection, but we want only OUR connection. - /// NOTE: This patch might fail if WebSocketSender is not accessible - that's OK, we have fallbacks. - /// + public static class HapticPlayerPatches { + public static void ApplyPatches(Harmony harmony) { + try { + harmony.PatchAll(typeof(WebSocketSenderConstructorPatch).Assembly); + ResoniteMod.Debug("HapticPlayerPatches applied successfully"); + } + catch (Exception ex) { + ResoniteMod.Error($"Failed to apply HapticPlayerPatches: {ex}"); + } + } + } + [HarmonyPatch] public class WebSocketSenderConstructorPatch { - static MethodBase TargetMethod() { + static MethodBase? TargetMethod() { try { - // Find WebSocketSender type (it's an inner class) var hapticPlayerType = typeof(LegacyBHaptics.HapticPlayer); - // Try multiple possible names for the nested type - Type senderType = null; + Type? senderType = null; foreach (var name in new[] { "WebSocketSender", "WebSocketConnection", "Sender", "_sender" }) { senderType = hapticPlayerType.GetNestedType(name, BindingFlags.NonPublic | BindingFlags.Public); if (senderType != null) { - ResoniteMod.Msg($"Found nested type: {name}"); + ResoniteMod.Debug($"Found nested type: {name}"); break; } } if (senderType == null) { - // Try to find it in the assembly var assembly = hapticPlayerType.Assembly; foreach (var type in assembly.GetTypes()) { if (type.Name.Contains("Sender") || type.Name.Contains("Socket")) { - ResoniteMod.Msg($"Found potential sender type in assembly: {type.FullName}"); + ResoniteMod.Debug($"Found potential sender type in assembly: {type.FullName}"); senderType = type; break; } @@ -48,18 +50,17 @@ static MethodBase TargetMethod() { } if (senderType == null) { - ResoniteMod.Warn("Could not find WebSocketSender type - patch will be skipped (this is OK, we have fallbacks)"); + ResoniteMod.Warn("Could not find WebSocketSender type - patch will be skipped"); return null; } - // Find constructor var constructor = senderType.GetConstructors(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(); if (constructor == null) { - ResoniteMod.Warn($"Could not find constructor for {senderType.Name} - patch will be skipped (this is OK)"); + ResoniteMod.Warn($"Could not find constructor for {senderType.Name} - patch will be skipped"); return null; } - ResoniteMod.Msg($"? Found and will patch {senderType.Name} constructor"); + ResoniteMod.Debug($"Found and will patch {senderType.Name} constructor"); return constructor; } catch (Exception ex) { @@ -69,52 +70,41 @@ static MethodBase TargetMethod() { } static bool Prefix() { - // Block WebSocketSender creation completely! - // This prevents FrooxEngine from creating its own connection to bHaptics Player - ResoniteMod.Msg("? Blocked WebSocketSender constructor (preventing duplicate connection)"); + ResoniteMod.Debug("Blocked WebSocketSender constructor (preventing duplicate connection)"); return false; } - // ALSO add exception handler to ensure it fails gracefully - static Exception Finalizer(Exception __exception) { + static Exception? Finalizer(Exception? __exception) { if (__exception != null) { - ResoniteMod.Msg("? WebSocketSender constructor threw exception (expected - we blocked it)"); - return null; // Suppress the exception + ResoniteMod.Debug("WebSocketSender constructor threw exception (expected - we blocked it)"); + return null; } return __exception; } } - /// - /// CRITICAL: Patches BHapticsDriver.RegisterInputs to stop the worker thread! - /// This is where FrooxEngine starts its competing worker thread. - /// We need to stop it AFTER it initializes devices but BEFORE it starts the worker. - /// [HarmonyPatch(typeof(BHapticsDriver), "RegisterInputs")] public class BHapticsDriverRegisterInputsPatch { static void Postfix(BHapticsDriver __instance) { try { - // The worker field is created in InitializeBhaptics and started in RegisterInputs - // We need to stop it using reflection var workerField = typeof(BHapticsDriver).GetField("worker", BindingFlags.NonPublic | BindingFlags.Instance); if (workerField != null) { var worker = workerField.GetValue(__instance); if (worker != null) { - // Try to stop the worker var stopMethod = worker.GetType().GetMethod("Stop", BindingFlags.Public | BindingFlags.Instance); if (stopMethod != null) { stopMethod.Invoke(worker, null); - ResoniteMod.Msg("? Stopped FrooxEngine's BHapticsDriver worker thread"); + ResoniteMod.Debug("Stopped FrooxEngine's BHapticsDriver worker thread"); } else { ResoniteMod.Warn("Could not find Stop method on worker - trying Dispose"); var disposeMethod = worker.GetType().GetMethod("Dispose", BindingFlags.Public | BindingFlags.Instance); if (disposeMethod != null) { disposeMethod.Invoke(worker, null); - ResoniteMod.Msg("? Disposed FrooxEngine's BHapticsDriver worker thread"); + ResoniteMod.Debug("Disposed FrooxEngine's BHapticsDriver worker thread"); } } } else { - ResoniteMod.Msg("Worker field is null - FrooxEngine's worker didn't start (this is OK)"); + ResoniteMod.Debug("Worker field is null - FrooxEngine's worker didn't start"); } } else { ResoniteMod.Warn("Could not find worker field on BHapticsDriver"); @@ -126,23 +116,18 @@ static void Postfix(BHapticsDriver __instance) { } } - /// - /// CRITICAL: Force _sender to null in ALL HapticPlayer constructors! - /// This is the ultimate fallback if WebSocketSender constructor patch fails. - /// [HarmonyPatch(typeof(LegacyBHaptics.HapticPlayer), MethodType.Constructor, new Type[] { typeof(string), typeof(string), typeof(Action), typeof(bool) })] public class HapticPlayerConstructorPatch { static void Postfix(LegacyBHaptics.HapticPlayer __instance) { try { - // FORCE _sender to null var senderField = typeof(LegacyBHaptics.HapticPlayer).GetField("_sender", BindingFlags.NonPublic | BindingFlags.Instance); if (senderField != null) { senderField.SetValue(__instance, null); - ResoniteMod.Msg("? HapticPlayer constructed - forced _sender to null (no duplicate connection)"); + ResoniteMod.Debug("HapticPlayer constructed - forced _sender to null"); } else { ResoniteMod.Warn("Could not find _sender field to nullify"); } @@ -153,9 +138,6 @@ static void Postfix(LegacyBHaptics.HapticPlayer __instance) { } } - /// - /// CRITICAL: Force _sender to null in overload constructor too! - /// [HarmonyPatch(typeof(LegacyBHaptics.HapticPlayer), MethodType.Constructor, new Type[] { typeof(string), typeof(string), typeof(bool) })] @@ -167,7 +149,7 @@ static void Postfix(LegacyBHaptics.HapticPlayer __instance) { if (senderField != null) { senderField.SetValue(__instance, null); - ResoniteMod.Msg("? HapticPlayer constructed (overload) - forced _sender to null"); + ResoniteMod.Debug("HapticPlayer constructed (overload) - forced _sender to null"); } } catch (Exception ex) { @@ -176,32 +158,20 @@ static void Postfix(LegacyBHaptics.HapticPlayer __instance) { } } - /// - /// Allows Dispose() to run safely (nothing to dispose since sender is null) - /// [HarmonyPatch(typeof(LegacyBHaptics.HapticPlayer), "Dispose")] public class HapticPlayerDisposePatch { static bool Prefix() { - // Let it run - there's nothing to dispose since sender is null return true; } } - /// - /// CRITICAL: Intercepts IsActive() to return modern device connection status! - /// This is what FrooxEngine uses to determine which devices to initialize and send data to! - /// [HarmonyPatch(typeof(LegacyBHaptics.HapticPlayer), "IsActive")] public class HapticPlayerIsActivePatch { static bool Prefix(LegacyBHaptics.PositionType type, ref bool __result) { try { - // Map legacy position to modern var modernPosition = PositionMapper.MapLegacyToModern(type); - - // Check if device is connected using modern API __result = ModernBHaptics.bHapticsManager.IsDeviceConnected(modernPosition); - - return false; // Skip original method + return false; } catch { __result = false; @@ -210,11 +180,6 @@ static bool Prefix(LegacyBHaptics.PositionType type, ref bool __result) { } } - /// - /// CRITICAL: Intercepts Submit() to prevent the legacy SDK from trying to send data! - /// Our ModernBHapticsWorkerThread handles all submission via LegacyCompatibilityLayer. - /// This patch ensures FrooxEngine's worker thread doesn't call the broken WebSocket. - /// [HarmonyPatch(typeof(LegacyBHaptics.HapticPlayer), "Submit", new Type[] { typeof(string), typeof(LegacyBHaptics.PositionType), @@ -223,15 +188,10 @@ static bool Prefix(LegacyBHaptics.PositionType type, ref bool __result) { })] public class HapticPlayerSubmitPatch { static bool Prefix() { - // Block the legacy Submit() - our ModernBHapticsWorkerThread handles all submission - // FrooxEngine's worker thread will call this, but we ignore it return false; } } - /// - /// Intercepts TurnOff(key) to immediately stop specific haptic patterns. - /// [HarmonyPatch(typeof(LegacyBHaptics.HapticPlayer), "TurnOff", new Type[] { typeof(string) })] public class HapticPlayerTurnOffKeyPatch { static bool Prefix(string key) { @@ -245,9 +205,6 @@ static bool Prefix(string key) { } } - /// - /// Intercepts TurnOff() to immediately stop ALL haptic patterns. - /// [HarmonyPatch(typeof(LegacyBHaptics.HapticPlayer), "TurnOff", new Type[0])] public class HapticPlayerTurnOffAllPatch { static bool Prefix() { diff --git a/bHapticsManager/LegacyCompatibilityLayer.cs b/bHapticsManager/LegacyCompatibilityLayer.cs index 311db1d..cdaaa51 100644 --- a/bHapticsManager/LegacyCompatibilityLayer.cs +++ b/bHapticsManager/LegacyCompatibilityLayer.cs @@ -1,15 +1,31 @@ -// LegacyCompatibilityLayer.cs -// Provides exact Bhaptics.Tact behavior using modern bHapticsLib - +using System; +using System.Collections.Generic; +using System.Linq; using LegacyBHaptics = Bhaptics.Tact; using ModernBHaptics = bHapticsLib; +using ResoniteModLoader; +using HarmonyLib; namespace bHapticsManager { public static class LegacyCompatibilityLayer { + public static void ApplyPatches(Harmony harmony) { + try { + ResoniteMod.Debug("LegacyCompatibilityLayer initialized successfully"); + } + catch (Exception ex) { + ResoniteMod.Error($"Failed to initialize LegacyCompatibilityLayer: {ex}"); + } + } + private static readonly Dictionary _lastSubmissionTime = new(); private static readonly object _submissionLock = new object(); - private const int MIN_SUBMISSION_INTERVAL_MS = 10; + + // Minimum interval between submissions for the same device to prevent flooding + private const int MIN_SUBMISSION_INTERVAL_MS = 5; + // Minimum buffer duration for haptic feedback frames (MessagePack allows for faster submission) + private const int MIN_BUFFER_DURATION_MS = 50; + // Intensity boost feature removed for clarity; signals pass through unchanged public static void SubmitFrame(string key, LegacyBHaptics.PositionType position, List dotPoints, int durationMillis) { @@ -47,6 +63,9 @@ public static void SubmitFrame(string key, LegacyBHaptics.PositionType position, if (_lastSubmissionTime.TryGetValue(deviceKey, out DateTime lastTime)) { double timeSinceLastMs = (DateTime.Now - lastTime).TotalMilliseconds; if (timeSinceLastMs < MIN_SUBMISSION_INTERVAL_MS) { + if (bHapticsManager.Config?.GetValue(bHapticsManager.ENABLE_DIAGNOSTIC_LOGGING) ?? false) { + ResoniteMod.Debug($"Throttled submission for {position} (only {timeSinceLastMs:F1}ms since last)"); + } return; } } @@ -76,25 +95,50 @@ public static void SubmitFrame(string key, LegacyBHaptics.PositionType position, int[] motors = new int[motorCount]; if (dotPoints != null) { + bool hasWeakSignals = false; + float maxIntensity = 0f; + + foreach (var legacy in dotPoints) { + if (legacy.Intensity > 0 && legacy.Intensity < INTENSITY_BOOST_THRESHOLD * 100) { + hasWeakSignals = true; + } + maxIntensity = Math.Max(maxIntensity, legacy.Intensity); + } + foreach (var legacy in dotPoints) { if (legacy.Intensity > 0 && legacy.Index < motorCount) { - motors[legacy.Index] = Math.Min(100, legacy.Intensity); + int intensity = legacy.Intensity; + + if (hasWeakSignals && intensity < INTENSITY_BOOST_THRESHOLD * 100 && intensity > 0) { + intensity = Math.Min(100, (int)(intensity * INTENSITY_BOOST_MULTIPLIER)); + + if (bHapticsManager.Config?.GetValue(bHapticsManager.ENABLE_DIAGNOSTIC_LOGGING) ?? false) { + ResoniteMod.Debug($"Boosted motor {legacy.Index} from {legacy.Intensity} to {intensity}"); + } + } + + motors[legacy.Index] = Math.Min(100, intensity); } } } - int extendedDuration = Math.Max(40, durationMillis); + int bufferedDuration = Math.Max(MIN_BUFFER_DURATION_MS, durationMillis * 3); + + if (bHapticsManager.Config?.GetValue(bHapticsManager.ENABLE_DIAGNOSTIC_LOGGING) ?? false) { + int activeMotors = motors.Count(m => m > 0); + ResoniteMod.Debug($"Submitting to {position}: {activeMotors} active motors, duration {bufferedDuration}ms (requested {durationMillis}ms)"); + } try { ModernBHaptics.bHapticsManager.PlayMotors( key, - extendedDuration, + bufferedDuration, modernPosition, motors ); } catch (Exception ex) { - ResoniteModLoader.ResoniteMod.Warn($"Failed to submit for {position}: {ex.Message}"); + ResoniteMod.Warn($"Failed to submit for {position}: {ex.Message}"); } } diff --git a/bHapticsManager/ModernBHapticsWorkerThread.cs b/bHapticsManager/ModernBHapticsWorkerThread.cs index 949d957..e426dec 100644 --- a/bHapticsManager/ModernBHapticsWorkerThread.cs +++ b/bHapticsManager/ModernBHapticsWorkerThread.cs @@ -1,7 +1,5 @@ -// ModernBHapticsWorkerThread.cs -// Replaces FrooxEngine's broken BHapticsDriver worker thread with a modern implementation -// Uses bHapticsLib (native .NET 9 WebSockets + MessagePack) instead of legacy SDK - +using System; +using System.Collections.Generic; using System.Reflection; using System.Threading; using Elements.Core; @@ -11,43 +9,72 @@ using ModernBHaptics = bHapticsLib; namespace bHapticsManager { - public class ModernBHapticsWorkerThread { - private class HapticPointData { - public HapticPoint Point { get; } + public class ModernBHapticsWorkerThread : IDisposable { + private class HapticPointData(HapticPoint point) { + public HapticPoint Point { get; } = point; public float TempPhi { get; set; } public float VibrationPhi { get; set; } - - public HapticPointData(HapticPoint point) { - Point = point; - TempPhi = 0f; - VibrationPhi = 0f; - } } - private const int UPDATE_INTERVAL_MS = 10; - private const int SUBMISSION_DURATION_MS = 40; + private const int UPDATE_INTERVAL_MS = 8; + private const int SUBMISSION_DURATION_MS = 100; private readonly InputInterface _inputInterface; - private readonly CancellationTokenSource _cancellationToken; + private readonly CancellationTokenSource _cancellationTokenSource = new(); private readonly Thread _workerThread; - private readonly Dictionary> _hapticPointsByDevice; - private readonly Dictionary _deviceKeys; + private readonly Dictionary> _hapticPointsByDevice = []; + private readonly Dictionary _deviceKeys = []; private float _globalPainPhi = 0f; private int _frameCount = 0; private DateTime _lastStatsReport = DateTime.Now; - + private volatile bool _running; + private volatile bool _disposed; + private readonly object _lock = new(); + private readonly List _points = new(); + private readonly Dictionary _deviceStates = new(); + private readonly AutoResetEvent _workEvent = new(false); + + private class DeviceState { + public string DeviceId { get; set; } = null!; + public LegacyBHaptics.PositionType Position { get; set; } + public bool IsConnected { get; set; } + public readonly object Lock = new(); + } + public ModernBHapticsWorkerThread(InputInterface inputInterface) { _inputInterface = inputInterface; - _cancellationToken = new CancellationTokenSource(); - _hapticPointsByDevice = new Dictionary>(); - _deviceKeys = new Dictionary(); - + + _workerThread = new Thread(WorkerThreadLoop) { + Priority = ThreadPriority.Highest, + IsBackground = true, + Name = "ModernBHapticsWorker" + }; + } + + public ModernBHapticsWorkerThread(List points) { + _inputInterface = Engine.Current.InputInterface; + lock (_lock) { + _points.AddRange(points); + bHapticsManager.Msg($"Starting worker thread with {points.Count} points across {GetDeviceCount()} devices"); + } + + _running = true; _workerThread = new Thread(WorkerThreadLoop) { Priority = ThreadPriority.Highest, IsBackground = true, Name = "ModernBHapticsWorker" }; + _workerThread.Start(); + ResoniteMod.Debug("Worker thread started successfully"); + } + + private int GetDeviceCount() { + var positions = new HashSet(); + foreach (var point in _points) { + positions.Add(GetDeviceTypeFromPosition(point.Position)); + } + return positions.Count; } public void Start() { @@ -61,21 +88,37 @@ public void Start() { return; } - ResoniteMod.Msg($"Starting worker thread with {GetTotalPointCount()} points across {_hapticPointsByDevice.Count} devices"); - _workerThread.Start(); + lock (_lock) { + _running = true; + ResoniteMod.Debug($"Starting worker thread with {GetTotalPointCount()} points across {_hapticPointsByDevice.Count} devices"); + _workerThread.Start(); + } } public void Stop() { - if (!_workerThread.IsAlive) { - return; - } + if (_disposed) return; + + ResoniteMod.Debug("Stopping worker thread..."); + _running = false; - _cancellationToken.Cancel(); + _cancellationTokenSource.Cancel(); - if (!_workerThread.Join(TimeSpan.FromSeconds(2))) { - ResoniteMod.Warn("Worker thread did not stop gracefully, aborting..."); - _workerThread.Interrupt(); + _workEvent.Set(); + + if (_workerThread != null && _workerThread.IsAlive && !_workerThread.Join(TimeSpan.FromSeconds(2))) { + bHapticsManager.Warn("Worker thread did not stop gracefully, interrupting..."); + try { + _workerThread.Interrupt(); + if (!_workerThread.Join(TimeSpan.FromSeconds(1))) { + bHapticsManager.Warn("Worker thread did not respond to interrupt"); + } + } + catch (Exception ex) { + bHapticsManager.Error($"Error interrupting worker thread: {ex}"); + } } + + ResoniteMod.Debug("Worker thread stopped"); } private bool PopulateHapticPoints() { @@ -85,21 +128,21 @@ private bool PopulateHapticPoints() { ResoniteMod.Warn("No haptic points registered in InputInterface"); return false; } - + for (int i = 0; i < totalPoints; i++) { HapticPoint point = _inputInterface.GetHapticPoint(i); if (point == null) continue; - + LegacyBHaptics.PositionType deviceType = GetDeviceTypeFromPosition(point.Position); - - if (!_hapticPointsByDevice.ContainsKey(deviceType)) { - _hapticPointsByDevice[deviceType] = new List(); + + if (!_hapticPointsByDevice.TryGetValue(deviceType, out List? value)) { + value = new List(); + _hapticPointsByDevice[deviceType] = value; _deviceKeys[deviceType] = Guid.NewGuid().ToString(); } - - _hapticPointsByDevice[deviceType].Add(new HapticPointData(point)); + value.Add(new(point)); } - + return true; } catch (Exception ex) { @@ -121,7 +164,7 @@ private LegacyBHaptics.PositionType GetDeviceTypeFromPosition(HapticPointPositio }; } - private LegacyBHaptics.PositionType GetArmSide(HapticPointPosition position) { + private static LegacyBHaptics.PositionType GetArmSide(HapticPointPosition position) { try { var sideProperty = position.GetType().GetProperty("Side"); if (sideProperty != null) { @@ -134,8 +177,8 @@ private LegacyBHaptics.PositionType GetArmSide(HapticPointPosition position) { catch { } return LegacyBHaptics.PositionType.ForearmR; } - - private LegacyBHaptics.PositionType GetHandSide(HapticPointPosition position) { + + private static LegacyBHaptics.PositionType GetHandSide(HapticPointPosition position) { try { var sideProperty = position.GetType().GetProperty("Side"); if (sideProperty != null) { @@ -148,8 +191,8 @@ private LegacyBHaptics.PositionType GetHandSide(HapticPointPosition position) { catch { } return LegacyBHaptics.PositionType.HandR; } - - private LegacyBHaptics.PositionType GetLegSide(HapticPointPosition position) { + + private static LegacyBHaptics.PositionType GetLegSide(HapticPointPosition position) { try { var sideProperty = position.GetType().GetProperty("Side"); if (sideProperty != null) { @@ -172,16 +215,25 @@ private int GetTotalPointCount() { } private void WorkerThreadLoop() { - var dotPoints = new List(); + List dotPoints = []; try { - while (!_cancellationToken.IsCancellationRequested) { - var frameStart = DateTime.Now; + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + long nextFrameTime = 0; + + while (!_cancellationTokenSource.Token.IsCancellationRequested && !_disposed && _running) { + long currentTime = stopwatch.ElapsedMilliseconds; + + if (currentTime < nextFrameTime) { + int sleepTime = (int)(nextFrameTime - currentTime); + if (sleepTime > 0) { + Thread.Sleep(sleepTime); + } + } - Thread.Sleep(UPDATE_INTERVAL_MS); + nextFrameTime = stopwatch.ElapsedMilliseconds + UPDATE_INTERVAL_MS; float dt = UPDATE_INTERVAL_MS / 1000f; - float maxPain = 0f; foreach (var deviceGroup in _hapticPointsByDevice.Values) { @@ -204,29 +256,27 @@ private void WorkerThreadLoop() { foreach (var pointData in points) { HapticPoint point = pointData.Point; - float intensity = point.Force; - + float painAmplitude = MathX.Pow(MathX.Abs(MathX.Sin(_globalPainPhi)), 2f) * (float)MathX.Max(0, MathX.Sign(MathX.Sin(_globalPainPhi * 0.5f))); painAmplitude *= MathX.Pow(point.Pain, 0.5f); painAmplitude += RandomX.Value * MathX.Pow(point.Pain, 0.25f) * 0.1f; intensity = MathX.Max(intensity, painAmplitude); - + float normalizedTemp = MathX.Abs(point.Temperature / 100f); pointData.TempPhi += normalizedTemp * 4f; pointData.TempPhi %= 20000f; float tempAmplitude = normalizedTemp * MathX.SimplexNoise(pointData.TempPhi); intensity = MathX.Max(intensity, tempAmplitude); - + pointData.VibrationPhi += MathF.PI * 2f * dt * MathX.Lerp(0.1f, 10f, point.Vibration); pointData.VibrationPhi %= MathF.PI * 2f; float vibrationAmplitude = (MathX.Sin(pointData.VibrationPhi) * 0.5f + 0.5f) * point.Vibration; intensity = MathX.Max(intensity, vibrationAmplitude); - + int intensityInt = MathX.Clamp(MathX.RoundToInt(intensity * 100f), 0, 100); - - dotPoints.Add(new LegacyBHaptics.DotPoint(motorIndex++, intensityInt)); + dotPoints.Add(new(motorIndex++, intensityInt)); } if (dotPoints.Count > 0) { @@ -238,18 +288,124 @@ private void WorkerThreadLoop() { if ((DateTime.Now - _lastStatsReport).TotalSeconds >= 10) { if (bHapticsManager.Config?.GetValue(bHapticsManager.ENABLE_DIAGNOSTIC_LOGGING) ?? false) { double avgFps = _frameCount / (DateTime.Now - _lastStatsReport).TotalSeconds; - ResoniteMod.Msg($"Worker thread: {avgFps:F1} Hz avg, {_hapticPointsByDevice.Count} devices, {GetTotalPointCount()} points"); + ResoniteMod.Debug($"Worker thread: {avgFps:F1} Hz avg, {_hapticPointsByDevice.Count} devices, {GetTotalPointCount()} points"); } _frameCount = 0; _lastStatsReport = DateTime.Now; } } + + ResoniteMod.Debug("Worker thread exiting normally"); } catch (ThreadInterruptedException) { + ResoniteMod.Debug("Worker thread interrupted"); + } + catch (ThreadAbortException) { + ResoniteMod.Debug("Worker thread aborted"); + } + catch (Exception ex) { + bHapticsManager.Error($"Worker thread error: {ex.Message}"); + } + finally { + try { + ModernBHaptics.bHapticsManager.StopPlayingAll(); + } + catch (Exception ex) { + bHapticsManager.Error($"Error stopping haptic playback: {ex}"); + } + + lock (_lock) { + _running = false; + } + ResoniteMod.Debug("Worker thread cleanup complete"); + } + } + + public void OnDeviceConnected(LegacyBHaptics.PositionType position) { + var deviceId = GetDeviceId(position); + lock (_deviceStates) { + if (_deviceStates.TryGetValue(deviceId, out var state)) { + lock (state.Lock) { + state.IsConnected = true; + } + } else { + _deviceStates[deviceId] = new DeviceState { + DeviceId = deviceId, + Position = position, + IsConnected = true + }; + } + } + ResoniteMod.Debug($"Device {deviceId} marked as connected in worker thread"); + } + + public void OnDeviceDisconnected(LegacyBHaptics.PositionType position) { + var deviceId = GetDeviceId(position); + lock (_deviceStates) { + if (_deviceStates.TryGetValue(deviceId, out var state)) { + lock (state.Lock) { + state.IsConnected = false; + } + } + } + ResoniteMod.Debug($"Device {deviceId} marked as disconnected in worker thread"); + } + + private string GetDeviceId(LegacyBHaptics.PositionType position) { + return position switch { + LegacyBHaptics.PositionType.Head => "Head", + LegacyBHaptics.PositionType.VestFront => "VestFront", + LegacyBHaptics.PositionType.VestBack => "VestBack", + LegacyBHaptics.PositionType.Vest => "VestFront", + LegacyBHaptics.PositionType.ForearmL => "ArmLeft", + LegacyBHaptics.PositionType.ForearmR => "ArmRight", + LegacyBHaptics.PositionType.FootL => "FootLeft", + LegacyBHaptics.PositionType.FootR => "FootRight", + LegacyBHaptics.PositionType.HandL => "GloveLeft", + LegacyBHaptics.PositionType.HandR => "GloveRight", + _ => "Unknown" + }; + } + + public void TriggerUpdate() { + if (!_disposed && _running) { + _workEvent.Set(); + } + } + + public void Dispose() { + if (_disposed) return; + + ResoniteMod.Debug("Disposing worker thread..."); + _disposed = true; + + Stop(); + + try { + _cancellationTokenSource.Dispose(); + } + catch (Exception ex) { + bHapticsManager.Error($"Error disposing cancellation token: {ex}"); + } + + try { + _workEvent.Dispose(); } catch (Exception ex) { - ResoniteMod.Error($"Worker thread error: {ex.Message}"); + bHapticsManager.Error($"Error disposing work event: {ex}"); + } + + lock (_lock) { + _points.Clear(); + _hapticPointsByDevice.Clear(); + _deviceKeys.Clear(); } + + lock (_deviceStates) { + _deviceStates.Clear(); + } + + ResoniteMod.Debug("Worker thread disposed"); } } } diff --git a/bHapticsManager/PositionMapper.cs b/bHapticsManager/PositionMapper.cs index dfbc027..eb76fa2 100644 --- a/bHapticsManager/PositionMapper.cs +++ b/bHapticsManager/PositionMapper.cs @@ -1,5 +1,3 @@ -// PositionMapper.cs -// Maps between legacy and modern bHaptics position types using LegacyBHaptics = Bhaptics.Tact; using ModernBHaptics = bHapticsLib; @@ -40,7 +38,7 @@ public static ModernBHaptics.PositionID MapLegacyToModern(LegacyBHaptics.Positio }; } - public static int[] ConvertDotPointsToMotorArray(List legacyPoints, ModernBHaptics.PositionID position) { + public static int[]? ConvertDotPointsToMotorArray(List legacyPoints, ModernBHaptics.PositionID position) { if (legacyPoints == null || legacyPoints.Count == 0) return null; diff --git a/bHapticsManager/Properties/AssemblyInfo.cs b/bHapticsManager/Properties/AssemblyInfo.cs index 3a796fe..ebe7bca 100644 --- a/bHapticsManager/Properties/AssemblyInfo.cs +++ b/bHapticsManager/Properties/AssemblyInfo.cs @@ -1,9 +1,11 @@ -using System.Reflection; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; [assembly: AssemblyTitle("bHapticsManager")] [assembly: AssemblyProduct("bHapticsManager")] -[assembly: AssemblyDescription("A Resonite Mod to implement a modern .NET9 bHaptics SDK with hot-plug support and battery monitoring")] +[assembly: AssemblyDescription("A Resonite Mod to implement a modern .NET9 bHaptics SDK with hot-plug support")] [assembly: AssemblyCompany("NalaTheThird")] -[assembly: AssemblyCopyright("Copyright © 2025 NalaTheThird")] +[assembly: AssemblyCopyright("Copyright (C) 2025 NalaTheThird")] [assembly: AssemblyVersion(bHapticsManager.bHapticsManager.VERSION_CONSTANT)] [assembly: AssemblyFileVersion(bHapticsManager.bHapticsManager.VERSION_CONSTANT)] diff --git a/bHapticsManager/RemoteHapticSource.cs b/bHapticsManager/RemoteHapticSource.cs index 5b465f7..c684050 100644 --- a/bHapticsManager/RemoteHapticSource.cs +++ b/bHapticsManager/RemoteHapticSource.cs @@ -1,7 +1,3 @@ -// RemoteHapticSource.cs -// Implements IDirectHapticSource to inject remote haptic data into the local HapticPoint sampling system. -// This allows remote users' haptics to be felt locally without breaking DirectTagHapticSource. - using Elements.Core; using FrooxEngine; using System.Collections.Concurrent; @@ -17,11 +13,12 @@ internal class RemoteHapticSource : IDirectHapticSource { private readonly int _hapticPointIndex; private readonly World _world; private readonly RefID _refId; + private readonly DummyWorldElement _dummyParent; public RefID ReferenceID => _refId; public string Name => $"RemoteHapticSource_{_hapticPointIndex}"; public World World => _world; - public IWorldElement Parent => null; + public IWorldElement Parent => _dummyParent; public bool IsLocalElement => false; public bool IsPersistent => false; public bool IsRemoved => false; @@ -31,6 +28,7 @@ private RemoteHapticSource(int hapticPointIndex, World world) { _world = world; ulong hash = (ulong)$"RemoteHapticSource_{hapticPointIndex}_{Guid.NewGuid()}".GetHashCode(); _refId = new RefID(hash); + _dummyParent = new DummyWorldElement(world); } public float GetIntensity(SensationClass sensation) { @@ -97,9 +95,29 @@ public static void CleanupStaleData() { } public void ChildChanged(IWorldElement child) { } - public DataTreeNode Save(SaveControl control) => null; + public DataTreeNode Save(SaveControl control) => default!; public void Load(DataTreeNode node, LoadControl control) { } - public string GetSyncMemberName(ISyncMember member) => null; + public string GetSyncMemberName(ISyncMember member) => string.Empty; + + private class DummyWorldElement : IWorldElement { + private readonly World _world; + + public DummyWorldElement(World world) { + _world = world; + } + + public RefID ReferenceID => RefID.Null; + public string Name => "DummyParent"; + public World World => _world; + public IWorldElement Parent => null; + public bool IsLocalElement => false; + public bool IsPersistent => false; + public bool IsRemoved => false; + public void ChildChanged(IWorldElement child) { } + public DataTreeNode Save(SaveControl control) => default!; + public void Load(DataTreeNode node, LoadControl control) { } + public string GetSyncMemberName(ISyncMember member) => string.Empty; + } private struct RemoteHapticData { public float Force; diff --git a/bHapticsManager/TorsoMapperFix.cs b/bHapticsManager/TorsoMapperFix.cs index a41f322..8668dd1 100644 --- a/bHapticsManager/TorsoMapperFix.cs +++ b/bHapticsManager/TorsoMapperFix.cs @@ -1,7 +1,3 @@ -// TorsoMapperFix.cs -// Harmony patch to fix the 90 degree rotation bug in TorsoHapticPointMapper -// The original code uses GetClosestAxis() which snaps to the wrong axis on rotated avatars - using HarmonyLib; using Elements.Core; using FrooxEngine; @@ -13,6 +9,18 @@ namespace bHapticsManager { + public static class TorsoMapperFix { + public static void ApplyPatches(Harmony harmony) { + try { + harmony.PatchAll(typeof(TorsoMapperFix)); + ResoniteMod.Debug("TorsoMapperFix patches applied successfully"); + } + catch (Exception ex) { + ResoniteMod.Error($"Failed to apply TorsoMapperFix: {ex}"); + } + } + } + [HarmonyPatch(typeof(CommonAvatarBuilder), "BuildAvatar")] public static class CommonAvatarBuilderPatch { @@ -20,7 +28,6 @@ public static class CommonAvatarBuilderPatch { static void Postfix(UserRoot userRoot) { try { - // Only apply the TorsoMapper patch once when first avatar is equipped if (!_torsoMapperPatchApplied) { var harmony = new Harmony("com.nalathethird.bHapticsManager.TorsoMapper"); ApplyTorsoMapperPatch(harmony); @@ -34,9 +41,8 @@ static void Postfix(UserRoot userRoot) { private static void ApplyTorsoMapperPatch(Harmony harmony) { try { - // Find TorsoHapticPointMapper type var frooxEngineAssembly = typeof(FrooxEngine.Engine).Assembly; - Type torsoMapperType = frooxEngineAssembly.GetType("FrooxEngine.TorsoHapticPointMapper", false) + Type? torsoMapperType = frooxEngineAssembly.GetType("FrooxEngine.TorsoHapticPointMapper", false) ?? frooxEngineAssembly.GetType("TorsoHapticPointMapper", false); if (torsoMapperType == null) { @@ -44,19 +50,17 @@ private static void ApplyTorsoMapperPatch(Harmony harmony) { return; } - // Find MapPoints method with correct signature: (HapticManager, float, Span) var mapPointsMethod = torsoMapperType.GetMethod("MapPoints", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, new Type[] { - frooxEngineAssembly.GetType("FrooxEngine.HapticManager"), + frooxEngineAssembly.GetType("FrooxEngine.HapticManager")!, typeof(float), typeof(Span) }, null); if (mapPointsMethod == null) { - // Fallback: try finding any MapPoints method mapPointsMethod = torsoMapperType.GetMethod("MapPoints", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); @@ -66,8 +70,7 @@ private static void ApplyTorsoMapperPatch(Harmony harmony) { } } - // Find SlotPositioning.GetClosestAxis method - we need this to identify it in IL - Type slotPositioningType = frooxEngineAssembly.GetType("FrooxEngine.SlotPositioning", false) + Type? slotPositioningType = frooxEngineAssembly.GetType("FrooxEngine.SlotPositioning", false) ?? frooxEngineAssembly.GetType("SlotPositioning", false); if (slotPositioningType == null) { @@ -82,7 +85,6 @@ private static void ApplyTorsoMapperPatch(Harmony harmony) { null); if (getClosestAxisMethod == null) { - // Try without byref parameter getClosestAxisMethod = slotPositioningType.GetMethod("GetClosestAxis", BindingFlags.Public | BindingFlags.Static); } @@ -92,7 +94,6 @@ private static void ApplyTorsoMapperPatch(Harmony harmony) { return; } - // Apply transpiler patch var transpiler = typeof(TorsoMapperFixTranspiler).GetMethod( nameof(TorsoMapperFixTranspiler.ReplaceGetClosestAxis), BindingFlags.Public | BindingFlags.Static); @@ -102,7 +103,6 @@ private static void ApplyTorsoMapperPatch(Harmony harmony) { return; } - // Store reference to GetClosestAxis for the transpiler TorsoMapperFixTranspiler.TargetMethod = getClosestAxisMethod; harmony.Patch(mapPointsMethod, transpiler: new HarmonyMethod(transpiler)); @@ -117,7 +117,7 @@ private static void ApplyTorsoMapperPatch(Harmony harmony) { public static class TorsoMapperFixTranspiler { - public static MethodInfo TargetMethod; + public static MethodInfo TargetMethod = null!; public static float3 IdentityGetClosestAxis(Slot slot, float3 direction) { return direction; @@ -132,7 +132,6 @@ public static IEnumerable ReplaceGetClosestAxis(IEnumerable(instructions); int patchedCount = 0; - // Get reference to our identity functions var identityMethod = typeof(TorsoMapperFixTranspiler).GetMethod( nameof(IdentityGetClosestAxis), BindingFlags.Public | BindingFlags.Static); @@ -146,23 +145,18 @@ public static IEnumerable ReplaceGetClosestAxis(IEnumerable 1 && parameters[1].ParameterType.IsByRef; - // Replace with call to our identity function codes[i] = new CodeInstruction(OpCodes.Call, useByRef ? identityMethodByRef : identityMethod); patchedCount++; @@ -171,7 +165,7 @@ public static IEnumerable ReplaceGetClosestAxis(IEnumerable 0) { - ResoniteMod.Msg($"Replaced {patchedCount} GetClosestAxis call(s)"); + ResoniteMod.Debug($"Replaced {patchedCount} GetClosestAxis call(s)"); } else { ResoniteMod.Warn("No GetClosestAxis calls found - method may have changed"); } @@ -180,7 +174,7 @@ public static IEnumerable ReplaceGetClosestAxis(IEnumerable "bHapticsManager"; public override string Author => "NalaTheThird"; public override string Version => VERSION_CONSTANT; public override string Link => "https://github.com/nalathethird/bHapticsManager"; - public static ModConfiguration Config; - private static bool _workerThreadStarted = false; + public static ModConfiguration? Config = null; [AutoRegisterConfigKey] public static readonly ModConfigurationKey ENABLE_HOTPLUG = @@ -32,86 +34,314 @@ public class bHapticsManager : ResoniteMod { internal const int MAX_RETRIES = 10; internal const bool AUTO_RECONNECT = true; - public override void OnEngineInit() { - Config = GetConfiguration(); - Config.Save(true); - - BHapticsConnection.Initialize(); - - var harmony = new Harmony("com.nalathethird.bHapticsManager"); - harmony.PatchAll(); - - if (!Config.GetValue(ENABLE_HOTPLUG)) { - Warn("Hot-plug is DISABLED - devices must be connected before starting Resonite"); - } - - if (Config.GetValue(ENABLE_DIAGNOSTIC_LOGGING)) { - Msg("Diagnostic logging ENABLED - expect verbose output"); - } - - FrooxEngine.Engine.Current.OnShutdown += OnEngineShutdown; - - Task.Run(async () => { - try { - if (Config.GetValue(ENABLE_DIAGNOSTIC_LOGGING)) { - Msg("Worker thread startup task executing - waiting for haptic points..."); - } - - int retries = 0; - const int maxRetries = 100; - - while (retries < maxRetries) { - await Task.Delay(100); - - var engine = FrooxEngine.Engine.Current; - if (engine?.InputInterface != null && engine.InputInterface.HapticPointCount > 0) { - if (Config.GetValue(ENABLE_DIAGNOSTIC_LOGGING)) { - Msg($"Haptic points registered (count: {engine.InputInterface.HapticPointCount}) after {retries * 100}ms"); - } - break; - } - - retries++; - } - - if (retries >= maxRetries) { - Warn("Timed out waiting for haptic points - worker thread may not function correctly"); - } - - await Task.Delay(500); - - if (_workerThreadStarted) { - return; - } - - BHapticsConnection.StartWorkerThread(); - _workerThreadStarted = true; - } - catch (Exception ex) { - Error($"Error in worker thread startup task: {ex.Message}"); - } - }); + private static ModernBHapticsWorkerThread? _workerThread = null; + private static DeviceEventHandler? _eventHandler = null; + private static bool _initialized = false; + private static bool _patchesApplied = false; + private static bool _shutdownHookRegistered = false; + + public override void OnEngineInit() + { + try + { + if (_initialized) + { + ResoniteMod.Debug("bHapticsManager already initialized, skipping"); + return; + } + + Config = GetConfiguration()!; + + if (Config == null) + { + Error("Failed to get mod configuration"); + return; + } + + if (!_patchesApplied) + { + try + { + Harmony harmony = new Harmony("com.bhaptics.resonite.fix"); + + HapticPlayerPatches.ApplyPatches(harmony); + ResoniteMod.Debug("HapticPlayerPatches applied"); + + HapticMethodPatches.ApplyPatches(harmony); + ResoniteMod.Debug("HapticMethodPatches applied"); + + TorsoMapperFix.ApplyPatches(harmony); + ResoniteMod.Debug("TorsoMapperFix applied"); + + LegacyCompatibilityLayer.ApplyPatches(harmony); + ResoniteMod.Debug("LegacyCompatibilityLayer applied"); + + _patchesApplied = true; + Msg("All patches applied successfully"); + } + catch (Exception ex) + { + Error($"Failed to apply patches: {ex}"); + } + } + + if (!BHapticsConnection.Initialize()) + { + Error("Failed to initialize bHaptics connection - is bHaptics Player running?"); + return; + } + + Msg("bHaptics connection initialized"); + + var engine = Engine.Current; + if (engine == null) + { + Error("Engine.Current is null"); + return; + } + + if (!_shutdownHookRegistered) + { + try + { + engine.OnShutdownRequest += OnEngineShutdown; + _shutdownHookRegistered = true; + ResoniteMod.Debug("Shutdown hook registered"); + } + catch (Exception ex) + { + Error($"Failed to register shutdown hook: {ex}"); + } + } + + if (engine.WorldManager == null) + { + Error("Engine.WorldManager is null"); + return; + } + + var focusedWorld = engine.WorldManager.FocusedWorld; + if (focusedWorld == null) + { + Warn("No focused world, initializing on next world focus"); + + void WorldFocusedHandler(World world) + { + if (world != null) + { + // Check if initialization is needed and atomically set _initialized to true + if (!Interlocked.CompareExchange(ref _initialized, true, false)) + { + try + { + world.RunSynchronously(() => InitializeHaptics()); + // Unsubscribe from the event after first successful initialization + engine.WorldManager.WorldFocused -= WorldFocusedHandler; + ResoniteMod.Debug("WorldFocused event handler unsubscribed after initialization"); + } + catch (Exception ex) + { + Error($"Failed to initialize haptics on world focus: {ex}"); + // Reset _initialized to allow retry on next world focus + Interlocked.Exchange(ref _initialized, false); + } + } + } + } + + engine.WorldManager.WorldFocused += WorldFocusedHandler; + } + else + { + // Check if initialization is needed and atomically set _initialized to true + if (!Interlocked.CompareExchange(ref _initialized, true, false)) + { + focusedWorld.RunSynchronously(() => InitializeHaptics()); + } + } + Msg("bHapticsManager initialized successfully"); + } + catch (Exception ex) + { + Error($"Failed to initialize bHapticsManager: {ex}"); + } + } + + private static void InitializeHaptics() + { + try + { + ResoniteMod.Debug("Initializing haptics system..."); + + var connectedDevices = BHapticsConnection.Instance?.GetConnectedDevices(); + if (connectedDevices == null || connectedDevices.Count == 0) + { + Warn("No bHaptics devices detected"); + return; + } + + Msg($"Detected {connectedDevices.Count} device(s)"); + + var allPoints = new List(); + foreach (var position in connectedDevices) + { + try + { + DeviceRegistration.RegisterDevice(position); + ResoniteMod.Debug($"Registered device: {position}"); + } + catch (Exception ex) + { + Error($"Failed to register device {position}: {ex}"); + } + } + + var inputInterface = Engine.Current?.InputInterface; + if (inputInterface == null) + { + Error("InputInterface is null, cannot initialize haptics"); + return; + } + + int pointCount = inputInterface.HapticPointCount; + ResoniteMod.Debug($"Found {pointCount} haptic points registered"); + + for (int i = 0; i < pointCount; i++) + { + var point = inputInterface.GetHapticPoint(i); + if (point != null) + { + allPoints.Add(point); + } + } + + if (allPoints.Count == 0) + { + Warn("No haptic points available"); + return; + } + + Msg($"Starting worker thread with {allPoints.Count} points across {connectedDevices.Count} devices"); + + try + { + _workerThread = new ModernBHapticsWorkerThread(allPoints); + ResoniteMod.Debug("Worker thread created successfully"); + } + catch (Exception ex) + { + Error($"Failed to create worker thread: {ex}"); + return; + } + + try + { + _eventHandler = new DeviceEventHandler(); + if (_workerThread != null) + { + _eventHandler.Initialize(_workerThread); + ResoniteMod.Debug("Event handlers subscribed successfully"); + } + } + catch (Exception ex) + { + Error($"Failed to initialize event handlers: {ex}"); + } + + Msg("Haptics system initialized successfully"); + } + catch (Exception ex) + { + Error($"Error initializing haptics: {ex}"); + } + } + + private static void OnEngineShutdown() + { + try + { + ResoniteMod.Debug("Engine shutdown requested."); + Msg("Starting bHapticsManager shutdown..."); + + if (_eventHandler != null) + { + try + { + ResoniteMod.Debug("Disposing event handler..."); + _eventHandler.Dispose(); + _eventHandler = null; + ResoniteMod.Debug("Event handler disposed"); + } + catch (Exception ex) + { + Error($"Error disposing event handler: {ex}"); + } + } + + if (_workerThread != null) + { + try + { + ResoniteMod.Debug("Disposing worker thread..."); + _workerThread.Dispose(); + _workerThread = null; + ResoniteMod.Debug("Worker thread disposed"); + } + catch (Exception ex) + { + Error($"Error disposing worker thread: {ex}"); + } + } + + try + { + ResoniteMod.Debug("Clearing device registrations..."); + DeviceRegistration.ClearAllRegistrations(); + ResoniteMod.Debug("Device registrations cleared"); + } + catch (Exception ex) + { + Error($"Error clearing device registrations: {ex}"); + } + + try + { + ResoniteMod.Debug("Shutting down bHaptics connection..."); + BHapticsConnection.Shutdown(); + ResoniteMod.Debug("bHaptics connection shutdown complete"); + } + catch (Exception ex) + { + Error($"Error shutting down bHaptics connection: {ex}"); + } + + _initialized = false; + Msg("bHapticsManager shutdown complete"); + } + catch (Exception ex) + { + Error($"Critical error during shutdown: {ex}"); + } + } + + public static void Msg(string message) + { + ResoniteMod.Msg($"[bHapticsManager] {message}"); + } + + public static void Warn(string message) + { + ResoniteMod.Warn($"[bHapticsManager] {message}"); } - private void OnEngineShutdown() { - try { - try { - ModernBHaptics.bHapticsManager.StopPlayingAll(); - } catch { } - - try { - BHapticsConnection.Shutdown(); - } catch { } - - Thread.Sleep(100); - } - catch (Exception ex) { - Error("Error during shutdown: " + ex.Message); - } + public static void Error(string message) + { + ResoniteMod.Error($"[bHapticsManager] {message}"); } - public static void Error(Exception ex) { - ResoniteMod.Error(ex); + public static void Error(Exception ex) + { + ResoniteMod.Error($"[bHapticsManager] {ex}"); } } } diff --git a/bHapticsManager/bHapticsManager.csproj b/bHapticsManager/bHapticsManager.csproj index 544eaf1..ace4877 100644 --- a/bHapticsManager/bHapticsManager.csproj +++ b/bHapticsManager/bHapticsManager.csproj @@ -13,45 +13,14 @@ $(MSBuildThisFileDirectory)Resonite/ E:\SteamLibrary\steamapps\common\Resonite\ + C:\Program Files (x86)\Steam\steamapps\common\Resonite\ $(HOME)/.steam/steam/steamapps/common/Resonite/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - E:\SteamLibrary\steamapps\common\Resonite\Bhaptics.Tact.dll + $(ResonitePath)Bhaptics.Tact.dll + False $(ResonitePath)rml_libs\bHapticsLib.dll @@ -59,13 +28,16 @@ - E:\SteamLibrary\steamapps\common\Resonite\Elements.Assets.dll + $(ResonitePath)Elements.Assets.dll + False - E:\SteamLibrary\steamapps\common\Resonite\Elements.Core.dll + $(ResonitePath)Elements.Core.dll + False - E:\SteamLibrary\steamapps\common\Resonite\Renderite.Shared.dll + $(ResonitePath)Renderite.Shared.dll + False $(ResonitePath)Libraries\ResoniteModLoader.dll @@ -80,7 +52,8 @@ False - E:\SteamLibrary\steamapps\common\Resonite\SkyFrost.Base.Models.dll + $(ResonitePath)SkyFrost.Base.Models.dll + False