diff --git a/src/VRCOscLib/Tests/vrcosclib.Test/Tracking/OscTrackerTest.cs b/src/VRCOscLib/Tests/vrcosclib.Test/Tracking/OscTrackerTest.cs new file mode 100644 index 0000000..e88df88 --- /dev/null +++ b/src/VRCOscLib/Tests/vrcosclib.Test/Tracking/OscTrackerTest.cs @@ -0,0 +1,128 @@ +using BuildSoft.OscCore; +using BuildSoft.OscCore.UnityObjects; +using BuildSoft.VRChat.Osc.Test; +using NUnit.Framework; + +namespace BuildSoft.VRChat.Osc.Tracking.Test; + +[TestOf(typeof(OscTracker))] +public class OscTrackerTest +{ + private static IEnumerable ValidRangeSource + { + get + { + yield return 0; + yield return OscTracker.SupportedTrackerCount / 2; + yield return OscTracker.SupportedTrackerCount - 1; + } + } + + private static IEnumerable InvalidRangeSource + { + get + { + yield return -1; + yield return OscTracker.SupportedTrackerCount; + yield return int.MaxValue; + yield return int.MinValue; + } + } + + + private OscClient _client = null!; + private OscServer _server = null!; + + [SetUp] + public void Setup() + { + OscParameter.Parameters.Clear(); + } + + [TearDown] + public void TearDown() + { + + } + + [OneTimeSetUp] + public void OneTimeSetUp() + { + _client = new OscClient("127.0.0.1", OscUtility.ReceivePort); + _server = new OscServer(OscUtility.SendPort); + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + _client.Dispose(); + _server.Dispose(); + } + + + [TestCaseSource(nameof(ValidRangeSource))] + public void Ctor_ValidRangeTest(int index) + { + Assert.DoesNotThrow(() => new OscTracker(index)); + + OscTracker tracker = new(index); + Assert.AreEqual($"/tracking/trackers/{index + 1}/position", tracker.PositionAddress); + Assert.AreEqual($"/tracking/trackers/{index + 1}/rotation", tracker.RotationAddress); + } + + [TestCaseSource(nameof(InvalidRangeSource))] + public void Ctor_InvalidRangeTest(int index) + { + var exception = Assert.Throws(() => new OscTracker(index)); + + Assert.NotNull(exception); + Assert.AreEqual("index", exception!.ParamName); + } + + + [Test] + public async Task PositionTest() + { + var tracker = new OscTracker(0); + Assert.AreEqual(new Vector3(), tracker.Position); + + OscMessageValues value = null!; + void valueReadMethod(OscMessageValues v) => value = v; + _server.TryAddMethod(tracker.PositionAddress, valueReadMethod); + + var expected = new Vector3(10.1f, 20.2f, 30.3f); + + tracker.Position = expected; + await TestUtility.LoopWhile(() => value == null, TestUtility.LatencyTimeout); + + Assert.AreEqual(expected, tracker.Position); + Assert.AreEqual(expected.x, value.ReadFloatElement(0)); + Assert.AreEqual(expected.y, value.ReadFloatElement(1)); + Assert.AreEqual(expected.z, value.ReadFloatElement(2)); + + Assert.AreEqual(expected, new OscTracker(0).Position); + } + + [Test] + public async Task RotationTest() + { + var tracker = new OscTracker(0); + Assert.AreEqual(new Vector3(), tracker.Rotation); + + OscMessageValues value = null!; + void valueReadMethod(OscMessageValues v) => value = v; + _server.TryAddMethod(tracker.RotationAddress, valueReadMethod); + + var expected = new Vector3(10.1f, 20.2f, 30.3f); + + tracker.Rotation = expected; + await TestUtility.LoopWhile(() => value == null, TestUtility.LatencyTimeout); + + Assert.AreEqual(expected, tracker.Rotation); + Assert.AreEqual(expected.x, value.ReadFloatElement(0)); + Assert.AreEqual(expected.y, value.ReadFloatElement(1)); + Assert.AreEqual(expected.z, value.ReadFloatElement(2)); + + Assert.AreEqual(expected, new OscTracker(0).Rotation); + } +} diff --git a/src/VRCOscLib/Tests/vrcosclib.Test/Tracking/OscTrackingTest.cs b/src/VRCOscLib/Tests/vrcosclib.Test/Tracking/OscTrackingTest.cs new file mode 100644 index 0000000..9466daa --- /dev/null +++ b/src/VRCOscLib/Tests/vrcosclib.Test/Tracking/OscTrackingTest.cs @@ -0,0 +1,145 @@ +using BuildSoft.OscCore; +using BuildSoft.OscCore.UnityObjects; +using BuildSoft.VRChat.Osc.Test; +using NUnit.Framework; + +namespace BuildSoft.VRChat.Osc.Tracking.Test; + +[TestOf(typeof(OscTracking))] +public class OscTrackingTest +{ + private OscClient _client = null!; + private OscServer _server = null!; + + [SetUp] + public void Setup() + { + OscParameter.Parameters.Clear(); + } + + [TearDown] + public void TearDown() + { + + } + + [OneTimeSetUp] + public void OneTimeSetUp() + { + _client = new OscClient("127.0.0.1", OscUtility.ReceivePort); + _server = new OscServer(OscUtility.SendPort); + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + _client.Dispose(); + _server.Dispose(); + } + + + [Test] + public void HeadTracker_AddressTest() + { + var headTracker = OscTracking.HeadTracker; + + Assert.AreEqual("/tracking/trackers/head/position", headTracker.PositionAddress); + Assert.AreEqual("/tracking/trackers/head/rotation", headTracker.RotationAddress); + } + + [Test] + public async Task HeadTracker_PositionSetTest() + { + var headTracker = OscTracking.HeadTracker; + + OscMessageValues value = null!; + void valueReadMethod(OscMessageValues v) => value = v; + _server.TryAddMethod(headTracker.PositionAddress, valueReadMethod); + + var expected = new Vector3(10.1f, 20.2f, 30.3f); + + headTracker.Position = expected; + await TestUtility.LoopWhile(() => value == null, TestUtility.LatencyTimeout); + + Assert.AreEqual(expected, headTracker.Position); + Assert.AreEqual(expected.x, value.ReadFloatElement(0)); + Assert.AreEqual(expected.y, value.ReadFloatElement(1)); + Assert.AreEqual(expected.z, value.ReadFloatElement(2)); + } + + [Test] + public async Task HeadTracker_RotationSetTest() + { + var headTracker = OscTracking.HeadTracker; + + OscMessageValues value = null!; + void valueReadMethod(OscMessageValues v) => value = v; + _server.TryAddMethod(headTracker.RotationAddress, valueReadMethod); + + var expected = new Vector3(10.1f, 20.2f, 30.3f); + + headTracker.Rotation = expected; + await TestUtility.LoopWhile(() => value == null, TestUtility.LatencyTimeout); + + Assert.AreEqual(expected, headTracker.Rotation); + Assert.AreEqual(expected.x, value.ReadFloatElement(0)); + Assert.AreEqual(expected.y, value.ReadFloatElement(1)); + Assert.AreEqual(expected.z, value.ReadFloatElement(2)); + } + + + [Test] + public async Task HeadTracker_PositionGetTest() + { + var headTracker = OscTracking.HeadTracker; + Assert.AreEqual(new Vector3(), headTracker.Position); + + OscMessageValues value = null!; + void valueReadMethod(OscMessageValues v) => value = v; + OscUtility.Server.TryAddMethod(headTracker.PositionAddress, valueReadMethod); + + var expected = new Vector3(10.1f, 20.2f, 30.3f); + _client.Send(headTracker.PositionAddress, expected); + + await TestUtility.LoopWhile(() => value == null, TestUtility.LatencyTimeout); + Assert.AreEqual(expected, headTracker.Position); + + OscUtility.Server.RemoveMethod(headTracker.PositionAddress, valueReadMethod); + } + + [Test] + public async Task HeadTracker_RotationGetTest() + { + var headTracker = OscTracking.HeadTracker; + Assert.AreEqual(new Vector3(), headTracker.Position); + + OscMessageValues value = null!; + void valueReadMethod(OscMessageValues v) => value = v; + OscUtility.Server.TryAddMethod(headTracker.RotationAddress, valueReadMethod); + + var expected = new Vector3(10.1f, 20.2f, 30.3f); + _client.Send(headTracker.RotationAddress, expected); + + await TestUtility.LoopWhile(() => value == null, TestUtility.LatencyTimeout); + Assert.AreEqual(expected, headTracker.Rotation); + + OscUtility.Server.RemoveMethod(headTracker.RotationAddress, valueReadMethod); + } + + + [Test] + public void TrackersTest() + { + var trackers = OscTracking.Trackers; + + Assert.AreEqual(OscTracker.SupportedTrackerCount, trackers.Length); + Assert.AreEqual("/tracking/trackers/1/position", trackers[0].PositionAddress); + Assert.AreEqual("/tracking/trackers/1/rotation", trackers[0].RotationAddress); + + for (int i = 0; i < OscTracker.SupportedTrackerCount; i++) + { + Assert.AreEqual($"/tracking/trackers/{i + 1}/position", trackers[i].PositionAddress); + Assert.AreEqual($"/tracking/trackers/{i + 1}/rotation", trackers[i].RotationAddress); + } + } +} diff --git a/src/VRCOscLib/VRCOscLib/Avatar/Utility/OscAvatarUtility.cs b/src/VRCOscLib/VRCOscLib/Avatar/Utility/OscAvatarUtility.cs index ee95d49..6575bfb 100644 --- a/src/VRCOscLib/VRCOscLib/Avatar/Utility/OscAvatarUtility.cs +++ b/src/VRCOscLib/VRCOscLib/Avatar/Utility/OscAvatarUtility.cs @@ -30,10 +30,12 @@ public static class OscAvatarUtility "Viseme", }; - internal static List> _avatarConfigs = new(); + private static List>? _avatarConfigs; + internal static List> AvatarConfigs => _avatarConfigs ??= new(); + internal static void RegisterAvaterConfig(OscAvatarConfig avatarConfig) { - _avatarConfigs.Add(new WeakReference(avatarConfig)); + AvatarConfigs.Add(new WeakReference(avatarConfig)); } public static IReadOnlyDictionary CommonParameters diff --git a/src/VRCOscLib/VRCOscLib/Collections/IReadOnlyOscParameterCollection.cs b/src/VRCOscLib/VRCOscLib/Collections/IReadOnlyOscParameterCollection.cs index 3c16792..9bae997 100644 --- a/src/VRCOscLib/VRCOscLib/Collections/IReadOnlyOscParameterCollection.cs +++ b/src/VRCOscLib/VRCOscLib/Collections/IReadOnlyOscParameterCollection.cs @@ -6,6 +6,6 @@ public interface IReadOnlyOscParameterCollection : IReadOnlyDictionary, IReadOnlyOsc private readonly Dictionary _items = new(); private Dictionary>? _handlersPerAddress; - private Dictionary> HandlersPerAddress => _handlersPerAddress ??= new(); public object? this[string address] { @@ -107,10 +106,16 @@ protected void OnValueChanged(ParameterChangedEventArgs args) protected void OnValueChangedByAddress(ParameterChangedEventArgs args) { - if (!HandlersPerAddress.TryGetValue(args.Address, out var list) || list.Count <= 0) + var handlersPerAddress = _handlersPerAddress; + if (handlersPerAddress == null) { return; } + if (!handlersPerAddress.TryGetValue(args.Address, out var list) || list.Count <= 0) + { + return; + } + var handlers = list.ToArray(); for (int i = 0; i < handlers.Length; i++) { @@ -128,8 +133,13 @@ protected void OnValueChangedByAddress(ParameterChangedEventArgs args) #region Event registration methods public void AddValueChangedEventByAddress(string address, ParamChangedHandler handler) { - var dict = HandlersPerAddress; - if (dict.TryGetValue(address, out var list)) + var dict = _handlersPerAddress; + if (dict == null) + { + dict = new(); + _handlersPerAddress = dict; + } + else if (dict.TryGetValue(address, out var list)) { list.Add(handler); return; @@ -139,7 +149,11 @@ public void AddValueChangedEventByAddress(string address, ParamChangedHandler ha public bool RemoveValueChangedEventByAddress(string address, ParamChangedHandler handler) { - var dict = HandlersPerAddress; + var dict = _handlersPerAddress; + if (dict == null) + { + return false; + } if (!dict.TryGetValue(address, out var list)) { return false; diff --git a/src/VRCOscLib/VRCOscLib/Tracking/OscTracker.cs b/src/VRCOscLib/VRCOscLib/Tracking/OscTracker.cs new file mode 100644 index 0000000..7055d67 --- /dev/null +++ b/src/VRCOscLib/VRCOscLib/Tracking/OscTracker.cs @@ -0,0 +1,45 @@ +using BuildSoft.OscCore.UnityObjects; + +namespace BuildSoft.VRChat.Osc.Tracking; +public class OscTracker +{ + public static readonly int SupportedTrackerCount = 8; + + public string PositionAddress { get; } + public string RotationAddress { get; } + + public Vector3 Position + { + get => OscParameter.GetValueAsVector3(PositionAddress) ?? default; + set => OscParameter.SendValue(PositionAddress, value); + } + + public Vector3 Rotation + { + get => OscParameter.GetValueAsVector3(RotationAddress) ?? default; + set => OscParameter.SendValue(RotationAddress, value); + } + + + /// + /// Create the OSC tracker. + /// + /// 0 base index. (0 ≦ < ) + /// + /// is negative or greater than or equal to . + /// (0 ≦ < ) + /// + public OscTracker(int index) : this((index + 1).ToString()) + { + if (index < 0 || index >= SupportedTrackerCount) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + } + + internal OscTracker(string part) + { + PositionAddress = $"/tracking/trackers/{part}/position"; + RotationAddress = $"/tracking/trackers/{part}/rotation"; + } +} diff --git a/src/VRCOscLib/VRCOscLib/Tracking/OscTracking.cs b/src/VRCOscLib/VRCOscLib/Tracking/OscTracking.cs new file mode 100644 index 0000000..d544b9b --- /dev/null +++ b/src/VRCOscLib/VRCOscLib/Tracking/OscTracking.cs @@ -0,0 +1,26 @@ +using System.Collections.Immutable; + +namespace BuildSoft.VRChat.Osc.Tracking; + +public class OscTracking +{ + private static OscTracker? _headTracker; + private static readonly Lazy> _trackers = new(CreateTrackers); + + public static OscTracker HeadTracker => _headTracker ??= new OscTracker("head"); + public static ImmutableArray Trackers => _trackers.Value; + + private static ImmutableArray CreateTrackers() + { + var trackers = ImmutableArray.CreateBuilder(OscTracker.SupportedTrackerCount); + for (int i = 0; i < OscTracker.SupportedTrackerCount; i++) + { + trackers.Add(new OscTracker(i)); + } + return trackers.ToImmutable(); + } + + private OscTracking() + { + } +} diff --git a/src/VRCOscLib/VRCOscLib/Utility/OscConnectionSettings.cs b/src/VRCOscLib/VRCOscLib/Utility/OscConnectionSettings.cs index 6eabd71..21fa944 100644 --- a/src/VRCOscLib/VRCOscLib/Utility/OscConnectionSettings.cs +++ b/src/VRCOscLib/VRCOscLib/Utility/OscConnectionSettings.cs @@ -39,13 +39,10 @@ public static int ReceivePort _server.Dispose(); _server = new OscServer(value); - if (_monitorCallbacks == null) + var monitorCallbacks = MonitorCallbacks; + for (int i = 0; i < monitorCallbacks.Count; i++) { - return; - } - for (int i = 0; i < _monitorCallbacks.Count; i++) - { - _server.AddMonitorCallback(_monitorCallbacks[i]); + _server.AddMonitorCallback(monitorCallbacks[i]); } } } @@ -91,6 +88,6 @@ public static string VrcIPAddress } } } - private static List? _monitorCallbacks; - internal static List MonitorCallbacks => _monitorCallbacks ??= new List(); + + internal static List MonitorCallbacks { get; } = new List(); } diff --git a/src/VRCOscLib/VRCOscLib/Utility/OscParameter.Receiver.cs b/src/VRCOscLib/VRCOscLib/Utility/OscParameter.Receiver.cs index 0faeb93..b18d207 100644 --- a/src/VRCOscLib/VRCOscLib/Utility/OscParameter.Receiver.cs +++ b/src/VRCOscLib/VRCOscLib/Utility/OscParameter.Receiver.cs @@ -3,6 +3,7 @@ using System.Text; using BlobHandles; using BuildSoft.OscCore; +using BuildSoft.OscCore.UnityObjects; namespace BuildSoft.VRChat.Osc; @@ -11,10 +12,49 @@ public static partial class OscParameter internal static void ReceiveMessage(BlobString address, OscMessageValues values) { var addressString = address.ToString(); + if (values.ElementCount <= 0) + { + return; + } + if (values.ElementCount == 1) + { + Parameters[addressString] = values.ReadValue(0); + return; + } + + object?[] objects = new object[values.ElementCount]; for (int i = 0; i < values.ElementCount; i++) { - Parameters[addressString] = values.ReadValue(i); + objects[i] = values.ReadValue(i); + } + Parameters[addressString] = objects; + } + + internal static Vector3? GetValueAsVector3(string address) + { + Parameters.TryGetValue(address, out var value); + if (value is Vector3 vector) + { + return vector; + } + if (value is not object[] array || array.Length != 3) + { + return null; + } + + if (array[0] is not float x) + { + return null; + } + if (array[1] is not float y) + { + return null; + } + if (array[2] is not float z) + { + return null; } + return new(x, y, z); } public static object? GetValue(string address) diff --git a/src/VRCOscLib/VRCOscLib/Utility/OscParameter.cs b/src/VRCOscLib/VRCOscLib/Utility/OscParameter.cs index 2784ef6..97a2c8c 100644 --- a/src/VRCOscLib/VRCOscLib/Utility/OscParameter.cs +++ b/src/VRCOscLib/VRCOscLib/Utility/OscParameter.cs @@ -8,8 +8,7 @@ namespace BuildSoft.VRChat.Osc; public static partial class OscParameter { - private static OscParameterCollection? _parameters; - internal static OscParameterCollection Parameters => _parameters ??= new(); + internal static OscParameterCollection Parameters { get; } = new(); public static event ParamChangedHandler ValueChanged { diff --git a/src/VRCOscLib/VRCOscLib/Utility/OscUtility.AvatarConfig.cs b/src/VRCOscLib/VRCOscLib/Utility/OscUtility.AvatarConfig.cs index ffbea69..7f5699d 100644 --- a/src/VRCOscLib/VRCOscLib/Utility/OscUtility.AvatarConfig.cs +++ b/src/VRCOscLib/VRCOscLib/Utility/OscUtility.AvatarConfig.cs @@ -46,7 +46,7 @@ internal static IEnumerable EnumerateOscAvatarConfigPathes() { if (!Directory.Exists(VRChatOscPath)) { - return Array.Empty(); + return Enumerable.Empty(); } try @@ -56,7 +56,7 @@ internal static IEnumerable EnumerateOscAvatarConfigPathes() } catch (DirectoryNotFoundException) { - return Array.Empty(); + return Enumerable.Empty(); } } } diff --git a/src/VRCOscLib/VRCOscLib/vrcosclib.csproj b/src/VRCOscLib/VRCOscLib/vrcosclib.csproj index 74290f1..72e8b30 100644 --- a/src/VRCOscLib/VRCOscLib/vrcosclib.csproj +++ b/src/VRCOscLib/VRCOscLib/vrcosclib.csproj @@ -10,7 +10,7 @@ ..\..\..\artifacts\bin VRCOscLib - 1.4.1 + 1.4.2 ChanyaKushima MIT A OSC library for VRChat @@ -19,9 +19,9 @@ - + - +