diff --git a/Plugin.cs b/Plugin.cs index 7e6d857..74a0e47 100644 --- a/Plugin.cs +++ b/Plugin.cs @@ -1,6 +1,10 @@ -using IPA; -using Newtonsoft.Json; +using DataPuller.Data; +using Discord; +using DiscordCore; +using IPA; using System; +using System.Text; +using System.Threading; using UnityEngine; using IPALogger = IPA.Logging.Logger; @@ -12,12 +16,9 @@ public class Plugin internal static Plugin Instance { get; private set; } internal static IPALogger Log { get; private set; } - private PluginSocketData mapDataSource; - private PluginSocketData liveDataSource; - private MapData mapData; - private LiveData liveData; - private Discord.Discord discord; + private DiscordInstance discord; private static long DiscordClientId = 1028340906740420711; + private Timer updateTimer; [Init] /// @@ -32,196 +33,301 @@ public void Init(IPALogger logger) Log.Info("bsrpc initialized."); } - #region BSIPA Config - //Uncomment to use BSIPA's config - /* - [Init] - public void InitWithConfig(Config conf) - { - Configuration.PluginConfig.Instance = conf.Generated(); - Log.Debug("Config loaded"); - } - */ - #endregion - [OnStart] public void OnApplicationStart() { - Log.Debug("OnApplicationStart"); new GameObject("bsrpcController").AddComponent(); - mapDataSource = new PluginSocketData("MapData", Log); - mapDataSource.Update += UpdateMapData; + MapData.Instance.OnUpdate += UpdateRichPresence; + LiveData.Instance.OnUpdate += UpdateRichPresence; - liveDataSource = new PluginSocketData("LiveData", Log); - liveDataSource.Update += UpdateLiveData; + discord = DiscordManager.instance.CreateInstance(new DiscordSettings + { + appId = DiscordClientId, + handleInvites = false, + modId = nameof(bsrpc), + modName = nameof(bsrpc), + }); - discord = new Discord.Discord(DiscordClientId, (UInt64)Discord.CreateFlags.Default); + SetUpdateTimer(0); } - private void UpdateMapData(string jsonData) + // Update rich presence regularly on a scedule even if the update events are not fired + private void SetUpdateTimer(int debounceMs = 500) { - try - { - mapData = Deserialize("map data", jsonData); - } - catch (Exception e) + if (updateTimer != null) { - Log.Error($"Error while updating map data: {e.Message}"); + updateTimer.Dispose(); + } + updateTimer = new Timer((e) => UpdateRichPresence(), null, debounceMs, 5000); + } + + private void UpdateRichPresence(string jsonData) + { UpdateRichPresence(); } - private void UpdateLiveData(string jsonData) + private void UpdateRichPresence() { - try - { - liveData = Deserialize("live data", jsonData); - } - catch (Exception e) + var activity = GetActivityData(); + + discord.UpdateActivity(activity); + SetUpdateTimer(); + } + + private string GetReadableDifficulty(string originalDifficulty) + { + switch (originalDifficulty) { - Log.Error($"Error while updating live data: {e.Message}"); + case "ExpertPlus": + return "Expert+"; + default: + return originalDifficulty; } - UpdateRichPresence(); } - private T Deserialize(string type, string jsonData) + private string GetReadableRank(string originalRank) { - Log.Debug($"Updating {type}: {jsonData}"); - var settings = new JsonSerializerSettings(); - settings.MissingMemberHandling = MissingMemberHandling.Ignore; - settings.NullValueHandling = NullValueHandling.Include; - settings.Error += (sender, e) => + switch (originalRank) { - Log.Error($"Deserialize: {e}"); - }; - return JsonConvert.DeserializeObject(jsonData, settings); + case "SSS": + // Match in-game rank shown during zen mode + return "E"; + default: + return originalRank; + } } - private void UpdateRichPresence() + private string GetPlayState() { - Log.Debug("Updating rich presence"); - var activityManager = discord.GetActivityManager(); - if (activityManager == null) + if (MapData.Instance.LevelPaused) { - Log.Error("Could not get Discord ActivityManager"); - return; + return "⏸️"; } - try + + if (MapData.Instance.LevelFailed) { + return "☠️"; + } + + if (MapData.Instance.LevelFinished) + { + + return "🎉"; + } - var activity = GetActivityData(); - Log.Debug($"Gathered activity data: {JsonConvert.SerializeObject(activity)}"); + if (LiveData.Instance.TimeElapsed > 0) + { + return "▶️"; + } - try + return "⌛"; + } + + private string GetModifiersState() + { + StringBuilder modifiersString = new StringBuilder(); + if (LiveData.Instance.TimeElapsed > 0) + { + if (MapData.Instance.Modifiers.ZenMode) { - activityManager.UpdateActivity(activity, (result) => + modifiersString.Append("🧘"); + } + else + { + // Life modifiers + if (MapData.Instance.Modifiers.NoFailOn0Energy || MapData.Instance.Modifiers.OneLife || MapData.Instance.Modifiers.FourLives) { - if (result == Discord.Result.Ok) + if (MapData.Instance.Modifiers.NoFailOn0Energy) { - Log.Debug("Sucessfully updated Discord Activity"); + modifiersString.Append("🛡"); } - else + else if (MapData.Instance.Modifiers.OneLife) { - Log.Error("Failed to update Discord Activity"); + modifiersString.Append("1🤍"); } - }); + else if (MapData.Instance.Modifiers.FourLives) + { + modifiersString.Append("4🤍"); + } + modifiersString.Append(" "); + } + + // Difficulty decreasing modifiers + if (MapData.Instance.Modifiers.NoBombs || MapData.Instance.Modifiers.NoWalls || MapData.Instance.Modifiers.NoArrows) + { + modifiersString.Append("🚫"); + if (MapData.Instance.Modifiers.NoBombs) + { + modifiersString.Append("💣"); + } + if (MapData.Instance.Modifiers.NoWalls) + { + modifiersString.Append("🧱"); + } + if (MapData.Instance.Modifiers.NoArrows) + { + modifiersString.Append("🔽"); + } + modifiersString.Append(" "); + } + + // Difficulty increasing modifiers + if (MapData.Instance.Modifiers.GhostNotes || MapData.Instance.Modifiers.DisappearingArrows) + { + if (MapData.Instance.Modifiers.GhostNotes) + { + modifiersString.Append("👻"); + } + else if (MapData.Instance.Modifiers.DisappearingArrows) + { + modifiersString.Append("🟦"); + } + modifiersString.Append(" "); + } + + if (MapData.Instance.Modifiers.SmallNotes || MapData.Instance.Modifiers.ProMode || MapData.Instance.Modifiers.StrictAngles) + { + if (MapData.Instance.Modifiers.SmallNotes) + { + modifiersString.Append("🔹"); + } + if (MapData.Instance.Modifiers.ProMode) + { + modifiersString.Append("⚔️"); + } + if (MapData.Instance.Modifiers.StrictAngles) + { + modifiersString.Append("📐"); + } + modifiersString.Append(" "); + } } - catch (Exception e) + + // Speed modifiers + if (MapData.Instance.Modifiers.SlowerSong) { - Log.Error($"Error while updating rich presence: {e.Message}\n{e.Source}"); + modifiersString.Append("🐌"); + } + else if (MapData.Instance.Modifiers.FasterSong) + { + modifiersString.Append("⏩"); + } + if (MapData.Instance.Modifiers.SuperFastSong) + { + modifiersString.Append("⏩"); } } - catch (NullReferenceException e) - { - Log.Error($"Null reference error while updating rich presence: {e.Message}\n{e.Source}"); - } - } - private string GetReadableDifficulty(string originalDifficulty) - { - switch (originalDifficulty) - { - case "ExpertPlus": - return "Expert+"; - default: - return originalDifficulty; - } + return modifiersString.ToString().Trim(); } - private string GetPlayState() + private ActivityAssets GetActivityAssets() { - if (mapData.LevelPaused) + var assets = new ActivityAssets(); + if (MapData.Instance.InLevel) { - return "Paused"; - } + assets.LargeImage = RichPresenceAssetKeys.TheFirst; - if (mapData.LevelFailed) - { - return "Failed"; + switch (MapData.Instance.MapType) + { + case "Standard": + assets.SmallImage = RichPresenceAssetKeys.Standard; + assets.SmallText = "Standard"; + break; + case "OneSaber": + assets.SmallImage = RichPresenceAssetKeys.OneSaber; + assets.SmallText = "One Saber"; + break; + case "NoArrows": + assets.SmallImage = RichPresenceAssetKeys.NoArrows; + assets.SmallText = "No Arrows"; + break; + case "360Degree": + assets.SmallImage = RichPresenceAssetKeys.ThreeSixty; + assets.SmallText = "360°"; + break; + case "90Degree": + assets.SmallImage = RichPresenceAssetKeys.Ninety; + assets.SmallText = "90°"; + break; + default: + assets.SmallImage = RichPresenceAssetKeys.BlankMapType; + assets.SmallText = MapData.Instance.MapType; + break; + } } - - if (mapData.LevelFinished) + else { - - return "Finished"; + if (MapData.Instance.IsMultiplayer) + { + assets.LargeImage = RichPresenceAssetKeys.MultiplayerLobby; + } + else + { + assets.LargeImage = RichPresenceAssetKeys.MainMenu; + } } - - if (liveData != null) + if (assets.LargeImage != null) { - return "Playing"; + assets.LargeText = $"Game version: {MapData.GameVersion}"; } - return "Waiting"; + return assets; } private Discord.Activity GetActivityData() { - Log.Debug("Building activity data"); - Log.Debug($"mapData = {JsonConvert.SerializeObject(mapData)}"); - Log.Debug($"liveData = {JsonConvert.SerializeObject(liveData)}"); var activity = new Discord.Activity(); - if (mapData != null) + activity.Assets = GetActivityAssets(); + if (MapData.Instance.InLevel) { - activity.Name = $"Beat Saber {mapData.GameVersion}"; - if (mapData.InLevel) - { - var playState = GetPlayState(); - var lobbyType = mapData.IsMultiplayer ? "Multiplayer" : "Solo"; - var rankedStatus = mapData.Star > 0 ? "Ranked" : "Unranked"; - activity.State = $"{playState} - {lobbyType} ({rankedStatus})"; + var playState = GetPlayState() + GetModifiersState(); + var lobbyType = MapData.Instance.IsMultiplayer ? "Multiplayer" : "Singleplayer"; + var rankedStatus = MapData.Instance.Star > 0 ? $" ⭐{MapData.Instance.Star:N}" : ""; + var playDetail = ""; - var difficulty = GetReadableDifficulty(mapData.Difficulty); - activity.Details = $"{mapData.SongName} by {mapData.SongAuthor} ({difficulty})"; + var difficulty = GetReadableDifficulty(MapData.Instance.Difficulty); + var songSubName = MapData.Instance.SongSubName.Length > 0 ? $" {MapData.Instance.SongSubName}" : ""; + var mapper = MapData.Instance.Mapper.Length > 0 ? $" [{MapData.Instance.Mapper}]" : ""; + activity.Details = $"{MapData.Instance.SongName}{songSubName} by {MapData.Instance.SongAuthor}{mapper} ({difficulty}{rankedStatus})"; - if (liveData != null && !mapData.LevelPaused) + if (LiveData.Instance.TimeElapsed > 0) + { + var rank = GetReadableRank(LiveData.Instance.Rank); + playDetail = $" {LiveData.Instance.Score:N} x{LiveData.Instance.Combo} {LiveData.Instance.Accuracy}% ({rank})"; + if (!MapData.Instance.LevelPaused) { - activity.Timestamps.Start = DateTime.UtcNow.Ticks - liveData.TimeElapsed; - activity.Timestamps.End = activity.Timestamps.Start + mapData.Length; + activity.Timestamps.End = DateTimeToUnixTimestamp(DateTime.Now.AddSeconds(-Convert.ToDouble(LiveData.Instance.TimeElapsed)).AddSeconds(MapData.Instance.Duration)); } } + + activity.State = $"{playState} {lobbyType}{playDetail}"; + } + else + { + if (MapData.Instance.IsMultiplayer) + { + activity.State = "Multiplayer Lobby"; + } else { - if (mapData.IsMultiplayer) - { - activity.State = "Multiplayer Lobby"; - } - else - { - activity.State = "Main Menu"; - } + activity.State = "Main Menu"; } } return activity; } + private long DateTimeToUnixTimestamp(DateTime ticks) + { + return ((DateTimeOffset)ticks).ToUnixTimeMilliseconds(); + } + [OnExit] public void OnApplicationQuit() { - Log.Debug("OnApplicationQuit"); - discord.Dispose(); - mapDataSource.Cleanup(); - liveDataSource.Cleanup(); + } } } diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index fb84ce5..56004f1 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("bsrpc")] -[assembly: AssemblyCopyright("Copyright © 2022")] +[assembly: AssemblyCopyright("Copyright © DJDavid98 2022")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.0.1")] -[assembly: AssemblyFileVersion("0.0.1")] +[assembly: AssemblyVersion("1.0.0")] +[assembly: AssemblyFileVersion("1.0.0")] diff --git a/README.md b/README.md index 19a24d7..949b05d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,14 @@ # bsrpc -To be able to build, follow the Discord SDK installation guide: [Code Primer - Non-Unity Projects (Csharp) -](https://discord.com/developers/docs/game-sdk/sdk-starter-guide#code-primer-nonunity-projects-csharp) +Discord Rich Presence support for Beat Saber. No customization options whatsoever, because I'm too lazy to add them (sorry). -The `discord_game_sdk.dll` file must be placed into the `Plugins` folder alongside the mod's DLL for it to function. \ No newline at end of file +
+ Discord account card showing the Beat Saber rich presence + Discord profile activity tab showing the Beat Saber rich presence +
+ +## Dependencies + +* BSIPA v4 (ModAssistant) +* DiscordCore v1.0.10 (https://github.com/FizzyApple12/DiscordCore/releases/tag/v1.0.10) +* DataPuller v2.1.0 (https://github.com/ReadieFur/BSDataPuller/releases/tag/2.1.0) \ No newline at end of file diff --git a/RichPresenceResources.cs b/RichPresenceResources.cs new file mode 100644 index 0000000..148f763 --- /dev/null +++ b/RichPresenceResources.cs @@ -0,0 +1,45 @@ +namespace bsrpc +{ + public static class RichPresenceAssetKeys + { + // Environments + public static readonly string BigMirror = "big_mirror"; + public static readonly string Billie = "billie"; + public static readonly string BTS = "bts"; + public static readonly string CrabRave = "crab_rave"; + public static readonly string Dragons = "dragons"; + public static readonly string EDM = "edm"; + public static readonly string FallOutBoy = "fall_out_boy"; + public static readonly string FitBeat = "fitbeat"; + public static readonly string Gaga = "gaga"; + public static readonly string GreenDay = "green_day"; + public static readonly string GreenDayGrenade = "green_day_grenade"; + public static readonly string Interscope = "interscope"; + public static readonly string Kaleidoscope = "kaleidoscope"; + public static readonly string KDA = "kda"; + public static readonly string LinkinPark = "linkin_park"; + public static readonly string Lizzo = "lizzo"; + public static readonly string MainMenu = "main_menu"; + public static readonly string Monstercat = "monstercat"; + public static readonly string MultiplayerLobby = "multiplayer_lobby"; + public static readonly string Nice = "nice"; + public static readonly string Origins = "origins"; + public static readonly string Panic = "panic"; + public static readonly string Rocket = "rocket"; + public static readonly string Skrillex = "skrillex"; + public static readonly string Spooky = "spooky"; + public static readonly string TheFirst = "the_first"; + public static readonly string TheSecond = "the_second"; + public static readonly string Timbaland = "timbaland"; + public static readonly string Triangle = "triangle"; + public static readonly string Weave = "weave"; + + // Map types + public static readonly string BlankMapType = "blank_map_type"; + public static readonly string Ninety = "90"; + public static readonly string NoArrows = "no_arrows"; + public static readonly string OneSaber = "one_saber"; + public static readonly string Standard = "standard"; + public static readonly string ThreeSixty = "360"; + } +} diff --git a/bsrpc.csproj b/bsrpc.csproj index 60caa44..14ba612 100644 --- a/bsrpc.csproj +++ b/bsrpc.csproj @@ -63,9 +63,9 @@ False False - - $(BeatSaberDir)\Plugins\SiraUtil.dll + False + $(BeatSaberDir)\Plugins\DiscordCore.dll False @@ -118,27 +118,13 @@ $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.VRModule.dll False - - False - $(BeatSaberDir)\Libs\websocket-sharp.dll - False - - - - - - - - - - - + @@ -146,6 +132,7 @@ + diff --git a/manifest.json b/manifest.json index 08a44f6..fabe5bd 100644 --- a/manifest.json +++ b/manifest.json @@ -3,12 +3,13 @@ "id": "bsrpc", "name": "bsrpc", "author": "", - "version": "0.0.1", + "version": "1.0.0", "description": "Discord Rich Presence integration for Beat Saber", "gameVersion": "1.20.0", "dependsOn": { "BSIPA": "^4.2.0", - "DiscordGameSDK": "3.2.1" + "DataPuller": "^2.1.0", + "DiscordCore": "^1.0.10" }, "links": { "project-source": "https://github.com/DJDavid98/bsrpc" diff --git a/screenshots/card.png b/screenshots/card.png new file mode 100644 index 0000000..ddfb8fc Binary files /dev/null and b/screenshots/card.png differ diff --git a/screenshots/profile.png b/screenshots/profile.png new file mode 100644 index 0000000..612a0b1 Binary files /dev/null and b/screenshots/profile.png differ