From abc811ddf640802c68a3f1305fee6b409fccd31d Mon Sep 17 00:00:00 2001 From: kafeijao Date: Thu, 15 Sep 2022 23:46:25 +0100 Subject: [PATCH] [OSC] Replaced OSC library. Refactored and fixed bugs. --- Kafe_CVR_Mods.sln | 6 - OSC/Events/Avatar.cs | 28 +++-- OSC/Events/Scene.cs | 7 +- OSC/Events/Spawnable.cs | 121 +++++++++++++------- OSC/Events/Tracking.cs | 46 +++++--- OSC/FodyWeavers.xml | 4 +- OSC/Handlers/HandlerOsc.cs | 147 ++++++++++++++---------- OSC/Handlers/OscModules/Avatar.cs | 71 +++++++++--- OSC/Handlers/OscModules/Config.cs | 20 ++-- OSC/Handlers/OscModules/IOscModule.cs | 5 +- OSC/Handlers/OscModules/Input.cs | 26 ++++- OSC/Handlers/OscModules/Spawnable.cs | 107 ++++++++++-------- OSC/Handlers/OscModules/Tracking.cs | 24 +++- OSC/HarmonyPatches.cs | 35 ++++-- OSC/Main.cs | 16 ++- OSC/OSC.csproj | 5 +- OSC/README.md | 154 ++++++++++++++++++-------- OSC/Utils/JsonConfigOsc.cs | 8 +- 18 files changed, 553 insertions(+), 277 deletions(-) diff --git a/Kafe_CVR_Mods.sln b/Kafe_CVR_Mods.sln index 3181a56..c5858ff 100644 --- a/Kafe_CVR_Mods.sln +++ b/Kafe_CVR_Mods.sln @@ -8,8 +8,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CCK.Debugger", "CCK.Debugge EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OSC", "OSC\OSC.csproj", "{81E84FD7-1C2F-48C2-B28F-B5045BDD0380}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpOSC", "Libs\SharpOSC\SharpOSC\SharpOSC.csproj", "{9B922B0B-5595-4E05-8270-F63FAAA6C299}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -32,9 +30,5 @@ Global {81E84FD7-1C2F-48C2-B28F-B5045BDD0380}.Debug|Any CPU.Build.0 = Debug|Any CPU {81E84FD7-1C2F-48C2-B28F-B5045BDD0380}.Release|Any CPU.ActiveCfg = Release|Any CPU {81E84FD7-1C2F-48C2-B28F-B5045BDD0380}.Release|Any CPU.Build.0 = Release|Any CPU - {9B922B0B-5595-4E05-8270-F63FAAA6C299}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9B922B0B-5595-4E05-8270-F63FAAA6C299}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9B922B0B-5595-4E05-8270-F63FAAA6C299}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9B922B0B-5595-4E05-8270-F63FAAA6C299}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/OSC/Events/Avatar.cs b/OSC/Events/Avatar.cs index 474f01b..66f7c27 100644 --- a/OSC/Events/Avatar.cs +++ b/OSC/Events/Avatar.cs @@ -27,6 +27,7 @@ public static class Avatar { // Configs cache private static bool _triggersEnabled; private static bool _setAvatarEnabled; + private static bool _debugConfigWarnings; // Misc private static readonly Stopwatch AvatarSetStopwatch = new(); @@ -49,6 +50,10 @@ static Avatar() { // Handle the set avatar enabled configuration _setAvatarEnabled = OSC.Instance.meOSCAvatarModuleSetAvatar.Value; OSC.Instance.meOSCAvatarModuleSetAvatar.OnValueChanged += (_, enabled) => _setAvatarEnabled = enabled; + + // Handle the warning when blocked osc command by config + _debugConfigWarnings = OSC.Instance.meOSCDebugConfigWarnings.Value; + OSC.Instance.meOSCDebugConfigWarnings.OnValueChanged += (_, enabled) => _debugConfigWarnings = enabled; } // Callers @@ -107,25 +112,29 @@ internal static async void OnAnimatorManagerUpdate(CVRAnimatorManager animatorMa AnimatorManagerUpdated?.Invoke(animatorManager); } - internal static void OnAvatarSet(string uuid) { + internal static void OnAvatarSet(string guid) { if (!_setAvatarEnabled) { - MelonLogger.Msg("[Info] Attempted to set the avatar via OSC, but that option is disabled on the mod configuration."); + if (_debugConfigWarnings) { + MelonLogger.Msg("[Config] Attempted to change the avatar via OSC, but that option is disabled on the mod configuration."); + } return; } // Ignore malformed guids - if (!Guid.TryParse(uuid, out _)) return; + if (!Guid.TryParse(guid, out var guidValue)) return; + var parsedGuid = guidValue.ToString("D"); // Timer to prevent spamming this (since it's an API call if (!AvatarSetStopwatch.IsRunning) AvatarSetStopwatch.Start(); - else if (AvatarSetStopwatch.Elapsed < TimeSpan.FromSeconds(30)) { - MelonLogger.Msg($"[Info] Attempted to change avatar to {uuid}, but changing avatar is still on cooldown (30 secs)..."); + else if (AvatarSetStopwatch.Elapsed < TimeSpan.FromSeconds(10)) { + MelonLogger.Msg($"[Info] Attempted to change avatar to {parsedGuid}, but changing avatar is still on cooldown " + + $"(10 secs)..."); return; } AvatarSetStopwatch.Restart(); - MelonLogger.Msg($"[Command] Received OSC command to change avatar to {uuid}. Changing..."); - AssetManagement.Instance.LoadLocalAvatar(uuid); + MelonLogger.Msg($"[Command] Received OSC command to change avatar to {parsedGuid}. Changing..."); + AssetManagement.Instance.LoadLocalAvatar(parsedGuid); } // Callers parameters changed @@ -176,7 +185,10 @@ internal static void OnParameterSetBool(string name, bool value) { internal static void OnParameterSetTrigger(string name) { if (!_triggersEnabled) { - MelonLogger.Msg("[Info] Attempted to set a trigger parameter, but that option is disabled in the mod configuration."); + if (_debugConfigWarnings) { + MelonLogger.Msg("[Config] Attempted to set a trigger parameter, but that option is disabled in " + + "the mod configuration."); + } return; } _localPlayerAnimatorManager?.SetAnimatorParameterTrigger(name); diff --git a/OSC/Events/Scene.cs b/OSC/Events/Scene.cs index 1d8e675..c88a8f4 100644 --- a/OSC/Events/Scene.cs +++ b/OSC/Events/Scene.cs @@ -1,6 +1,4 @@ -using MelonLoader; - -namespace OSC.Events; +namespace OSC.Events; public static class Scene { @@ -25,5 +23,8 @@ internal static void ResetAll() { // Re-initialize spawnables Spawnable.Reset(); + + // Clear devices connection status + Tracking.Reset(); } } diff --git a/OSC/Events/Spawnable.cs b/OSC/Events/Spawnable.cs index 90d5d68..93ea1ad 100644 --- a/OSC/Events/Spawnable.cs +++ b/OSC/Events/Spawnable.cs @@ -1,23 +1,39 @@ -using ABI_RC.Core.Util; +using ABI_RC.Core.Player; +using ABI_RC.Core.Util; using ABI.CCK.Components; using Assets.ABI_RC.Systems.Safety.AdvancedSafety; +using MelonLoader; using UnityEngine; namespace OSC.Events; public static class Spawnable { + private static bool _debugMode; + // Caches for spawnable output (because some parameters might get spammed like hell) - private static readonly Dictionary PropCache = new(); - private static readonly Dictionary> PropParametersCacheOutFloat = new(); - private static readonly Dictionary PropAvailabilityCache = new(); + private static readonly Dictionary PropCache; + private static readonly Dictionary> SpawnableParametersCacheOutFloat; + private static readonly Dictionary PropAvailabilityCache; public static event Action SpawnableCreated; - public static event Action SpawnableDeleted; + public static event Action SpawnableDeleted; public static event Action SpawnableParameterChanged; public static event Action SpawnableAvailable; public static event Action SpawnableLocationTrackingTicked; + static Spawnable() { + + // Handle config debug value and changes + _debugMode = OSC.Instance.meOSCDebug.Value; + OSC.Instance.meOSCDebug.OnValueChanged += (_, newValue) => _debugMode = newValue; + + // Instantiate caches + PropCache = new Dictionary(); + SpawnableParametersCacheOutFloat = new Dictionary>(); + PropAvailabilityCache = new Dictionary(); + } + // Events from the game internal static void Reset() { @@ -29,41 +45,38 @@ internal static void Reset() { internal static void OnSpawnableCreated(CVRSyncHelper.PropData propData) { if (propData?.Spawnable == null || !propData.Spawnable.IsMine()) return; - // Add prop to caches + // Add prop data to caches if (!PropCache.ContainsKey(propData.InstanceId)) { PropCache.Add(propData.InstanceId, propData); } - if (!PropParametersCacheOutFloat.ContainsKey(propData.InstanceId)) { - PropParametersCacheOutFloat.Add(propData.InstanceId, new Dictionary()); + if (!SpawnableParametersCacheOutFloat.ContainsKey(propData.InstanceId)) { + SpawnableParametersCacheOutFloat.Add(propData.InstanceId, new Dictionary()); } - //MelonLogger.Msg($"[Spawnable] Spawnable {propData.Spawnable.instanceId} was created!"); - SpawnableCreated?.Invoke(propData); // Update availability because spawning doesn't trigger UpdateFromNetwork OnSpawnableUpdateFromNetwork(propData, propData.Spawnable); } - internal static void OnSpawnableDeleted(CVRSyncHelper.PropData propData) { + internal static void OnSpawnableDestroyed(CVRSpawnable spawnable) { + if (spawnable == null || !PropCache.ContainsKey(spawnable.instanceId)) return; - // Remove prop from caches - if (PropCache.ContainsKey(propData.InstanceId)) { - PropCache.Remove(propData.InstanceId); + // Remove spawnable from caches + if (PropCache.ContainsKey(spawnable.instanceId)) { + PropCache.Remove(spawnable.instanceId); } - if (PropParametersCacheOutFloat.ContainsKey(propData.InstanceId)) { - PropParametersCacheOutFloat.Remove(propData.InstanceId); + if (SpawnableParametersCacheOutFloat.ContainsKey(spawnable.instanceId)) { + SpawnableParametersCacheOutFloat.Remove(spawnable.instanceId); } - //MelonLogger.Msg($"[Spawnable] Spawnable {propData.Spawnable.instanceId} was deleted!"); - - SpawnableDeleted?.Invoke(propData); + SpawnableDeleted?.Invoke(spawnable); } internal static void OnSpawnableParameterChanged(CVRSpawnable spawnable, CVRSpawnableValue spawnableValue) { - if (spawnable == null || spawnableValue == null || !spawnable.IsMine() || !PropParametersCacheOutFloat.ContainsKey(spawnable.instanceId)) return; + if (spawnable == null || spawnableValue == null || !spawnable.IsMine() || !SpawnableParametersCacheOutFloat.ContainsKey(spawnable.instanceId)) return; - var cache = PropParametersCacheOutFloat[spawnable.instanceId]; + var cache = SpawnableParametersCacheOutFloat[spawnable.instanceId]; // Value already exists and it's updated if (cache.ContainsKey(spawnableValue.name) && Mathf.Approximately(cache[spawnableValue.name], spawnableValue.currentValue)) return; @@ -101,33 +114,51 @@ internal static void OnTrackingTick() { internal static void OnSpawnableParameterSet(string spawnableInstanceId, string spawnableParamName, float spawnableParamValue) { if (!PropCache.ContainsKey(spawnableInstanceId) || spawnableParamName == "" || spawnableParamValue.IsAbsurd()) return; - var spawnable = PropCache[spawnableInstanceId].Spawnable; + var spawnable = PropCache[spawnableInstanceId]?.Spawnable; // Prevent NullReferenceException when we're setting the location of a prop that was just deleted if (spawnable == null) return; + var spawnableValueIndex = spawnable.syncValues.FindIndex( match => match.name == spawnableParamName); if (spawnableValueIndex == -1) return; - //MelonLogger.Msg($"[Spawnable] Setting spawnable prop {spawnableInstanceId} {spawnableParamName} parameter to {spawnableParamValue}!"); - // Value is already up to date -> Ignore if (Mathf.Approximately(spawnable.syncValues[spawnableValueIndex].currentValue, spawnableParamValue)) return; if (!ShouldControl(spawnable, true)) return; - spawnable.SetValue(spawnableValueIndex, spawnableParamValue); + if (_debugMode) { + MelonLogger.Msg($"[Debug] Set p+{spawnable.guid}~{spawnableInstanceId} {spawnableParamName} parameter" + + $" to {spawnableParamValue}"); + } - //SpawnableParameterSet?.Invoke(spawnable, spawnable.syncValues[spawnableValueIndex]); + spawnable.SetValue(spawnableValueIndex, spawnableParamValue); } internal static void OnSpawnableLocationSet(string spawnableInstanceId, Vector3 pos, Vector3 rot, int? subIndex = null) { - if (!PropCache.ContainsKey(spawnableInstanceId) || pos.IsAbsurd() || pos.IsBad() || rot.IsAbsurd() || rot.IsBad()) return; - var spawnable = PropCache[spawnableInstanceId].Spawnable; + + if (_debugMode) MelonLogger.Msg($"[Debug] Attempting to set {spawnableInstanceId} [{(subIndex.HasValue ? subIndex.Value : "Main")}] location..."); + + if (!PropCache.ContainsKey(spawnableInstanceId) || pos.IsAbsurd() || pos.IsBad() || rot.IsAbsurd() || rot.IsBad()) { + if (_debugMode) { + MelonLogger.Msg($"[Debug] Attempted to fetch {spawnableInstanceId} from cache. But it was missing or" + + $"the location was borked! InCache: {PropCache.ContainsKey(spawnableInstanceId)}" + + $"\n\t\t\tpos: {pos.ToString()}, rot: {rot.ToString()}"); + } + return; + } + var spawnable = PropCache[spawnableInstanceId]?.Spawnable; // Prevent NullReferenceException when we're setting the location of a prop that was just deleted - if (spawnable == null) return; + if (spawnable == null) { + if (_debugMode) { + MelonLogger.Msg($"[Debug] Attempted to fetch {spawnableInstanceId} from cache. But the associated " + + $"spawnable was null..."); + } + return; + } Transform transformToSet; // The transform is a subSync of the spawnable @@ -141,22 +172,33 @@ internal static void OnSpawnableLocationSet(string spawnableInstanceId, Vector3 transformToSet = spawnable.transform; } - //MelonLogger.Msg($"[Spawnable] Setting spawnable prop {spawnableInstanceId} {spawnableParamName} parameter to {spawnableParamValue}!"); - - if (!ShouldControl(spawnable)) return; + if (!ShouldControl(spawnable)) { + if (_debugMode) { + MelonLogger.Msg($"[Debug] Attempted to control {spawnableInstanceId} but got refused! " + + $"Sync: {spawnable.SyncType} IsPhysics: {spawnable.isPhysicsSynced}"); + } + return; + } // Update location transformToSet.position = pos; transformToSet.eulerAngles = rot; spawnable.ForceUpdate(); - //SpawnableLocationSet?.Invoke(); + if (_debugMode) MelonLogger.Msg($"[Debug] \t{spawnableInstanceId} [{(subIndex.HasValue ? subIndex.Value : "Main")}] location set!"); } - internal static void OnSpawnableCreate(string propGuid, float posX = 0f, float posY = 0f, float posZ = 0f) { - if (Guid.TryParse(propGuid, out _) && !posX.IsAbsurd() && !posY.IsAbsurd() && !posZ.IsAbsurd()) { - CVRSyncHelper.SpawnProp(propGuid, posX, posY, posZ); + internal static void OnSpawnableCreate(string propGuid, float? posX = null, float? posY = null, float? posZ = null) { + if (Guid.TryParse(propGuid, out _)) { + if (posX.HasValue && posX.Value.IsAbsurd() && posY.HasValue && posY.Value.IsAbsurd() && posZ.HasValue && posZ.Value.IsAbsurd()) { + // Spawn prop with the local coordinates provided + CVRSyncHelper.SpawnProp(propGuid, posX.Value, posY.Value, posZ.Value); + } + else { + // Spawn prop without coordinates -> spawns in front of the player + PlayerSetup.Instance.DropProp(propGuid); + } } } @@ -172,13 +214,6 @@ private static bool ShouldControl(CVRSpawnable spawnable, bool allowWhenLocalPla // Spawned by other people -> Ignore if (!spawnable.IsMine()) return false; - // var pickup = Traverse.Create(spawnable).Field("pickup").Value; - // var attachments = Traverse.Create(spawnable).Field>("_attachments").Value; - - // Ignore prop if we're not grabbing it nor it is attached to us - //if ((pickup == null || pickup.grabbedBy != MetaPort.Instance.ownerId) && - // (attachments.Count <= 0 || !attachments.Any(a => a.IsAttached()))) return; - // Other people are syncing it (grabbing/telegrabbing/attatched) -> Ignore if (spawnable.SyncType != 0) return false; diff --git a/OSC/Events/Tracking.cs b/OSC/Events/Tracking.cs index 735e5b8..1334923 100644 --- a/OSC/Events/Tracking.cs +++ b/OSC/Events/Tracking.cs @@ -23,6 +23,9 @@ public static class Tracking { private static Transform _playerSpaceTransform; private static Transform _playerHmdTransform; + private static readonly Dictionary TrackerLastState = new(); + + public static event Action TrackingDeviceConnected; public static event Action TrackingDataDeviceUpdated; public static event Action TrackingDataPlaySpaceUpdated; @@ -41,8 +44,8 @@ static Tracking() { }; // Set the play space transform when it loads - Events.Scene.PlayerSetup += () => _playerSpaceTransform = PlayerSetup.Instance.transform; - Events.Scene.PlayerSetup += () => _playerHmdTransform = PlayerSetup.Instance.vrCamera.transform; + Scene.PlayerSetup += () => _playerSpaceTransform = PlayerSetup.Instance.transform; + Scene.PlayerSetup += () => _playerHmdTransform = PlayerSetup.Instance.vrCamera.transform; } public static void OnTrackingDataDeviceUpdated(VRTrackerManager trackerManager) { @@ -56,21 +59,17 @@ public static void OnTrackingDataDeviceUpdated(VRTrackerManager trackerManager) // Handle trackers foreach (var vrTracker in trackerManager.trackers) { + // Manage Connected/Disconnected trackers + if ((!TrackerLastState.ContainsKey(vrTracker) && vrTracker.active) || (TrackerLastState.ContainsKey(vrTracker) && TrackerLastState[vrTracker] != vrTracker.active)) { + TrackingDeviceConnected?.Invoke(vrTracker.active, GetSource(vrTracker), GetIndex(vrTracker), vrTracker.deviceName); + TrackerLastState[vrTracker] = vrTracker.active; + } + // Ignore inactive trackers if (!vrTracker.active) continue; - var index = (int) Traverse.Create(vrTracker).Field("_trackedObject").Value.index; var transform = vrTracker.transform; - - var source = vrTracker.role switch { - ETrackedControllerRole.Invalid => vrTracker.deviceName == "" ? TrackingDataSource.base_station : TrackingDataSource.unknown, - ETrackedControllerRole.LeftHand => TrackingDataSource.left_controller, - ETrackedControllerRole.RightHand => TrackingDataSource.right_controller, - ETrackedControllerRole.OptOut => TrackingDataSource.tracker, - _ => TrackingDataSource.unknown - }; - - TrackingDataDeviceUpdated?.Invoke(source, index, vrTracker.deviceName, transform.position, transform.rotation.eulerAngles, vrTracker.batteryStatus); + TrackingDataDeviceUpdated?.Invoke(GetSource(vrTracker), GetIndex(vrTracker), vrTracker.deviceName, transform.position, transform.rotation.eulerAngles, vrTracker.batteryStatus); } // Handle HMD @@ -87,4 +86,25 @@ public static void OnTrackingDataDeviceUpdated(VRTrackerManager trackerManager) Spawnable.OnTrackingTick(); } } + + internal static void Reset() { + // Clear the cache to force an update to the connected devices + TrackerLastState.Clear(); + } + + private static int GetIndex(VRTracker vrTracker) { + return (int)Traverse.Create(vrTracker).Field("_trackedObject").Value.index; + } + + private static TrackingDataSource GetSource(VRTracker vrTracker) { + return vrTracker.role switch { + ETrackedControllerRole.Invalid => vrTracker.deviceName == "" + ? TrackingDataSource.base_station + : TrackingDataSource.unknown, + ETrackedControllerRole.LeftHand => TrackingDataSource.left_controller, + ETrackedControllerRole.RightHand => TrackingDataSource.right_controller, + ETrackedControllerRole.OptOut => TrackingDataSource.tracker, + _ => TrackingDataSource.unknown + }; + } } diff --git a/OSC/FodyWeavers.xml b/OSC/FodyWeavers.xml index 0d6e19e..3728e42 100644 --- a/OSC/FodyWeavers.xml +++ b/OSC/FodyWeavers.xml @@ -1,3 +1,3 @@  - - \ No newline at end of file + + diff --git a/OSC/Handlers/HandlerOsc.cs b/OSC/Handlers/HandlerOsc.cs index 657d312..74f5d8f 100644 --- a/OSC/Handlers/HandlerOsc.cs +++ b/OSC/Handlers/HandlerOsc.cs @@ -1,6 +1,8 @@ -using MelonLoader; -using SharpOSC; +using System.Collections; +using System.Net; +using MelonLoader; using OSC.Handlers.OscModules; +using Rug.Osc; namespace OSC.Handlers; @@ -8,14 +10,14 @@ internal class HandlerOsc { private static HandlerOsc Instance; - private UDPListener _listener; - private UDPSender _sender; + private OscReceiver _receiver; + private OscSender _sender; - private Avatar AvatarHandler; - private Input InputHandler; - private Spawnable SpawnableHandler; - private Tracking TrackingHandler; - private Config ConfigHandler; + private readonly Avatar AvatarHandler; + private readonly Input InputHandler; + private readonly Spawnable SpawnableHandler; + private readonly Tracking TrackingHandler; + private readonly Config ConfigHandler; private static bool _debugMode; @@ -28,17 +30,28 @@ static HandlerOsc() { public HandlerOsc() { - // Start listener - _listener = new UDPListener(OSC.Instance.meOSCServerInPort.Value, ReceiveMessageHandler); - MelonLogger.Msg($"[Server] OSC Server started listening on the port {OSC.Instance.meOSCServerInPort.Value}."); + try { + // Start the osc msg receiver in a Coroutine + _receiver = new OscReceiver(OSC.Instance.meOSCServerInPort.Value); + _receiver.Connect(); + MelonCoroutines.Start(HandleOscMessages()); + + MelonLogger.Msg($"[Server] OSC Server started listening on the port {OSC.Instance.meOSCServerInPort.Value}."); + } + catch (Exception e) { + MelonLogger.Error($"Failed initializing OSC receiver Coroutine!."); + MelonLogger.Error(e); + throw; + } // Handle config listener port changes OSC.Instance.meOSCServerInPort.OnValueChanged += (oldPort, newPort) => { if (oldPort == newPort) return; MelonLogger.Msg("[Server] OSC server port config has changed. Restarting server..."); try { - _listener?.Close(); - _listener = new UDPListener(newPort, ReceiveMessageHandler); + _receiver?.Close(); + _receiver = new OscReceiver(newPort); + _receiver.Connect(); MelonLogger.Msg($"[Server] OSC Server started listening on the port {newPort}."); } catch (Exception e) { @@ -72,9 +85,11 @@ public HandlerOsc() { } private bool ConnectSender(string ip, int port) { - if (_sender != null && _sender.Port == port && _sender.Address == ip) return false; + var parsedIp = IPAddress.Parse(ip); + if (_sender != null && _sender.Port == port && Equals(_sender.RemoteAddress, parsedIp)) return false; var oldSender = _sender; - _sender = new UDPSender(ip, port); + _sender = new OscSender(parsedIp, port); + _sender.Connect(); oldSender?.Close(); return true; } @@ -83,50 +98,70 @@ public static void SendMessage(string address, params object[] data) { Instance._sender.Send(new OscMessage(address, data)); } - private static void ReceiveMessageHandler(OscPacket packet) { + private IEnumerator HandleOscMessages() { - // Ignore packets that had errors - if (packet == null) { - if (_debugMode) MelonLogger.Msg("[Debug] Received a malformed OSC msg, could not parse it. " + - "We're using SharpOSC which follows this spec: " + - "https://opensoundcontrol.stanford.edu/spec-1_0.html"); - return; - } + while (_receiver.State != OscSocketState.Closed) { + if (_receiver.State != OscSocketState.Connected) yield return null;; - var oscMessage = (OscMessage) packet; + try { + // Execute while has messages + while (_receiver.TryReceive(out var packet)) { + + // Only check osc messages + if (packet is not OscMessage oscMessage) continue; + + if (_debugMode) { + var debugMsg = $"[Debug] Received OSC Message -> Address: {oscMessage.Address}, Args:"; + debugMsg = oscMessage.Aggregate(debugMsg, + (current, arg) => current + $"\n\t\t\t{arg} [{arg?.GetType()}]"); + MelonLogger.Msg(debugMsg); + } + + try { + var addressLower = oscMessage.Address.ToLower(); + switch (addressLower) { + case not null when addressLower.StartsWith(Avatar.AddressPrefixAvatar): + AvatarHandler.ReceiveMessageHandler(oscMessage); + break; + case not null when addressLower.StartsWith(Input.AddressPrefixInput): + InputHandler.ReceiveMessageHandler(oscMessage); + break; + case not null when addressLower.StartsWith(Spawnable.AddressPrefixSpawnable): + SpawnableHandler.ReceiveMessageHandler(oscMessage); + break; + case not null when addressLower.StartsWith(Tracking.AddressPrefixTracking): + TrackingHandler.ReceiveMessageHandler(oscMessage); + break; + case not null when addressLower.StartsWith(Config.AddressPrefixConfig): + ConfigHandler.ReceiveMessageHandler(oscMessage); + break; + } + } + catch (Exception e) { + var debugMsg = $"Failed executing the ReceiveMessageHandler from OSC." + + $"[Error] Received OSC Message -> Address: {oscMessage.Address}, Args:"; + debugMsg = oscMessage.Aggregate(debugMsg, + (current, arg) => current + $"\n\t\t\t{arg} [{arg?.GetType()}]"); + MelonLogger.Error(debugMsg); + MelonLogger.Error(e); + } + + } + } + catch (Exception e) { + if (_receiver.State == OscSocketState.Connected) { + MelonLogger.Error($"Failed executing the ReceiveMessageHandler from OSC."); + MelonLogger.Error(e); + } + } - if (_debugMode) { - var debugMsg = $"[Debug] Received OSC Message -> Address: {oscMessage.Address}, Args:"; - debugMsg = oscMessage.Arguments.Aggregate(debugMsg, (current, arg) => current + $"\n\t\t\t{arg} [{arg?.GetType()}]"); - MelonLogger.Msg(debugMsg); + // Has no messages -> Wait for next frame + yield return null; } + } - try { - var address = oscMessage.Address; - var addressLower = oscMessage.Address.ToLower(); - switch (addressLower) { - case not null when addressLower.StartsWith(Avatar.AddressPrefixAvatar): - Instance.AvatarHandler.ReceiveMessageHandler(address, oscMessage.Arguments); - break; - case not null when addressLower.StartsWith(Input.AddressPrefixInput): - Instance.InputHandler.ReceiveMessageHandler(address, oscMessage.Arguments); - break; - case not null when addressLower.StartsWith(Spawnable.AddressPrefixSpawnable): - Instance.SpawnableHandler.ReceiveMessageHandler(address, oscMessage.Arguments); - break; - case not null when addressLower.StartsWith(Tracking.AddressPrefixTracking): - Instance.TrackingHandler.ReceiveMessageHandler(address, oscMessage.Arguments); - break; - case not null when addressLower.StartsWith(Config.AddressPrefixConfig): - Instance.ConfigHandler.ReceiveMessageHandler(address, oscMessage.Arguments); - break; - } - } - catch (Exception e) { - MelonLogger.Error($"Failed executing the ReceiveMessageHandler from OSC. Contact the mod creator. " + - $"Address: {oscMessage.Address} Args: {oscMessage.Arguments} Type: {oscMessage.Arguments.GetType()}"); - MelonLogger.Error(e); - throw; - } + public void Close() { + _receiver.Close(); + _sender.Close(); } } diff --git a/OSC/Handlers/OscModules/Avatar.cs b/OSC/Handlers/OscModules/Avatar.cs index a589833..a6642f5 100644 --- a/OSC/Handlers/OscModules/Avatar.cs +++ b/OSC/Handlers/OscModules/Avatar.cs @@ -4,6 +4,7 @@ using HarmonyLib; using MelonLoader; using OSC.Utils; +using Rug.Osc; using UnityEngine; namespace OSC.Handlers.OscModules; @@ -11,13 +12,15 @@ namespace OSC.Handlers.OscModules; public class Avatar : OscHandler { internal const string AddressPrefixAvatar = "/avatar/"; - internal const string AddressPrefixAvatarParameters = $"{AddressPrefixAvatar}parameters/"; + internal const string AddressPrefixAvatarParameters = $"{AddressPrefixAvatar}parameter"; + internal const string AddressPrefixAvatarParametersLegacy = $"{AddressPrefixAvatar}parameters/"; private const string AddressPrefixAvatarChange = $"{AddressPrefixAvatar}change"; private static readonly HashSet CoreParameters = Traverse.Create(typeof(CVRAnimatorManager)).Field("coreParameters").GetValue>(); private bool _enabled; private bool _bypassJsonConfig; + private bool _debugConfigWarnings; private readonly Dictionary _parameterAddressCache = new(); private readonly Action _animatorManagerUpdated; @@ -29,7 +32,7 @@ public class Avatar : OscHandler { public Avatar() { // Execute actions on local avatar changed - _animatorManagerUpdated = InitializeNewAvatar;; + _animatorManagerUpdated = InitializeNewAvatar; // Send avatar float parameter change events _parameterChangedFloat = (parameter, value) => SendAvatarParamToConfigAddress(parameter, ConvertToConfigType(parameter, value)); @@ -60,6 +63,10 @@ public Avatar() { } _bypassJsonConfig = newValue; }; + + // Handle the warning when blocked osc command by config + _debugConfigWarnings = OSC.Instance.meOSCDebugConfigWarnings.Value; + OSC.Instance.meOSCDebugConfigWarnings.OnValueChanged += (_, enabled) => _debugConfigWarnings = enabled; } internal sealed override void Enable() { @@ -80,23 +87,46 @@ internal sealed override void Disable() { _enabled = false; } - internal sealed override void ReceiveMessageHandler(string address, List args) { - if (!_enabled) return; + internal sealed override void ReceiveMessageHandler(OscMessage oscMsg) { + if (!_enabled) { + if (_debugConfigWarnings) { + MelonLogger.Msg($"[Config] Sent an osc msg to {AddressPrefixAvatar}, but this module is disabled " + + $"in the configuration file, so this will be ignored."); + } + return; + } - var addressLower = address.ToLower(); + var addressLower= oscMsg.Address.ToLower(); // Get only the first value and assume no values to be null - var valueObj = args.Count > 0 ? args[0] : null; + var valueObj = oscMsg.Count > 0 ? oscMsg[0] : null; + var valueObj2 = oscMsg.Count > 1 ? oscMsg[1] : null; // Handle the change parameter requests - if (addressLower.StartsWith(AddressPrefixAvatarParameters)) { - ParameterChangeHandler(address, valueObj); + if (addressLower.Equals(AddressPrefixAvatarParameters)) { + if (valueObj2 is not string paramName) { + MelonLogger.Msg($"[Error] Attempted to change an avatar parameter, but the parameter name " + + $"provided is not a string :( you sent a {valueObj2?.GetType()} type argument."); + return; + } + ParameterChangeHandler(paramName, valueObj); } // Handle the change avtar requests - else if (addressLower.Equals(AddressPrefixAvatarChange) && valueObj is string valueStr) { + else if (addressLower.Equals(AddressPrefixAvatarChange)) { + if (valueObj is not string valueStr) { + MelonLogger.Msg($"[Error] Attempted to change the avatar, but the guid provided is not a string " + + $":( you sent a {valueObj?.GetType()} type argument."); + return; + } Events.Avatar.OnAvatarSet(valueStr); } + + // Handle the change parameter requests [Deprecated] + else if (addressLower.StartsWith(AddressPrefixAvatarParametersLegacy)) { + var parameter = oscMsg.Address.Substring(AddressPrefixAvatarParametersLegacy.Length); + ParameterChangeHandler(parameter, valueObj); + } } private void InitializeNewAvatar(CVRAnimatorManager manager) { @@ -105,7 +135,9 @@ private void InitializeNewAvatar(CVRAnimatorManager manager) { _parameterAddressCache.Clear(); // Send change avatar event - HandlerOsc.SendMessage(AddressPrefixAvatarChange, MetaPort.Instance.currentAvatarGuid); + HandlerOsc.SendMessage(AddressPrefixAvatarChange, + MetaPort.Instance.currentAvatarGuid, + JsonConfigOsc.GetConfigFilePath(MetaPort.Instance.ownerId, MetaPort.Instance.currentAvatarGuid)); // Send all parameter values when loads a new avatar foreach (var param in manager.animator.parameters) { @@ -135,8 +167,7 @@ private void InitializeNewAvatar(CVRAnimatorManager manager) { } } - private void ParameterChangeHandler(string address, object valueObj) { - var parameter = address.Substring(AddressPrefixAvatarParameters.Length); + private void ParameterChangeHandler(string parameter, object valueObj) { // Reject core parameters if (CoreParameters.Contains(parameter)) { @@ -148,7 +179,14 @@ private void ParameterChangeHandler(string address, object valueObj) { } // Reject non-config parameters - if (!_bypassJsonConfig && !_parameterAddressCache.ContainsKey(parameter)) return; + if (!_bypassJsonConfig && !_parameterAddressCache.ContainsKey(parameter)) { + if (_debugConfigWarnings) { + MelonLogger.Msg($"[Config] Ignoring the {parameter} change because it's not present in the json " + + $"config file, and you set on the configure file to not bypass checking if the " + + $"address is present in the json config."); + } + return; + } // Sort their types and call the correct handler if (valueObj is float floatValue) Events.Avatar.OnParameterSetFloat(parameter, floatValue); @@ -215,7 +253,9 @@ private void SendAvatarParamToConfigAddress(string paramName, params object[] da // If there is no config OR is not in the config but we're bypassing -> revert to default behavior if (JsonConfigOsc.CurrentAvatarConfig == null || (_bypassJsonConfig && !_parameterAddressCache.ContainsKey(paramName))) { - HandlerOsc.SendMessage($"{AddressPrefixAvatarChange}{paramName}", data); + // Send to both endpoints to support vrc endpoint + HandlerOsc.SendMessage($"{AddressPrefixAvatarParameters}", data, paramName); + HandlerOsc.SendMessage($"{AddressPrefixAvatarParametersLegacy}{paramName}", data); return; } @@ -224,6 +264,7 @@ private void SendAvatarParamToConfigAddress(string paramName, params object[] da // Ignore non-mapped addresses in the config if (paramEntity == null) return; - HandlerOsc.SendMessage(paramEntity.address, data); + // Send the parameter name as the second argument so it is compatible with both legacy and new way + HandlerOsc.SendMessage(paramEntity.address, data, paramName); } } diff --git a/OSC/Handlers/OscModules/Config.cs b/OSC/Handlers/OscModules/Config.cs index c39d129..4426604 100644 --- a/OSC/Handlers/OscModules/Config.cs +++ b/OSC/Handlers/OscModules/Config.cs @@ -1,4 +1,5 @@ using MelonLoader; +using Rug.Osc; namespace OSC.Handlers.OscModules; @@ -10,25 +11,18 @@ public class Config : OscHandler { internal const string AddressPrefixConfig = "/config/"; - private bool _enabled = true; + internal sealed override void Enable() {} - internal sealed override void Enable() { - _enabled = true; - } - - internal sealed override void Disable() { - _enabled = false; - } + internal sealed override void Disable() {} - internal sealed override void ReceiveMessageHandler(string address, List args) { - if (!_enabled) return; + internal sealed override void ReceiveMessageHandler(OscMessage oscMsg) { - var addressParts = address.Split('/'); + var addressParts =oscMsg.Address.Split('/'); // Validate Length if (addressParts.Length != 3) { MelonLogger.Msg($"[Error] Attempted to set a config but the address is invalid." + - $"\n\t\t\tAddress attempted: \"{address}\"" + + $"\n\t\t\tAddress attempted: \"{oscMsg.Address}\"" + $"\n\t\t\tThe correct format should be: \"{AddressPrefixConfig}\"" + $"\n\t\t\tAnd the allowed ops are: {string.Join(", ", Enum.GetNames(typeof(ConfigOperation)))}"); return; @@ -43,7 +37,7 @@ internal sealed override void ReceiveMessageHandler(string address, List default: MelonLogger.Msg( "[Error] Attempted to set a config but the address is invalid." + - $"\n\t\t\tAddress attempted: \"{address}\"" + + $"\n\t\t\tAddress attempted: \"{oscMsg.Address}\"" + $"\n\t\t\tThe correct format should be: \"{AddressPrefixConfig}\"" + $"\n\t\t\tAnd the allowed ops are: {string.Join(", ", Enum.GetNames(typeof(ConfigOperation)))}" ); diff --git a/OSC/Handlers/OscModules/IOscModule.cs b/OSC/Handlers/OscModules/IOscModule.cs index 5b9919f..634cd0a 100644 --- a/OSC/Handlers/OscModules/IOscModule.cs +++ b/OSC/Handlers/OscModules/IOscModule.cs @@ -1,4 +1,5 @@ using MelonLoader; +using Rug.Osc; namespace OSC.Handlers.OscModules; @@ -6,9 +7,9 @@ public abstract class OscHandler { internal abstract void Enable(); internal abstract void Disable(); - internal virtual void ReceiveMessageHandler(string address, List args) { + internal virtual void ReceiveMessageHandler(OscMessage oscMsg) { MelonLogger.Msg("[Info] You attempted to send a message to a module that doesn't not allow receiving. " + - $"Address Attempted: {address} I hope you are sending these on a loop, because I TOLD you" + + $"Address Attempted: {oscMsg.Address} I hope you are sending these on a loop, because I TOLD you" + $"to not send to this endpoint on the docs :eyes_rolling: git spammed x)"); } } diff --git a/OSC/Handlers/OscModules/Input.cs b/OSC/Handlers/OscModules/Input.cs index e50ca6b..db73999 100644 --- a/OSC/Handlers/OscModules/Input.cs +++ b/OSC/Handlers/OscModules/Input.cs @@ -1,5 +1,7 @@ using MelonLoader; using OSC.Components; +using Rug.Osc; +using UnityEngine; namespace OSC.Handlers.OscModules; @@ -9,6 +11,7 @@ public class Input : OscHandler { private bool _enabled; private List _inputBlacklist; + private bool _debugConfigWarnings; public Input() { @@ -22,6 +25,10 @@ public Input() { // Set the blacklist and listen for it's changes according to the config _inputBlacklist = OSC.Instance.meOSCInputModuleBlacklist.Value; OSC.Instance.meOSCInputModuleBlacklist.OnValueChanged += (_, newValue) => _inputBlacklist = newValue; + + // Handle the warning when blocked osc command by config + _debugConfigWarnings = OSC.Instance.meOSCDebugConfigWarnings.Value; + OSC.Instance.meOSCDebugConfigWarnings.OnValueChanged += (_, enabled) => _debugConfigWarnings = enabled; } internal sealed override void Enable() { @@ -32,17 +39,25 @@ internal sealed override void Disable() { _enabled = false; } - internal sealed override void ReceiveMessageHandler(string address, List args) { - if (!_enabled) return; + internal sealed override void ReceiveMessageHandler(OscMessage oscMsg) { + if (!_enabled) { + if (_debugConfigWarnings) { + MelonLogger.Msg($"[Config] Sent an osc msg to {AddressPrefixInput}, but this module is disabled " + + $"in the configuration file, so this will be ignored."); + } + return; + } // Get only the first value and assume no values to be null - var valueObj = args.Count > 0 ? args[0] : null; + var valueObj = oscMsg.Count > 0 ? oscMsg[0] : null; - var inputName = address.Substring(AddressPrefixInput.Length); + var inputName =oscMsg.Address.Substring(AddressPrefixInput.Length); // Reject blacklisted inputs (ignore case) if (_inputBlacklist.Contains(inputName, StringComparer.OrdinalIgnoreCase)) { - MelonLogger.Msg($"[Info] The OSC config has {inputName} blacklisted. Edit the config to allow."); + if (_debugConfigWarnings) { + MelonLogger.Msg($"[Config] The OSC config has {inputName} blacklisted. Edit the config to allow."); + } return; } @@ -75,6 +90,7 @@ void UpdateButtonValue(ButtonNames button, bool? value) { } if (valueObj is bool boolValue) UpdateButtonValue(buttonName, boolValue); else if (valueObj is int intValue) UpdateButtonValue(buttonName, intValue == 1 ? true : intValue == 0 ? false : null); + else if (valueObj is float floatValue) UpdateButtonValue(buttonName, Mathf.Approximately(floatValue, 1f) ? true : Mathf.Approximately(floatValue, 0f) ? false : null); else if (valueObj is string valueStr) { if (int.TryParse(valueStr, out var valueInt)) UpdateButtonValue(buttonName, valueInt == 1 ? true : valueInt == 0 ? false : null); else if (bool.TryParse(valueStr, out var valueBool)) UpdateButtonValue(buttonName, valueBool); diff --git a/OSC/Handlers/OscModules/Spawnable.cs b/OSC/Handlers/OscModules/Spawnable.cs index 3129321..e21ae79 100644 --- a/OSC/Handlers/OscModules/Spawnable.cs +++ b/OSC/Handlers/OscModules/Spawnable.cs @@ -1,6 +1,7 @@ using ABI_RC.Core.Util; using ABI.CCK.Components; using MelonLoader; +using Rug.Osc; using UnityEngine; namespace OSC.Handlers.OscModules; @@ -9,7 +10,7 @@ enum SpawnableOperation { create, delete, available, - parameters, + parameter, location, location_sub, } @@ -19,9 +20,10 @@ public class Spawnable : OscHandler { internal const string AddressPrefixSpawnable = "/prop/"; private bool _enabled; + private bool _debugConfigWarnings; private readonly Action _spawnableCreated; - private readonly Action _spawnableDeleted; + private readonly Action _spawnableDeleted; private readonly Action _spawnableAvailabilityChanged; private readonly Action _spawnableParameterChanged; private readonly Action _spawnableLocationChanged; @@ -45,11 +47,9 @@ public Spawnable() { }; // Execute actions on spawnable deletion - _spawnableDeleted = propData => { - var spawnable = propData.Spawnable; - + _spawnableDeleted = spawnable => { // Send the delete event - HandlerOsc.SendMessage($"{AddressPrefixSpawnable}{nameof(SpawnableOperation.delete)}", spawnable.guid, GetInstanceId(spawnable)); + HandlerOsc.SendMessage($"{AddressPrefixSpawnable}{nameof(SpawnableOperation.delete)}", GetGuid(spawnable), GetInstanceId(spawnable)); }; // Send spawnable availability change events @@ -59,7 +59,7 @@ public Spawnable() { // Send spawnable parameter change events _spawnableParameterChanged = (spawnable, spawnableValue) => { - HandlerOsc.SendMessage($"{AddressPrefixSpawnable}{nameof(SpawnableOperation.parameters)}", spawnable.guid, GetInstanceId(spawnable), spawnableValue.name, spawnableValue.currentValue); + HandlerOsc.SendMessage($"{AddressPrefixSpawnable}{nameof(SpawnableOperation.parameter)}", spawnable.guid, GetInstanceId(spawnable), spawnableValue.name, spawnableValue.currentValue); }; // Send spawnable location change events @@ -83,6 +83,7 @@ public Spawnable() { spawnable.guid, GetInstanceId(spawnable), index, sPos.x, sPos.y, sPos.z, sRot.x, sRot.y, sRot.z); + //sPos.x + pos.x, sPos.y + pos.y, sPos.z + pos.z, sRot.x + rot.x, sRot.y + rot.y, sRot.z + rot.z); } }; @@ -93,6 +94,10 @@ public Spawnable() { if (newValue && !oldValue) Enable(); else if (!newValue && oldValue) Disable(); }; + + // Handle the warning when blocked osc command by config + _debugConfigWarnings = OSC.Instance.meOSCDebugConfigWarnings.Value; + OSC.Instance.meOSCDebugConfigWarnings.OnValueChanged += (_, enabled) => _debugConfigWarnings = enabled; } internal sealed override void Enable() { @@ -113,15 +118,21 @@ internal sealed override void Disable() { _enabled = false; } - internal sealed override void ReceiveMessageHandler(string address, List args) { - if (!_enabled) return; + internal sealed override void ReceiveMessageHandler(OscMessage oscMsg) { + if (!_enabled) { + if (_debugConfigWarnings) { + MelonLogger.Msg($"[Config] Sent an osc msg to {AddressPrefixSpawnable}, but this module is disabled " + + $"in the configuration file, so this will be ignored."); + } + return; + } - var addressParts = address.Split('/'); + var addressParts =oscMsg.Address.Split('/'); // Validate Length if (addressParts.Length != 3) { MelonLogger.Msg($"[Error] Attempted to interact with a prop but the address is invalid." + - $"\n\t\t\tAddress attempted: \"{address}\"" + + $"\n\t\t\tAddress attempted: \"{oscMsg.Address}\"" + $"\n\t\t\tThe correct format should be: \"{AddressPrefixSpawnable}\"" + $"\n\t\t\tAnd the allowed ops are: {string.Join(", ", Enum.GetNames(typeof(SpawnableOperation)))}"); return; @@ -130,20 +141,20 @@ internal sealed override void ReceiveMessageHandler(string address, List Enum.TryParse(addressParts[2], true, out var spawnableOperation); switch (spawnableOperation) { - case SpawnableOperation.parameters: - ReceivedParameterHandler(args); + case SpawnableOperation.parameter: + ReceivedParameterHandler(oscMsg); return; case SpawnableOperation.location: - ReceivedLocationHandler(args); + ReceivedLocationHandler(oscMsg); return; case SpawnableOperation.location_sub: - ReceivedLocationSubHandler(args); + ReceivedLocationSubHandler(oscMsg); return; case SpawnableOperation.delete: - ReceivedDeleteHandler(args); + ReceivedDeleteHandler(oscMsg); return; case SpawnableOperation.create: - ReceivedCreateHandler(args); + ReceivedCreateHandler(oscMsg); return; case SpawnableOperation.available: MelonLogger.Msg($"[Error] Attempted set the availability for a prop, this is not allowed."); @@ -151,7 +162,7 @@ internal sealed override void ReceiveMessageHandler(string address, List default: MelonLogger.Msg( "[Error] Attempted to interact with a prop but the address is invalid." + - $"\n\t\t\tAddress attempted: \"{address}\"" + + $"\n\t\t\tAddress attempted: \"{oscMsg.Address}\"" + $"\n\t\t\tThe correct format should be: \"{AddressPrefixSpawnable}\"" + $"\n\t\t\tAnd the allowed ops are: {string.Join(", ", Enum.GetNames(typeof(SpawnableOperation)))}" ); @@ -159,15 +170,15 @@ internal sealed override void ReceiveMessageHandler(string address, List } } - private static void ReceivedCreateHandler(List args) { + private static void ReceivedCreateHandler(OscMessage oscMessage) { - if (args.Count is not (1 or 4)) { + if (oscMessage.Count is not (1 or 4)) { MelonLogger.Msg($"[Error] Attempted to create a prop, but provided an invalid number of arguments. " + $"Expected either 1 or 4 arguments, for the guid and optionally position coordinates."); return; } - var possibleGuid = args[0]; + var possibleGuid = oscMessage[0]; if (!TryParseSpawnableGuid(possibleGuid, out var spawnableGuid)) { MelonLogger.Msg($"[Error] Attempted to create a prop, but provided an invalid GUID. " + $"GUID attempted: \"{possibleGuid}\" Type: {possibleGuid?.GetType()}" + @@ -175,7 +186,7 @@ private static void ReceivedCreateHandler(List args) { return; } - if (TryParseVector3(args[1], args[2], args[3], out var floats)) { + if (oscMessage.Count == 4 && TryParseVector3(oscMessage[1], oscMessage[2], oscMessage[3], out var floats)) { Events.Spawnable.OnSpawnableCreate(spawnableGuid, floats.Item1, floats.Item2, floats.Item3); } else { @@ -183,15 +194,15 @@ private static void ReceivedCreateHandler(List args) { } } - private static void ReceivedDeleteHandler(List args) { + private static void ReceivedDeleteHandler(OscMessage oscMessage) { - if (args.Count != 2) { + if (oscMessage.Count != 2) { MelonLogger.Msg($"[Error] Attempted to delete a prop, but provided an invalid number of arguments. " + $"Expected 2 arguments, for the prop GUID and Instance ID."); return; } - var possibleGuid = args[0]; + var possibleGuid = oscMessage[0]; if (!TryParseSpawnableGuid(possibleGuid, out var spawnableGuid)) { MelonLogger.Msg($"[Error] Attempted to delete a prop, but provided an invalid GUID. " + $"GUID attempted: \"{possibleGuid}\" Type: {possibleGuid?.GetType()}" + @@ -199,7 +210,7 @@ private static void ReceivedDeleteHandler(List args) { return; } - var possibleInstanceId = args[1]; + var possibleInstanceId = oscMessage[1]; if (!TryParseSpawnableInstanceId(possibleInstanceId, out var spawnableInstanceId)) { MelonLogger.Msg($"[Error] Attempted to delete a prop, but provided an invalid Instance ID. " + $"Prop Instance ID attempted: \"{possibleInstanceId}\" Type: {possibleInstanceId?.GetType()}" + @@ -210,15 +221,15 @@ private static void ReceivedDeleteHandler(List args) { Events.Spawnable.OnSpawnableDelete($"p+{spawnableGuid}~{spawnableInstanceId}"); } - private static void ReceivedParameterHandler(List args) { + private static void ReceivedParameterHandler(OscMessage oscMessage) { - if (args.Count != 4) { + if (oscMessage.Count != 4) { MelonLogger.Msg($"[Error] Attempted to set a prop synced param, but provided an invalid number of arguments. " + $"Expected 4 arguments, for the prop GUID, and Instance ID, sync param name, and param value"); return; } - var possibleGuid = args[0]; + var possibleGuid = oscMessage[0]; if (!TryParseSpawnableGuid(possibleGuid, out var spawnableGuid)) { MelonLogger.Msg($"[Error] Attempted to set a prop synced param, but provided an invalid GUID. " + $"GUID attempted: \"{possibleGuid}\" Type: {possibleGuid?.GetType()}" + @@ -226,7 +237,7 @@ private static void ReceivedParameterHandler(List args) { return; } - var possibleInstanceId = args[1]; + var possibleInstanceId = oscMessage[1]; if (!TryParseSpawnableInstanceId(possibleInstanceId, out var spawnableInstanceId)) { MelonLogger.Msg($"[Error] Attempted to set a prop synced param, but provided an invalid Instance ID. " + $"Prop Instance ID attempted: \"{possibleInstanceId}\" Type: {possibleInstanceId?.GetType()}" + @@ -234,7 +245,7 @@ private static void ReceivedParameterHandler(List args) { return; } - var possibleParamName = args[2]; + var possibleParamName = oscMessage[2]; if (possibleParamName is not string spawnableParameterName) { MelonLogger.Msg($"[Error] Attempted to set a prop synced param, but provided an invalid name. " + $"Attempted: \"{possibleParamName}\" Type: {possibleParamName?.GetType()}" + @@ -242,7 +253,7 @@ private static void ReceivedParameterHandler(List args) { return; } - var possibleFloat = args[3]; + var possibleFloat = oscMessage[3]; if (!Utils.Converters.TryHardToParseFloat(possibleFloat, out var parsedFloat)) { MelonLogger.Msg( $"[Error] Attempted to change a prop synced parameter {spawnableParameterName} to {possibleFloat}, " + @@ -256,16 +267,16 @@ private static void ReceivedParameterHandler(List args) { Events.Spawnable.OnSpawnableParameterSet(spawnableFullId, spawnableParameterName, parsedFloat); } - private static void ReceivedLocationHandler(List args) { + private static void ReceivedLocationHandler(OscMessage oscMessage) { - if (args.Count != 8) { + if (oscMessage.Count != 8) { MelonLogger.Msg($"[Error] Attempted to set a prop location, but provided an invalid number of arguments. " + $"Expected 8 arguments, for the prop GUID, and Instance ID, 3 floats for position, and 3" + $"floats for the rotation (euler angles)."); return; } - var possibleGuid = args[0]; + var possibleGuid = oscMessage[0]; if (!TryParseSpawnableGuid(possibleGuid, out var spawnableGuid)) { MelonLogger.Msg($"[Error] Attempted to set a prop location, but provided an invalid GUID. " + $"GUID attempted: \"{possibleGuid}\" Type: {possibleGuid?.GetType()}" + @@ -273,7 +284,7 @@ private static void ReceivedLocationHandler(List args) { return; } - var possibleInstanceId = args[1]; + var possibleInstanceId = oscMessage[1]; if (!TryParseSpawnableInstanceId(possibleInstanceId, out var spawnableInstanceId)) { MelonLogger.Msg($"[Error] Attempted to set a prop location, but provided an invalid Instance ID. " + $"Prop Instance ID attempted: \"{possibleInstanceId}\" Type: {possibleInstanceId?.GetType()}" + @@ -282,8 +293,8 @@ private static void ReceivedLocationHandler(List args) { } // Validate position and rotation floats - if (!TryParseVector3(args[2], args[3], args[4], out var posFloats)) return; - if (!TryParseVector3(args[5], args[6], args[7], out var rotFloats)) return; + if (!TryParseVector3(oscMessage[2], oscMessage[3], oscMessage[4], out var posFloats)) return; + if (!TryParseVector3(oscMessage[5], oscMessage[6], oscMessage[7], out var rotFloats)) return; var spawnableFullId = $"p+{spawnableGuid}~{spawnableInstanceId}"; @@ -293,16 +304,16 @@ private static void ReceivedLocationHandler(List args) { Events.Spawnable.OnSpawnableLocationSet(spawnableFullId, position, rotation); } - private static void ReceivedLocationSubHandler(List args) { + private static void ReceivedLocationSubHandler(OscMessage oscMessage) { - if (args.Count != 9) { + if (oscMessage.Count != 9) { MelonLogger.Msg($"[Error] Attempted to set a prop location with an invalid number of arguments. " + $"Expected 8 arguments, for the prop GUID, and Instance ID, 3 floats for position, and 3" + $"floats for the rotation (euler angles)."); return; } - var possibleGuid = args[0]; + var possibleGuid = oscMessage[0]; if (!TryParseSpawnableGuid(possibleGuid, out var spawnableGuid)) { MelonLogger.Msg($"[Error] Attempted to set a prop sub-sync location, but provided an invalid GUID. " + $"GUID attempted: \"{possibleGuid}\" Type: {possibleGuid?.GetType()}" + @@ -310,7 +321,7 @@ private static void ReceivedLocationSubHandler(List args) { return; } - var possibleInstanceId = args[1]; + var possibleInstanceId = oscMessage[1]; if (!TryParseSpawnableInstanceId(possibleInstanceId, out var spawnableInstanceId)) { MelonLogger.Msg($"[Error] Attempted to set a prop sub-sync location, with an invalid Instance ID. " + $"Instance ID attempted: \"{possibleInstanceId}\" Type: {possibleInstanceId?.GetType()}" + @@ -319,7 +330,7 @@ private static void ReceivedLocationSubHandler(List args) { } // Validate index - var possibleIndex = args[2]; + var possibleIndex = oscMessage[2]; if (!Utils.Converters.TryToParseInt(possibleIndex, out var subIndex) || subIndex < 0) { MelonLogger.Msg($"[Error] Attempted set the location of prop sub-sync with an invalid/negative index. " + $"Value attempted to parse: {possibleIndex} [{possibleIndex?.GetType()}]"); @@ -327,8 +338,8 @@ private static void ReceivedLocationSubHandler(List args) { } // Validate position and rotation floats - if (!TryParseVector3(args[2], args[3], args[4], out var posFloats)) return; - if (!TryParseVector3(args[5], args[6], args[7], out var rotFloats)) return; + if (!TryParseVector3(oscMessage[3], oscMessage[4], oscMessage[5], out var posFloats)) return; + if (!TryParseVector3(oscMessage[6], oscMessage[7], oscMessage[8], out var rotFloats)) return; var spawnableFullId = $"p+{spawnableGuid}~{spawnableInstanceId}"; @@ -338,6 +349,12 @@ private static void ReceivedLocationSubHandler(List args) { Events.Spawnable.OnSpawnableLocationSet(spawnableFullId, position, rotation, subIndex); } + private static string GetGuid(CVRSpawnable spawnable) { + // Spawnable instance id example: p+047576d5-e028-483a-9870-89e62f0ed3a4~FF00984F7C5A + // p+~ + return spawnable.instanceId.Substring(3, 36); + } + private static string GetInstanceId(CVRSpawnable spawnable) { // Spawnable instance id example: p+047576d5-e028-483a-9870-89e62f0ed3a4~FF00984F7C5A // p+~ diff --git a/OSC/Handlers/OscModules/Tracking.cs b/OSC/Handlers/OscModules/Tracking.cs index 887bf87..3cd8bc3 100644 --- a/OSC/Handlers/OscModules/Tracking.cs +++ b/OSC/Handlers/OscModules/Tracking.cs @@ -3,30 +3,42 @@ namespace OSC.Handlers.OscModules; -enum TrackingOperation { +enum TrackingEntity { device, play_space, } +enum TrackingOperations { + status, + data, +} + public class Tracking : OscHandler { internal const string AddressPrefixTracking = "/tracking/"; + private readonly Action _trackingDeviceConnected; private readonly Action _trackingDataDeviceUpdated; private readonly Action _trackingDataPlaySpaceUpdated; public Tracking() { - // Send tracking data device update events + // Send tracking device stats events + _trackingDeviceConnected = (connected, source, id, deviceName) => { + const string address = $"{AddressPrefixTracking}{nameof(TrackingEntity.device)}/{nameof(TrackingOperations.status)}"; + HandlerOsc.SendMessage(address, connected, Enum.GetName(typeof(TrackingDataSource), source), id, deviceName); + }; + + // Send tracking device data update events _trackingDataDeviceUpdated = (source, id, deviceName, pos, rot, battery) => { - var address = $"{AddressPrefixTracking}{nameof(TrackingOperation.device)}"; + const string address = $"{AddressPrefixTracking}{nameof(TrackingEntity.device)}/{nameof(TrackingOperations.data)}"; HandlerOsc.SendMessage(address, Enum.GetName(typeof(TrackingDataSource), source), id, deviceName, pos.x, pos.y, pos.z, rot.x, rot.y, rot.z, battery); }; - // Send tracking data play space update events + // Send tracking play space data update events _trackingDataPlaySpaceUpdated = (pos, rot) => { - var address = $"{AddressPrefixTracking}{nameof(TrackingOperation.play_space)}"; + const string address = $"{AddressPrefixTracking}{nameof(TrackingEntity.play_space)}/{nameof(TrackingOperations.data)}"; HandlerOsc.SendMessage(address, pos.x, pos.y, pos.z, rot.x, rot.y, rot.z); }; @@ -39,11 +51,13 @@ public Tracking() { } internal sealed override void Enable() { + Events.Tracking.TrackingDeviceConnected += _trackingDeviceConnected; Events.Tracking.TrackingDataDeviceUpdated += _trackingDataDeviceUpdated; Events.Tracking.TrackingDataPlaySpaceUpdated += _trackingDataPlaySpaceUpdated; } internal sealed override void Disable() { + Events.Tracking.TrackingDeviceConnected -= _trackingDeviceConnected; Events.Tracking.TrackingDataDeviceUpdated -= _trackingDataDeviceUpdated; Events.Tracking.TrackingDataPlaySpaceUpdated -= _trackingDataPlaySpaceUpdated; } diff --git a/OSC/HarmonyPatches.cs b/OSC/HarmonyPatches.cs index b7b6adb..c387672 100644 --- a/OSC/HarmonyPatches.cs +++ b/OSC/HarmonyPatches.cs @@ -6,12 +6,21 @@ using ABI_RC.Systems.MovementSystem; using ABI.CCK.Components; using HarmonyLib; +using Rug.Osc; namespace OSC; [HarmonyPatch] internal class HarmonyPatches { + private static bool _performanceMode; + + static HarmonyPatches() { + // Handle performance mod changes + _performanceMode = OSC.Instance.meOSCPerformanceMode.Value; + OSC.Instance.meOSCPerformanceMode.OnValueChanged += (_, enabled) => _performanceMode = enabled; + } + // Avatar [HarmonyPrefix] [HarmonyPatch(typeof(AvatarDetails_t), "Recycle")] @@ -29,28 +38,28 @@ internal static void AfterUpdateAnimatorManager(CVRAnimatorManager manager) { [HarmonyPostfix] [HarmonyPatch(typeof(CVRAnimatorManager), nameof(CVRAnimatorManager.SetAnimatorParameterFloat))] internal static void AfterSetAnimatorParameterFloat(string name, float value, CVRAnimatorManager __instance, bool ____parametersChanged) { - if (!____parametersChanged) return; + if (!____parametersChanged || _performanceMode) return; Events.Avatar.OnParameterChangedFloat(__instance, name, value); } [HarmonyPostfix] [HarmonyPatch(typeof(CVRAnimatorManager), nameof(CVRAnimatorManager.SetAnimatorParameterInt))] internal static void AfterSetAnimatorParameterInt(string name, int value, CVRAnimatorManager __instance, bool ____parametersChanged) { - if (!____parametersChanged) return; + if (!____parametersChanged || _performanceMode) return; Events.Avatar.OnParameterChangedInt(__instance, name, value); } [HarmonyPostfix] [HarmonyPatch(typeof(CVRAnimatorManager), nameof(CVRAnimatorManager.SetAnimatorParameterBool))] internal static void AfterSetAnimatorParameterBool(string name, bool value, CVRAnimatorManager __instance, bool ____parametersChanged) { - if (!____parametersChanged) return; + if (!____parametersChanged || _performanceMode) return; Events.Avatar.OnParameterChangedBool(__instance, name, value); } [HarmonyPostfix] [HarmonyPatch(typeof(CVRAnimatorManager), nameof(CVRAnimatorManager.SetAnimatorParameterTrigger))] internal static void AfterSetAnimatorParameterTrigger(string name, CVRAnimatorManager __instance, bool ____parametersChanged) { - if (!____parametersChanged) return; + if (!____parametersChanged || _performanceMode) return; Events.Avatar.OnParameterChangedTrigger(__instance, name); } @@ -58,11 +67,13 @@ internal static void AfterSetAnimatorParameterTrigger(string name, CVRAnimatorMa [HarmonyPostfix] [HarmonyPatch(typeof(CVRSpawnable), nameof(CVRSpawnable.UpdateMultiPurposeFloat), typeof(CVRSpawnableValue), typeof(float), typeof(int))] internal static void AfterUpdateMultiPurposeFloat(CVRSpawnableValue spawnableValue, CVRSpawnable __instance) { + if (_performanceMode) return; Events.Spawnable.OnSpawnableParameterChanged(__instance, spawnableValue); } [HarmonyPostfix] [HarmonyPatch(typeof(CVRSpawnable), nameof(CVRSpawnable.UpdateFromNetwork), typeof(CVRSyncHelper.PropData))] internal static void AfterSpawnableUpdateFromNetwork(CVRSyncHelper.PropData propData, CVRSpawnable __instance) { + if (_performanceMode) return; Events.Spawnable.OnSpawnableUpdateFromNetwork(propData, __instance); } [HarmonyPostfix] @@ -71,15 +82,16 @@ internal static void AfterApplyPropValuesSpawn(CVRSyncHelper.PropData propData) Events.Spawnable.OnSpawnableCreated(propData); } [HarmonyPrefix] - [HarmonyPatch(typeof(CVRSyncHelper.PropData), nameof(CVRSyncHelper.PropData.Recycle))] - internal static void BeforePropDataRecycle(CVRSyncHelper.PropData __instance) { - Events.Spawnable.OnSpawnableDeleted(__instance); + [HarmonyPatch(typeof(CVRSpawnable), nameof(CVRSpawnable.OnDestroy))] + internal static void BeforeSpawnableDestroy(CVRSpawnable __instance) { + Events.Spawnable.OnSpawnableDestroyed(__instance); } // Trackers [HarmonyPostfix] [HarmonyPatch(typeof(VRTrackerManager), nameof(VRTrackerManager.Update))] internal static void AfterVRTrackerManagerUpdate(VRTrackerManager __instance) { + if (_performanceMode) return; Events.Tracking.OnTrackingDataDeviceUpdated(__instance); } @@ -95,4 +107,13 @@ internal static void AfterPlayerSetup() { private static void AfterInputManagerCreated() { Events.Scene.OnInputManagerCreated(); } + + // OSC Lib Actually following the spec ;_; + // Let's nuke the address validation so we can get the juicy #ParamName working in the address + [HarmonyPrefix] + [HarmonyPatch(typeof(OscAddress), nameof(OscAddress.IsValidAddressPattern))] + private static bool BeforeOscAddressIsValid(ref bool __result) { + __result = true; + return false; + } } diff --git a/OSC/Main.cs b/OSC/Main.cs index e0f2f86..22534c0 100644 --- a/OSC/Main.cs +++ b/OSC/Main.cs @@ -42,6 +42,8 @@ public class OSC : MelonMod { // Misc public MelonPreferences_Entry meOSCDebug; + public MelonPreferences_Entry meOSCDebugConfigWarnings; + public MelonPreferences_Entry meOSCPerformanceMode; private HandlerOsc _handlerOsc; @@ -123,11 +125,19 @@ public override void OnApplicationStart() { meOSCTrackingModuleUpdateInterval = _mcOsc.CreateEntry("TrackingModuleUpdateInterval", 0f, description: "Minimum of seconds between each tracking data update. Default: 0 (will update every frame) " + - "Eg: 0.05 will update every 50 milliseconds."); + "Eg: 0.050 will update every 50 milliseconds."); // Misc meOSCDebug = _mcOsc.CreateEntry("Debug", false, description: "Whether should spam the console with debug messages or not."); + meOSCDebugConfigWarnings = _mcOsc.CreateEntry("DebugConfigWarnings", true, + description: "Whether it should warn everytime you send an osc msg and it gets ignored because a config " + + "setting. (for example sending a trigger parameter when triggers are disabled)."); + meOSCPerformanceMode = _mcOsc.CreateEntry("PerformanceMode", false, + description: "If performance mode is activated the mod will stop listening to most game events. " + + "This will result on a lot of the osc messages going out from our mod to not work. If you" + + "only want the mod to listen to osc messages going into CVR you should have this option on! " + + "As there's a lot of processing/msg sending when listening the all the game events."); // Load env variables (may change the melon config) @@ -146,4 +156,8 @@ public override void OnApplicationStart() { // Start OSC server _handlerOsc = new HandlerOsc(); } + + public override void OnApplicationQuit() { + _handlerOsc.Close(); + } } diff --git a/OSC/OSC.csproj b/OSC/OSC.csproj index c58acf8..3854459 100644 --- a/OSC/OSC.csproj +++ b/OSC/OSC.csproj @@ -41,10 +41,7 @@ - - - - + diff --git a/OSC/README.md b/OSC/README.md index ea34b87..f03d17b 100644 --- a/OSC/README.md +++ b/OSC/README.md @@ -3,11 +3,10 @@ This **Melon Loader** mod allows you to use OSC to interact with **ChilloutVR**. It tries to be compatible with other social VR games that also use OSC. This way allows the usage of tools made for other games with relative ease. -- [OSC Avatar Changes](#OSC-Avatar-Changes) -- [OSC Avatar Parameters](#OSC-Avatar-Parameters) +- [OSC Avatar](#OSC-Avatar) - [OSC Inputs](#OSC-Inputs) - [OSC Props](#OSC-Props) -- [OSC Tracking Data](#OSC-Tracking-Data) +- [OSC Tracking](#OSC-Tracking) - [Avatar Json Configurations](#Avatar-Json-Configurations) - [OSC Config](#OSC-Config) - [Debugging](#Debugging) @@ -15,39 +14,85 @@ games that also use OSC. This way allows the usage of tools made for other games For now there are 6 categories of endpoints you can use: -- [OSC Avatar Changes](#OSC-Avatar-Changes) for listening/triggering avatar changes. -- [OSC Avatar Parameters](#OSC-Avatar-Parameters) for listening/triggering avatar parameter changes. +- [OSC Avatar](#OSC-Avatar) for listening/triggering avatar changes, and also their parameters - [OSC Inputs](#OSC-Inputs) for triggering inputs/actions. - [OSC Props](#OSC-Props) for interacting with props. -- [OSC Tracking data](#OSC-Tracking-Data) to fetch tracking information (headset, controllers, trackers, play space). +- [OSC Tracking](#OSC-Tracking) to fetch tracking information (headset, controllers, trackers, play space). - [OSC Config](#OSC-Config) configuration/utilities via OSC. + + + --- -## OSC Avatar Changes +## OSC Avatar +The first module is the avatar module, where you are able to interface with avatar related stuff. You can +change the current avatar you're using via OSC, and also listen to those changes (you'll get the event either way). + +Also you're able to change and listen to the avatar parameters. This part part has a lot of customization options +because you're able to change the addresses and types via a config (not required). + + +### OSC Avatar Change Whenever you load into an avatar the mod will send a message to `/avatar/change` containing as the first and only argument a string representing the avatar UUID. -```console -/avatar/change -``` +#### Address +```/avatar/change``` + +**Mod will send:** +#### Arguments +- `arg#1` - Avatar GUID [*string*] +- `arg#2` - Path to the avatar json config [*string*], this is new to this mod, but as it is an additional param it +won't break existing osc apps + +**Mod will receive:** +#### Arguments +- `arg#1` - Avatar GUID [*string*] **New:** The mod will also listen to the address `/avatar/change`, so if you send a UUID of an avatar (that you have permission to use), it will change your avatar in-game. This is `enabled` by default, but you can go to the configurations and disable it. + --- ## OSC Avatar Parameters You can listen and trigger parameter changes on your avatar via OSC messages, by default the endpoint to change and -listen to parameter changes is: -```console -/avatar/parameters/ -``` +listen to parameter changes as follow. + +#### Address [`deprecated`] +```/avatar/parameters/``` + Where `` would be the name of your parameter. *The parameter name is case sensitive!* -And then the value is sent as the first argument, this argument should be sent as the same type as the parameter is -defined in the animator. But you can also send as a `string` or some other type that has a conversion implied. **Note:** -Sending the correct type will require less code to run, making it more performant. +#### Arguments [`deprecated`] +- `arg#1` - Parameter value [ *float *|* int *|* bool | null* ], for triggers you can ignore sending a parameter, the +value will be ignored either way. + + +These are certain limitations using the endpoint above, because according to the OSC spec you can't have `#` in the +last member of the OSC address. So some OSC clients will have issues setting local parameters (because in cvr they +require a `#`). I had to hack my way to force my client to allow `#` on the address ;_; + +I marked it as deprecated but will still support it for compatibility reasons. Use the alternative ones bellow if you're +implementing something new (please). + +As for sending I'll be sending on both endpoints so just pick one to listen to. + + +#### Address [`preferred`] +```/avatar/parameter``` + +#### Arguments [`preferred`] +- `arg#1` - Parameter value [ *float *|* int *|* bool | null* ], for triggers you can ignore sending a parameter, the +value will be ignored either way. +- `arg#2` - Parameter name [ *string* ], *The parameter name is case sensitive!* + + +The Parameter value argument should be sent as the same type as the parameter is defined in the animator. But you can +also send as a `string` or some other type that has a conversion implied. + +**Note:** Sending the correct type will require less code to run, making it more performant. We support all animator parameter types, `Float`, `Int`, `Bool`, and `Trigger`([*](#Triggers)) @@ -68,17 +113,21 @@ apps. It uses the same parameter change address, but it sends just the address w And when listening the same thing, you will receive an OSC message to the parameter address, but there won't be a value. + + + --- ## OSC Inputs Here is where you can interact with the game in a more generic ways, allow you to send controller inputs or triggering actions in the game. -The endpoint for the inputs is: -```console -/input/ -``` -Where the `` is the actual name of the input you're trying to change, and then it takes as the first -argument the value. +#### Address +```/input/``` + +Where the `` is the actual name of the input you're trying to change. + +#### Arguments +- `arg#1` - Input value [ *float* | *int* | *bool* ] There are some inputs that are not present that exist in other VR Social platforms, this is due CVR not having those features implemented yet. Like rotating the object you're holding with keyboard inputs. And some others that are new, @@ -89,11 +138,12 @@ will act the same as you holding down the key to Jump, and it will only be relea value `0`. So don't forget to reset them in your apps, otherwise you might end up jumping forever. There are 3 types of Inputs: -- [Axes](#Axes) -- [Buttons](#Buttons) -- [Values](#Values) +- [Axes](#OSC-Inputs-Axes) +- [Buttons](#OSC-Inputs-Buttons) +- [Values](#OSC-Inputs-Values) -### Axes + +### OSC Inputs Axes Axes expecting a `float` value that ranges between `-1`/`0` and `1`. They are namely used for things that require a range of values instead of a on/off, for example the Movement, where `Horizontal` can be set to `-0.5` which would be the same as having the thumbstick on your controller to the left (because it's a negative value) but only halfway @@ -107,7 +157,8 @@ the same as having the thumbstick on your controller to the left (because it's a - `/intput/GripLeftValue` - [*new*] Left hand trigger grip released `0` or pulled to max `1` - `/intput/GripRightValue` - [*new*] Right hand trigger grip released `0` or pulled to max `1` -### Buttons + +### OSC Inputs Buttons Buttons are expecting `boolean` values, which can be represented by the boolean types `true` for button pressed and `false` for released. You can also send `integers` with the values `1` for pressed and `0` for released. @@ -162,7 +213,7 @@ are on the blacklist (`Reload` at the time of writing was bugged and would crash You can also disable the whole input module on the configuration as well. -### Values +### OSC Inputs Values Values are similar to `Axes` but removes the restriction of being between `-1` and `1`, they are used to send values to certain properties of the game. The values are of the type `float` or `int` and their range is dependent on each entry. @@ -176,7 +227,6 @@ default value otherwise they will remain the last value you sent. - --- ## OSC Props This mod module allows to interact with props. I've purposely added limitations to some interactions with props, you can @@ -191,7 +241,7 @@ prop in an instance, and the best way to obtain it is by listening to the `Creat -### Create +### OSC Props Create You're able to spawn props by providing their GUID. Keep in mind that you can only spawn props you have access to and there is a limit of 20 props spawned by yourself. @@ -224,7 +274,7 @@ space. If you want to provide a value, you need to provide all of them. If no va -### Delete +### OSC Props Delete As the name suggests you can delete props that you've spawned. You can do so by providing the GUID of the prop as well as their instance ID to uniquely identify them. @@ -239,7 +289,8 @@ won't become available for interaction anymore. - `arg#2` - Instance ID of the prop spawned [*string*] -### Availability + +### OSC Props Availability This address will be called every time a prop has their availability changed. What what I mean by availability is where you are able to control or not this prop. The props become available when no remote player is *grabbing*, *telegrabbing*, nor has it *attached* to themselves. Also this **only** affects props spawned by yourself! @@ -259,7 +310,7 @@ Obviously this is an address set by the game, so you can't send osc messages to -### Parameters +### OSC Props Parameters Here you will be able to listen and write to the prop's synced parameters. You will need to provide the GUID of the prop and it's instance ID to do so. @@ -268,7 +319,7 @@ and it's instance ID to do so. - The prop **not** being controlled by a remote player [*grabbed* | *telegrabbed* | *attached*] #### Address -```/prop/parameters``` +```/prop/parameter``` #### Arguments @@ -283,7 +334,7 @@ the actual name of parameter inside of the animator. This name is **case sensiti -### Location +### OSC Props Location You are also able to listen and set the location of a prop. This is very powerful as you can for example link the tracking data you receive form the tracking module to a prop so it's controlled by the tracker. @@ -309,7 +360,7 @@ their positions without any issue tho. -### Location Sub +### OSC Props Location Sub You are also able to listen and set the location of a prop's sub-sync transforms. This is very powerful as you can for example link the tracking data you receive form the tracking module to a sub-sync so it's controlled by the tracker. @@ -338,14 +389,13 @@ CVR Spawnable Component --- -## OSC Tracking Data - +## OSC Tracking This mod module allows to read tracking data from the game namely from tracked devices, and the play space. You can only listen to these, don't try messages to those (or else). -### Play Space +### OSC Tracking Play Space Data The mod will keep sending the current play space position and rotation. This is especially useful if you want to create avatar animations to drive the position of objects. Because the avatar origin is the play space origin. Meaning if you have world space coordinates you want to make local to the avatar, you can do it by using the play space location data @@ -365,7 +415,23 @@ The values are sent as `float` type arguments, and the values order is the follo -### Devices +### OSC Tracking Device Status +You can listen here for steam vr device connected change status. Every device starts assuming it is disconnected so you +will always receive a connected = `True` as the first event from a device. + +#### Address +```/tracking/device/status``` + +#### Arguments +- `arg#1` - Connected [*bool*], whether the device was connected `True` or disconnected `False` +- `arg#2` - Device type [*string*], Possible values: `hmd`, `base_station`, `left_controller`, `right_controller`, `tracker`, and + `unknown` +- `arg#3` - Steam tracked index [*int*], given by SteamVR, it's unique for each device (*see note bellow*) +- `arg#4` - Device name [*string*], given by SteamVR, in some cases (like base stations) there is no name the string will be empty. + + + +### OSC Tracking Devices Data The mod also exposes the tracking information for tracked devices, like base stations, controllers, and trackers. Both the position and rotation are for world space, and the rotation is sent in euler angles. @@ -407,7 +473,7 @@ defined in seconds. This mod module allows configure/interact with the mod via osc. -### Reset +### OSC Config Reset This endpoint will reset the caches for both avatar and props, and re-send the init events. It's useful if you start your osc application after the game is running and require those initial events. Since this mod doesn't keep spamming updates this is very useful sync the state with your app (if you need). @@ -417,9 +483,7 @@ keep spamming updates this is very useful sync the state with your app (if you n ```/config/reset``` #### Arguments -- `arg#1` - Send literally anything, this shouldn't be needed (because OSC spec allows Nil as argument), but some osc -libs don't have it implemented. So feel free to not send anything, but the lib you're using might fail. So you could -just send something and call it a day, like an empty string. +`N/A` diff --git a/OSC/Utils/JsonConfigOsc.cs b/OSC/Utils/JsonConfigOsc.cs index f902cb8..3258f9e 100644 --- a/OSC/Utils/JsonConfigOsc.cs +++ b/OSC/Utils/JsonConfigOsc.cs @@ -21,7 +21,7 @@ internal static void ClearCurrentAvatarConfig() { CurrentAvatarConfig = null; } - private static string GetConfigFilePath(string userUuid, string avatarUuid) { + internal static string GetConfigFilePath(string userUuid, string avatarUuid) { var basePath = Application.persistentDataPath; // Replace the base path with the override @@ -40,11 +40,11 @@ public static void CreateConfig(string userGuid, string avatarGuid, string avata List parameters = new(); foreach (var parameter in animatorManager.animator.parameters) { var input = new JsonConfigParameterEntry { - address = Handlers.OscModules.Avatar.AddressPrefixAvatarParameters + parameter.name, + address = Handlers.OscModules.Avatar.AddressPrefixAvatarParametersLegacy + parameter.name, type = parameter.type, }; var output = new JsonConfigParameterEntry { - address = Handlers.OscModules.Avatar.AddressPrefixAvatarParameters + parameter.name, + address = Handlers.OscModules.Avatar.AddressPrefixAvatarParametersLegacy + parameter.name, type = parameter.type, }; @@ -105,7 +105,7 @@ public static JsonConfigAvatar GetConfig(string userGuid, string avatarGuid) { public static JsonConfigParameterEntry GetJsonConfigParameterEntry(string name, object value) { if (Converters.GetParameterType(value).HasValue) { return new JsonConfigParameterEntry { - address = Handlers.OscModules.Avatar.AddressPrefixAvatarParameters + name, + address = Handlers.OscModules.Avatar.AddressPrefixAvatarParametersLegacy + name, type = Converters.GetParameterType(value).Value, }; }