osu/osu.Desktop/DiscordRichPresence.cs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

311 lines
11 KiB
C#
Raw Normal View History

2019-12-18 05:07:53 +00:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
2019-12-23 10:34:12 +00:00
using System;
using System.Diagnostics;
using System.Text;
2019-12-18 05:07:53 +00:00
using DiscordRPC;
using DiscordRPC.Message;
using Newtonsoft.Json;
2019-12-18 05:07:53 +00:00
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Development;
2019-12-18 05:07:53 +00:00
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game;
using osu.Game.Configuration;
using osu.Game.Extensions;
2019-12-18 05:07:53 +00:00
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
2019-12-18 05:07:53 +00:00
using osu.Game.Rulesets;
using osu.Game.Users;
using LogLevel = osu.Framework.Logging.LogLevel;
namespace osu.Desktop
{
internal partial class DiscordRichPresence : Component
{
2024-03-11 08:55:49 +00:00
private const string client_id = "1216669957799018608";
2019-12-18 05:07:53 +00:00
private DiscordRpcClient client = null!;
2019-12-18 05:07:53 +00:00
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
2019-12-18 05:07:53 +00:00
private IBindable<APIUser> user = null!;
2019-12-18 05:07:53 +00:00
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved]
private OsuGame game { get; set; } = null!;
private LoginOverlay? login { get; set; }
[Resolved]
private MultiplayerClient multiplayerClient { get; set; } = null!;
private readonly IBindable<UserStatus?> status = new Bindable<UserStatus?>();
2019-12-18 05:07:53 +00:00
private readonly IBindable<UserActivity> activity = new Bindable<UserActivity>();
2021-01-06 15:05:12 +00:00
private readonly Bindable<DiscordRichPresenceMode> privacyMode = new Bindable<DiscordRichPresenceMode>();
2019-12-18 05:07:53 +00:00
2024-03-19 21:03:30 +00:00
private int usersCurrentlyInLobby;
2019-12-18 05:07:53 +00:00
private readonly RichPresence presence = new RichPresence
{
Assets = new Assets { LargeImageKey = "osu_logo_lazer" },
Secrets = new Secrets
{
JoinSecret = null,
SpectateSecret = null,
},
2019-12-18 05:07:53 +00:00
};
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
2019-12-18 05:07:53 +00:00
{
login = game.Dependencies.Get<LoginOverlay>();
2019-12-18 05:07:53 +00:00
client = new DiscordRpcClient(client_id)
{
SkipIdenticalPresence = false // handles better on discord IPC loss, see updateStatus call in onReady.
};
client.OnReady += onReady;
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network, LogLevel.Error);
// A URI scheme is required to support game invitations, as well as informing Discord of the game executable path to support launching the game when a user clicks on join/spectate.
client.RegisterUriScheme();
client.Subscribe(EventType.Join);
client.OnJoin += onJoin;
2019-12-18 05:07:53 +00:00
2021-01-06 15:05:12 +00:00
config.BindWith(OsuSetting.DiscordRichPresence, privacyMode);
2022-06-09 03:32:30 +00:00
user = api.LocalUser.GetBoundCopy();
user.BindValueChanged(u =>
2019-12-18 05:07:53 +00:00
{
status.UnbindBindings();
status.BindTo(u.NewValue.Status);
activity.UnbindBindings();
activity.BindTo(u.NewValue.Activity);
}, true);
ruleset.BindValueChanged(_ => updateStatus());
status.BindValueChanged(_ => updateStatus());
activity.BindValueChanged(_ => updateStatus());
2021-01-06 15:05:12 +00:00
privacyMode.BindValueChanged(_ => updateStatus());
2019-12-18 05:07:53 +00:00
client.Initialize();
}
private void onReady(object _, ReadyMessage __)
{
Logger.Log("Discord RPC Client ready.", LoggingTarget.Network, LogLevel.Debug);
2024-03-19 20:02:06 +00:00
Schedule(updateStatus);
2019-12-18 05:07:53 +00:00
}
private void updateStatus()
{
2024-03-19 20:02:06 +00:00
Debug.Assert(ThreadSafety.IsUpdateThread);
if (!client.IsInitialized)
return;
if (status.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off)
2019-12-18 05:07:53 +00:00
{
client.ClearPresence();
return;
}
bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb;
if (activity.Value != null)
2019-12-18 05:07:53 +00:00
{
presence.State = truncate(activity.Value.GetStatus(hideIdentifiableInformation));
presence.Details = truncate(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
if (getBeatmapID(activity.Value) is int beatmapId && beatmapId > 0)
{
presence.Buttons = new[]
{
new Button
{
Label = "View beatmap",
Url = $@"{api.WebsiteRootUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}"
}
};
}
else
{
presence.Buttons = null;
}
}
else
{
presence.State = "Idle";
presence.Details = string.Empty;
}
if (!hideIdentifiableInformation && multiplayerClient.Room != null)
{
MultiplayerRoom room = multiplayerClient.Room;
if (room.Users.Count == usersCurrentlyInLobby)
return;
presence.Party = new Party
{
Privacy = string.IsNullOrEmpty(room.Settings.Password) ? Party.PrivacySetting.Public : Party.PrivacySetting.Private,
ID = room.RoomID.ToString(),
// technically lobbies can have infinite users, but Discord needs this to be set to something.
// to make party display sensible, assign a powers of two above participants count (8 at minimum).
Max = (int)Math.Max(8, Math.Pow(2, Math.Ceiling(Math.Log2(room.Users.Count)))),
Size = room.Users.Count,
};
RoomSecret roomSecret = new RoomSecret
{
RoomID = room.RoomID,
Password = room.Settings.Password,
};
presence.Secrets.JoinSecret = JsonConvert.SerializeObject(roomSecret);
usersCurrentlyInLobby = room.Users.Count;
2019-12-18 05:07:53 +00:00
}
else
{
presence.Party = null;
presence.Secrets.JoinSecret = null;
usersCurrentlyInLobby = 0;
2019-12-18 05:07:53 +00:00
}
Logger.Log($"Updating Discord RPC presence with activity status: {presence.State}, details: {presence.Details}", LoggingTarget.Network, LogLevel.Debug);
2019-12-18 05:07:53 +00:00
// update user information
2021-01-06 15:05:12 +00:00
if (privacyMode.Value == DiscordRichPresenceMode.Limited)
presence.Assets.LargeImageText = string.Empty;
else
{
if (user.Value.RulesetsStatistics != null && user.Value.RulesetsStatistics.TryGetValue(ruleset.Value.ShortName, out UserStatistics? statistics))
presence.Assets.LargeImageText = $"{user.Value.Username}" + (statistics.GlobalRank > 0 ? $" (rank #{statistics.GlobalRank:N0})" : string.Empty);
else
presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty);
}
2019-12-18 05:07:53 +00:00
// update ruleset
presence.Assets.SmallImageKey = ruleset.Value.IsLegacyRuleset() ? $"mode_{ruleset.Value.OnlineID}" : "mode_custom";
2019-12-18 05:07:53 +00:00
presence.Assets.SmallImageText = ruleset.Value.Name;
client.SetPresence(presence);
}
private void onJoin(object sender, JoinMessage args)
{
game.Window?.Raise();
if (!api.IsLoggedIn)
{
Schedule(() => login?.Show());
return;
}
2024-03-10 22:01:26 +00:00
Logger.Log($"Received room secret from Discord RPC Client: \"{args.Secret}\"", LoggingTarget.Network, LogLevel.Debug);
2024-03-10 22:01:26 +00:00
// Stable and lazer share the same Discord client ID, meaning they can accept join requests from each other.
// Since they aren't compatible in multi, see if stable's format is being used and log to avoid confusion.
if (args.Secret[0] != '{' || !tryParseRoomSecret(args.Secret, out long roomId, out string? password))
{
2024-03-10 22:01:26 +00:00
Logger.Log("Could not join multiplayer room, invitation is invalid or incompatible.", LoggingTarget.Network, LogLevel.Important);
return;
}
var request = new GetRoomRequest(roomId);
request.Success += room => Schedule(() =>
{
game.PresentMultiplayerMatch(room, password);
});
2024-03-10 22:01:26 +00:00
request.Failure += _ => Logger.Log($"Could not join multiplayer room, room could not be found (room ID: {roomId}).", LoggingTarget.Network, LogLevel.Important);
api.Queue(request);
}
2019-12-25 02:14:40 +00:00
private static readonly int ellipsis_length = Encoding.UTF8.GetByteCount(new[] { '…' });
private static string truncate(string str)
2019-12-23 09:55:44 +00:00
{
2019-12-23 10:56:05 +00:00
if (Encoding.UTF8.GetByteCount(str) <= 128)
2019-12-25 03:04:28 +00:00
return str;
ReadOnlyMemory<char> strMem = str.AsMemory();
2019-12-23 09:55:44 +00:00
2019-12-23 10:34:12 +00:00
do
{
2019-12-25 03:04:28 +00:00
strMem = strMem[..^1];
} while (Encoding.UTF8.GetByteCount(strMem.Span) + ellipsis_length > 128);
2019-12-23 10:34:12 +00:00
2019-12-25 03:04:28 +00:00
return string.Create(strMem.Length + 1, strMem, (span, mem) =>
{
mem.Span.CopyTo(span);
span[^1] = '…';
});
2019-12-23 09:55:44 +00:00
}
private static bool tryParseRoomSecret(string secretJson, out long roomId, out string? password)
{
roomId = 0;
password = null;
RoomSecret? roomSecret;
try
{
roomSecret = JsonConvert.DeserializeObject<RoomSecret>(secretJson);
}
catch
{
return false;
}
if (roomSecret == null) return false;
roomId = roomSecret.RoomID;
password = roomSecret.Password;
return true;
}
private static int? getBeatmapID(UserActivity activity)
{
switch (activity)
{
case UserActivity.InGame game:
return game.BeatmapID;
case UserActivity.EditingBeatmap edit:
return edit.BeatmapID;
}
return null;
}
2019-12-18 05:07:53 +00:00
protected override void Dispose(bool isDisposing)
{
client.Dispose();
base.Dispose(isDisposing);
}
private class RoomSecret
{
[JsonProperty(@"roomId", Required = Required.Always)]
public long RoomID { get; set; }
[JsonProperty(@"password", Required = Required.AllowNull)]
public string? Password { get; set; }
}
2019-12-18 05:07:53 +00:00
}
}