From f984ce33ff78de2c1688bf0825820f38285bd11c Mon Sep 17 00:00:00 2001 From: NalaTheThird <36301692+nalathethird@users.noreply.github.com> Date: Sat, 22 Nov 2025 20:18:03 -0600 Subject: [PATCH 01/40] Refactor and enhance bHapticsManager architecture Refactored `BHapticsConnection` into a singleton for better state management and added event-based device handling. Improved thread safety and logging in initialization and shutdown processes. Replaced static `DeviceEventHandler` with an instance-based implementation supporting `IDisposable`. Enhanced `DeviceRegistration` with thread-safe methods, a `ClearAllRegistrations` function, and improved logging. Upgraded `ModernBHapticsWorkerThread` with dynamic device handling, better thread management, and detailed diagnostics. Added intensity boosting for weak signals in `LegacyCompatibilityLayer` and reduced submission intervals for responsiveness. Modularized patch application in `HapticPlayerPatches`, `HapticMethodPatches`, and `TorsoMapperFix`. Improved error handling and compatibility with the modern bHaptics SDK. Streamlined mod initialization in `bHapticsManager`, added lazy haptics initialization, and ensured proper shutdown handling. Updated `bHapticsManager.csproj` to support additional Resonite paths and removed unused references. General improvements include better maintainability, enhanced modularity, and updated documentation. --- bHapticsManager/BHapticsConnection.cs | 130 +++++-- bHapticsManager/DeviceEventHandler.cs | 231 ++++++----- bHapticsManager/DeviceRegistration.cs | 33 +- bHapticsManager/DiagnosticPatches.cs | 184 ++++----- bHapticsManager/HapticMethodPatches.cs | 24 +- bHapticsManager/HapticPlayerPatches.cs | 107 ++--- bHapticsManager/LegacyCompatibilityLayer.cs | 59 ++- bHapticsManager/ModernBHapticsWorkerThread.cs | 279 ++++++++++--- bHapticsManager/PositionMapper.cs | 4 +- bHapticsManager/Properties/AssemblyInfo.cs | 8 +- bHapticsManager/RemoteHapticSource.cs | 32 +- bHapticsManager/TorsoMapperFix.cs | 42 +- bHapticsManager/bHapticsManager.cs | 367 ++++++++++++++---- bHapticsManager/bHapticsManager.csproj | 49 +-- 14 files changed, 999 insertions(+), 550 deletions(-) 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..2fe5980 100644 --- a/bHapticsManager/DeviceEventHandler.cs +++ b/bHapticsManager/DeviceEventHandler.cs @@ -1,130 +1,125 @@ -// 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 bool _disposed; + private ModernBHapticsWorkerThread _workerThread = null!; + + public void Initialize(ModernBHapticsWorkerThread workerThread) + { + _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}"); + } + }); + } + } + + _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}"); - } - } + _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; + + 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}"); - } - } - } + _workerThread = null!; + _disposed = true; + } + } } diff --git a/bHapticsManager/DeviceRegistration.cs b/bHapticsManager/DeviceRegistration.cs index ff9645a..ffbf14d 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,14 +12,34 @@ namespace bHapticsManager { public static class DeviceRegistration { - private static readonly HashSet _registeredDevices = new(); + private static readonly object _lock = new object(); + private static readonly HashSet _registeredDevices = new HashSet(); private static readonly Dictionary _pendingRegistrations = new(); + private static readonly Dictionary> _devicePoints = new Dictionary>(); private static InputInterface? _inputInterface; private static BHapticsDriver? _bhapticsDriver; 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(); + _devicePoints.Clear(); + } + } + public static Task TryRegisterDeviceAsync(ModernBHaptics.PositionID position) { lock (_registrationLock) { if (_registeredDevices.Contains(position)) { @@ -65,7 +86,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 +96,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 +280,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..d4d3d5b 100644 --- a/bHapticsManager/DiagnosticPatches.cs +++ b/bHapticsManager/DiagnosticPatches.cs @@ -1,127 +1,107 @@ -// 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 { + } } } - } - - /// - /// 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 { } - } - catch { - // Silent fail } } } diff --git a/bHapticsManager/HapticMethodPatches.cs b/bHapticsManager/HapticMethodPatches.cs index 602fa6a..33a6631 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(IsActivePatch).Assembly); + 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..61329ae 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; + return false; } } - /// - /// 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..c8cf62b 100644 --- a/bHapticsManager/LegacyCompatibilityLayer.cs +++ b/bHapticsManager/LegacyCompatibilityLayer.cs @@ -1,15 +1,30 @@ -// 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; + + private const int MIN_SUBMISSION_INTERVAL_MS = 5; + private const int MIN_BUFFER_DURATION_MS = 100; + private const float INTENSITY_BOOST_THRESHOLD = 0.3f; + private const float INTENSITY_BOOST_MULTIPLIER = 1.5f; public static void SubmitFrame(string key, LegacyBHaptics.PositionType position, List dotPoints, int durationMillis) { @@ -47,6 +62,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 +94,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..173c29d 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,44 +9,73 @@ 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() { if (_workerThread.IsAlive) { @@ -61,21 +88,43 @@ 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; + + try { + _cancellationTokenSource.Cancel(); + } + catch (ObjectDisposedException) { } - _cancellationToken.Cancel(); + _workEvent.Set(); - if (!_workerThread.Join(TimeSpan.FromSeconds(2))) { - ResoniteMod.Warn("Worker thread did not stop gracefully, aborting..."); - _workerThread.Interrupt(); + if (_workerThread != null && _workerThread.IsAlive) { + if (!_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 +134,22 @@ 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(point)]); + _hapticPointsByDevice[deviceType] = value; _deviceKeys[deviceType] = Guid.NewGuid().ToString(); + } else { + value.Add(new(point)); } - - _hapticPointsByDevice[deviceType].Add(new HapticPointData(point)); } - + return true; } catch (Exception ex) { @@ -121,7 +171,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 +184,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 +198,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 +222,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 +263,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 +295,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) { - ResoniteMod.Error($"Worker thread error: {ex.Message}"); + 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) { + 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..f9d67c4 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 => this; + 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..81c7f02 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(CommonAvatarBuilderPatch).Assembly); + 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,293 @@ 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"); + engine.WorldManager.WorldFocused += (world) => + { + if (!_initialized && world != null) + { + world.RunSynchronously(() => InitializeHaptics()); + } + }; + } + else + { + focusedWorld.RunSynchronously(() => InitializeHaptics()); + } + + _initialized = true; + 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(string reason) + { + try + { + ResoniteMod.Debug($"Engine shutdown requested: {reason}"); + 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 From 87892cd2a781041f8bfa40ff43c4da5d84664193 Mon Sep 17 00:00:00 2001 From: Zeia Nala <36301692+nalathethird@users.noreply.github.com> Date: Sat, 22 Nov 2025 20:26:10 -0600 Subject: [PATCH 02/40] Update bHapticsManager/HapticPlayerPatches.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- bHapticsManager/HapticPlayerPatches.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bHapticsManager/HapticPlayerPatches.cs b/bHapticsManager/HapticPlayerPatches.cs index 61329ae..3a04c2b 100644 --- a/bHapticsManager/HapticPlayerPatches.cs +++ b/bHapticsManager/HapticPlayerPatches.cs @@ -161,7 +161,7 @@ static void Postfix(LegacyBHaptics.HapticPlayer __instance) { [HarmonyPatch(typeof(LegacyBHaptics.HapticPlayer), "Dispose")] public class HapticPlayerDisposePatch { static bool Prefix() { - return false; + return true; } } From 34ab9eba7e6ac00aa2df0bc0d1fa1f6f0a438d8f Mon Sep 17 00:00:00 2001 From: Zeia Nala <36301692+nalathethird@users.noreply.github.com> Date: Sat, 22 Nov 2025 20:30:59 -0600 Subject: [PATCH 03/40] Update bHapticsManager/DiagnosticPatches.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- bHapticsManager/DiagnosticPatches.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bHapticsManager/DiagnosticPatches.cs b/bHapticsManager/DiagnosticPatches.cs index d4d3d5b..6afb41a 100644 --- a/bHapticsManager/DiagnosticPatches.cs +++ b/bHapticsManager/DiagnosticPatches.cs @@ -80,7 +80,8 @@ static void Postfix(HapticPoint __instance) { } } } - catch { + catch (Exception ex) { + ResoniteMod.Error($"Error in HapticPoint sample diagnostic: {ex.Message}"); } } } From de8ff8208d8f19e7b9892fd7068996b31a1fefb8 Mon Sep 17 00:00:00 2001 From: Zeia Nala <36301692+nalathethird@users.noreply.github.com> Date: Sat, 22 Nov 2025 20:31:25 -0600 Subject: [PATCH 04/40] Update bHapticsManager/DiagnosticPatches.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- bHapticsManager/DiagnosticPatches.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bHapticsManager/DiagnosticPatches.cs b/bHapticsManager/DiagnosticPatches.cs index 6afb41a..285a6a2 100644 --- a/bHapticsManager/DiagnosticPatches.cs +++ b/bHapticsManager/DiagnosticPatches.cs @@ -101,7 +101,8 @@ static void Postfix(DirectTagHapticSource __instance, SensationClass sensation, _lastLog = DateTime.Now; } } - catch { + catch (Exception ex) { + ResoniteMod.Debug($"[DirectTag Diagnostic] Exception: {ex}"); } } } From b4c946da16f19f045bf0c20d7656be3243a9ac4a Mon Sep 17 00:00:00 2001 From: Zeia Nala <36301692+nalathethird@users.noreply.github.com> Date: Sat, 22 Nov 2025 20:34:43 -0600 Subject: [PATCH 05/40] Update bHapticsManager/RemoteHapticSource.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- bHapticsManager/RemoteHapticSource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bHapticsManager/RemoteHapticSource.cs b/bHapticsManager/RemoteHapticSource.cs index f9d67c4..c684050 100644 --- a/bHapticsManager/RemoteHapticSource.cs +++ b/bHapticsManager/RemoteHapticSource.cs @@ -109,7 +109,7 @@ public DummyWorldElement(World world) { public RefID ReferenceID => RefID.Null; public string Name => "DummyParent"; public World World => _world; - public IWorldElement Parent => this; + public IWorldElement Parent => null; public bool IsLocalElement => false; public bool IsPersistent => false; public bool IsRemoved => false; From e82763c2dc7989abb957459c57bab300dc60e55a Mon Sep 17 00:00:00 2001 From: Zeia Nala <36301692+nalathethird@users.noreply.github.com> Date: Sat, 22 Nov 2025 20:35:29 -0600 Subject: [PATCH 06/40] Update bHapticsManager/bHapticsManager.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- bHapticsManager/bHapticsManager.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/bHapticsManager/bHapticsManager.cs b/bHapticsManager/bHapticsManager.cs index 256e443..575d574 100644 --- a/bHapticsManager/bHapticsManager.cs +++ b/bHapticsManager/bHapticsManager.cs @@ -126,18 +126,24 @@ public override void OnEngineInit() Warn("No focused world, initializing on next world focus"); engine.WorldManager.WorldFocused += (world) => { - if (!_initialized && world != null) + if (world != null) { - world.RunSynchronously(() => InitializeHaptics()); + // Atomically set _initialized to true if it was false + if (Interlocked.CompareExchange(ref _initialized, true, false) == false) + { + world.RunSynchronously(() => InitializeHaptics()); + } } }; } else { - focusedWorld.RunSynchronously(() => InitializeHaptics()); + // Atomically set _initialized to true if it was false + if (Interlocked.CompareExchange(ref _initialized, true, false) == false) + { + focusedWorld.RunSynchronously(() => InitializeHaptics()); + } } - - _initialized = true; Msg("bHapticsManager initialized successfully"); } catch (Exception ex) From a0aef2d6470b88b3ce61fa0ca6c847059b422655 Mon Sep 17 00:00:00 2001 From: Zeia Nala <36301692+nalathethird@users.noreply.github.com> Date: Sat, 22 Nov 2025 20:36:35 -0600 Subject: [PATCH 07/40] Update bHapticsManager/ModernBHapticsWorkerThread.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- bHapticsManager/ModernBHapticsWorkerThread.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bHapticsManager/ModernBHapticsWorkerThread.cs b/bHapticsManager/ModernBHapticsWorkerThread.cs index 173c29d..78c5658 100644 --- a/bHapticsManager/ModernBHapticsWorkerThread.cs +++ b/bHapticsManager/ModernBHapticsWorkerThread.cs @@ -104,7 +104,8 @@ public void Stop() { try { _cancellationTokenSource.Cancel(); } - catch (ObjectDisposedException) { + catch (ObjectDisposedException ex) { + ResoniteMod.Warn($"Cancellation token source already disposed: {ex}"); } _workEvent.Set(); From 8b08ab34099c2525c125a6f269f90ffa971a8a59 Mon Sep 17 00:00:00 2001 From: Zeia Nala <36301692+nalathethird@users.noreply.github.com> Date: Sat, 22 Nov 2025 20:41:16 -0600 Subject: [PATCH 08/40] Update bHapticsManager/TorsoMapperFix.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- bHapticsManager/TorsoMapperFix.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bHapticsManager/TorsoMapperFix.cs b/bHapticsManager/TorsoMapperFix.cs index 81c7f02..8668dd1 100644 --- a/bHapticsManager/TorsoMapperFix.cs +++ b/bHapticsManager/TorsoMapperFix.cs @@ -12,7 +12,7 @@ namespace bHapticsManager { public static class TorsoMapperFix { public static void ApplyPatches(Harmony harmony) { try { - harmony.PatchAll(typeof(CommonAvatarBuilderPatch).Assembly); + harmony.PatchAll(typeof(TorsoMapperFix)); ResoniteMod.Debug("TorsoMapperFix patches applied successfully"); } catch (Exception ex) { From babddda797bb3135cfbed4b28899236b5abbf5d5 Mon Sep 17 00:00:00 2001 From: Zeia Nala <36301692+nalathethird@users.noreply.github.com> Date: Sat, 22 Nov 2025 20:41:25 -0600 Subject: [PATCH 09/40] Update bHapticsManager/HapticMethodPatches.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- bHapticsManager/HapticMethodPatches.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bHapticsManager/HapticMethodPatches.cs b/bHapticsManager/HapticMethodPatches.cs index 33a6631..9016264 100644 --- a/bHapticsManager/HapticMethodPatches.cs +++ b/bHapticsManager/HapticMethodPatches.cs @@ -13,7 +13,7 @@ namespace bHapticsManager { public static class HapticMethodPatches { public static void ApplyPatches(Harmony harmony) { try { - harmony.PatchAll(typeof(IsActivePatch).Assembly); + harmony.PatchAll(typeof(HapticMethodPatches)); ResoniteMod.Debug("HapticMethodPatches applied successfully"); } catch (Exception ex) { From 098752e8a36f5e8349acd44ef4c88b554ba105cd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 02:44:56 +0000 Subject: [PATCH 10/40] Initial plan From be18ff7672d2a88b4d249ef060066aa487f7431d Mon Sep 17 00:00:00 2001 From: Zeia Nala <36301692+nalathethird@users.noreply.github.com> Date: Sat, 22 Nov 2025 20:44:57 -0600 Subject: [PATCH 11/40] Update bHapticsManager/bHapticsManager.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- bHapticsManager/bHapticsManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bHapticsManager/bHapticsManager.cs b/bHapticsManager/bHapticsManager.cs index 575d574..76a0eaf 100644 --- a/bHapticsManager/bHapticsManager.cs +++ b/bHapticsManager/bHapticsManager.cs @@ -241,11 +241,11 @@ private static void InitializeHaptics() } } - private static void OnEngineShutdown(string reason) + private static void OnEngineShutdown() { try { - ResoniteMod.Debug($"Engine shutdown requested: {reason}"); + ResoniteMod.Debug("Engine shutdown requested."); Msg("Starting bHapticsManager shutdown..."); if (_eventHandler != null) From 7ac12ded67c772d3c5db26dcbe9ed40708a2f737 Mon Sep 17 00:00:00 2001 From: Zeia Nala <36301692+nalathethird@users.noreply.github.com> Date: Sat, 22 Nov 2025 20:45:19 -0600 Subject: [PATCH 12/40] Update bHapticsManager/bHapticsManager.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- bHapticsManager/bHapticsManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bHapticsManager/bHapticsManager.cs b/bHapticsManager/bHapticsManager.cs index 76a0eaf..c116c0e 100644 --- a/bHapticsManager/bHapticsManager.cs +++ b/bHapticsManager/bHapticsManager.cs @@ -139,7 +139,7 @@ public override void OnEngineInit() else { // Atomically set _initialized to true if it was false - if (Interlocked.CompareExchange(ref _initialized, true, false) == false) + if (!Interlocked.CompareExchange(ref _initialized, true, false)) { focusedWorld.RunSynchronously(() => InitializeHaptics()); } From 3dd7d45e8b6abbacac036a6feae1021ec6765774 Mon Sep 17 00:00:00 2001 From: Zeia Nala <36301692+nalathethird@users.noreply.github.com> Date: Sat, 22 Nov 2025 20:45:50 -0600 Subject: [PATCH 13/40] Update bHapticsManager/ModernBHapticsWorkerThread.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- bHapticsManager/ModernBHapticsWorkerThread.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bHapticsManager/ModernBHapticsWorkerThread.cs b/bHapticsManager/ModernBHapticsWorkerThread.cs index 78c5658..e6be6db 100644 --- a/bHapticsManager/ModernBHapticsWorkerThread.cs +++ b/bHapticsManager/ModernBHapticsWorkerThread.cs @@ -143,12 +143,11 @@ private bool PopulateHapticPoints() { LegacyBHaptics.PositionType deviceType = GetDeviceTypeFromPosition(point.Position); if (!_hapticPointsByDevice.TryGetValue(deviceType, out List? value)) { - value = ([new(point)]); + value = new List(); _hapticPointsByDevice[deviceType] = value; _deviceKeys[deviceType] = Guid.NewGuid().ToString(); - } else { - value.Add(new(point)); } + value.Add(new(point)); } return true; From d4c9ba36f230493467d5fae1eb0826480dae00ae Mon Sep 17 00:00:00 2001 From: Zeia Nala <36301692+nalathethird@users.noreply.github.com> Date: Sat, 22 Nov 2025 20:46:16 -0600 Subject: [PATCH 14/40] Update bHapticsManager/bHapticsManager.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- bHapticsManager/bHapticsManager.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/bHapticsManager/bHapticsManager.cs b/bHapticsManager/bHapticsManager.cs index c116c0e..a89f352 100644 --- a/bHapticsManager/bHapticsManager.cs +++ b/bHapticsManager/bHapticsManager.cs @@ -126,13 +126,10 @@ public override void OnEngineInit() Warn("No focused world, initializing on next world focus"); engine.WorldManager.WorldFocused += (world) => { - if (world != null) + if (world != null && Interlocked.CompareExchange(ref _initialized, true, false) == false) { // Atomically set _initialized to true if it was false - if (Interlocked.CompareExchange(ref _initialized, true, false) == false) - { - world.RunSynchronously(() => InitializeHaptics()); - } + world.RunSynchronously(() => InitializeHaptics()); } }; } From 48d889ac23aa6aeb3907ae7cbb3a1f6f88020a9a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 02:46:16 +0000 Subject: [PATCH 15/40] Initial plan From 263d1a0a76efcecda3fc18c0a5e8ad6ee882cf27 Mon Sep 17 00:00:00 2001 From: Zeia Nala <36301692+nalathethird@users.noreply.github.com> Date: Sat, 22 Nov 2025 20:46:33 -0600 Subject: [PATCH 16/40] Update bHapticsManager/ModernBHapticsWorkerThread.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- bHapticsManager/ModernBHapticsWorkerThread.cs | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/bHapticsManager/ModernBHapticsWorkerThread.cs b/bHapticsManager/ModernBHapticsWorkerThread.cs index e6be6db..10786ed 100644 --- a/bHapticsManager/ModernBHapticsWorkerThread.cs +++ b/bHapticsManager/ModernBHapticsWorkerThread.cs @@ -110,19 +110,17 @@ public void Stop() { _workEvent.Set(); - if (_workerThread != null && _workerThread.IsAlive) { - if (!_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}"); + 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"); From df76a6fdc1bfe351056ad762473b723786c68037 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 02:46:34 +0000 Subject: [PATCH 17/40] Initial plan From 4e12e48f1ce57f69629136e51f3c0ad3d4243486 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 02:47:40 +0000 Subject: [PATCH 18/40] Fix WorldFocused event handler race condition by unsubscribing after initialization Co-authored-by: nalathethird <36301692+nalathethird@users.noreply.github.com> --- bHapticsManager/bHapticsManager.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/bHapticsManager/bHapticsManager.cs b/bHapticsManager/bHapticsManager.cs index 575d574..332544e 100644 --- a/bHapticsManager/bHapticsManager.cs +++ b/bHapticsManager/bHapticsManager.cs @@ -124,7 +124,8 @@ public override void OnEngineInit() if (focusedWorld == null) { Warn("No focused world, initializing on next world focus"); - engine.WorldManager.WorldFocused += (world) => + Action? worldFocusedHandler = null; + worldFocusedHandler = (world) => { if (world != null) { @@ -132,9 +133,16 @@ public override void OnEngineInit() if (Interlocked.CompareExchange(ref _initialized, true, false) == false) { world.RunSynchronously(() => InitializeHaptics()); + // Unsubscribe from the event after first successful initialization + if (worldFocusedHandler != null) + { + engine.WorldManager.WorldFocused -= worldFocusedHandler; + ResoniteMod.Debug("WorldFocused event handler unsubscribed after initialization"); + } } } }; + engine.WorldManager.WorldFocused += worldFocusedHandler; } else { From 88e9073fa2197e03531040c7b406fe6c9ad9a6b7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 02:48:36 +0000 Subject: [PATCH 19/40] Refactor WorldFocused handler to use local function for better readability Co-authored-by: nalathethird <36301692+nalathethird@users.noreply.github.com> --- bHapticsManager/bHapticsManager.cs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/bHapticsManager/bHapticsManager.cs b/bHapticsManager/bHapticsManager.cs index 332544e..890cc0a 100644 --- a/bHapticsManager/bHapticsManager.cs +++ b/bHapticsManager/bHapticsManager.cs @@ -124,8 +124,8 @@ public override void OnEngineInit() if (focusedWorld == null) { Warn("No focused world, initializing on next world focus"); - Action? worldFocusedHandler = null; - worldFocusedHandler = (world) => + + void WorldFocusedHandler(World world) { if (world != null) { @@ -134,15 +134,13 @@ public override void OnEngineInit() { world.RunSynchronously(() => InitializeHaptics()); // Unsubscribe from the event after first successful initialization - if (worldFocusedHandler != null) - { - engine.WorldManager.WorldFocused -= worldFocusedHandler; - ResoniteMod.Debug("WorldFocused event handler unsubscribed after initialization"); - } + engine.WorldManager.WorldFocused -= WorldFocusedHandler; + ResoniteMod.Debug("WorldFocused event handler unsubscribed after initialization"); } } - }; - engine.WorldManager.WorldFocused += worldFocusedHandler; + } + + engine.WorldManager.WorldFocused += WorldFocusedHandler; } else { From f83e0637ae74adcf6bb5d80805e94728d09d2e84 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 02:48:58 +0000 Subject: [PATCH 20/40] Simplify boolean comparison: replace '== false' with '!' Co-authored-by: nalathethird <36301692+nalathethird@users.noreply.github.com> --- bHapticsManager/bHapticsManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bHapticsManager/bHapticsManager.cs b/bHapticsManager/bHapticsManager.cs index a89f352..a6207d8 100644 --- a/bHapticsManager/bHapticsManager.cs +++ b/bHapticsManager/bHapticsManager.cs @@ -126,7 +126,7 @@ public override void OnEngineInit() Warn("No focused world, initializing on next world focus"); engine.WorldManager.WorldFocused += (world) => { - if (world != null && Interlocked.CompareExchange(ref _initialized, true, false) == false) + if (world != null && !Interlocked.CompareExchange(ref _initialized, true, false)) { // Atomically set _initialized to true if it was false world.RunSynchronously(() => InitializeHaptics()); From 7bad13f674585d63196e1395c1bea752a98c8f07 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 02:49:26 +0000 Subject: [PATCH 21/40] Add thread synchronization to DeviceEventHandler Co-authored-by: nalathethird <36301692+nalathethird@users.noreply.github.com> --- bHapticsManager/DeviceEventHandler.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/bHapticsManager/DeviceEventHandler.cs b/bHapticsManager/DeviceEventHandler.cs index 2fe5980..71a685c 100644 --- a/bHapticsManager/DeviceEventHandler.cs +++ b/bHapticsManager/DeviceEventHandler.cs @@ -12,6 +12,7 @@ public class DeviceEventHandler : IDisposable { private bool _disposed; private ModernBHapticsWorkerThread _workerThread = null!; + private readonly object _lock = new object(); public void Initialize(ModernBHapticsWorkerThread workerThread) { @@ -57,7 +58,10 @@ private void OnDeviceConnected(LegacyBHaptics.PositionType position) } } - _workerThread?.OnDeviceConnected(position); + lock (_lock) + { + _workerThread?.OnDeviceConnected(position); + } } catch (Exception ex) { @@ -92,7 +96,10 @@ private void OnDeviceDisconnected(LegacyBHaptics.PositionType position) } } - _workerThread?.OnDeviceDisconnected(position); + lock (_lock) + { + _workerThread?.OnDeviceDisconnected(position); + } } catch (Exception ex) { @@ -118,8 +125,11 @@ public void Dispose() bHapticsManager.Error($"Error disposing event handlers: {ex}"); } - _workerThread = null!; - _disposed = true; + lock (_lock) + { + _workerThread = null!; + _disposed = true; + } } } } From 9ff03b8553ee88b9aac26a51b0ee14bb7f9296a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 02:49:29 +0000 Subject: [PATCH 22/40] Add error handling to allow retry attempts on initialization failure Co-authored-by: nalathethird <36301692+nalathethird@users.noreply.github.com> --- bHapticsManager/bHapticsManager.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/bHapticsManager/bHapticsManager.cs b/bHapticsManager/bHapticsManager.cs index 890cc0a..ea3d357 100644 --- a/bHapticsManager/bHapticsManager.cs +++ b/bHapticsManager/bHapticsManager.cs @@ -132,10 +132,19 @@ void WorldFocusedHandler(World world) // Atomically set _initialized to true if it was false if (Interlocked.CompareExchange(ref _initialized, true, false) == false) { - world.RunSynchronously(() => InitializeHaptics()); - // Unsubscribe from the event after first successful initialization - engine.WorldManager.WorldFocused -= WorldFocusedHandler; - ResoniteMod.Debug("WorldFocused event handler unsubscribed after initialization"); + 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 + _initialized = false; + } } } } From 905da5bcad22d5171a0242574272deb1e65b6fc7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 02:50:07 +0000 Subject: [PATCH 23/40] Improve disposal pattern to prevent potential deadlock Co-authored-by: nalathethird <36301692+nalathethird@users.noreply.github.com> --- bHapticsManager/DeviceEventHandler.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bHapticsManager/DeviceEventHandler.cs b/bHapticsManager/DeviceEventHandler.cs index 71a685c..e530691 100644 --- a/bHapticsManager/DeviceEventHandler.cs +++ b/bHapticsManager/DeviceEventHandler.cs @@ -111,6 +111,8 @@ public void Dispose() { if (_disposed) return; + _disposed = true; + try { if (BHapticsConnection.Instance != null) @@ -128,7 +130,6 @@ public void Dispose() lock (_lock) { _workerThread = null!; - _disposed = true; } } } From 095eaea8a88a5cd143ce14cf51734f2eb260807c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 02:50:42 +0000 Subject: [PATCH 24/40] Use Interlocked.Exchange for thread-safe reset of _initialized flag Co-authored-by: nalathethird <36301692+nalathethird@users.noreply.github.com> --- bHapticsManager/bHapticsManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bHapticsManager/bHapticsManager.cs b/bHapticsManager/bHapticsManager.cs index ea3d357..31eeb6c 100644 --- a/bHapticsManager/bHapticsManager.cs +++ b/bHapticsManager/bHapticsManager.cs @@ -143,7 +143,7 @@ void WorldFocusedHandler(World world) { Error($"Failed to initialize haptics on world focus: {ex}"); // Reset _initialized to allow retry on next world focus - _initialized = false; + Interlocked.Exchange(ref _initialized, false); } } } From 441d69020105157756f63dac0cf7f1fbb107f49b Mon Sep 17 00:00:00 2001 From: Zeia Nala <36301692+nalathethird@users.noreply.github.com> Date: Sat, 22 Nov 2025 21:01:56 -0600 Subject: [PATCH 25/40] Update bHapticsManager/DeviceEventHandler.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- bHapticsManager/DeviceEventHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bHapticsManager/DeviceEventHandler.cs b/bHapticsManager/DeviceEventHandler.cs index e530691..fb15721 100644 --- a/bHapticsManager/DeviceEventHandler.cs +++ b/bHapticsManager/DeviceEventHandler.cs @@ -10,7 +10,7 @@ namespace bHapticsManager { public class DeviceEventHandler : IDisposable { - private bool _disposed; + private volatile bool _disposed; private ModernBHapticsWorkerThread _workerThread = null!; private readonly object _lock = new object(); From 4fcf10e50bdcfb34da5adbab6c979f837acb2b0f Mon Sep 17 00:00:00 2001 From: Zeia Nala <36301692+nalathethird@users.noreply.github.com> Date: Sat, 22 Nov 2025 21:02:13 -0600 Subject: [PATCH 26/40] Update bHapticsManager/DeviceEventHandler.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- bHapticsManager/DeviceEventHandler.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/bHapticsManager/DeviceEventHandler.cs b/bHapticsManager/DeviceEventHandler.cs index fb15721..55d1751 100644 --- a/bHapticsManager/DeviceEventHandler.cs +++ b/bHapticsManager/DeviceEventHandler.cs @@ -98,6 +98,7 @@ private void OnDeviceDisconnected(LegacyBHaptics.PositionType position) lock (_lock) { + if (_disposed) return; _workerThread?.OnDeviceDisconnected(position); } } From e2e576d93ffb45bb596dcce014ef472637e1a198 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 03:04:41 +0000 Subject: [PATCH 27/40] Add _disposed checks inside locks and protect Initialize Co-authored-by: nalathethird <36301692+nalathethird@users.noreply.github.com> --- bHapticsManager/DeviceEventHandler.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bHapticsManager/DeviceEventHandler.cs b/bHapticsManager/DeviceEventHandler.cs index 55d1751..b31450f 100644 --- a/bHapticsManager/DeviceEventHandler.cs +++ b/bHapticsManager/DeviceEventHandler.cs @@ -16,7 +16,10 @@ public class DeviceEventHandler : IDisposable public void Initialize(ModernBHapticsWorkerThread workerThread) { - _workerThread = workerThread; + lock (_lock) + { + _workerThread = workerThread; + } try { @@ -60,6 +63,7 @@ private void OnDeviceConnected(LegacyBHaptics.PositionType position) lock (_lock) { + if (_disposed) return; _workerThread?.OnDeviceConnected(position); } } From 939180151184a1a24c9d383775be3f066f95d2cc Mon Sep 17 00:00:00 2001 From: Zeia Nala <36301692+nalathethird@users.noreply.github.com> Date: Sat, 22 Nov 2025 21:05:57 -0600 Subject: [PATCH 28/40] Update bHapticsManager/bHapticsManager.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- bHapticsManager/bHapticsManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bHapticsManager/bHapticsManager.cs b/bHapticsManager/bHapticsManager.cs index e108165..0dd5831 100644 --- a/bHapticsManager/bHapticsManager.cs +++ b/bHapticsManager/bHapticsManager.cs @@ -130,7 +130,7 @@ void WorldFocusedHandler(World world) if (world != null && Interlocked.CompareExchange(ref _initialized, true, false) == false) { // Atomically set _initialized to true if it was false - if (Interlocked.CompareExchange(ref _initialized, true, false) == false) + if (!Interlocked.CompareExchange(ref _initialized, true, false)) { try { From 03f5898d8b475f42e56c71b083de29f6711d657b Mon Sep 17 00:00:00 2001 From: Zeia Nala <36301692+nalathethird@users.noreply.github.com> Date: Sat, 22 Nov 2025 21:10:53 -0600 Subject: [PATCH 29/40] Update bHapticsManager/ModernBHapticsWorkerThread.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- bHapticsManager/ModernBHapticsWorkerThread.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/bHapticsManager/ModernBHapticsWorkerThread.cs b/bHapticsManager/ModernBHapticsWorkerThread.cs index 10786ed..e426dec 100644 --- a/bHapticsManager/ModernBHapticsWorkerThread.cs +++ b/bHapticsManager/ModernBHapticsWorkerThread.cs @@ -101,12 +101,7 @@ public void Stop() { ResoniteMod.Debug("Stopping worker thread..."); _running = false; - try { - _cancellationTokenSource.Cancel(); - } - catch (ObjectDisposedException ex) { - ResoniteMod.Warn($"Cancellation token source already disposed: {ex}"); - } + _cancellationTokenSource.Cancel(); _workEvent.Set(); From 377f8e3215bdc7103e643af21a595aa60ce4a82b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 03:11:36 +0000 Subject: [PATCH 30/40] Initial plan From c420dfba0109ad5c5d0c7f63c3e53576e6888470 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 03:11:38 +0000 Subject: [PATCH 31/40] Initial plan From dd95c316038bca280a8d77169d2af5d699ae6162 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 03:14:48 +0000 Subject: [PATCH 32/40] Remove unused _devicePoints dictionary and _lock object from DeviceRegistration Co-authored-by: nalathethird <36301692+nalathethird@users.noreply.github.com> --- bHapticsManager/DeviceRegistration.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/bHapticsManager/DeviceRegistration.cs b/bHapticsManager/DeviceRegistration.cs index ffbf14d..e145c48 100644 --- a/bHapticsManager/DeviceRegistration.cs +++ b/bHapticsManager/DeviceRegistration.cs @@ -12,10 +12,8 @@ namespace bHapticsManager { public static class DeviceRegistration { - private static readonly object _lock = new object(); private static readonly HashSet _registeredDevices = new HashSet(); private static readonly Dictionary _pendingRegistrations = new(); - private static readonly Dictionary> _devicePoints = new Dictionary>(); private static InputInterface? _inputInterface; private static BHapticsDriver? _bhapticsDriver; @@ -36,7 +34,6 @@ public static void ClearAllRegistrations() { lock (_registrationLock) { _registeredDevices.Clear(); _pendingRegistrations.Clear(); - _devicePoints.Clear(); } } From 458d750c53a99c68e4b176e20f2dec0d42cfec20 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 03:14:49 +0000 Subject: [PATCH 33/40] Fix race condition in WorldFocused event handler Co-authored-by: nalathethird <36301692+nalathethird@users.noreply.github.com> --- bHapticsManager/bHapticsManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bHapticsManager/bHapticsManager.cs b/bHapticsManager/bHapticsManager.cs index 69150d1..0318dd1 100644 --- a/bHapticsManager/bHapticsManager.cs +++ b/bHapticsManager/bHapticsManager.cs @@ -127,10 +127,10 @@ public override void OnEngineInit() void WorldFocusedHandler(World world) { - if (world != null && !Interlocked.CompareExchange(ref _initialized, true, false)) + if (world != null) { // Atomically set _initialized to true if it was false - if (!Interlocked.CompareExchange(ref _initialized, true, false)) + if (Interlocked.CompareExchange(ref _initialized, true, false) == false) { try { @@ -154,7 +154,7 @@ void WorldFocusedHandler(World world) else { // Atomically set _initialized to true if it was false - if (!Interlocked.CompareExchange(ref _initialized, true, false)) + if (Interlocked.CompareExchange(ref _initialized, true, false) == false) { focusedWorld.RunSynchronously(() => InitializeHaptics()); } From b4e066cbe22fdcd5349b66ebec3246b432c6e9af Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 03:15:52 +0000 Subject: [PATCH 34/40] Address user feedback: remove unused _devicePoints and adjust haptic timing Co-authored-by: nalathethird <36301692+nalathethird@users.noreply.github.com> --- bHapticsManager/DeviceRegistration.cs | 2 -- bHapticsManager/LegacyCompatibilityLayer.cs | 9 ++++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/bHapticsManager/DeviceRegistration.cs b/bHapticsManager/DeviceRegistration.cs index ffbf14d..787c906 100644 --- a/bHapticsManager/DeviceRegistration.cs +++ b/bHapticsManager/DeviceRegistration.cs @@ -15,7 +15,6 @@ public static class DeviceRegistration { private static readonly object _lock = new object(); private static readonly HashSet _registeredDevices = new HashSet(); private static readonly Dictionary _pendingRegistrations = new(); - private static readonly Dictionary> _devicePoints = new Dictionary>(); private static InputInterface? _inputInterface; private static BHapticsDriver? _bhapticsDriver; @@ -36,7 +35,6 @@ public static void ClearAllRegistrations() { lock (_registrationLock) { _registeredDevices.Clear(); _pendingRegistrations.Clear(); - _devicePoints.Clear(); } } diff --git a/bHapticsManager/LegacyCompatibilityLayer.cs b/bHapticsManager/LegacyCompatibilityLayer.cs index c8cf62b..abc21d2 100644 --- a/bHapticsManager/LegacyCompatibilityLayer.cs +++ b/bHapticsManager/LegacyCompatibilityLayer.cs @@ -21,10 +21,13 @@ public static void ApplyPatches(Harmony harmony) { private static readonly Dictionary _lastSubmissionTime = new(); private static readonly object _submissionLock = new object(); + // Minimum interval between submissions for the same device to prevent flooding private const int MIN_SUBMISSION_INTERVAL_MS = 5; - private const int MIN_BUFFER_DURATION_MS = 100; - private const float INTENSITY_BOOST_THRESHOLD = 0.3f; - private const float INTENSITY_BOOST_MULTIPLIER = 1.5f; + // Minimum buffer duration for haptic feedback frames (MessagePack allows for faster submission) + private const int MIN_BUFFER_DURATION_MS = 50; + // Intensity boost disabled - using 1.0x multiplier to maintain base vibrations + private const float INTENSITY_BOOST_THRESHOLD = 1.0f; + private const float INTENSITY_BOOST_MULTIPLIER = 1.0f; public static void SubmitFrame(string key, LegacyBHaptics.PositionType position, List dotPoints, int durationMillis) { From 788d3f518860f1e1d1163c6c928533cf66b0842c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 03:17:02 +0000 Subject: [PATCH 35/40] Improve comments based on code review feedback Co-authored-by: nalathethird <36301692+nalathethird@users.noreply.github.com> --- bHapticsManager/LegacyCompatibilityLayer.cs | 3 ++- bHapticsManager/bHapticsManager.cs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/bHapticsManager/LegacyCompatibilityLayer.cs b/bHapticsManager/LegacyCompatibilityLayer.cs index abc21d2..6fcb72a 100644 --- a/bHapticsManager/LegacyCompatibilityLayer.cs +++ b/bHapticsManager/LegacyCompatibilityLayer.cs @@ -25,7 +25,8 @@ public static void ApplyPatches(Harmony harmony) { 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 disabled - using 1.0x multiplier to maintain base vibrations + // Intensity boost feature disabled: threshold at 1.0 (100%) means no signals qualify as "weak" + // and multiplier at 1.0 means no boost is applied, maintaining base vibrations as intended private const float INTENSITY_BOOST_THRESHOLD = 1.0f; private const float INTENSITY_BOOST_MULTIPLIER = 1.0f; diff --git a/bHapticsManager/bHapticsManager.cs b/bHapticsManager/bHapticsManager.cs index 0318dd1..c0d8ab3 100644 --- a/bHapticsManager/bHapticsManager.cs +++ b/bHapticsManager/bHapticsManager.cs @@ -129,7 +129,7 @@ void WorldFocusedHandler(World world) { if (world != null) { - // Atomically set _initialized to true if it was false + // Check if initialization is needed and atomically set _initialized to true if (Interlocked.CompareExchange(ref _initialized, true, false) == false) { try @@ -153,7 +153,7 @@ void WorldFocusedHandler(World world) } else { - // Atomically set _initialized to true if it was false + // Check if initialization is needed and atomically set _initialized to true if (Interlocked.CompareExchange(ref _initialized, true, false) == false) { focusedWorld.RunSynchronously(() => InitializeHaptics()); From 9f84bca663f802e9c09cc6d6036645539a85ea0e Mon Sep 17 00:00:00 2001 From: Zeia Nala <36301692+nalathethird@users.noreply.github.com> Date: Sat, 22 Nov 2025 21:25:31 -0600 Subject: [PATCH 36/40] Update bHapticsManager/bHapticsManager.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- bHapticsManager/bHapticsManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bHapticsManager/bHapticsManager.cs b/bHapticsManager/bHapticsManager.cs index c0d8ab3..b43a277 100644 --- a/bHapticsManager/bHapticsManager.cs +++ b/bHapticsManager/bHapticsManager.cs @@ -130,7 +130,7 @@ 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) == false) + if (!Interlocked.CompareExchange(ref _initialized, true, false)) { try { From 4ca23531f081c12e337d054ab8dfc5f23771de5a Mon Sep 17 00:00:00 2001 From: Zeia Nala <36301692+nalathethird@users.noreply.github.com> Date: Sat, 22 Nov 2025 21:25:38 -0600 Subject: [PATCH 37/40] Update bHapticsManager/bHapticsManager.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- bHapticsManager/bHapticsManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bHapticsManager/bHapticsManager.cs b/bHapticsManager/bHapticsManager.cs index b43a277..97e1ffc 100644 --- a/bHapticsManager/bHapticsManager.cs +++ b/bHapticsManager/bHapticsManager.cs @@ -154,7 +154,7 @@ void WorldFocusedHandler(World world) else { // Check if initialization is needed and atomically set _initialized to true - if (Interlocked.CompareExchange(ref _initialized, true, false) == false) + if (!Interlocked.CompareExchange(ref _initialized, true, false)) { focusedWorld.RunSynchronously(() => InitializeHaptics()); } From 1b0fdf758ac8128c077ade52ccf0ee2c89c4737d Mon Sep 17 00:00:00 2001 From: Zeia Nala <36301692+nalathethird@users.noreply.github.com> Date: Sat, 22 Nov 2025 21:25:47 -0600 Subject: [PATCH 38/40] Update bHapticsManager/LegacyCompatibilityLayer.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- bHapticsManager/LegacyCompatibilityLayer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bHapticsManager/LegacyCompatibilityLayer.cs b/bHapticsManager/LegacyCompatibilityLayer.cs index 6fcb72a..efa1124 100644 --- a/bHapticsManager/LegacyCompatibilityLayer.cs +++ b/bHapticsManager/LegacyCompatibilityLayer.cs @@ -25,8 +25,8 @@ public static void ApplyPatches(Harmony harmony) { 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 disabled: threshold at 1.0 (100%) means no signals qualify as "weak" - // and multiplier at 1.0 means no boost is applied, maintaining base vibrations as intended + // Intensity boost feature effectively disabled: multiplier at 1.0 means signals pass through unchanged + // (signals with intensity < 100 still enter boost logic, but no actual boost is applied) private const float INTENSITY_BOOST_THRESHOLD = 1.0f; private const float INTENSITY_BOOST_MULTIPLIER = 1.0f; From 89b2654613fd5806d104244db735e6fdd0f95534 Mon Sep 17 00:00:00 2001 From: Zeia Nala <36301692+nalathethird@users.noreply.github.com> Date: Sat, 22 Nov 2025 21:35:07 -0600 Subject: [PATCH 39/40] Update bHapticsManager/bHapticsManager.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- bHapticsManager/bHapticsManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bHapticsManager/bHapticsManager.cs b/bHapticsManager/bHapticsManager.cs index 97e1ffc..c92cbc7 100644 --- a/bHapticsManager/bHapticsManager.cs +++ b/bHapticsManager/bHapticsManager.cs @@ -15,7 +15,7 @@ public class bHapticsManager : ResoniteMod { public override string Version => VERSION_CONSTANT; public override string Link => "https://github.com/nalathethird/bHapticsManager"; - public static ModConfiguration Config = null!; + public static ModConfiguration? Config = null; [AutoRegisterConfigKey] public static readonly ModConfigurationKey ENABLE_HOTPLUG = From 86964dad53f6f8e04614bf1171d8796ac4532e49 Mon Sep 17 00:00:00 2001 From: Zeia Nala <36301692+nalathethird@users.noreply.github.com> Date: Sat, 22 Nov 2025 21:35:58 -0600 Subject: [PATCH 40/40] Update bHapticsManager/LegacyCompatibilityLayer.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- bHapticsManager/LegacyCompatibilityLayer.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/bHapticsManager/LegacyCompatibilityLayer.cs b/bHapticsManager/LegacyCompatibilityLayer.cs index efa1124..cdaaa51 100644 --- a/bHapticsManager/LegacyCompatibilityLayer.cs +++ b/bHapticsManager/LegacyCompatibilityLayer.cs @@ -25,10 +25,7 @@ public static void ApplyPatches(Harmony harmony) { 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 effectively disabled: multiplier at 1.0 means signals pass through unchanged - // (signals with intensity < 100 still enter boost logic, but no actual boost is applied) - private const float INTENSITY_BOOST_THRESHOLD = 1.0f; - private const float INTENSITY_BOOST_MULTIPLIER = 1.0f; + // Intensity boost feature removed for clarity; signals pass through unchanged public static void SubmitFrame(string key, LegacyBHaptics.PositionType position, List dotPoints, int durationMillis) {