Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Discord game invites in multiplayer lobbies #27443

Merged
merged 21 commits into from Mar 20, 2024
Merged
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
111 changes: 107 additions & 4 deletions osu.Desktop/DiscordRichPresence.cs
Expand Up @@ -5,14 +5,18 @@
using System.Text;
using DiscordRPC;
using DiscordRPC.Message;
using Newtonsoft.Json;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game;
using osu.Game.Configuration;
using osu.Game.Extensions;
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.Rulesets;
using osu.Game.Users;
using LogLevel = osu.Framework.Logging.LogLevel;
Expand All @@ -33,14 +37,25 @@ internal partial class DiscordRichPresence : Component
[Resolved]
private IAPIProvider api { get; set; } = null!;

[Resolved]
private OsuGame game { get; set; } = null!;

[Resolved]
private MultiplayerClient multiplayerClient { 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", }
Assets = new Assets { LargeImageKey = "osu_logo_lazer" },
Secrets = new Secrets
{
JoinSecret = null,
SpectateSecret = null,
},
};

[BackgroundDependencyLoader]
Expand All @@ -52,8 +67,12 @@ private void load(OsuConfigManager config)
};

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();
jvyden marked this conversation as resolved.
Show resolved Hide resolved
client.Subscribe(EventType.Join);
client.OnJoin += onJoin;
jvyden marked this conversation as resolved.
Show resolved Hide resolved

config.BindWith(OsuSetting.DiscordRichPresence, privacyMode);

Expand Down Expand Up @@ -114,6 +133,34 @@ private void updateStatus()
{
presence.Buttons = null;
}

if (!hideIdentifiableInformation && multiplayerClient.Room != null)
{
MultiplayerRoom room = multiplayerClient.Room;
presence.Party = new Party
frenzibyte marked this conversation as resolved.
Show resolved Hide resolved
{
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.
// 1024 just happens to look nice.
// https://discord.com/channels/188630481301012481/188630652340404224/1212967974793642034
Max = 1024,
jvyden marked this conversation as resolved.
Show resolved Hide resolved
Size = room.Users.Count,
jvyden marked this conversation as resolved.
Show resolved Hide resolved
};

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;
}
}
else
{
Expand All @@ -139,9 +186,32 @@ private void updateStatus()
client.SetPresence(presence);
}

private void onJoin(object sender, JoinMessage args)
{
game.Window?.Raise();

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);
}
jvyden marked this conversation as resolved.
Show resolved Hide resolved

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)
return str;
Expand All @@ -160,7 +230,31 @@ private string truncate(string str)
});
}

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)
{
Expand All @@ -179,5 +273,14 @@ 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; }
}
}
}