1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-15 10:22:56 +08:00

Merge pull request #13861 from peppy/add-password-support

Add multiplayer room password support
This commit is contained in:
Dean Herbert 2021-07-19 23:32:28 +09:00 committed by GitHub
commit 844152e1b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 645 additions and 182 deletions

View File

@ -4,13 +4,11 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Tests.Visual.OnlinePlay;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
@ -29,7 +27,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
JoinRequested = joinRequested
};
});
@ -43,11 +40,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("has 2 rooms", () => container.Rooms.Count == 2);
AddAssert("first room removed", () => container.Rooms.All(r => r.Room.RoomID.Value != 0));
AddStep("select first room", () => container.Rooms.First().Action?.Invoke());
AddStep("select first room", () => container.Rooms.First().Click());
AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First()));
AddStep("join first room", () => container.Rooms.First().Action?.Invoke());
AddAssert("first room joined", () => RoomManager.Rooms.First().Status.Value is JoinedRoomStatus);
}
[Test]
@ -66,9 +60,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
press(Key.Down);
press(Key.Down);
AddAssert("last room selected", () => checkRoomSelected(RoomManager.Rooms.Last()));
press(Key.Enter);
AddAssert("last room joined", () => RoomManager.Rooms.Last().Status.Value is JoinedRoomStatus);
}
[Test]
@ -123,15 +114,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("3 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 3);
}
private bool checkRoomSelected(Room room) => SelectedRoom.Value == room;
private void joinRequested(Room room) => room.Status.Value = new JoinedRoomStatus();
private class JoinedRoomStatus : RoomStatus
[Test]
public void TestPasswordProtectedRooms()
{
public override string Message => "Joined";
public override Color4 GetAppropriateColour(OsuColour colours) => colours.Yellow;
AddStep("add rooms", () => RoomManager.AddRooms(3, withPassword: true));
}
private bool checkRoomSelected(Room room) => SelectedRoom.Value == room;
}
}

View File

@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
@ -22,6 +23,7 @@ using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
@ -85,6 +87,124 @@ namespace osu.Game.Tests.Visual.Multiplayer
// used to test the flow of multiplayer from visual tests.
}
[Test]
public void TestCreateRoomWithoutPassword()
{
createRoom(() => new Room
{
Name = { Value = "Test Room" },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
});
}
[Test]
public void TestJoinRoomWithoutPassword()
{
AddStep("create room", () =>
{
API.Queue(new CreateRoomRequest(new Room
{
Name = { Value = "Test Room" },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
}));
});
AddStep("refresh rooms", () => multiplayerScreen.RoomManager.Filter.Value = new FilterCriteria());
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("join room", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddUntilStep("wait for join", () => client.Room != null);
}
[Test]
public void TestCreateRoomWithPassword()
{
createRoom(() => new Room
{
Name = { Value = "Test Room" },
Password = { Value = "password" },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
});
AddAssert("room has password", () => client.APIRoom?.Password.Value == "password");
}
[Test]
public void TestJoinRoomWithPassword()
{
AddStep("create room", () =>
{
API.Queue(new CreateRoomRequest(new Room
{
Name = { Value = "Test Room" },
Password = { Value = "password" },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
}));
});
AddStep("refresh rooms", () => multiplayerScreen.RoomManager.Filter.Value = new FilterCriteria());
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("join room", () => InputManager.Key(Key.Enter));
DrawableRoom.PasswordEntryPopover passwordEntryPopover = null;
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableRoom.PasswordEntryPopover>().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "password");
AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType<OsuButton>().First().Click());
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddUntilStep("wait for join", () => client.Room != null);
}
[Test]
public void TestLocalPasswordUpdatedWhenMultiplayerSettingsChange()
{
createRoom(() => new Room
{
Name = { Value = "Test Room" },
Password = { Value = "password" },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
});
AddStep("change password", () => client.ChangeSettings(password: "password2"));
AddUntilStep("local password changed", () => client.APIRoom?.Password.Value == "password2");
}
[Test]
public void TestUserSetToIdleWhenBeatmapDeleted()
{

View File

@ -0,0 +1,89 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Tests.Visual.OnlinePlay;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayerLoungeSubScreen : OnlinePlayTestScene
{
protected new BasicTestRoomManager RoomManager => (BasicTestRoomManager)base.RoomManager;
private LoungeSubScreen loungeScreen;
private Room lastJoinedRoom;
private string lastJoinedPassword;
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("push screen", () => LoadScreen(loungeScreen = new MultiplayerLoungeSubScreen()));
AddUntilStep("wait for present", () => loungeScreen.IsCurrentScreen());
AddStep("bind to event", () =>
{
lastJoinedRoom = null;
lastJoinedPassword = null;
RoomManager.JoinRoomRequested = onRoomJoined;
});
}
[Test]
public void TestJoinRoomWithoutPassword()
{
AddStep("add room", () => RoomManager.AddRooms(1, withPassword: false));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("join room", () => InputManager.Key(Key.Enter));
AddAssert("room join requested", () => lastJoinedRoom == RoomManager.Rooms.First());
AddAssert("room join password correct", () => lastJoinedPassword == null);
}
[Test]
public void TestPopoverHidesOnLeavingScreen()
{
AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => InputManager.ChildrenOfType<DrawableRoom.PasswordEntryPopover>().Any());
AddStep("exit screen", () => Stack.Exit());
AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType<DrawableRoom.PasswordEntryPopover>().Any());
}
[Test]
public void TestJoinRoomWithPassword()
{
DrawableRoom.PasswordEntryPopover passwordEntryPopover = null;
AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableRoom.PasswordEntryPopover>().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "password");
AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType<OsuButton>().First().Click());
AddAssert("room join requested", () => lastJoinedRoom == RoomManager.Rooms.First());
AddAssert("room join password correct", () => lastJoinedPassword == "password");
}
private void onRoomJoined(Room room, string password)
{
lastJoinedRoom = room;
lastJoinedPassword = password;
}
}
}

View File

@ -2,8 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
@ -12,40 +16,66 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneRoomStatus : OsuTestScene
{
public TestSceneRoomStatus()
[Test]
public void TestMultipleStatuses()
{
Child = new FillFlowContainer
AddStep("create rooms", () =>
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
Children = new Drawable[]
Child = new FillFlowContainer
{
new DrawableRoom(new Room
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
Children = new Drawable[]
{
Name = { Value = "Open - ending in 1 day" },
Status = { Value = new RoomStatusOpen() },
EndDate = { Value = DateTimeOffset.Now.AddDays(1) }
}) { MatchingFilter = true },
new DrawableRoom(new Room
{
Name = { Value = "Playing - ending in 1 day" },
Status = { Value = new RoomStatusPlaying() },
EndDate = { Value = DateTimeOffset.Now.AddDays(1) }
}) { MatchingFilter = true },
new DrawableRoom(new Room
{
Name = { Value = "Ended" },
Status = { Value = new RoomStatusEnded() },
EndDate = { Value = DateTimeOffset.Now }
}) { MatchingFilter = true },
new DrawableRoom(new Room
{
Name = { Value = "Open" },
Status = { Value = new RoomStatusOpen() },
Category = { Value = RoomCategory.Realtime }
}) { MatchingFilter = true },
}
};
new DrawableRoom(new Room
{
Name = { Value = "Open - ending in 1 day" },
Status = { Value = new RoomStatusOpen() },
EndDate = { Value = DateTimeOffset.Now.AddDays(1) }
}) { MatchingFilter = true },
new DrawableRoom(new Room
{
Name = { Value = "Playing - ending in 1 day" },
Status = { Value = new RoomStatusPlaying() },
EndDate = { Value = DateTimeOffset.Now.AddDays(1) }
}) { MatchingFilter = true },
new DrawableRoom(new Room
{
Name = { Value = "Ended" },
Status = { Value = new RoomStatusEnded() },
EndDate = { Value = DateTimeOffset.Now }
}) { MatchingFilter = true },
new DrawableRoom(new Room
{
Name = { Value = "Open" },
Status = { Value = new RoomStatusOpen() },
Category = { Value = RoomCategory.Realtime }
}) { MatchingFilter = true },
}
};
});
}
[Test]
public void TestEnableAndDisablePassword()
{
DrawableRoom drawableRoom = null;
Room room = null;
AddStep("create room", () => Child = drawableRoom = new DrawableRoom(room = new Room
{
Name = { Value = "Room with password" },
Status = { Value = new RoomStatusOpen() },
Category = { Value = RoomCategory.Realtime },
}) { MatchingFilter = true });
AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType<DrawableRoom.PasswordProtectedIcon>().Single().Alpha));
AddStep("set password", () => room.Password.Value = "password");
AddAssert("password icon visible", () => Precision.AlmostEquals(1, drawableRoom.ChildrenOfType<DrawableRoom.PasswordProtectedIcon>().Single().Alpha));
AddStep("unset password", () => room.Password.Value = string.Empty);
AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType<DrawableRoom.PasswordProtectedIcon>().Single().Alpha));
}
}
}

View File

@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Playlists
AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms.First()));
AddStep("select last room", () => roomsContainer.Rooms.Last().Action?.Invoke());
AddStep("select last room", () => roomsContainer.Rooms.Last().Click());
AddUntilStep("first room is masked", () => !checkRoomVisible(roomsContainer.Rooms.First()));
AddUntilStep("last room is not masked", () => checkRoomVisible(roomsContainer.Rooms.Last()));

View File

@ -152,7 +152,7 @@ namespace osu.Game.Tests.Visual.Playlists
onSuccess?.Invoke(room);
}
public void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null) => throw new NotImplementedException();
public void JoinRoom(Room room, string password, Action<Room> onSuccess = null, Action<string> onError = null) => throw new NotImplementedException();
public void PartRoom() => throw new NotImplementedException();
}

View File

@ -15,6 +15,16 @@ namespace osu.Game.Online.Multiplayer
/// </summary>
/// <param name="roomId">The databased room ID.</param>
/// <exception cref="InvalidStateException">If the user is already in the requested (or another) room.</exception>
/// <exception cref="InvalidPasswordException">If the room required a password.</exception>
Task<MultiplayerRoom> JoinRoom(long roomId);
/// <summary>
/// Request to join a multiplayer room with a provided password.
/// </summary>
/// <param name="roomId">The databased room ID.</param>
/// <param name="password">The password for the join request.</param>
/// <exception cref="InvalidStateException">If the user is already in the requested (or another) room.</exception>
/// <exception cref="InvalidPasswordException">If the room provided password was incorrect.</exception>
Task<MultiplayerRoom> JoinRoomWithPassword(long roomId, string password);
}
}

View File

@ -0,0 +1,22 @@
// 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.Runtime.Serialization;
using Microsoft.AspNetCore.SignalR;
namespace osu.Game.Online.Multiplayer
{
[Serializable]
public class InvalidPasswordException : HubException
{
public InvalidPasswordException()
{
}
protected InvalidPasswordException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}

View File

@ -92,7 +92,7 @@ namespace osu.Game.Online.Multiplayer
[Resolved]
private UserLookupCache userLookupCache { get; set; } = null!;
private Room? apiRoom;
protected Room? APIRoom { get; private set; }
[BackgroundDependencyLoader]
private void load()
@ -115,7 +115,8 @@ namespace osu.Game.Online.Multiplayer
/// Joins the <see cref="MultiplayerRoom"/> for a given API <see cref="Room"/>.
/// </summary>
/// <param name="room">The API <see cref="Room"/>.</param>
public async Task JoinRoom(Room room)
/// <param name="password">An optional password to use for the join operation.</param>
public async Task JoinRoom(Room room, string? password = null)
{
var cancellationSource = joinCancellationSource = new CancellationTokenSource();
@ -127,7 +128,7 @@ namespace osu.Game.Online.Multiplayer
Debug.Assert(room.RoomID.Value != null);
// Join the server-side room.
var joinedRoom = await JoinRoom(room.RoomID.Value.Value).ConfigureAwait(false);
var joinedRoom = await JoinRoom(room.RoomID.Value.Value, password ?? room.Password.Value).ConfigureAwait(false);
Debug.Assert(joinedRoom != null);
// Populate users.
@ -138,7 +139,7 @@ namespace osu.Game.Online.Multiplayer
await scheduleAsync(() =>
{
Room = joinedRoom;
apiRoom = room;
APIRoom = room;
foreach (var user in joinedRoom.Users)
updateUserPlayingState(user.UserID, user.State);
}, cancellationSource.Token).ConfigureAwait(false);
@ -152,8 +153,9 @@ namespace osu.Game.Online.Multiplayer
/// Joins the <see cref="MultiplayerRoom"/> with a given ID.
/// </summary>
/// <param name="roomId">The room ID.</param>
/// <param name="password">An optional password to use when joining the room.</param>
/// <returns>The joined <see cref="MultiplayerRoom"/>.</returns>
protected abstract Task<MultiplayerRoom> JoinRoom(long roomId);
protected abstract Task<MultiplayerRoom> JoinRoom(long roomId, string? password = null);
public Task LeaveRoom()
{
@ -166,7 +168,7 @@ namespace osu.Game.Online.Multiplayer
// For example, if a room was left and the user immediately pressed the "create room" button, then the user could be taken into the lobby if the value of Room is not reset in time.
var scheduledReset = scheduleAsync(() =>
{
apiRoom = null;
APIRoom = null;
Room = null;
CurrentMatchPlayingUserIds.Clear();
@ -189,8 +191,9 @@ namespace osu.Game.Online.Multiplayer
/// A room must be joined for this to have any effect.
/// </remarks>
/// <param name="name">The new room name, if any.</param>
/// <param name="password">The new password, if any.</param>
/// <param name="item">The new room playlist item, if any.</param>
public Task ChangeSettings(Optional<string> name = default, Optional<PlaylistItem> item = default)
public Task ChangeSettings(Optional<string> name = default, Optional<string> password = default, Optional<PlaylistItem> item = default)
{
if (Room == null)
throw new InvalidOperationException("Must be joined to a match to change settings.");
@ -212,6 +215,7 @@ namespace osu.Game.Online.Multiplayer
return ChangeSettings(new MultiplayerRoomSettings
{
Name = name.GetOr(Room.Settings.Name),
Password = password.GetOr(Room.Settings.Password),
BeatmapID = item.GetOr(existingPlaylistItem).BeatmapID,
BeatmapChecksum = item.GetOr(existingPlaylistItem).Beatmap.Value.MD5Hash,
RulesetID = item.GetOr(existingPlaylistItem).RulesetID,
@ -301,22 +305,22 @@ namespace osu.Game.Online.Multiplayer
if (Room == null)
return;
Debug.Assert(apiRoom != null);
Debug.Assert(APIRoom != null);
Room.State = state;
switch (state)
{
case MultiplayerRoomState.Open:
apiRoom.Status.Value = new RoomStatusOpen();
APIRoom.Status.Value = new RoomStatusOpen();
break;
case MultiplayerRoomState.Playing:
apiRoom.Status.Value = new RoomStatusPlaying();
APIRoom.Status.Value = new RoomStatusPlaying();
break;
case MultiplayerRoomState.Closed:
apiRoom.Status.Value = new RoomStatusEnded();
APIRoom.Status.Value = new RoomStatusEnded();
break;
}
@ -377,12 +381,12 @@ namespace osu.Game.Online.Multiplayer
if (Room == null)
return;
Debug.Assert(apiRoom != null);
Debug.Assert(APIRoom != null);
var user = Room.Users.FirstOrDefault(u => u.UserID == userId);
Room.Host = user;
apiRoom.Host.Value = user?.User;
APIRoom.Host.Value = user?.User;
RoomUpdated?.Invoke();
}, false);
@ -525,11 +529,12 @@ namespace osu.Game.Online.Multiplayer
if (Room == null)
return;
Debug.Assert(apiRoom != null);
Debug.Assert(APIRoom != null);
// Update a few properties of the room instantaneously.
Room.Settings = settings;
apiRoom.Name.Value = Room.Settings.Name;
APIRoom.Name.Value = Room.Settings.Name;
APIRoom.Password.Value = Room.Settings.Password;
// The current item update is delayed until an online beatmap lookup (below) succeeds.
// In-order for the client to not display an outdated beatmap, the current item is forcefully cleared here.
@ -551,7 +556,7 @@ namespace osu.Game.Online.Multiplayer
if (Room == null || !Room.Settings.Equals(settings))
return;
Debug.Assert(apiRoom != null);
Debug.Assert(APIRoom != null);
var beatmap = beatmapSet.Beatmaps.Single(b => b.OnlineBeatmapID == settings.BeatmapID);
beatmap.MD5Hash = settings.BeatmapChecksum;
@ -561,7 +566,7 @@ namespace osu.Game.Online.Multiplayer
var allowedMods = settings.AllowedMods.Select(m => m.ToMod(ruleset));
// Try to retrieve the existing playlist item from the API room.
var playlistItem = apiRoom.Playlist.FirstOrDefault(i => i.ID == settings.PlaylistItemId);
var playlistItem = APIRoom.Playlist.FirstOrDefault(i => i.ID == settings.PlaylistItemId);
if (playlistItem != null)
updateItem(playlistItem);
@ -569,7 +574,7 @@ namespace osu.Game.Online.Multiplayer
{
// An existing playlist item does not exist, so append a new one.
updateItem(playlistItem = new PlaylistItem());
apiRoom.Playlist.Add(playlistItem);
APIRoom.Playlist.Add(playlistItem);
}
CurrentMatchPlayingItem.Value = playlistItem;

View File

@ -36,12 +36,16 @@ namespace osu.Game.Online.Multiplayer
[Key(6)]
public long PlaylistItemId { get; set; }
[Key(7)]
public string Password { get; set; } = string.Empty;
public bool Equals(MultiplayerRoomSettings other)
=> BeatmapID == other.BeatmapID
&& BeatmapChecksum == other.BeatmapChecksum
&& RequiredMods.SequenceEqual(other.RequiredMods)
&& AllowedMods.SequenceEqual(other.AllowedMods)
&& RulesetID == other.RulesetID
&& Password.Equals(other.Password, StringComparison.Ordinal)
&& Name.Equals(other.Name, StringComparison.Ordinal)
&& PlaylistItemId == other.PlaylistItemId;
@ -49,6 +53,7 @@ namespace osu.Game.Online.Multiplayer
+ $" Beatmap:{BeatmapID} ({BeatmapChecksum})"
+ $" RequiredMods:{string.Join(',', RequiredMods)}"
+ $" AllowedMods:{string.Join(',', AllowedMods)}"
+ $" Password:{(string.IsNullOrEmpty(Password) ? "no" : "yes")}"
+ $" Ruleset:{RulesetID}"
+ $" Item:{PlaylistItemId}";
}

View File

@ -62,12 +62,12 @@ namespace osu.Game.Online.Multiplayer
}
}
protected override Task<MultiplayerRoom> JoinRoom(long roomId)
protected override Task<MultiplayerRoom> JoinRoom(long roomId, string? password = null)
{
if (!IsConnected.Value)
return Task.FromCanceled<MultiplayerRoom>(new CancellationToken(true));
return connection.InvokeAsync<MultiplayerRoom>(nameof(IMultiplayerServer.JoinRoom), roomId);
return connection.InvokeAsync<MultiplayerRoom>(nameof(IMultiplayerServer.JoinRoomWithPassword), roomId, password ?? string.Empty);
}
protected override Task LeaveRoomInternal()

View File

@ -9,11 +9,13 @@ namespace osu.Game.Online.Rooms
{
public class JoinRoomRequest : APIRequest
{
private readonly Room room;
public readonly Room Room;
public readonly string Password;
public JoinRoomRequest(Room room)
public JoinRoomRequest(Room room, string password)
{
this.room = room;
Room = room;
Password = password;
}
protected override WebRequest CreateWebRequest()
@ -23,6 +25,7 @@ namespace osu.Game.Online.Rooms
return req;
}
protected override string Target => $"rooms/{room.RoomID.Value}/users/{User.Id}";
// Todo: Password needs to be specified here rather than via AddParameter() because this is a PUT request. May be a framework bug.
protected override string Target => $"rooms/{Room.RoomID.Value}/users/{User.Id}?password={Password}";
}
}

View File

@ -49,10 +49,6 @@ namespace osu.Game.Online.Rooms
set => Category.Value = value;
}
[Cached]
[JsonIgnore]
public readonly Bindable<TimeSpan?> Duration = new Bindable<TimeSpan?>();
[Cached]
[JsonIgnore]
public readonly Bindable<int?> MaxAttempts = new Bindable<int?>();
@ -77,6 +73,9 @@ namespace osu.Game.Online.Rooms
[JsonProperty("current_user_score")]
public readonly Bindable<PlaylistAggregateScore> UserScore = new Bindable<PlaylistAggregateScore>();
[JsonProperty("has_password")]
public readonly BindableBool HasPassword = new BindableBool();
[Cached]
[JsonProperty("recent_participants")]
public readonly BindableList<User> RecentParticipants = new BindableList<User>();
@ -85,6 +84,16 @@ namespace osu.Game.Online.Rooms
[JsonProperty("participant_count")]
public readonly Bindable<int> ParticipantCount = new Bindable<int>();
#region Properties only used for room creation request
[Cached(Name = nameof(Password))]
[JsonProperty("password")]
public readonly Bindable<string> Password = new Bindable<string>();
[Cached]
[JsonIgnore]
public readonly Bindable<TimeSpan?> Duration = new Bindable<TimeSpan?>();
[JsonProperty("duration")]
private int? duration
{
@ -98,6 +107,8 @@ namespace osu.Game.Online.Rooms
}
}
#endregion
// Only supports retrieval for now
[Cached]
[JsonProperty("ends_at")]
@ -117,6 +128,11 @@ namespace osu.Game.Online.Rooms
[JsonIgnore]
public readonly Bindable<int> Position = new Bindable<int>(-1);
public Room()
{
Password.BindValueChanged(p => HasPassword.Value = !string.IsNullOrEmpty(p.NewValue));
}
/// <summary>
/// Create a copy of this room without online information.
/// Should be used to create a local copy of a room for submitting in the future.
@ -145,6 +161,7 @@ namespace osu.Game.Online.Rooms
ChannelId.Value = other.ChannelId.Value;
Status.Value = other.Status.Value;
Availability.Value = other.Availability.Value;
HasPassword.Value = other.HasPassword.Value;
Type.Value = other.Type.Value;
MaxParticipants.Value = other.MaxParticipants.Value;
ParticipantCount.Value = other.ParticipantCount.Value;

View File

@ -13,6 +13,7 @@ using osu.Framework.Development;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.IO.Stores;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
@ -341,7 +342,11 @@ namespace osu.Game
globalBindings = new GlobalActionContainer(this)
};
MenuCursorContainer.Child = content = new OsuTooltipContainer(MenuCursorContainer.Cursor) { RelativeSizeAxes = Axes.Both };
MenuCursorContainer.Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = content = new OsuTooltipContainer(MenuCursorContainer.Cursor) { RelativeSizeAxes = Axes.Both }
};
base.Content.Add(CreateScalingContainer().WithChildren(mainContent));

View File

@ -84,10 +84,10 @@ namespace osu.Game.Screens.OnlinePlay.Components
private JoinRoomRequest currentJoinRoomRequest;
public virtual void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
public virtual void JoinRoom(Room room, string password = null, Action<Room> onSuccess = null, Action<string> onError = null)
{
currentJoinRoomRequest?.Cancel();
currentJoinRoomRequest = new JoinRoomRequest(room);
currentJoinRoomRequest = new JoinRoomRequest(room, password);
currentJoinRoomRequest.Success += () =>
{

View File

@ -6,6 +6,8 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Online.Rooms;
#nullable enable
namespace osu.Game.Screens.OnlinePlay
{
[Cached(typeof(IRoomManager))]
@ -32,15 +34,16 @@ namespace osu.Game.Screens.OnlinePlay
/// <param name="room">The <see cref="Room"/> to create.</param>
/// <param name="onSuccess">An action to be invoked if the creation succeeds.</param>
/// <param name="onError">An action to be invoked if an error occurred.</param>
void CreateRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null);
void CreateRoom(Room room, Action<Room>? onSuccess = null, Action<string>? onError = null);
/// <summary>
/// Joins a <see cref="Room"/>.
/// </summary>
/// <param name="room">The <see cref="Room"/> to join. <see cref="Room.RoomID"/> must be populated.</param>
/// <param name="password">An optional password to use for the join operation.</param>
/// <param name="onSuccess"></param>
/// <param name="onError"></param>
void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null);
void JoinRoom(Room room, string? password = null, Action<Room>? onSuccess = null, Action<string>? onError = null);
/// <summary>
/// Parts the currently-joined <see cref="Room"/>.

View File

@ -6,19 +6,25 @@ using System.Collections.Generic;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Input.Bindings;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components;
using osuTK;
@ -26,7 +32,7 @@ using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public class DrawableRoom : OsuClickableContainer, IStateful<SelectionState>, IFilterable, IHasContextMenu
public class DrawableRoom : OsuClickableContainer, IStateful<SelectionState>, IFilterable, IHasContextMenu, IHasPopover, IKeyBindingHandler<GlobalAction>
{
public const float SELECTION_BORDER_WIDTH = 4;
private const float corner_radius = 5;
@ -46,6 +52,12 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
[Resolved]
private BeatmapManager beatmaps { get; set; }
[Resolved(canBeNull: true)]
private Bindable<Room> selectedRoom { get; set; }
[Resolved(canBeNull: true)]
private LoungeSubScreen lounge { get; set; }
public readonly Room Room;
private SelectionState state;
@ -91,6 +103,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
public bool FilteringActive { get; set; }
private PasswordProtectedIcon passwordIcon;
private readonly Bindable<bool> hasPassword = new Bindable<bool>();
public DrawableRoom(Room room)
{
Room = room;
@ -200,6 +216,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
},
},
},
passwordIcon = new PasswordProtectedIcon { Alpha = 0 }
},
},
},
@ -222,10 +239,69 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
this.FadeInFromZero(transition_duration);
else
Alpha = 0;
hasPassword.BindTo(Room.HasPassword);
hasPassword.BindValueChanged(v => passwordIcon.Alpha = v.NewValue ? 1 : 0, true);
}
public Popover GetPopover() => new PasswordEntryPopover(Room) { JoinRequested = lounge.Join };
public MenuItem[] ContextMenuItems => new MenuItem[]
{
new OsuMenuItem("Create copy", MenuItemType.Standard, () =>
{
parentScreen?.OpenNewRoom(Room.DeepClone());
})
};
public bool OnPressed(GlobalAction action)
{
if (selectedRoom.Value != Room)
return false;
switch (action)
{
case GlobalAction.Select:
Click();
return true;
}
return false;
}
public void OnReleased(GlobalAction action)
{
}
protected override bool ShouldBeConsideredForInput(Drawable child) => state == SelectionState.Selected;
protected override bool OnMouseDown(MouseDownEvent e)
{
if (selectedRoom.Value != Room)
return true;
return base.OnMouseDown(e);
}
protected override bool OnClick(ClickEvent e)
{
if (Room != selectedRoom.Value)
{
selectedRoom.Value = Room;
return true;
}
if (Room.HasPassword.Value)
{
this.ShowPopover();
return true;
}
lounge?.Join(Room, null);
return base.OnClick(e);
}
private class RoomName : OsuSpriteText
{
[Resolved(typeof(Room), nameof(Online.Rooms.Room.Name))]
@ -238,12 +314,83 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
}
}
public MenuItem[] ContextMenuItems => new MenuItem[]
public class PasswordProtectedIcon : CompositeDrawable
{
new OsuMenuItem("Create copy", MenuItemType.Standard, () =>
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
parentScreen?.OpenNewRoom(Room.DeepClone());
})
};
Anchor = Anchor.TopRight;
Origin = Anchor.TopRight;
Size = new Vector2(32);
InternalChildren = new Drawable[]
{
new Box
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopCentre,
Colour = colours.Gray5,
Rotation = 45,
RelativeSizeAxes = Axes.Both,
Width = 2,
},
new SpriteIcon
{
Icon = FontAwesome.Solid.Lock,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Margin = new MarginPadding(6),
Size = new Vector2(14),
}
};
}
}
public class PasswordEntryPopover : OsuPopover
{
private readonly Room room;
public Action<Room, string> JoinRequested;
public PasswordEntryPopover(Room room)
{
this.room = room;
}
private OsuPasswordTextBox passwordTextbox;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Child = new FillFlowContainer
{
Margin = new MarginPadding(10),
Spacing = new Vector2(5),
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
passwordTextbox = new OsuPasswordTextBox
{
Width = 200,
},
new TriangleButton
{
Width = 80,
Text = "Join Room",
Action = () => JoinRequested?.Invoke(room, passwordTextbox.Text)
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Schedule(() => GetContainingInputManager().ChangeFocus(passwordTextbox));
}
}
}
}

View File

@ -24,8 +24,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public class RoomsContainer : CompositeDrawable, IKeyBindingHandler<GlobalAction>
{
public Action<Room> JoinRequested;
private readonly IBindableList<Room> rooms = new BindableList<Room>();
private readonly FillFlowContainer<DrawableRoom> roomFlow;
@ -121,19 +119,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
foreach (var room in rooms)
{
roomFlow.Add(new DrawableRoom(room)
{
Action = () =>
{
if (room == selectedRoom.Value)
{
joinSelected();
return;
}
selectRoom(room);
}
});
roomFlow.Add(new DrawableRoom(room));
}
Filter(filter?.Value);
@ -150,7 +136,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
roomFlow.Remove(toRemove);
selectRoom(null);
selectedRoom.Value = null;
}
}
@ -160,18 +146,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
roomFlow.SetLayoutPosition(room, room.Room.Position.Value);
}
private void selectRoom(Room room) => selectedRoom.Value = room;
private void joinSelected()
{
if (selectedRoom.Value == null) return;
JoinRequested?.Invoke(selectedRoom.Value);
}
protected override bool OnClick(ClickEvent e)
{
selectRoom(null);
selectedRoom.Value = null;
return base.OnClick(e);
}
@ -181,10 +158,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
switch (action)
{
case GlobalAction.Select:
joinSelected();
return true;
case GlobalAction.SelectNext:
beginRepeatSelection(() => selectNext(1), action);
return true;
@ -253,7 +226,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
// we already have a valid selection only change selection if we still have a room to switch to.
if (room != null)
selectRoom(room);
selectedRoom.Value = room;
}
#endregion

View File

@ -6,6 +6,7 @@ using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
@ -46,10 +47,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
[CanBeNull]
private IDisposable joiningRoomOperation { get; set; }
private RoomsContainer roomsContainer;
[BackgroundDependencyLoader]
private void load()
{
RoomsContainer roomsContainer;
OsuScrollContainer scrollContainer;
InternalChildren = new Drawable[]
@ -70,7 +72,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
RelativeSizeAxes = Axes.Both,
ScrollbarOverlapsContent = false,
Padding = new MarginPadding(10),
Child = roomsContainer = new RoomsContainer { JoinRequested = joinRequested }
Child = roomsContainer = new RoomsContainer()
},
loadingLayer = new LoadingLayer(true),
}
@ -150,31 +152,39 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
onReturning();
}
private void onReturning()
{
filter.HoldFocus = true;
}
public override bool OnExiting(IScreen next)
{
filter.HoldFocus = false;
onLeaving();
return base.OnExiting(next);
}
public override void OnSuspending(IScreen next)
{
onLeaving();
base.OnSuspending(next);
filter.HoldFocus = false;
}
private void joinRequested(Room room)
private void onReturning()
{
filter.HoldFocus = true;
}
private void onLeaving()
{
filter.HoldFocus = false;
// ensure any password prompt is dismissed.
this.HidePopover();
}
public void Join(Room room, string password)
{
if (joiningRoomOperation != null)
return;
joiningRoomOperation = ongoingOperationTracker?.BeginOperation();
RoomManager?.JoinRoom(room, r =>
RoomManager?.JoinRoom(room, password, r =>
{
Open(room);
joiningRoomOperation?.Dispose();

View File

@ -25,8 +25,12 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
private void load()
{
Masking = true;
Add(Settings = CreateSettings());
}
protected abstract OnlinePlayComposite CreateSettings();
protected override void PopIn()
{
Settings.MoveToY(0, TRANSITION_DURATION, Easing.OutQuint);

View File

@ -27,16 +27,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
public class MultiplayerMatchSettingsOverlay : MatchSettingsOverlay
{
[BackgroundDependencyLoader]
private void load()
{
Child = Settings = new MatchSettings
protected override OnlinePlayComposite CreateSettings()
=> new MatchSettings
{
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Y,
SettingsApplied = Hide
};
}
protected class MatchSettings : OnlinePlayComposite
{
@ -47,6 +44,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
public OsuTextBox NameField, MaxParticipantsField;
public RoomAvailabilityPicker AvailabilityPicker;
public GameTypePicker TypePicker;
public OsuTextBox PasswordTextBox;
public TriangleButton ApplyButton;
public OsuSpriteText ErrorText;
@ -193,12 +191,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
},
new Section("Password (optional)")
{
Alpha = disabled_alpha,
Child = new SettingsPasswordTextBox
Child = PasswordTextBox = new SettingsPasswordTextBox
{
RelativeSizeAxes = Axes.X,
TabbableContentContainer = this,
ReadOnly = true,
},
},
}
@ -275,6 +271,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
Type.BindValueChanged(type => TypePicker.Current.Value = type.NewValue, true);
MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true);
RoomID.BindValueChanged(roomId => initialBeatmapControl.Alpha = roomId.NewValue == null ? 1 : 0, true);
Password.BindValueChanged(password => PasswordTextBox.Text = password.NewValue ?? string.Empty, true);
operationInProgress.BindTo(ongoingOperationTracker.InProgress);
operationInProgress.BindValueChanged(v =>
@ -307,7 +304,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
// Otherwise, update the room directly in preparation for it to be submitted to the API on match creation.
if (client.Room != null)
{
client.ChangeSettings(name: NameField.Text).ContinueWith(t => Schedule(() =>
client.ChangeSettings(name: NameField.Text, password: PasswordTextBox.Text).ContinueWith(t => Schedule(() =>
{
if (t.IsCompletedSuccessfully)
onSuccess(currentRoom.Value);
@ -320,6 +317,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
currentRoom.Value.Name.Value = NameField.Text;
currentRoom.Value.Availability.Value = AvailabilityPicker.Current.Value;
currentRoom.Value.Type.Value = TypePicker.Current.Value;
currentRoom.Value.Password.Value = PasswordTextBox.Current.Value;
if (int.TryParse(MaxParticipantsField.Text, out int max))
currentRoom.Value.MaxParticipants.Value = max;

View File

@ -38,9 +38,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
}
public override void CreateRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
=> base.CreateRoom(room, r => joinMultiplayerRoom(r, onSuccess, onError), onError);
=> base.CreateRoom(room, r => joinMultiplayerRoom(r, r.Password.Value, onSuccess, onError), onError);
public override void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
public override void JoinRoom(Room room, string password = null, Action<Room> onSuccess = null, Action<string> onError = null)
{
if (!multiplayerClient.IsConnected.Value)
{
@ -56,7 +56,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
return;
}
base.JoinRoom(room, r => joinMultiplayerRoom(r, onSuccess, onError), onError);
base.JoinRoom(room, password, r => joinMultiplayerRoom(r, password, onSuccess, onError), onError);
}
public override void PartRoom()
@ -79,11 +79,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
});
}
private void joinMultiplayerRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
private void joinMultiplayerRoom(Room room, string password, Action<Room> onSuccess = null, Action<string> onError = null)
{
Debug.Assert(room.RoomID.Value != null);
multiplayerClient.JoinRoom(room).ContinueWith(t =>
multiplayerClient.JoinRoom(room, password).ContinueWith(t =>
{
if (t.IsCompletedSuccessfully)
Schedule(() => onSuccess?.Invoke(room));

View File

@ -56,6 +56,9 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved(typeof(Room))]
protected Bindable<RoomAvailability> Availability { get; private set; }
[Resolved(typeof(Room), nameof(Room.Password))]
public Bindable<string> Password { get; private set; }
[Resolved(typeof(Room))]
protected Bindable<TimeSpan?> Duration { get; private set; }

View File

@ -26,16 +26,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{
public Action EditPlaylist;
[BackgroundDependencyLoader]
private void load()
{
Child = Settings = new MatchSettings
protected override OnlinePlayComposite CreateSettings()
=> new MatchSettings
{
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Y,
EditPlaylist = () => EditPlaylist?.Invoke()
};
}
protected class MatchSettings : OnlinePlayComposite
{

View File

@ -28,7 +28,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public override IBindable<bool> IsConnected => isConnected;
private readonly Bindable<bool> isConnected = new Bindable<bool>(true);
public Room? APIRoom { get; private set; }
public new Room? APIRoom => base.APIRoom;
public Action<MultiplayerRoom>? RoomSetupAction;
@ -115,10 +115,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
((IMultiplayerClient)this).UserBeatmapAvailabilityChanged(userId, newBeatmapAvailability);
}
protected override Task<MultiplayerRoom> JoinRoom(long roomId)
protected override Task<MultiplayerRoom> JoinRoom(long roomId, string? password = null)
{
var apiRoom = roomManager.Rooms.Single(r => r.RoomID.Value == roomId);
if (password != apiRoom.Password.Value)
throw new InvalidOperationException("Invalid password.");
var localUser = new MultiplayerRoomUser(api.LocalUser.Value.Id)
{
User = api.LocalUser.Value
@ -134,7 +137,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
BeatmapChecksum = apiRoom.Playlist.Last().Beatmap.Value.MD5Hash,
RequiredMods = apiRoom.Playlist.Last().RequiredMods.Select(m => new APIMod(m)).ToArray(),
AllowedMods = apiRoom.Playlist.Last().AllowedMods.Select(m => new APIMod(m)).ToArray(),
PlaylistItemId = apiRoom.Playlist.Last().ID
PlaylistItemId = apiRoom.Playlist.Last().ID,
// ReSharper disable once ConstantNullCoalescingCondition Incorrect inspection due to lack of nullable in Room.cs.
Password = password ?? string.Empty,
},
Users = { localUser },
Host = localUser
@ -143,16 +148,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
RoomSetupAction?.Invoke(room);
RoomSetupAction = null;
APIRoom = apiRoom;
return Task.FromResult(room);
}
protected override Task LeaveRoomInternal()
{
APIRoom = null;
return Task.CompletedTask;
}
protected override Task LeaveRoomInternal() => Task.CompletedTask;
public override Task TransferHost(int userId) => ((IMultiplayerClient)this).HostChanged(userId);

View File

@ -45,21 +45,38 @@ namespace osu.Game.Tests.Visual.Multiplayer
switch (req)
{
case CreateRoomRequest createRoomRequest:
var createdRoom = new APICreatedRoom();
var apiRoom = new Room();
createdRoom.CopyFrom(createRoomRequest.Room);
createdRoom.RoomID.Value ??= currentRoomId++;
apiRoom.CopyFrom(createRoomRequest.Room);
apiRoom.RoomID.Value ??= currentRoomId++;
for (int i = 0; i < createdRoom.Playlist.Count; i++)
createdRoom.Playlist[i].ID = currentPlaylistItemId++;
// Passwords are explicitly not copied between rooms.
apiRoom.HasPassword.Value = !string.IsNullOrEmpty(createRoomRequest.Room.Password.Value);
apiRoom.Password.Value = createRoomRequest.Room.Password.Value;
Rooms.Add(createdRoom);
createRoomRequest.TriggerSuccess(createdRoom);
for (int i = 0; i < apiRoom.Playlist.Count; i++)
apiRoom.Playlist[i].ID = currentPlaylistItemId++;
var responseRoom = new APICreatedRoom();
responseRoom.CopyFrom(createResponseRoom(apiRoom, false));
Rooms.Add(apiRoom);
createRoomRequest.TriggerSuccess(responseRoom);
return true;
case JoinRoomRequest joinRoomRequest:
{
var room = Rooms.Single(r => r.RoomID.Value == joinRoomRequest.Room.RoomID.Value);
if (joinRoomRequest.Password != room.Password.Value)
{
joinRoomRequest.TriggerFailure(new InvalidOperationException("Invalid password."));
return true;
}
joinRoomRequest.TriggerSuccess();
return true;
}
case PartRoomRequest partRoomRequest:
partRoomRequest.TriggerSuccess();
@ -69,20 +86,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
var roomsWithoutParticipants = new List<Room>();
foreach (var r in Rooms)
{
var newRoom = new Room();
newRoom.CopyFrom(r);
newRoom.RecentParticipants.Clear();
roomsWithoutParticipants.Add(newRoom);
}
roomsWithoutParticipants.Add(createResponseRoom(r, false));
getRoomsRequest.TriggerSuccess(roomsWithoutParticipants);
return true;
case GetRoomRequest getRoomRequest:
getRoomRequest.TriggerSuccess(Rooms.Single(r => r.RoomID.Value == getRoomRequest.RoomId));
getRoomRequest.TriggerSuccess(createResponseRoom(Rooms.Single(r => r.RoomID.Value == getRoomRequest.RoomId), true));
return true;
case GetBeatmapSetRequest getBeatmapSetRequest:
@ -118,6 +128,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
};
}
private Room createResponseRoom(Room room, bool withParticipants)
{
var responseRoom = new Room();
responseRoom.CopyFrom(room);
responseRoom.Password.Value = null;
if (!withParticipants)
responseRoom.RecentParticipants.Clear();
return responseRoom;
}
public new void ClearRooms() => base.ClearRooms();
public new void Schedule(Action action) => base.Schedule(action);

View File

@ -26,6 +26,8 @@ namespace osu.Game.Tests.Visual.OnlinePlay
public readonly BindableList<Room> Rooms = new BindableList<Room>();
public Action<Room, string> JoinRoomRequested;
public IBindable<bool> InitialRoomsReceived { get; } = new Bindable<bool>(true);
IBindableList<Room> IRoomManager.Rooms => Rooms;
@ -37,13 +39,17 @@ namespace osu.Game.Tests.Visual.OnlinePlay
onSuccess?.Invoke(room);
}
public void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null) => onSuccess?.Invoke(room);
public void JoinRoom(Room room, string password, Action<Room> onSuccess = null, Action<string> onError = null)
{
JoinRoomRequested?.Invoke(room, password);
onSuccess?.Invoke(room);
}
public void PartRoom()
{
}
public void AddRooms(int count, RulesetInfo ruleset = null)
public void AddRooms(int count, RulesetInfo ruleset = null, bool withPassword = false)
{
for (int i = 0; i < count; i++)
{
@ -53,7 +59,8 @@ namespace osu.Game.Tests.Visual.OnlinePlay
Name = { Value = $"Room {i}" },
Host = { Value = new User { Username = "Host" } },
EndDate = { Value = DateTimeOffset.Now + TimeSpan.FromSeconds(10) },
Category = { Value = i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal }
Category = { Value = i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal },
Password = { Value = withPassword ? "password" : string.Empty }
};
if (ruleset != null)

View File

@ -3,6 +3,7 @@
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing.Input;
using osu.Game.Graphics.Cursor;
@ -34,9 +35,16 @@ namespace osu.Game.Tests.Visual
{
MenuCursorContainer cursorContainer;
CompositeDrawable mainContent =
(cursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both })
.WithChild(content = new OsuTooltipContainer(cursorContainer.Cursor) { RelativeSizeAxes = Axes.Both });
CompositeDrawable mainContent = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = cursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both, }
};
cursorContainer.Child = content = new OsuTooltipContainer(cursorContainer.Cursor)
{
RelativeSizeAxes = Axes.Both
};
if (CreateNestedActionContainer)
{