Skip to content

Commit d5557bb

Browse files
authored
Merge pull request #25005 from minetoblend/multiplayer-invites
Add ability to invite players to multiplayer rooms
2 parents 7c36848 + e04a57d commit d5557bb

22 files changed

+322
-26
lines changed

osu.Game.Benchmarks/osu.Game.Benchmarks.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="BenchmarkDotNet" Version="0.13.8" />
10+
<PackageReference Include="BenchmarkDotNet" Version="0.13.9" />
1111
<PackageReference Include="nunit" Version="3.13.3" />
1212
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
1313
</ItemGroup>

osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs

+15
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
using System.Collections.Generic;
66
using System.Linq;
77
using NUnit.Framework;
8+
using osu.Framework.Extensions;
89
using osu.Framework.Graphics;
910
using osu.Framework.Graphics.Containers;
1011
using osu.Framework.Graphics.Sprites;
1112
using osu.Framework.Testing;
1213
using osu.Framework.Utils;
14+
using osu.Game.Database;
1315
using osu.Game.Graphics.Sprites;
1416
using osu.Game.Online.Multiplayer;
1517
using osu.Game.Overlays;
@@ -31,6 +33,8 @@ public partial class TestSceneNotificationOverlay : OsuManualInputManagerTestSce
3133

3234
public double TimeToCompleteProgress { get; set; } = 2000;
3335

36+
private readonly UserLookupCache userLookupCache = new TestUserLookupCache();
37+
3438
[SetUp]
3539
public void SetUp() => Schedule(() =>
3640
{
@@ -60,6 +64,7 @@ public void TestBasicFlow()
6064
AddStep(@"simple #2", sendAmazingNotification);
6165
AddStep(@"progress #1", sendUploadProgress);
6266
AddStep(@"progress #2", sendDownloadProgress);
67+
AddStep(@"User notification", sendUserNotification);
6368

6469
checkProgressingCount(2);
6570

@@ -537,6 +542,16 @@ private void sendDownloadProgress()
537542
progressingNotifications.Add(n);
538543
}
539544

545+
private void sendUserNotification()
546+
{
547+
var user = userLookupCache.GetUserAsync(0).GetResultSafely();
548+
if (user == null) return;
549+
550+
var n = new UserAvatarNotification(user, $"{user.Username} invited you to a multiplayer match!");
551+
552+
notificationOverlay.Post(n);
553+
}
554+
540555
private void sendUploadProgress()
541556
{
542557
var n = new ProgressNotification

osu.Game/Localisation/ContextMenuStrings.cs

+5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ public static class ContextMenuStrings
1919
/// </summary>
2020
public static LocalisableString ViewBeatmap => new TranslatableString(getKey(@"view_beatmap"), @"View beatmap");
2121

22+
/// <summary>
23+
/// "Invite player"
24+
/// </summary>
25+
public static LocalisableString InvitePlayer => new TranslatableString(getKey(@"invite_player"), @"Invite player");
26+
2227
private static string getKey(string key) => $@"{prefix}:{key}";
2328
}
2429
}

osu.Game/Localisation/NotificationsStrings.cs

+5
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ public static class NotificationsStrings
9393
/// </summary>
9494
public static LocalisableString YourNameWasMentioned(string username) => new TranslatableString(getKey(@"your_name_was_mentioned"), @"Your name was mentioned in chat by '{0}'. Click to find out why!", username);
9595

96+
/// <summary>
97+
/// "{0} invited you to the multiplayer match &quot;{1}&quot;! Click to join."
98+
/// </summary>
99+
public static LocalisableString InvitedYouToTheMultiplayer(string username, string roomName) => new TranslatableString(getKey(@"invited_you_to_the_multiplayer"), @"{0} invited you to the multiplayer match ""{1}""! Click to join.", username, roomName);
100+
96101
/// <summary>
97102
/// "You do not have the beatmap for this replay."
98103
/// </summary>

osu.Game/Localisation/OnlinePlayStrings.cs

+10
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ public static class OnlinePlayStrings
1414
/// </summary>
1515
public static LocalisableString SupporterOnlyDurationNotice => new TranslatableString(getKey(@"supporter_only_duration_notice"), @"Playlist durations longer than 2 weeks require an active osu!supporter tag.");
1616

17+
/// <summary>
18+
/// "Can&#39;t invite this user as you have blocked them or they have blocked you."
19+
/// </summary>
20+
public static LocalisableString InviteFailedUserBlocked => new TranslatableString(getKey(@"cant_invite_this_user_as"), @"Can't invite this user as you have blocked them or they have blocked you.");
21+
22+
/// <summary>
23+
/// "Can&#39;t invite this user as they have opted out of non-friend communications."
24+
/// </summary>
25+
public static LocalisableString InviteFailedUserOptOut => new TranslatableString(getKey(@"cant_invite_this_user_as1"), @"Can't invite this user as they have opted out of non-friend communications.");
26+
1727
private static string getKey(string key) => $@"{prefix}:{key}";
1828
}
1929
}

osu.Game/Online/Multiplayer/IMultiplayerClient.cs

+8
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ public interface IMultiplayerClient
4242
/// <param name="user">The user.</param>
4343
Task UserKicked(MultiplayerRoomUser user);
4444

45+
/// <summary>
46+
/// Signals that the local user has been invited into a multiplayer room.
47+
/// </summary>
48+
/// <param name="invitedBy">Id of user that invited the player.</param>
49+
/// <param name="roomID">Id of the room the user got invited to.</param>
50+
/// <param name="password">Password to join the room.</param>
51+
Task Invited(int invitedBy, long roomID, string password);
52+
4553
/// <summary>
4654
/// Signal that the host of the room has changed.
4755
/// </summary>

osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs

+8
Original file line numberDiff line numberDiff line change
@@ -99,5 +99,13 @@ public interface IMultiplayerRoomServer
9999
/// </summary>
100100
/// <param name="playlistItemId">The item to remove.</param>
101101
Task RemovePlaylistItem(long playlistItemId);
102+
103+
/// <summary>
104+
/// Invites a player to the current room.
105+
/// </summary>
106+
/// <param name="userId">The user to invite.</param>
107+
/// <exception cref="UserBlockedException">The user has blocked or has been blocked by the invited user.</exception>
108+
/// <exception cref="UserBlocksPMsException">The invited user does not accept private messages.</exception>
109+
Task InvitePlayer(int userId);
102110
}
103111
}

osu.Game/Online/Multiplayer/MultiplayerClient.cs

+37
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,16 @@
2323
using osu.Game.Rulesets;
2424
using osu.Game.Rulesets.Mods;
2525
using osu.Game.Utils;
26+
using osu.Game.Localisation;
2627

2728
namespace osu.Game.Online.Multiplayer
2829
{
2930
public abstract partial class MultiplayerClient : Component, IMultiplayerClient, IMultiplayerRoomServer
3031
{
3132
public Action<Notification>? PostNotification { protected get; set; }
3233

34+
public Action<Room, string>? PresentMatch { protected get; set; }
35+
3336
/// <summary>
3437
/// Invoked when any change occurs to the multiplayer room.
3538
/// </summary>
@@ -260,6 +263,8 @@ public Task LeaveRoom()
260263

261264
protected abstract Task LeaveRoomInternal();
262265

266+
public abstract Task InvitePlayer(int userId);
267+
263268
/// <summary>
264269
/// Change the current <see cref="MultiplayerRoom"/> settings.
265270
/// </summary>
@@ -440,6 +445,38 @@ Task IMultiplayerClient.UserKicked(MultiplayerRoomUser user)
440445
return handleUserLeft(user, UserKicked);
441446
}
442447

448+
async Task IMultiplayerClient.Invited(int invitedBy, long roomID, string password)
449+
{
450+
APIUser? apiUser = await userLookupCache.GetUserAsync(invitedBy).ConfigureAwait(false);
451+
Room? apiRoom = await getRoomAsync(roomID).ConfigureAwait(false);
452+
453+
if (apiUser == null || apiRoom == null) return;
454+
455+
PostNotification?.Invoke(
456+
new UserAvatarNotification(apiUser, NotificationsStrings.InvitedYouToTheMultiplayer(apiUser.Username, apiRoom.Name.Value))
457+
{
458+
Activated = () =>
459+
{
460+
PresentMatch?.Invoke(apiRoom, password);
461+
return true;
462+
}
463+
}
464+
);
465+
466+
Task<Room?> getRoomAsync(long id)
467+
{
468+
TaskCompletionSource<Room?> taskCompletionSource = new TaskCompletionSource<Room?>();
469+
470+
var request = new GetRoomRequest(id);
471+
request.Success += room => taskCompletionSource.TrySetResult(room);
472+
request.Failure += _ => taskCompletionSource.TrySetResult(null);
473+
474+
API.Queue(request);
475+
476+
return taskCompletionSource.Task;
477+
}
478+
}
479+
443480
private void addUserToAPIRoom(MultiplayerRoomUser user)
444481
{
445482
Debug.Assert(APIRoom != null);

osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs

+29
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
using osu.Framework.Bindables;
1313
using osu.Game.Online.API;
1414
using osu.Game.Online.Rooms;
15+
using osu.Game.Overlays.Notifications;
16+
using osu.Game.Localisation;
1517

1618
namespace osu.Game.Online.Multiplayer
1719
{
@@ -50,6 +52,7 @@ private void load(IAPIProvider api)
5052
connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined);
5153
connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft);
5254
connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserKicked), ((IMultiplayerClient)this).UserKicked);
55+
connection.On<int, long, string>(nameof(IMultiplayerClient.Invited), ((IMultiplayerClient)this).Invited);
5356
connection.On<int>(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged);
5457
connection.On<MultiplayerRoomSettings>(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged);
5558
connection.On<int, MultiplayerUserState>(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged);
@@ -106,6 +109,32 @@ protected override Task LeaveRoomInternal()
106109
return connection.InvokeAsync(nameof(IMultiplayerServer.LeaveRoom));
107110
}
108111

112+
public override async Task InvitePlayer(int userId)
113+
{
114+
if (!IsConnected.Value)
115+
return;
116+
117+
Debug.Assert(connection != null);
118+
119+
try
120+
{
121+
await connection.InvokeAsync(nameof(IMultiplayerServer.InvitePlayer), userId).ConfigureAwait(false);
122+
}
123+
catch (HubException exception)
124+
{
125+
switch (exception.GetHubExceptionMessage())
126+
{
127+
case UserBlockedException.MESSAGE:
128+
PostNotification?.Invoke(new SimpleErrorNotification { Text = OnlinePlayStrings.InviteFailedUserBlocked });
129+
break;
130+
131+
case UserBlocksPMsException.MESSAGE:
132+
PostNotification?.Invoke(new SimpleErrorNotification { Text = OnlinePlayStrings.InviteFailedUserOptOut });
133+
break;
134+
}
135+
}
136+
}
137+
109138
public override Task TransferHost(int userId)
110139
{
111140
if (!IsConnected.Value)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2+
// See the LICENCE file in the repository root for full licence text.
3+
4+
using System;
5+
using System.Runtime.Serialization;
6+
using Microsoft.AspNetCore.SignalR;
7+
8+
namespace osu.Game.Online.Multiplayer
9+
{
10+
[Serializable]
11+
public class UserBlockedException : HubException
12+
{
13+
public const string MESSAGE = @"Cannot perform action due to user being blocked.";
14+
15+
public UserBlockedException()
16+
: base(MESSAGE)
17+
{
18+
}
19+
20+
protected UserBlockedException(SerializationInfo info, StreamingContext context)
21+
: base(info, context)
22+
{
23+
}
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2+
// See the LICENCE file in the repository root for full licence text.
3+
4+
using System;
5+
using System.Runtime.Serialization;
6+
using Microsoft.AspNetCore.SignalR;
7+
8+
namespace osu.Game.Online.Multiplayer
9+
{
10+
[Serializable]
11+
public class UserBlocksPMsException : HubException
12+
{
13+
public const string MESSAGE = "Cannot perform action because user has disabled non-friend communications.";
14+
15+
public UserBlocksPMsException()
16+
: base(MESSAGE)
17+
{
18+
}
19+
20+
protected UserBlocksPMsException(SerializationInfo info, StreamingContext context)
21+
: base(info, context)
22+
{
23+
}
24+
}
25+
}

osu.Game/OsuGame.cs

+21
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
using osu.Game.Localisation;
4747
using osu.Game.Online;
4848
using osu.Game.Online.Chat;
49+
using osu.Game.Online.Rooms;
4950
using osu.Game.Overlays;
5051
using osu.Game.Overlays.BeatmapListing;
5152
using osu.Game.Overlays.Music;
@@ -58,6 +59,7 @@
5859
using osu.Game.Scoring;
5960
using osu.Game.Screens;
6061
using osu.Game.Screens.Menu;
62+
using osu.Game.Screens.OnlinePlay.Multiplayer;
6163
using osu.Game.Screens.Play;
6264
using osu.Game.Screens.Ranking;
6365
using osu.Game.Screens.Select;
@@ -643,6 +645,24 @@ public void PresentBeatmap(IBeatmapSetInfo beatmap, Predicate<BeatmapInfo> diffi
643645
});
644646
}
645647

648+
/// <summary>
649+
/// Join a multiplayer match immediately.
650+
/// </summary>
651+
/// <param name="room">The room to join.</param>
652+
/// <param name="password">The password to join the room, if any is given.</param>
653+
public void PresentMultiplayerMatch(Room room, string password)
654+
{
655+
PerformFromScreen(screen =>
656+
{
657+
if (!(screen is Multiplayer multiplayer))
658+
screen.Push(multiplayer = new Multiplayer());
659+
660+
multiplayer.Join(room, password);
661+
});
662+
// TODO: We should really be able to use `validScreens: new[] { typeof(Multiplayer) }` here
663+
// but `PerformFromScreen` doesn't understand nested stacks.
664+
}
665+
646666
/// <summary>
647667
/// Present a score's replay immediately.
648668
/// The user should have already requested this interactively.
@@ -853,6 +873,7 @@ protected override void LoadComplete()
853873
ScoreManager.PresentImport = items => PresentScore(items.First().Value);
854874

855875
MultiplayerClient.PostNotification = n => Notifications.Post(n);
876+
MultiplayerClient.PresentMatch = PresentMultiplayerMatch;
856877

857878
// make config aware of how to lookup skins for on-screen display purposes.
858879
// if this becomes a more common thing, tracked settings should be reconsidered to allow local DI.

osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ private void onPlayingUsersChanged(object sender, NotifyCollectionChangedEventAr
119119
{
120120
users.GetUserAsync(userId).ContinueWith(task =>
121121
{
122-
var user = task.GetResultSafely();
122+
APIUser user = task.GetResultSafely();
123123

124124
if (user == null)
125125
return;
@@ -130,6 +130,9 @@ private void onPlayingUsersChanged(object sender, NotifyCollectionChangedEventAr
130130
if (!playingUsers.Contains(user.Id))
131131
return;
132132

133+
// TODO: remove this once online state is being updated more correctly.
134+
user.IsOnline = true;
135+
133136
userFlow.Add(createUserPanel(user));
134137
});
135138
});

osu.Game/Overlays/NotificationOverlay.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ private void load()
113113
RelativeSizeAxes = Axes.X,
114114
Children = new[]
115115
{
116-
new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(SimpleNotification) }),
116+
// The main section adds as a catch-all for notifications which don't group into other sections.
117+
new NotificationSection(AccountsStrings.NotificationsTitle),
117118
new NotificationSection(NotificationsStrings.RunningTasks, new[] { typeof(ProgressNotification) }),
118119
}
119120
}
@@ -205,7 +206,8 @@ private void addPermanently(Notification notification)
205206
var ourType = notification.GetType();
206207
int depth = notification.DisplayOnTop ? -runningDepth : runningDepth;
207208

208-
var section = sections.Children.First(s => s.AcceptedNotificationTypes.Any(accept => accept.IsAssignableFrom(ourType)));
209+
var section = sections.Children.FirstOrDefault(s => s.AcceptedNotificationTypes?.Any(accept => accept.IsAssignableFrom(ourType)) == true)
210+
?? sections.First();
209211

210212
section.Add(notification, depth);
211213

0 commit comments

Comments
 (0)