1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-15 12:42:54 +08:00

Merge branch 'master' into remove-breadcrumb-control

This commit is contained in:
Salman Ahmed 2024-03-20 08:11:03 +03:00 committed by GitHub
commit 0d6daef42b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -5,14 +5,21 @@ using System;
using System.Text; using System.Text;
using DiscordRPC; using DiscordRPC;
using DiscordRPC.Message; using DiscordRPC.Message;
using Newtonsoft.Json;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Threading;
using osu.Game;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Users; using osu.Game.Users;
using LogLevel = osu.Framework.Logging.LogLevel; using LogLevel = osu.Framework.Logging.LogLevel;
@ -21,7 +28,7 @@ namespace osu.Desktop
{ {
internal partial class DiscordRichPresence : Component internal partial class DiscordRichPresence : Component
{ {
private const string client_id = "367827983903490050"; private const string client_id = "1216669957799018608";
private DiscordRpcClient client = null!; private DiscordRpcClient client = null!;
@ -33,6 +40,15 @@ namespace osu.Desktop
[Resolved] [Resolved]
private IAPIProvider api { get; set; } = null!; private IAPIProvider api { get; set; } = null!;
[Resolved]
private OsuGame game { get; set; } = null!;
[Resolved]
private LoginOverlay? login { get; set; }
[Resolved]
private MultiplayerClient multiplayerClient { get; set; } = null!;
private readonly IBindable<UserStatus?> status = new Bindable<UserStatus?>(); private readonly IBindable<UserStatus?> status = new Bindable<UserStatus?>();
private readonly IBindable<UserActivity> activity = new Bindable<UserActivity>(); private readonly IBindable<UserActivity> activity = new Bindable<UserActivity>();
@ -40,7 +56,12 @@ namespace osu.Desktop
private readonly RichPresence presence = new RichPresence private readonly RichPresence presence = new RichPresence
{ {
Assets = new Assets { LargeImageKey = "osu_logo_lazer", } Assets = new Assets { LargeImageKey = "osu_logo_lazer" },
Secrets = new Secrets
{
JoinSecret = null,
SpectateSecret = null,
},
}; };
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -48,12 +69,18 @@ namespace osu.Desktop
{ {
client = new DiscordRpcClient(client_id) client = new DiscordRpcClient(client_id)
{ {
SkipIdenticalPresence = false // handles better on discord IPC loss, see updateStatus call in onReady. // SkipIdenticalPresence allows us to fire SetPresence at any point and leave it to the underlying implementation
// to check whether a difference has actually occurred before sending a command to Discord (with a minor caveat that's handled in onReady).
SkipIdenticalPresence = true
}; };
client.OnReady += onReady; client.OnReady += onReady;
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network, LogLevel.Error);
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network); // 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;
config.BindWith(OsuSetting.DiscordRichPresence, privacyMode); config.BindWith(OsuSetting.DiscordRichPresence, privacyMode);
@ -67,10 +94,11 @@ namespace osu.Desktop
activity.BindTo(u.NewValue.Activity); activity.BindTo(u.NewValue.Activity);
}, true); }, true);
ruleset.BindValueChanged(_ => updateStatus()); ruleset.BindValueChanged(_ => updatePresence());
status.BindValueChanged(_ => updateStatus()); status.BindValueChanged(_ => updatePresence());
activity.BindValueChanged(_ => updateStatus()); activity.BindValueChanged(_ => updatePresence());
privacyMode.BindValueChanged(_ => updateStatus()); privacyMode.BindValueChanged(_ => updatePresence());
multiplayerClient.RoomUpdated += onRoomUpdated;
client.Initialize(); client.Initialize();
} }
@ -78,24 +106,46 @@ namespace osu.Desktop
private void onReady(object _, ReadyMessage __) private void onReady(object _, ReadyMessage __)
{ {
Logger.Log("Discord RPC Client ready.", LoggingTarget.Network, LogLevel.Debug); Logger.Log("Discord RPC Client ready.", LoggingTarget.Network, LogLevel.Debug);
updateStatus();
// when RPC is lost and reconnected, we have to clear presence state for updatePresence to work (see DiscordRpcClient.SkipIdenticalPresence).
if (client.CurrentPresence != null)
client.SetPresence(null);
updatePresence();
} }
private void updateStatus() private void onRoomUpdated() => updatePresence();
private ScheduledDelegate? presenceUpdateDelegate;
private void updatePresence()
{ {
if (!client.IsInitialized) presenceUpdateDelegate?.Cancel();
return; presenceUpdateDelegate = Scheduler.AddDelayed(() =>
if (status.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off)
{ {
client.ClearPresence(); if (!client.IsInitialized)
return; return;
}
if (status.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off)
{
client.ClearPresence();
return;
}
if (activity.Value != null)
{
bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb; bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb;
updatePresenceStatus(hideIdentifiableInformation);
updatePresenceParty(hideIdentifiableInformation);
updatePresenceAssets();
client.SetPresence(presence);
}, 200);
}
private void updatePresenceStatus(bool hideIdentifiableInformation)
{
if (activity.Value != null)
{
presence.State = truncate(activity.Value.GetStatus(hideIdentifiableInformation)); presence.State = truncate(activity.Value.GetStatus(hideIdentifiableInformation));
presence.Details = truncate(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty); presence.Details = truncate(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
@ -120,7 +170,41 @@ namespace osu.Desktop
presence.State = "Idle"; presence.State = "Idle";
presence.Details = string.Empty; presence.Details = string.Empty;
} }
}
private void updatePresenceParty(bool hideIdentifiableInformation)
{
if (!hideIdentifiableInformation && multiplayerClient.Room != null)
{
MultiplayerRoom room = multiplayerClient.Room;
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);
}
else
{
presence.Party = null;
presence.Secrets.JoinSecret = null;
}
}
private void updatePresenceAssets()
{
// update user information // update user information
if (privacyMode.Value == DiscordRichPresenceMode.Limited) if (privacyMode.Value == DiscordRichPresenceMode.Limited)
presence.Assets.LargeImageText = string.Empty; presence.Assets.LargeImageText = string.Empty;
@ -135,13 +219,40 @@ namespace osu.Desktop
// update ruleset // update ruleset
presence.Assets.SmallImageKey = ruleset.Value.IsLegacyRuleset() ? $"mode_{ruleset.Value.OnlineID}" : "mode_custom"; presence.Assets.SmallImageKey = ruleset.Value.IsLegacyRuleset() ? $"mode_{ruleset.Value.OnlineID}" : "mode_custom";
presence.Assets.SmallImageText = ruleset.Value.Name; presence.Assets.SmallImageText = ruleset.Value.Name;
client.SetPresence(presence);
} }
private void onJoin(object sender, JoinMessage args) => Scheduler.AddOnce(() =>
{
game.Window?.Raise();
if (!api.IsLoggedIn)
{
login?.Show();
return;
}
Logger.Log($"Received room secret from Discord RPC Client: \"{args.Secret}\"", LoggingTarget.Network, LogLevel.Debug);
// 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))
{
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);
});
request.Failure += _ => Logger.Log($"Could not join multiplayer room, room could not be found (room ID: {roomId}).", LoggingTarget.Network, LogLevel.Important);
api.Queue(request);
});
private static readonly int ellipsis_length = Encoding.UTF8.GetByteCount(new[] { '…' }); private static readonly int ellipsis_length = Encoding.UTF8.GetByteCount(new[] { '…' });
private string truncate(string str) private static string truncate(string str)
{ {
if (Encoding.UTF8.GetByteCount(str) <= 128) if (Encoding.UTF8.GetByteCount(str) <= 128)
return str; return str;
@ -160,7 +271,31 @@ namespace osu.Desktop
}); });
} }
private int? getBeatmapID(UserActivity activity) 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) switch (activity)
{ {
@ -176,8 +311,20 @@ namespace osu.Desktop
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
if (multiplayerClient.IsNotNull())
multiplayerClient.RoomUpdated -= onRoomUpdated;
client.Dispose(); client.Dispose();
base.Dispose(isDisposing); 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; }
}
} }
} }