diff --git a/ShockOsc/OscQueryLibrary/HostInfo.cs b/ShockOsc/OscQueryLibrary/HostInfo.cs deleted file mode 100644 index 643e17b..0000000 --- a/ShockOsc/OscQueryLibrary/HostInfo.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Net; -using System.Text.Json.Serialization; -using OpenShock.ShockOsc.Utils; - -// ReSharper disable InconsistentNaming - -namespace OpenShock.ShockOsc.OscQueryLibrary; - -public sealed class HostInfo -{ - [JsonPropertyName("NAME")] - public required string Name { get; set; } - - [JsonPropertyName("OSC_IP")] - [JsonConverter(typeof(JsonIPAddressConverter))] - public required IPAddress OscIp { get; set; } - - [JsonPropertyName("OSC_PORT")] - public required ushort OscPort { get; set; } - - [JsonPropertyName("OSC_TRANSPORT")] - [JsonConverter(typeof(JsonStringEnumConverter))] - public required OscTransportType OscTransport { get; set; } - - [JsonPropertyName("EXTENSIONS")] - public required ExtensionsNode Extensions { get; set; } - - public enum OscTransportType - { - TCP, - UDP - } - - public sealed class ExtensionsNode - { - [JsonPropertyName("ACCESS")] - public required bool Access { get; set; } - - [JsonPropertyName("CLIPMODE")] - public required bool ClipMode { get; set; } - - [JsonPropertyName("RANGE")] - public required bool Range { get; set; } - - [JsonPropertyName("TYPE")] - public required bool Type { get; set; } - - [JsonPropertyName("VALUE")] - public required bool Value { get; set; } - } -} \ No newline at end of file diff --git a/ShockOsc/OscQueryLibrary/Node.cs b/ShockOsc/OscQueryLibrary/Node.cs deleted file mode 100644 index cb6c9be..0000000 --- a/ShockOsc/OscQueryLibrary/Node.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Text.Json.Serialization; - -namespace OpenShock.ShockOsc.OscQueryLibrary; - -// technically every class in the JSON is this "Node" class but that's gross -public class Node -{ - [JsonPropertyOrder(-4)] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("DESCRIPTION")] - public string? Description { get; set; } - - [JsonPropertyOrder(-3)] - [JsonPropertyName("FULL_PATH")] - public required string FullPath { get; set; } - - [JsonPropertyOrder(-2)] - [JsonPropertyName("ACCESS")] - public required int Access { get; set; } -} - -public class Node : Node -{ - [JsonPropertyOrder(-1)] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("CONTENTS")] - public T? Contents { get; set; } -} \ No newline at end of file diff --git a/ShockOsc/OscQueryLibrary/OscQueryModels.cs b/ShockOsc/OscQueryLibrary/OscQueryModels.cs deleted file mode 100644 index ede6f67..0000000 --- a/ShockOsc/OscQueryLibrary/OscQueryModels.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Text.Json.Serialization; - -namespace OpenShock.ShockOsc.OscQueryLibrary; - -public sealed class RootNode : Node -{ - public sealed class RootContents - { - [JsonPropertyName("avatar")] public Node? Avatar { get; set; } - } -} - -public sealed class AvatarContents -{ - [JsonPropertyName("change")] public required OscParameterNodeEnd Change { get; set; } - - [JsonPropertyName("parameters")] public Node>? Parameters { get; set; } -} - -public sealed class OscParameterNode : Node> -{ - [JsonPropertyName("TYPE")] public string? Type { get; set; } - - [JsonPropertyName("VALUE")] public IEnumerable? Value { get; set; } -} - -public sealed class OscParameterNodeEnd : Node -{ - [JsonPropertyName("TYPE")] public required string Type { get; set; } - - [JsonPropertyName("VALUE")] public required IEnumerable Value { get; set; } -} \ No newline at end of file diff --git a/ShockOsc/OscQueryLibrary/OscQueryServer.cs b/ShockOsc/OscQueryLibrary/OscQueryServer.cs deleted file mode 100644 index 77bf338..0000000 --- a/ShockOsc/OscQueryLibrary/OscQueryServer.cs +++ /dev/null @@ -1,334 +0,0 @@ -using System.Net; -using System.Net.Mime; -using System.Net.Sockets; -using System.Text; -using System.Text.Json; -using MeaMod.DNS.Model; -using MeaMod.DNS.Multicast; -using Serilog; -using EmbedIO; -using EmbedIO.Actions; -using Microsoft.Extensions.Hosting; -using OpenShock.SDK.CSharp.Live.Utils; -using OpenShock.SDK.CSharp.Utils; -using OpenShock.ShockOsc.Config; -using OpenShock.ShockOsc.Utils; -using ILogger = Serilog.ILogger; - -namespace OpenShock.ShockOsc.OscQueryLibrary; - -public class OscQueryServer : IDisposable -{ - private static readonly ILogger Logger = Log.ForContext(typeof(OscQueryServer)); - - private static readonly HttpClient Client = new(); - - private readonly ushort _httpPort; - private readonly IPAddress _ipAddress; - private readonly ConfigManager _configManager; - public readonly ushort ShockOscReceivePort; - private const string OscHttpServiceName = "_oscjson._tcp"; - private const string OscUdpServiceName = "_osc._udp"; - private readonly MulticastService _multicastService; - private readonly ServiceDiscovery _serviceDiscovery; - private readonly string _serviceName; - private HostInfo? _hostInfo; - private RootNode? _queryData; - - private readonly HashSet FoundServices = new(); - private IPEndPoint? _lastVrcHttpServer; - - - public event Func? FoundVrcClient; - public event Func, string, Task>? ParameterUpdate; - - private readonly Dictionary ParameterList = new(); - - private readonly WebServer _httpServer; - private readonly string _httpServerUrl; - - public OscQueryServer(string serviceName, IPAddress ipAddress, ConfigManager configManager) - { - Swan.Logging.Logger.NoLogging(); - - _serviceName = serviceName; - _ipAddress = ipAddress; - _configManager = configManager; - ShockOscReceivePort = FindAvailableUdpPort(); - _httpPort = FindAvailableTcpPort(); - SetupJsonObjects(); - // ignore our own service - FoundServices.Add($"{_serviceName.ToLower()}.{OscHttpServiceName}.local:{_httpPort}"); - - // HTTP Server - _httpServerUrl = $"http://{_ipAddress}:{_httpPort}/"; - _httpServer = new WebServer(o => o - .WithUrlPrefix(_httpServerUrl) - .WithMode(HttpListenerMode.EmbedIO)) - .WithModule(new ActionModule("/", HttpVerbs.Get, - ctx => ctx.SendStringAsync( - ctx.Request.RawUrl.Contains("HOST_INFO") - ? JsonSerializer.Serialize(_hostInfo) - : JsonSerializer.Serialize(_queryData), MediaTypeNames.Application.Json, Encoding.UTF8))); - - // mDNS - _multicastService = new MulticastService - { - UseIpv6 = false, - IgnoreDuplicateMessages = true - }; - _serviceDiscovery = new ServiceDiscovery(_multicastService); - } - - public void Start() - { - if (!_configManager.Config.Osc.OscQuery) - { - Logger.Debug("OSCQuery: Disabled"); - return; - } - OsTask.Run(() => _httpServer.RunAsync()); - Logger.Information("HTTP Server listening at {Prefix}", _httpServerUrl); - - ListenForServices(); - _multicastService.Start(); - AdvertiseOscQueryServer(); - } - - private void AdvertiseOscQueryServer() - { - var httpProfile = - new ServiceProfile(_serviceName, OscHttpServiceName, _httpPort, - new[] { _ipAddress }); - var oscProfile = - new ServiceProfile(_serviceName, OscUdpServiceName, ShockOscReceivePort, - new[] { _ipAddress }); - _serviceDiscovery.Advertise(httpProfile); - _serviceDiscovery.Advertise(oscProfile); - } - - private void ListenForServices() - { - _multicastService.NetworkInterfaceDiscovered += (_, args) => - { - Logger.Debug("OSCQueryMDNS: Network interface discovered"); - _multicastService.SendQuery($"{OscHttpServiceName}.local"); - _multicastService.SendQuery($"{OscUdpServiceName}.local"); - }; - - _multicastService.AnswerReceived += OnAnswerReceived; - } - - private void OnAnswerReceived(object? sender, MessageEventArgs args) - { - var response = args.Message; - try - { - foreach (var record in response.AdditionalRecords.OfType()) - { - var domainName = record.Name.Labels; - var instanceName = domainName[0]; - var type = domainName[2]; - var serviceId = $"{record.CanonicalName}:{record.Port}"; - if (type == "_udp") - continue; // ignore UDP services - - if (record.TTL == TimeSpan.Zero) - { - Logger.Debug("OSCQueryMDNS: Goodbye message from {RecordCanonicalName}", record.CanonicalName); - FoundServices.Remove(serviceId); - continue; - } - - if (FoundServices.Contains(serviceId)) - continue; - - var ips = response.AdditionalRecords.OfType().Select(r => r.Address); - // TODO: handle more than one IP address - var ipAddress = ips.FirstOrDefault(); - FoundServices.Add(serviceId); - Logger.Debug("OSCQueryMDNS: Found service {ServiceId} {InstanceName} {IpAddress}:{RecordPort}", serviceId, instanceName, ipAddress, record.Port); - - if (instanceName.StartsWith("VRChat-Client-") && ipAddress != null) - { - FoundNewVrcClient(ipAddress, record.Port).GetAwaiter(); - } - } - } - catch (Exception ex) - { - Logger.Debug("Failed to parse from {ArgsRemoteEndPoint}: {ExMessage}", args.RemoteEndPoint, ex.Message); - } - } - - private async Task FoundNewVrcClient(IPAddress ipAddress, int port) - { - _lastVrcHttpServer = new IPEndPoint(ipAddress, port); - var oscEndpoint = await FetchOscSendPortFromVrc(ipAddress, port); - if(oscEndpoint == null) return; - FoundVrcClient?.Raise(oscEndpoint); - await FetchJsonFromVrc(ipAddress, port); - } - - private async Task FetchOscSendPortFromVrc(IPAddress ipAddress, int port) - { - var url = $"http://{ipAddress}:{port}?HOST_INFO"; - Logger.Debug("OSCQueryHttpClient: Fetching OSC send port from {Url}", url); - var response = string.Empty; - - try - { - response = await Client.GetStringAsync(url); - var rootNode = JsonSerializer.Deserialize(response); - if (rootNode?.OscPort == null) - { - Logger.Error("OSCQueryHttpClient: Error no OSC port found"); - return null; - } - - return new IPEndPoint(rootNode.OscIp, rootNode.OscPort); - } - catch (HttpRequestException ex) - { - Logger.Error("OSCQueryHttpClient: Error {ExMessage}", ex.Message); - } - catch (Exception ex) - { - Logger.Error("OSCQueryHttpClient: Error {ExMessage}\\n{Response}", ex.Message, response); - } - - return null; - } - - private static bool _fetchInProgress; - - private async Task FetchJsonFromVrc(IPAddress ipAddress, int port) - { - if (_fetchInProgress) return; - _fetchInProgress = true; - var url = $"http://{ipAddress}:{port}/"; - Logger.Debug("OSCQueryHttpClient: Fetching new parameters from {Url}", url); - var response = string.Empty; - var avatarId = string.Empty; - try - { - response = await Client.GetStringAsync(url); - - var rootNode = JsonSerializer.Deserialize(response); - if (rootNode?.Contents?.Avatar?.Contents?.Parameters?.Contents == null) - { - Logger.Debug("OSCQueryHttpClient: Error no parameters found"); - return; - } - - ParameterList.Clear(); - foreach (var node in rootNode.Contents.Avatar.Contents.Parameters.Contents!) - { - RecursiveParameterLookup(node.Value); - } - - avatarId = rootNode.Contents.Avatar.Contents.Change.Value.FirstOrDefault() ?? string.Empty; - if(ParameterUpdate != null) await ParameterUpdate.Raise(ParameterList, avatarId); - } - catch (HttpRequestException ex) - { - _lastVrcHttpServer = null; - ParameterList.Clear(); - ParameterUpdate?.Raise(ParameterList, avatarId); - Logger.Error(ex, "HTTP request failed"); - } - catch (Exception ex) - { - Logger.Error(ex, "Unexpected exception while receiving parameters via osc query"); - } - finally - { - _fetchInProgress = false; - } - } - - private void RecursiveParameterLookup(OscParameterNode node) - { - if (node.Contents == null) - { - ParameterList.Add(node.FullPath, node.Value?.FirstOrDefault()); - return; - } - - foreach (var subNode in node.Contents) - RecursiveParameterLookup(subNode.Value); - } - - public async Task GetParameters() - { - if (_lastVrcHttpServer == null) - return; - - await FetchJsonFromVrc(_lastVrcHttpServer.Address, _lastVrcHttpServer.Port); - } - - private ushort FindAvailableTcpPort() - { - using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - socket.Bind(new IPEndPoint(_ipAddress, port: 0)); - ushort port = 0; - if (socket.LocalEndPoint != null) - port = (ushort)((IPEndPoint)socket.LocalEndPoint).Port; - return port; - } - - private ushort FindAvailableUdpPort() - { - using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - socket.Bind(new IPEndPoint(_ipAddress, port: 0)); - ushort port = 0; - if (socket.LocalEndPoint != null) - port = (ushort)((IPEndPoint)socket.LocalEndPoint).Port; - return port; - } - - private void SetupJsonObjects() - { - _queryData = new RootNode - { - FullPath = "/", - Access = 0, - Contents = new RootNode.RootContents - { - Avatar = new Node - { - FullPath = "/avatar", - Access = 2 - } - } - }; - - _hostInfo = new HostInfo - { - Name = _serviceName, - OscPort = ShockOscReceivePort, - OscIp = _ipAddress, - OscTransport = HostInfo.OscTransportType.UDP, - Extensions = new HostInfo.ExtensionsNode - { - Access = true, - ClipMode = true, - Range = true, - Type = true, - Value = true - } - }; - } - - public void Dispose() - { - GC.SuppressFinalize(this); - _multicastService.Dispose(); - _serviceDiscovery.Dispose(); - } - - ~OscQueryServer() - { - Dispose(); - } -} \ No newline at end of file diff --git a/ShockOsc/Services/ShockOsc.cs b/ShockOsc/Services/ShockOsc.cs index 0018b6c..addcd8d 100644 --- a/ShockOsc/Services/ShockOsc.cs +++ b/ShockOsc/Services/ShockOsc.cs @@ -6,8 +6,8 @@ using OpenShock.ShockOsc.Backend; using OpenShock.ShockOsc.Config; using OpenShock.ShockOsc.Models; -using OpenShock.ShockOsc.OscQueryLibrary; using OpenShock.ShockOsc.Utils; +using OscQueryLibrary; #pragma warning disable CS4014 @@ -87,7 +87,7 @@ public ShockOsc(ILogger logger, if (!_configManager.Config.Osc.OscQuery) { - FoundVrcClient(null); + FoundVrcClient(null, null); } _logger.LogInformation("Started ShockOsc.cs"); @@ -108,17 +108,17 @@ private void OnParamChange(bool shockOscParam) OnParamsChange?.Invoke(shockOscParam); } - private async Task FoundVrcClient(IPEndPoint? oscClient) + private async Task FoundVrcClient(OscQueryServer oscQueryServer, IPEndPoint ipEndPoint) { _logger.LogInformation("Found VRC client"); // stop tasks _oscServerActive = false; Task.Delay(1000).Wait(); // wait for tasks to stop - if (oscClient != null) + if (ipEndPoint != null) { - _oscClient.CreateGameConnection(oscClient.Address, _oscQueryServer.ShockOscReceivePort, - (ushort)oscClient.Port); + _oscClient.CreateGameConnection(ipEndPoint.Address, oscQueryServer.OscReceivePort, + (ushort)ipEndPoint.Port); } else { diff --git a/ShockOsc/ShockOsc.csproj b/ShockOsc/ShockOsc.csproj index f3b3e54..d091f1b 100644 --- a/ShockOsc/ShockOsc.csproj +++ b/ShockOsc/ShockOsc.csproj @@ -84,6 +84,7 @@ + diff --git a/ShockOsc/ShockOscBootstrap.cs b/ShockOsc/ShockOscBootstrap.cs index 4cb31af..1a6cd0f 100644 --- a/ShockOsc/ShockOscBootstrap.cs +++ b/ShockOsc/ShockOscBootstrap.cs @@ -1,14 +1,13 @@ using System.Net; -using Microsoft.Extensions.DependencyInjection; using MudBlazor.Services; using OpenShock.SDK.CSharp.Hub; using OpenShock.ShockOsc.Backend; using OpenShock.ShockOsc.Config; using OpenShock.ShockOsc.Logging; -using OpenShock.ShockOsc.OscQueryLibrary; using OpenShock.ShockOsc.Services; using OpenShock.ShockOsc.Services.Pipes; using OpenShock.ShockOsc.Utils; +using OscQueryLibrary; using Serilog; namespace OpenShock.ShockOsc; @@ -47,7 +46,7 @@ public static void AddShockOscServices(this IServiceCollection services) services.AddMemoryCache(); services.AddSingleton(); - + services.AddSingleton(); services.AddSingleton(); @@ -70,12 +69,12 @@ public static void AddShockOscServices(this IServiceCollection services) { var config = provider.GetRequiredService(); var listenAddress = config.Config.Osc.QuestSupport ? IPAddress.Any : IPAddress.Loopback; - return new OscQueryServer("ShockOsc", listenAddress, config); + return new OscQueryServer("ShockOsc", listenAddress); }); services.AddSingleton(); services.AddSingleton(); - + services.AddSingleton(); } @@ -109,11 +108,14 @@ public static void StartShockOscServices(this IServiceProvider services, bool he #endregion + var config = services.GetRequiredService(); + // <---- Warmup ----> services.GetRequiredService(); - services.GetRequiredService().Start(); services.GetRequiredService().StartServer(); + + if (config.Config.Osc.OscQuery) services.GetRequiredService().Start(); var updater = services.GetRequiredService(); OsTask.Run(updater.CheckUpdate);