diff --git a/BeatSaverSharp/BeatSaver.cs b/BeatSaverSharp/BeatSaver.cs index 5d18395..33ecfdf 100644 --- a/BeatSaverSharp/BeatSaver.cs +++ b/BeatSaverSharp/BeatSaver.cs @@ -5,12 +5,16 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Web; +using BeatSaverSharp.Websocket; +using IWebsocketClientLite.PCL; +using Newtonsoft.Json; namespace BeatSaverSharp { @@ -23,11 +27,13 @@ public class BeatSaver : IDisposable private BeatSaverOptions _options; private readonly IHttpService _httpService; + private readonly IWebsocketClient _websocketClient; private readonly object _bLock = new object(); private readonly object _uLock = new object(); private readonly object _pLock = new object(); private static readonly (string, PropertyInfo)[] _filterProperties; private static readonly (string, PropertyInfo)[] _playlistFilterProperties; + private static readonly JsonSerializer _jsonSerializer = new JsonSerializer(); private readonly ConcurrentDictionary _fetchedUsers = new ConcurrentDictionary(); private readonly ConcurrentDictionary _fetchedUsernames = new ConcurrentDictionary(); private readonly ConcurrentDictionary _fetchedBeatmaps = new ConcurrentDictionary(); @@ -68,6 +74,8 @@ public BeatSaver(BeatSaverOptions beatSaverOptions) #else _httpService = new HttpClientService(beatSaverOptions.BeatSaverAPI.ToString(), beatSaverOptions.Timeout, userAgent); #endif + _websocketClient = new WebsocketClient(beatSaverOptions.WebsocketAPI.ToString(), userAgent); + _websocketClient.MessageRecievedEvent += OnWebsocketMessageRecieved; } public BeatSaver(string applicationName, Version version) : this(new BeatSaverOptions(applicationName, version)) @@ -501,6 +509,25 @@ public async Task Vote(string levelHash, Vote.Type voteType, Vote. #endregion + #region Websocket + + public event Action? WebsocketMessageRecievedEvent; + + private void OnWebsocketMessageRecieved(IDataframe dataframe) + { + if (WebsocketMessageRecievedEvent == null || dataframe.Message == null) + return; + + using StringReader reader = new StringReader(dataframe.Message); + using JsonTextReader jsonTextReader = new JsonTextReader(reader); + WsBeatmap wsBeatmap = _jsonSerializer.Deserialize(jsonTextReader)!; + GetOrAddBeatmapToCache(wsBeatmap.Map, out var cachedAndOrBeatmap); + wsBeatmap.Map = cachedAndOrBeatmap; + WebsocketMessageRecievedEvent.Invoke(wsBeatmap); + } + + #endregion + private void ProcessCache() { if (_options.MaximumCacheSize == null || _options.MaximumCacheSize == 0) @@ -826,8 +853,11 @@ public void Clear() public void Dispose() { GC.SuppressFinalize(this); - if (_httpService is IDisposable disposable) - disposable.Dispose(); + _websocketClient.MessageRecievedEvent -= OnWebsocketMessageRecieved; + if (_httpService is IDisposable httpDisposable) + httpDisposable.Dispose(); + if (_websocketClient is IDisposable wsDisposable) + wsDisposable.Dispose(); IsDisposed = true; } } diff --git a/BeatSaverSharp/BeatSaverOptions.cs b/BeatSaverSharp/BeatSaverOptions.cs index 8e282a4..2a0fe2b 100644 --- a/BeatSaverSharp/BeatSaverOptions.cs +++ b/BeatSaverSharp/BeatSaverOptions.cs @@ -7,6 +7,7 @@ public class BeatSaverOptions public Version Version { get; set; } public string ApplicationName { get; set; } public Uri BeatSaverAPI { get; set; } = new Uri("https://api.beatsaver.com/"); + public Uri WebsocketAPI { get; set; } = new Uri("wss://ws.beatsaver.com/maps"); public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(30); public bool Cache { get; set; } = true; diff --git a/BeatSaverSharp/BeatSaverSharp.csproj b/BeatSaverSharp/BeatSaverSharp.csproj index 05d68a4..0291cfe 100644 --- a/BeatSaverSharp/BeatSaverSharp.csproj +++ b/BeatSaverSharp/BeatSaverSharp.csproj @@ -16,6 +16,7 @@ beatsaber beatsaver True + true @@ -35,6 +36,7 @@ + @@ -57,4 +59,5 @@ + diff --git a/BeatSaverSharp/ILRepack.targets b/BeatSaverSharp/ILRepack.targets new file mode 100644 index 0000000..00f1218 --- /dev/null +++ b/BeatSaverSharp/ILRepack.targets @@ -0,0 +1,54 @@ + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + $(ProjectDir)$(OutputPath) + Merged\ + $(ILRepackOutputDir)$(AssemblyName).dll + + + + + + + + + + + + + + <_ReferenceDirsDupl Include="@(ReferencePath->'%(RelativeDir)')" /> + + + + + + + + + + <_ILRCmdArg Include="/union"/> + <_ILRCmdArg Include="/parallel"/> + <_ILRCmdArg Include="/internalize"/> + <_ILRCmdArg Include="/renameInternalized"/> + <_ILRCmdArg Include="/lib:%(_ReferenceDirs.Identity)" Condition="'$(OS)' != 'Windows_NT'" /> + <_ILRCmdArg Include="/xmldocs"/> + <_ILRCmdArg Include="/out:$(ILRepackOutput)"/> + <_ILRCmdArg Include="%(RepackInputAssemblies.Identity)"/> + + + + + + \ No newline at end of file diff --git a/BeatSaverSharp/Models/WsBeatmap.cs b/BeatSaverSharp/Models/WsBeatmap.cs new file mode 100644 index 0000000..6410d13 --- /dev/null +++ b/BeatSaverSharp/Models/WsBeatmap.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace BeatSaverSharp.Models +{ + public class WsBeatmap + { + [JsonConverter(typeof(StringEnumConverter))] + [JsonProperty("type")] + public WsMessageType Type { get; set; } + + [JsonProperty("msg")] + public Beatmap Map { get; set; } = null!; + } + + public enum WsMessageType + { + MAP_UPDATE, + MAP_DELETE + } +} \ No newline at end of file diff --git a/BeatSaverSharp/Websocket/IWebsocketClient.cs b/BeatSaverSharp/Websocket/IWebsocketClient.cs new file mode 100644 index 0000000..39dc29c --- /dev/null +++ b/BeatSaverSharp/Websocket/IWebsocketClient.cs @@ -0,0 +1,10 @@ +using System; +using IWebsocketClientLite.PCL; + +namespace BeatSaverSharp.Websocket +{ + public interface IWebsocketClient + { + public event Action? MessageRecievedEvent; + } +} \ No newline at end of file diff --git a/BeatSaverSharp/Websocket/WebsocketClient.cs b/BeatSaverSharp/Websocket/WebsocketClient.cs new file mode 100644 index 0000000..6f7e005 --- /dev/null +++ b/BeatSaverSharp/Websocket/WebsocketClient.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using IWebsocketClientLite.PCL; +using WebsocketClientLite.PCL; + +namespace BeatSaverSharp.Websocket +{ + internal class WebsocketClient : IWebsocketClient, IDisposable + { + private readonly MessageWebsocketRx _websocketLiteClient; + private readonly IDisposable _observable; + public event Action? MessageRecievedEvent; + + public WebsocketClient(string url, string userAgent) + { + _websocketLiteClient = new MessageWebsocketRx() + { + Headers = new Dictionary {{ "User-Agent", userAgent }} + }; + + var websocketConnectionObservable = _websocketLiteClient.WebsocketConnectObservable(new Uri(url)); + _observable = websocketConnectionObservable.Subscribe(OnNext); + } + + private void OnNext(IDataframe? dataframe) + { + if (dataframe != null) + { + MessageRecievedEvent?.Invoke(dataframe); + } + } + + public void Dispose() + { + GC.SuppressFinalize(this); + _websocketLiteClient.Dispose(); + _observable.Dispose(); + } + } +} \ No newline at end of file