mirror of
https://github.com/ppy/osu.git
synced 2024-09-21 18:07:23 +08:00
cb823f367f
Up until now, the `UserActivity` class hierarchy contained things like beatmap info, room info, full replay info, etc. While this was convenient, it is soon going to be less so, as the data is sent over the wire to the spectator server so that the user's activity can be broadcast to other clients. To counteract this without creating a second separate and slimmed-down class hierarchy, slim down the `UserActivity` structure to contain the bare minimum amounts of data such that the structures aren't overly large and complex to serialise, but also contain enough data that they can be used by receiving clients directly without having to do beatmap or score lookups.
183 lines
6.3 KiB
C#
183 lines
6.3 KiB
C#
// 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.
|
|
|
|
using System;
|
|
using System.Text;
|
|
using DiscordRPC;
|
|
using DiscordRPC.Message;
|
|
using osu.Framework.Allocation;
|
|
using osu.Framework.Bindables;
|
|
using osu.Framework.Graphics;
|
|
using osu.Framework.Logging;
|
|
using osu.Game.Configuration;
|
|
using osu.Game.Extensions;
|
|
using osu.Game.Online.API;
|
|
using osu.Game.Online.API.Requests.Responses;
|
|
using osu.Game.Rulesets;
|
|
using osu.Game.Users;
|
|
using LogLevel = osu.Framework.Logging.LogLevel;
|
|
|
|
namespace osu.Desktop
|
|
{
|
|
internal partial class DiscordRichPresence : Component
|
|
{
|
|
private const string client_id = "367827983903490050";
|
|
|
|
private DiscordRpcClient client = null!;
|
|
|
|
[Resolved]
|
|
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
|
|
|
private IBindable<APIUser> user = null!;
|
|
|
|
[Resolved]
|
|
private IAPIProvider api { get; set; } = null!;
|
|
|
|
private readonly IBindable<UserStatus> status = new Bindable<UserStatus>();
|
|
private readonly IBindable<UserActivity> activity = new Bindable<UserActivity>();
|
|
|
|
private readonly Bindable<DiscordRichPresenceMode> privacyMode = new Bindable<DiscordRichPresenceMode>();
|
|
|
|
private readonly RichPresence presence = new RichPresence
|
|
{
|
|
Assets = new Assets { LargeImageKey = "osu_logo_lazer", }
|
|
};
|
|
|
|
[BackgroundDependencyLoader]
|
|
private void load(OsuConfigManager config)
|
|
{
|
|
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);
|
|
|
|
config.BindWith(OsuSetting.DiscordRichPresence, privacyMode);
|
|
|
|
user = api.LocalUser.GetBoundCopy();
|
|
user.BindValueChanged(u =>
|
|
{
|
|
status.UnbindBindings();
|
|
status.BindTo(u.NewValue.Status);
|
|
|
|
activity.UnbindBindings();
|
|
activity.BindTo(u.NewValue.Activity);
|
|
}, true);
|
|
|
|
ruleset.BindValueChanged(_ => updateStatus());
|
|
status.BindValueChanged(_ => updateStatus());
|
|
activity.BindValueChanged(_ => updateStatus());
|
|
privacyMode.BindValueChanged(_ => updateStatus());
|
|
|
|
client.Initialize();
|
|
}
|
|
|
|
private void onReady(object _, ReadyMessage __)
|
|
{
|
|
Logger.Log("Discord RPC Client ready.", LoggingTarget.Network, LogLevel.Debug);
|
|
updateStatus();
|
|
}
|
|
|
|
private void updateStatus()
|
|
{
|
|
if (!client.IsInitialized)
|
|
return;
|
|
|
|
if (status.Value is UserStatusOffline || privacyMode.Value == DiscordRichPresenceMode.Off)
|
|
{
|
|
client.ClearPresence();
|
|
return;
|
|
}
|
|
|
|
if (status.Value is UserStatusOnline && activity.Value != null)
|
|
{
|
|
bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited;
|
|
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;
|
|
}
|
|
|
|
// update user information
|
|
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);
|
|
}
|
|
|
|
// update ruleset
|
|
presence.Assets.SmallImageKey = ruleset.Value.IsLegacyRuleset() ? $"mode_{ruleset.Value.OnlineID}" : "mode_custom";
|
|
presence.Assets.SmallImageText = ruleset.Value.Name;
|
|
|
|
client.SetPresence(presence);
|
|
}
|
|
|
|
private static readonly int ellipsis_length = Encoding.UTF8.GetByteCount(new[] { '…' });
|
|
|
|
private string truncate(string str)
|
|
{
|
|
if (Encoding.UTF8.GetByteCount(str) <= 128)
|
|
return str;
|
|
|
|
ReadOnlyMemory<char> strMem = str.AsMemory();
|
|
|
|
do
|
|
{
|
|
strMem = strMem[..^1];
|
|
} while (Encoding.UTF8.GetByteCount(strMem.Span) + ellipsis_length > 128);
|
|
|
|
return string.Create(strMem.Length + 1, strMem, (span, mem) =>
|
|
{
|
|
mem.Span.CopyTo(span);
|
|
span[^1] = '…';
|
|
});
|
|
}
|
|
|
|
private int? getBeatmapID(UserActivity activity)
|
|
{
|
|
switch (activity)
|
|
{
|
|
case UserActivity.InGame game:
|
|
return game.BeatmapID;
|
|
|
|
case UserActivity.EditingBeatmap edit:
|
|
return edit.BeatmapID;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
protected override void Dispose(bool isDisposing)
|
|
{
|
|
client.Dispose();
|
|
base.Dispose(isDisposing);
|
|
}
|
|
}
|
|
}
|