DiscordIntegrationModule.cs 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. using DiscordRPC;
  2. using Gommon;
  3. using MsgPack;
  4. using Ryujinx.Ava.Utilities;
  5. using Ryujinx.Ava.Utilities.AppLibrary;
  6. using Ryujinx.Ava.Utilities.Configuration;
  7. using Ryujinx.Ava.Utilities.PlayReport;
  8. using Ryujinx.Common;
  9. using Ryujinx.Common.Logging;
  10. using Ryujinx.HLE;
  11. using Ryujinx.HLE.Loaders.Processes;
  12. using Ryujinx.Horizon;
  13. using System.Text;
  14. namespace Ryujinx.Ava
  15. {
  16. public static class DiscordIntegrationModule
  17. {
  18. public static Timestamps EmulatorStartedAt { get; set; }
  19. public static Timestamps GuestAppStartedAt { get; set; }
  20. private static string VersionString
  21. => (ReleaseInformation.IsCanaryBuild ? "Canary " : string.Empty) + $"v{ReleaseInformation.Version}";
  22. private static readonly string _description =
  23. ReleaseInformation.IsValid
  24. ? $"{VersionString} {ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelSourceRepo}@{ReleaseInformation.BuildGitHash}"
  25. : "dev build";
  26. private const string ApplicationId = "1293250299716173864";
  27. private const int ApplicationByteLimit = 128;
  28. private const string Ellipsis = "…";
  29. private static DiscordRpcClient _discordClient;
  30. private static RichPresence _discordPresenceMain;
  31. private static RichPresence _discordPresencePlaying;
  32. private static ApplicationMetadata _currentApp;
  33. public static void Initialize()
  34. {
  35. _discordPresenceMain = new RichPresence
  36. {
  37. Assets = new Assets
  38. {
  39. LargeImageKey = "ryujinx", LargeImageText = TruncateToByteLength(_description)
  40. },
  41. Details = "Main Menu",
  42. State = "Idling",
  43. Timestamps = EmulatorStartedAt
  44. };
  45. ConfigurationState.Instance.EnableDiscordIntegration.Event += Update;
  46. TitleIDs.CurrentApplication.Event += (_, e) => Use(e.NewValue);
  47. HorizonStatic.PlayReport += HandlePlayReport;
  48. }
  49. private static void Update(object sender, ReactiveEventArgs<bool> evnt)
  50. {
  51. if (evnt.OldValue != evnt.NewValue)
  52. {
  53. // If the integration was active, disable it and unload everything
  54. if (evnt.OldValue)
  55. {
  56. _discordClient?.Dispose();
  57. _discordClient = null;
  58. }
  59. // If we need to activate it and the client isn't active, initialize it
  60. if (evnt.NewValue && _discordClient == null)
  61. {
  62. _discordClient = new DiscordRpcClient(ApplicationId);
  63. _discordClient.Initialize();
  64. Use(TitleIDs.CurrentApplication);
  65. }
  66. }
  67. }
  68. public static void Use(Optional<string> titleId)
  69. {
  70. if (titleId.TryGet(out string tid))
  71. SwitchToPlayingState(
  72. ApplicationLibrary.LoadAndSaveMetaData(tid),
  73. Switch.Shared.Processes.ActiveApplication
  74. );
  75. else
  76. SwitchToMainState();
  77. }
  78. private static RichPresence CreatePlayingState(ApplicationMetadata appMeta, ProcessResult procRes) =>
  79. new()
  80. {
  81. Assets = new Assets
  82. {
  83. LargeImageKey = TitleIDs.GetDiscordGameAsset(procRes.ProgramIdText),
  84. LargeImageText = TruncateToByteLength($"{appMeta.Title} (v{procRes.DisplayVersion})"),
  85. SmallImageKey = "ryujinx",
  86. SmallImageText = TruncateToByteLength(_description)
  87. },
  88. Details = TruncateToByteLength($"Playing {appMeta.Title}"),
  89. State = appMeta.LastPlayed.HasValue && appMeta.TimePlayed.TotalSeconds > 5
  90. ? $"Total play time: {ValueFormatUtils.FormatTimeSpan(appMeta.TimePlayed)}"
  91. : "Never played",
  92. Timestamps = GuestAppStartedAt ??= Timestamps.Now
  93. };
  94. private static void SwitchToPlayingState(ApplicationMetadata appMeta, ProcessResult procRes)
  95. {
  96. _discordClient?.SetPresence(_discordPresencePlaying ??= CreatePlayingState(appMeta, procRes));
  97. _currentApp = appMeta;
  98. }
  99. private static void SwitchToMainState()
  100. {
  101. _discordClient?.SetPresence(_discordPresenceMain);
  102. _discordPresencePlaying = null;
  103. _currentApp = null;
  104. }
  105. private static void HandlePlayReport(MessagePackObject playReport)
  106. {
  107. if (_discordClient is null) return;
  108. if (!TitleIDs.CurrentApplication.Value.HasValue) return;
  109. if (_discordPresencePlaying is null) return;
  110. Analyzer.FormattedValue formattedValue =
  111. PlayReports.Analyzer.Format(TitleIDs.CurrentApplication.Value, _currentApp, playReport);
  112. if (!formattedValue.Handled) return;
  113. _discordPresencePlaying.Details = formattedValue.Reset
  114. ? $"Playing {_currentApp.Title}"
  115. : formattedValue.FormattedString;
  116. if (_discordClient.CurrentPresence.Details.Equals(_discordPresencePlaying.Details))
  117. return; //don't trigger an update if the set presence Details are identical to current
  118. _discordClient.SetPresence(_discordPresencePlaying);
  119. Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report.");
  120. }
  121. private static string TruncateToByteLength(string input)
  122. {
  123. if (Encoding.UTF8.GetByteCount(input) <= ApplicationByteLimit)
  124. {
  125. return input;
  126. }
  127. // Find the length to trim the string to guarantee we have space for the trailing ellipsis.
  128. int trimLimit = ApplicationByteLimit - Encoding.UTF8.GetByteCount(Ellipsis);
  129. // Make sure the string is long enough to perform the basic trim.
  130. // Amount of bytes != Length of the string
  131. if (input.Length > trimLimit)
  132. {
  133. // Basic trim to best case scenario of 1 byte characters.
  134. input = input[..trimLimit];
  135. }
  136. while (Encoding.UTF8.GetByteCount(input) > trimLimit)
  137. {
  138. // Remove one character from the end of the string at a time.
  139. input = input[..^1];
  140. }
  141. return input.TrimEnd() + Ellipsis;
  142. }
  143. public static void Exit()
  144. {
  145. _discordClient?.Dispose();
  146. }
  147. }
  148. }