diff --git a/README.md b/README.md index 9cbe8e5..6d23a4f 100644 --- a/README.md +++ b/README.md @@ -14,4 +14,5 @@ Merged set of MelonLoader mods for ChilloutVR. |[Player Pick Up](/ml_ppu/README.md)|1.0.0 [:arrow_down:](../../releases/latest/download/PlayerPickUp.dll)| |[Player Ragdoll Mod](/ml_prm/README.md)|1.2.3 [:arrow_down:](../../releases/latest/download/PlayerRagdollMod.dll)| |[Video Player Cookies](/ml_vpc/README.md)|1.0.1 [:arrow_down:](../../releases/latest/download/VideoPlayerCookies.dll)| +|[Vive Eye Tracking](/ml_vet/README.md)|1.0.0 [:arrow_down:](../../releases/latest/download/ViveEyeTracking.dll)| |[Vive Extended Input](/ml_vei/README.md)|1.1.1 [:arrow_down:](../../releases/latest/download/ViveExtendedInput.dll)| diff --git a/ml_mods_cvr.sln b/ml_mods_cvr.sln index 782d4e6..1a8e1ef 100644 --- a/ml_mods_cvr.sln +++ b/ml_mods_cvr.sln @@ -30,6 +30,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ml_vpc", "ml_vpc\ml_vpc.csp EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ml_ppu", "ml_ppu\ml_ppu.csproj", "{F16DF16B-D127-4A2A-81FF-2FD80F320E64}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ml_vet", "ml_vet\ml_vet.csproj", "{8DB32590-FC5B-46A8-9747-344E86B18ACF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -77,6 +79,10 @@ Global {F16DF16B-D127-4A2A-81FF-2FD80F320E64}.Debug|x64.Build.0 = Debug|x64 {F16DF16B-D127-4A2A-81FF-2FD80F320E64}.Release|x64.ActiveCfg = Release|x64 {F16DF16B-D127-4A2A-81FF-2FD80F320E64}.Release|x64.Build.0 = Release|x64 + {8DB32590-FC5B-46A8-9747-344E86B18ACF}.Debug|x64.ActiveCfg = Debug|x64 + {8DB32590-FC5B-46A8-9747-344E86B18ACF}.Debug|x64.Build.0 = Debug|x64 + {8DB32590-FC5B-46A8-9747-344E86B18ACF}.Release|x64.ActiveCfg = Release|x64 + {8DB32590-FC5B-46A8-9747-344E86B18ACF}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ml_vet/Main.cs b/ml_vet/Main.cs new file mode 100644 index 0000000..506b4f6 --- /dev/null +++ b/ml_vet/Main.cs @@ -0,0 +1,171 @@ +using ABI_RC.Core; +using ABI_RC.Core.Player; +using ABI_RC.Core.Player.EyeMovement; +using ABI_RC.Core.Savior; +using ABI_RC.Systems.FaceTracking; +using ABI_RC.Systems.FaceTracking.Impl; +using System.Reflection; +using System.Threading; +using UnityEngine; +using ViveSR.anipal.Eye; + +namespace ml_vet +{ + public class ViveEyeTracking : MelonLoader.MelonMod + { + const string c_gameSettingName = "ImplementationVRViveFaceTracking"; + + static ViveEyeTracking ms_instance = null; + + readonly object m_lock = new object(); + EyeData_v2 m_threadEyeData; // Shared between threads + SingleEyeData m_combinedEyeData; + SingleEyeData m_leftEyeData; + SingleEyeData m_rightEyeData; + + Vector2 m_openness = Vector2.one; + Vector3 m_gazeDirection = Vector3.forward; + + bool m_faceTrackingEnabled = false; + + public override void OnInitializeMelon() + { + ms_instance = this; + + Settings.Init(); + + HarmonyInstance.Patch( + typeof(SRanipalTrackingModule).GetMethod(nameof(SRanipalTrackingModule.Initialize), BindingFlags.Public | BindingFlags.Instance), + new HarmonyLib.HarmonyMethod(typeof(ViveEyeTracking).GetMethod(nameof(SRanipalTrackingModuleUpdate_Prefix), BindingFlags.NonPublic | BindingFlags.Static)) + ); + HarmonyInstance.Patch(typeof(EyeMovementController).GetMethod("Update", BindingFlags.NonPublic | BindingFlags.Instance), + null, + new HarmonyLib.HarmonyMethod(typeof(ViveEyeTracking).GetMethod(nameof(EyeMovementControllerUpdate_Postfix), BindingFlags.NonPublic | BindingFlags.Static)) + ); + HarmonyInstance.Patch( + typeof(PlayerSetup).GetMethod("UpdatePlayerAvatarMovementData", BindingFlags.Instance | BindingFlags.NonPublic), + null, + new HarmonyLib.HarmonyMethod(typeof(ViveEyeTracking).GetMethod(nameof(OnPlayerAvatarMovementDataUpdate_Postfix), BindingFlags.Static | BindingFlags.NonPublic)) + ); + + MelonLoader.MelonCoroutines.Start(WaitForRootLogic(HarmonyInstance)); + } + + System.Collections.IEnumerator WaitForRootLogic(HarmonyLib.Harmony p_harmony) + { + while(RootLogic.Instance == null) + yield return null; + while(MetaPort.Instance == null) + yield return null; + + p_harmony.Patch( + typeof(FaceTrackingManager).GetMethod(nameof(FaceTrackingManager.SubmitNewEyeData), BindingFlags.Public | BindingFlags.Instance), + null, + new HarmonyLib.HarmonyMethod(typeof(ViveEyeTracking).GetMethod(nameof(FaceTrackingManagerSubmitNewEyeData_Postfix), BindingFlags.Static | BindingFlags.NonPublic)) + ); + + m_faceTrackingEnabled = MetaPort.Instance.settings.GetSettingsBool(c_gameSettingName); + MetaPort.Instance.settings.settingBoolChanged.AddListener(this.OnGameSettingBoolChanged); + } + + public override void OnDeinitializeMelon() + { + ms_instance = null; + } + + public override void OnUpdate() + { + if(Settings.Enabled && m_faceTrackingEnabled && Utils.IsInVR()) + { + if(Monitor.TryEnter(m_lock)) + { + m_combinedEyeData = m_threadEyeData.verbose_data.combined.eye_data; + m_leftEyeData = m_threadEyeData.verbose_data.left; + m_rightEyeData = m_threadEyeData.verbose_data.right; + Monitor.Exit(m_lock); + } + + if(m_combinedEyeData.GetValidity(SingleEyeDataValidity.SINGLE_EYE_DATA_GAZE_DIRECTION_VALIDITY)) + { + m_gazeDirection = m_combinedEyeData.gaze_direction_normalized; + m_gazeDirection.x *= -1f; + } + + if(m_leftEyeData.GetValidity(SingleEyeDataValidity.SINGLE_EYE_DATA_EYE_OPENNESS_VALIDITY)) + m_openness.x = m_leftEyeData.eye_openness; + + if(m_rightEyeData.GetValidity(SingleEyeDataValidity.SINGLE_EYE_DATA_EYE_OPENNESS_VALIDITY)) + m_openness.y = m_rightEyeData.eye_openness; + } + } + + void OnGameSettingBoolChanged(string p_name, bool p_state) + { + if(p_name == c_gameSettingName) + m_faceTrackingEnabled = p_state; + } + + bool IsReadyForUpdate() => (Settings.Enabled && m_faceTrackingEnabled && FaceTrackingManager.Instance.IsEyeDataAvailable()); + + // Patches + static void SRanipalTrackingModuleUpdate_Prefix(ref bool useEye, bool useLip) + { + // Hijack face tracking module to be eye tracking module as well, because it is SRanipal too and can initialize without issues + if(useLip && Settings.Enabled) + useEye = true; + } + + static void FaceTrackingManagerSubmitNewEyeData_Postfix(ref EyeData_v2 eyeData) => ms_instance?.OnEyeDataPostSubmit(ref eyeData); + void OnEyeDataPostSubmit(ref EyeData_v2 p_data) + { + // This is called not in main thread + try + { + Monitor.Enter(m_lock); + m_threadEyeData = p_data; + Monitor.Exit(m_lock); + } + catch(System.Exception e) + { + MelonLoader.MelonLogger.Error(e); + } + } + + static void EyeMovementControllerUpdate_Postfix(ref EyeMovementController __instance) => ms_instance?.OnEyeMovementControllerPostUpdate(__instance); + void OnEyeMovementControllerPostUpdate(EyeMovementController p_controller) + { + try + { + if(p_controller.IsLocal && IsReadyForUpdate()) + { + p_controller.manualBlinking = true; + p_controller.blinkProgress = 1f - Mathf.Clamp01((m_openness.x + m_openness.y) * 0.5f); + + p_controller.manualViewTarget = true; + p_controller.targetViewPosition = p_controller.ViewPointTransform.position + p_controller.ViewPointTransform.rotation * (m_gazeDirection * 5f); + } + } + catch(System.Exception e) + { + MelonLoader.MelonLogger.Error(e); + } + } + + static void OnPlayerAvatarMovementDataUpdate_Postfix(ref PlayerSetup __instance, PlayerAvatarMovementData ____playerAvatarMovementData) => ms_instance?.OnPlayerAvatarMovementDataPostUpdate(__instance, ____playerAvatarMovementData); + void OnPlayerAvatarMovementDataPostUpdate(PlayerSetup p_instance, PlayerAvatarMovementData p_data) + { + try + { + if(IsReadyForUpdate() && (p_instance.EyeMovementController != null)) + { + p_data.EyeTrackingBlinkProgressLeft = 1f - Mathf.Clamp01(m_openness.x); + p_data.EyeTrackingBlinkProgressRight = 1f - Mathf.Clamp01(m_openness.y); + } + } + catch(System.Exception e) + { + MelonLoader.MelonLogger.Error(e); + } + } + } +} diff --git a/ml_vet/Properties/AssemblyInfo.cs b/ml_vet/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..a9c4cdd --- /dev/null +++ b/ml_vet/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +[assembly: MelonLoader.MelonInfo(typeof(ml_vet.ViveEyeTracking), "ViveEyeTracking", "1.0.0", "SDraw", "https://github.com/SDraw/ml_mods_cvr")] +[assembly: MelonLoader.MelonGame(null, "ChilloutVR")] +[assembly: MelonLoader.MelonPlatform(MelonLoader.MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)] +[assembly: MelonLoader.MelonPlatformDomain(MelonLoader.MelonPlatformDomainAttribute.CompatibleDomains.MONO)] diff --git a/ml_vet/README.md b/ml_vet/README.md new file mode 100644 index 0000000..56dcb5d --- /dev/null +++ b/ml_vet/README.md @@ -0,0 +1,21 @@ +# Vive Eye Tracking +This mod complements functionality of in-game SRanipal face tracking module and makes it work additionaly as eye tracking module. + +# Why? +* Game has unfinished/unused and forcibly disabled eye tracking code that actually uses SRanipal API instead of TobiiXR. +* Implemented native TobiiXR eye tracking is very unreliable. It freezes main thread with 66% chance at game launch and has limited detection of eyes openness. + +# Benefits? +* SRanipal API supports ranged eyes openness detection, unlike TobiiXR. You can finally smirk now. +* There is no main thread freeze at game launch. +* Semi-ready for future game update with separated eyes blinking. + +# Installation +* Install [latest MelonLoader](https://github.com/LavaGang/MelonLoader) +* Get [latest release DLL](../../../releases/latest): + * Put `ViveEyeTracking.dll` in `Mods` folder of game + +# Usage +Available mod's settings in `Settings - Implementation - Vive Eye Tracking`: +* **Enable eye tracking:** eye tracking state; `false` by default. + * Note: If Vive face tracking is already running, enabling requires Vive face tracking restart. diff --git a/ml_vet/ResourcesHandler.cs b/ml_vet/ResourcesHandler.cs new file mode 100644 index 0000000..9651d2e --- /dev/null +++ b/ml_vet/ResourcesHandler.cs @@ -0,0 +1,30 @@ +using System; +using System.IO; +using System.Reflection; + +namespace ml_vet +{ + static class ResourcesHandler + { + readonly static string ms_namespace = typeof(ResourcesHandler).Namespace; + + public static string GetEmbeddedResource(string p_name) + { + string l_result = ""; + Assembly l_assembly = Assembly.GetExecutingAssembly(); + + try + { + Stream l_libraryStream = l_assembly.GetManifestResourceStream(ms_namespace + ".resources." + p_name); + StreamReader l_streadReader = new StreamReader(l_libraryStream); + l_result = l_streadReader.ReadToEnd(); + } + catch(Exception e) + { + MelonLoader.MelonLogger.Warning(e); + } + + return l_result; + } + } +} diff --git a/ml_vet/Settings.cs b/ml_vet/Settings.cs new file mode 100644 index 0000000..8df996f --- /dev/null +++ b/ml_vet/Settings.cs @@ -0,0 +1,90 @@ +using ABI_RC.Core.InteractionSystem; +using System; +using System.Collections.Generic; + +namespace ml_vet +{ + static class Settings + { + internal class SettingEvent + { + event Action m_action; + public void AddListener(Action p_listener) => m_action += p_listener; + public void RemoveListener(Action p_listener) => m_action -= p_listener; + public void Invoke(T p_value) => m_action?.Invoke(p_value); + } + + enum ModSetting + { + Enabled = 0 + } + + public static bool Enabled { get; private set; } = false; + + static MelonLoader.MelonPreferences_Category ms_category = null; + static List ms_entries = null; + + public static readonly SettingEvent OnEnabledChanged = new SettingEvent(); + + internal static void Init() + { + ms_category = MelonLoader.MelonPreferences.CreateCategory("VET", null, true); + + ms_entries = new List() + { + ms_category.CreateEntry(ModSetting.Enabled.ToString(), Enabled), + }; + + Enabled = (bool)ms_entries[(int)ModSetting.Enabled].BoxedValue; + + MelonLoader.MelonCoroutines.Start(WaitMainMenuUi()); + } + + static System.Collections.IEnumerator WaitMainMenuUi() + { + while(ViewManager.Instance == null) + yield return null; + while(ViewManager.Instance.gameMenuView == null) + yield return null; + while(ViewManager.Instance.gameMenuView.Listener == null) + yield return null; + + ViewManager.Instance.gameMenuView.Listener.ReadyForBindings += () => + { + ViewManager.Instance.gameMenuView.View.BindCall("OnToggleUpdate_" + ms_category.Identifier, new Action(OnToggleUpdate)); + }; + ViewManager.Instance.gameMenuView.Listener.FinishLoad += (_) => + { + ViewManager.Instance.gameMenuView.View.ExecuteScript(ResourcesHandler.GetEmbeddedResource("mods_extension.js")); + ViewManager.Instance.gameMenuView.View.ExecuteScript(ResourcesHandler.GetEmbeddedResource("mod_menu.js")); + foreach(var l_entry in ms_entries) + ViewManager.Instance.gameMenuView.View.TriggerEvent("updateModSetting", ms_category.Identifier, l_entry.DisplayName, l_entry.GetValueAsString()); + }; + } + + static void OnToggleUpdate(string p_name, string p_value) + { + try + { + if(Enum.TryParse(p_name, out ModSetting l_setting) && bool.TryParse(p_value,out bool l_value)) + { + switch(l_setting) + { + case ModSetting.Enabled: + { + Enabled = l_value; + OnEnabledChanged.Invoke(Enabled); + } + break; + } + + ms_entries[(int)l_setting].BoxedValue = l_value; + } + } + catch(Exception e) + { + MelonLoader.MelonLogger.Error(e); + } + } + } +} diff --git a/ml_vet/Utils.cs b/ml_vet/Utils.cs new file mode 100644 index 0000000..63e658c --- /dev/null +++ b/ml_vet/Utils.cs @@ -0,0 +1,15 @@ +using ABI_RC.Core.Savior; +using ABI_RC.Core.UI; +using System.Reflection; + +namespace ml_vet +{ + public static class Utils + { + static readonly FieldInfo ms_view = typeof(CohtmlControlledViewWrapper).GetField("_view", BindingFlags.Instance | BindingFlags.NonPublic); + + public static void ExecuteScript(this CohtmlControlledViewWrapper p_instance, string p_script) => (ms_view?.GetValue(p_instance) as cohtml.Net.View)?.ExecuteScript(p_script); + + public static bool IsInVR() => ((MetaPort.Instance != null) && MetaPort.Instance.isUsingVr); + } +} diff --git a/ml_vet/ml_vet.csproj b/ml_vet/ml_vet.csproj new file mode 100644 index 0000000..a84de2c --- /dev/null +++ b/ml_vet/ml_vet.csproj @@ -0,0 +1,70 @@ + + + + netstandard2.1 + x64 + ViveEyeTracking + 1.0.0 + SDraw + SDraw + ViveEyeTracking + ViveEyeTracking + + + + embedded + true + + + + + + + + + + + + + + D:\games\Steam\steamapps\common\ChilloutVR\MelonLoader\net35\0Harmony.dll + false + false + + + D:\games\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\Assembly-CSharp.dll + false + false + + + D:\games\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\cohtml.Net.dll + false + false + + + D:\games\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\Cohtml.Runtime.dll + false + false + + + D:\games\Steam\steamapps\common\ChilloutVR\MelonLoader\net35\MelonLoader.dll + false + false + + + D:\games\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\UnityEngine.dll + false + false + + + D:\Games\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\UnityEngine.CoreModule.dll + false + false + + + + + + + + diff --git a/ml_vet/resources/mod_menu.js b/ml_vet/resources/mod_menu.js new file mode 100644 index 0000000..b69fc21 --- /dev/null +++ b/ml_vet/resources/mod_menu.js @@ -0,0 +1,24 @@ +{ + let l_block = document.createElement('div'); + l_block.innerHTML = ` +
+
Vive Eye Tracking
+
+
+ +
+
Enable eye tracking:
+
+
+
+
+
+

Requires Vive Face tracking restart at first.

+
+ `; + document.getElementById('settings-implementation').appendChild(l_block); + + // Toggles + for (let l_toggle of l_block.querySelectorAll('.inp_toggle')) + modsExtension.addSetting('VET', l_toggle.id, modsExtension.createToggle(l_toggle, 'OnToggleUpdate_VET')); +}