1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-15 23:53:21 +08:00

Merge branch 'master' into taiko-dho-pooling

This commit is contained in:
Dean Herbert 2020-12-21 16:24:40 +09:00 committed by GitHub
commit e135c324d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1814 additions and 41 deletions

View File

@ -0,0 +1,116 @@
// 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;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Screens.Multi.RealtimeMultiplayer.Participants;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
{
public class TestSceneParticipantsList : RealtimeMultiplayerTestScene
{
[SetUp]
public new void Setup() => Schedule(() =>
{
Child = new ParticipantsList
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
Size = new Vector2(380, 0.7f)
};
});
[Test]
public void TestAddUser()
{
AddAssert("one unique panel", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.User).Distinct().Count() == 1);
AddStep("add user", () => Client.AddUser(new User
{
Id = 3,
Username = "Second",
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}));
AddAssert("two unique panels", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.User).Distinct().Count() == 2);
}
[Test]
public void TestRemoveUser()
{
User secondUser = null;
AddStep("add a user", () =>
{
Client.AddUser(secondUser = new User
{
Id = 3,
Username = "Second",
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
});
});
AddStep("remove host", () => Client.RemoveUser(API.LocalUser.Value));
AddAssert("single panel is for second user", () => this.ChildrenOfType<ParticipantPanel>().Single().User.User == secondUser);
}
[Test]
public void TestToggleReadyState()
{
AddAssert("ready mark invisible", () => !this.ChildrenOfType<ReadyMark>().Single().IsPresent);
AddStep("make user ready", () => Client.ChangeState(MultiplayerUserState.Ready));
AddUntilStep("ready mark visible", () => this.ChildrenOfType<ReadyMark>().Single().IsPresent);
AddStep("make user idle", () => Client.ChangeState(MultiplayerUserState.Idle));
AddUntilStep("ready mark invisible", () => !this.ChildrenOfType<ReadyMark>().Single().IsPresent);
}
[Test]
public void TestCrownChangesStateWhenHostTransferred()
{
AddStep("add user", () => Client.AddUser(new User
{
Id = 3,
Username = "Second",
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}));
AddUntilStep("first user crown visible", () => this.ChildrenOfType<ParticipantPanel>().ElementAt(0).ChildrenOfType<SpriteIcon>().First().Alpha == 1);
AddUntilStep("second user crown hidden", () => this.ChildrenOfType<ParticipantPanel>().ElementAt(1).ChildrenOfType<SpriteIcon>().First().Alpha == 0);
AddStep("make second user host", () => Client.TransferHost(3));
AddUntilStep("first user crown hidden", () => this.ChildrenOfType<ParticipantPanel>().ElementAt(0).ChildrenOfType<SpriteIcon>().First().Alpha == 0);
AddUntilStep("second user crown visible", () => this.ChildrenOfType<ParticipantPanel>().ElementAt(1).ChildrenOfType<SpriteIcon>().First().Alpha == 1);
}
[Test]
public void TestManyUsers()
{
AddStep("add many users", () =>
{
for (int i = 0; i < 20; i++)
{
Client.AddUser(new User
{
Id = i,
Username = $"User {i}",
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
});
if (i % 2 == 0)
Client.ChangeUserState(i, MultiplayerUserState.Ready);
}
});
}
}
}

View File

@ -0,0 +1,135 @@
// 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.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Rulesets;
using osu.Game.Screens.Multi.RealtimeMultiplayer;
using osu.Game.Tests.Resources;
using osu.Game.Users;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
{
public class TestSceneRealtimeReadyButton : RealtimeMultiplayerTestScene
{
private RealtimeReadyButton button;
private BeatmapManager beatmaps;
private RulesetStore rulesets;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default));
beatmaps.Import(TestResources.GetTestBeatmapForImport(true)).Wait();
}
[SetUp]
public new void Setup() => Schedule(() =>
{
var beatmap = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First().Beatmaps.First();
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap);
Child = button = new RealtimeReadyButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
SelectedItem =
{
Value = new PlaylistItem
{
Beatmap = { Value = beatmap },
Ruleset = { Value = beatmap.Ruleset }
}
}
};
Client.AddUser(API.LocalUser.Value);
});
[Test]
public void TestToggleStateWhenNotHost()
{
AddStep("add second user as host", () =>
{
Client.AddUser(new User { Id = 2, Username = "Another user" });
Client.TransferHost(2);
});
addClickButtonStep();
AddAssert("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready);
addClickButtonStep();
AddAssert("user is idle", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle);
}
[TestCase(true)]
[TestCase(false)]
public void TestToggleStateWhenHost(bool allReady)
{
AddStep("setup", () =>
{
Client.TransferHost(Client.Room?.Users[0].UserID ?? 0);
if (!allReady)
Client.AddUser(new User { Id = 2, Username = "Another user" });
});
addClickButtonStep();
AddAssert("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready);
addClickButtonStep();
AddAssert("match started", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad);
}
[Test]
public void TestBecomeHostWhileReady()
{
AddStep("add host", () =>
{
Client.AddUser(new User { Id = 2, Username = "Another user" });
Client.TransferHost(2);
});
addClickButtonStep();
AddStep("make user host", () => Client.TransferHost(Client.Room?.Users[0].UserID ?? 0));
addClickButtonStep();
AddAssert("match started", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad);
}
[Test]
public void TestLoseHostWhileReady()
{
AddStep("setup", () =>
{
Client.TransferHost(Client.Room?.Users[0].UserID ?? 0);
Client.AddUser(new User { Id = 2, Username = "Another user" });
});
addClickButtonStep();
AddStep("transfer host", () => Client.TransferHost(Client.Room?.Users[1].UserID ?? 0));
addClickButtonStep();
AddAssert("match not started", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle);
}
private void addClickButtonStep() => AddStep("click button", () =>
{
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
}
}

View File

@ -0,0 +1,153 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Online.Multiplayer;
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
{
[HeadlessTest]
public class TestSceneRealtimeRoomManager : MultiplayerTestScene
{
private TestRealtimeRoomContainer roomContainer;
private TestRealtimeRoomManager roomManager => roomContainer.RoomManager;
[Test]
public void TestPollsInitially()
{
AddStep("create room manager with a few rooms", () =>
{
createRoomManager().With(d => d.OnLoadComplete += _ =>
{
roomManager.CreateRoom(new Room { Name = { Value = "1" } });
roomManager.PartRoom();
roomManager.CreateRoom(new Room { Name = { Value = "2" } });
roomManager.PartRoom();
roomManager.ClearRooms();
});
});
AddAssert("manager polled for rooms", () => roomManager.Rooms.Count == 2);
AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value);
}
[Test]
public void TestRoomsClearedOnDisconnection()
{
AddStep("create room manager with a few rooms", () =>
{
createRoomManager().With(d => d.OnLoadComplete += _ =>
{
roomManager.CreateRoom(new Room());
roomManager.PartRoom();
roomManager.CreateRoom(new Room());
roomManager.PartRoom();
});
});
AddStep("disconnect", () => roomContainer.Client.Disconnect());
AddAssert("rooms cleared", () => roomManager.Rooms.Count == 0);
AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value);
}
[Test]
public void TestRoomsPolledOnReconnect()
{
AddStep("create room manager with a few rooms", () =>
{
createRoomManager().With(d => d.OnLoadComplete += _ =>
{
roomManager.CreateRoom(new Room());
roomManager.PartRoom();
roomManager.CreateRoom(new Room());
roomManager.PartRoom();
});
});
AddStep("disconnect", () => roomContainer.Client.Disconnect());
AddStep("connect", () => roomContainer.Client.Connect());
AddAssert("manager polled for rooms", () => roomManager.Rooms.Count == 2);
AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value);
}
[Test]
public void TestRoomsNotPolledWhenJoined()
{
AddStep("create room manager with a room", () =>
{
createRoomManager().With(d => d.OnLoadComplete += _ =>
{
roomManager.CreateRoom(new Room());
roomManager.ClearRooms();
});
});
AddAssert("manager not polled for rooms", () => roomManager.Rooms.Count == 0);
AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value);
}
[Test]
public void TestMultiplayerRoomJoinedWhenCreated()
{
AddStep("create room manager with a room", () =>
{
createRoomManager().With(d => d.OnLoadComplete += _ =>
{
roomManager.CreateRoom(new Room());
});
});
AddAssert("multiplayer room joined", () => roomContainer.Client.Room != null);
}
[Test]
public void TestMultiplayerRoomPartedWhenAPIRoomParted()
{
AddStep("create room manager with a room", () =>
{
createRoomManager().With(d => d.OnLoadComplete += _ =>
{
roomManager.CreateRoom(new Room());
roomManager.PartRoom();
});
});
AddAssert("multiplayer room parted", () => roomContainer.Client.Room == null);
}
[Test]
public void TestMultiplayerRoomJoinedWhenAPIRoomJoined()
{
AddStep("create room manager with a room", () =>
{
createRoomManager().With(d => d.OnLoadComplete += _ =>
{
var r = new Room();
roomManager.CreateRoom(r);
roomManager.PartRoom();
roomManager.JoinRoom(r);
});
});
AddAssert("multiplayer room joined", () => roomContainer.Client.Room != null);
}
private TestRealtimeRoomManager createRoomManager()
{
Child = roomContainer = new TestRealtimeRoomContainer
{
RoomManager =
{
TimeBetweenListingPolls = { Value = 1 },
TimeBetweenSelectionPolls = { Value = 1 }
}
};
return roomManager;
}
}
}

View File

@ -7,16 +7,16 @@ namespace osu.Game.Online.API.Requests
{
public class GetBeatmapSetRequest : APIRequest<APIBeatmapSet>
{
private readonly int id;
private readonly BeatmapSetLookupType type;
public readonly int ID;
public readonly BeatmapSetLookupType Type;
public GetBeatmapSetRequest(int id, BeatmapSetLookupType type = BeatmapSetLookupType.SetId)
{
this.id = id;
this.type = type;
ID = id;
Type = type;
}
protected override string Target => type == BeatmapSetLookupType.SetId ? $@"beatmapsets/{id}" : $@"beatmapsets/lookup?beatmap_id={id}";
protected override string Target => Type == BeatmapSetLookupType.SetId ? $@"beatmapsets/{ID}" : $@"beatmapsets/lookup?beatmap_id={ID}";
}
public enum BeatmapSetLookupType

View File

@ -10,11 +10,11 @@ namespace osu.Game.Online.Multiplayer
{
public class CreateRoomRequest : APIRequest<APICreatedRoom>
{
private readonly Room room;
public readonly Room Room;
public CreateRoomRequest(Room room)
{
this.room = room;
Room = room;
}
protected override WebRequest CreateWebRequest()
@ -24,7 +24,7 @@ namespace osu.Game.Online.Multiplayer
req.ContentType = "application/json";
req.Method = HttpMethod.Post;
req.AddRaw(JsonConvert.SerializeObject(room));
req.AddRaw(JsonConvert.SerializeObject(Room));
return req;
}

View File

@ -7,13 +7,13 @@ namespace osu.Game.Online.Multiplayer
{
public class GetRoomRequest : APIRequest<Room>
{
private readonly int roomId;
public readonly int RoomId;
public GetRoomRequest(int roomId)
{
this.roomId = roomId;
RoomId = roomId;
}
protected override string Target => $"rooms/{roomId}";
protected override string Target => $"rooms/{RoomId}";
}
}

View File

@ -0,0 +1,408 @@
// 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.
#nullable enable
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.RoomStatuses;
using osu.Game.Rulesets;
using osu.Game.Users;
using osu.Game.Utils;
namespace osu.Game.Online.RealtimeMultiplayer
{
public abstract class StatefulMultiplayerClient : Component, IMultiplayerClient, IMultiplayerRoomServer
{
/// <summary>
/// Invoked when any change occurs to the multiplayer room.
/// </summary>
public event Action? RoomChanged;
/// <summary>
/// Invoked when the multiplayer server requests the current beatmap to be loaded into play.
/// </summary>
public event Action? LoadRequested;
/// <summary>
/// Invoked when the multiplayer server requests gameplay to be started.
/// </summary>
public event Action? MatchStarted;
/// <summary>
/// Invoked when the multiplayer server has finished collating results.
/// </summary>
public event Action? ResultsReady;
/// <summary>
/// Whether the <see cref="StatefulMultiplayerClient"/> is currently connected.
/// </summary>
public abstract IBindable<bool> IsConnected { get; }
/// <summary>
/// The joined <see cref="MultiplayerRoom"/>.
/// </summary>
public MultiplayerRoom? Room { get; private set; }
/// <summary>
/// The users currently in gameplay.
/// </summary>
public readonly BindableList<int> PlayingUsers = new BindableList<int>();
[Resolved]
private UserLookupCache userLookupCache { get; set; } = null!;
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
private Room? apiRoom;
// Todo: This is temporary, until the multiplayer server returns the item id on match start or otherwise.
private int playlistItemId;
/// <summary>
/// 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)
{
if (Room != null)
throw new InvalidOperationException("Cannot join a multiplayer room while already in one.");
Debug.Assert(room.RoomID.Value != null);
apiRoom = room;
playlistItemId = room.Playlist.SingleOrDefault()?.ID ?? 0;
Room = await JoinRoom(room.RoomID.Value.Value);
Debug.Assert(Room != null);
foreach (var user in Room.Users)
await PopulateUser(user);
updateLocalRoomSettings(Room.Settings);
}
/// <summary>
/// Joins the <see cref="MultiplayerRoom"/> with a given ID.
/// </summary>
/// <param name="roomId">The room ID.</param>
/// <returns>The joined <see cref="MultiplayerRoom"/>.</returns>
protected abstract Task<MultiplayerRoom> JoinRoom(long roomId);
public virtual Task LeaveRoom()
{
if (Room == null)
return Task.CompletedTask;
apiRoom = null;
Room = null;
Schedule(() => RoomChanged?.Invoke());
return Task.CompletedTask;
}
/// <summary>
/// Change the current <see cref="MultiplayerRoom"/> settings.
/// </summary>
/// <remarks>
/// A room must be joined for this to have any effect.
/// </remarks>
/// <param name="name">The new room name, if any.</param>
/// <param name="item">The new room playlist item, if any.</param>
public void ChangeSettings(Optional<string> name = default, Optional<PlaylistItem> item = default)
{
if (Room == null)
return;
// A dummy playlist item filled with the current room settings (except mods).
var existingPlaylistItem = new PlaylistItem
{
Beatmap =
{
Value = new BeatmapInfo
{
OnlineBeatmapID = Room.Settings.BeatmapID,
MD5Hash = Room.Settings.BeatmapChecksum
}
},
RulesetID = Room.Settings.RulesetID
};
ChangeSettings(new MultiplayerRoomSettings
{
Name = name.GetOr(Room.Settings.Name),
BeatmapID = item.GetOr(existingPlaylistItem).BeatmapID,
BeatmapChecksum = item.GetOr(existingPlaylistItem).Beatmap.Value.MD5Hash,
RulesetID = item.GetOr(existingPlaylistItem).RulesetID,
Mods = item.HasValue ? item.Value.AsNonNull().RequiredMods.Select(m => new APIMod(m)).ToList() : Room.Settings.Mods
});
}
public abstract Task TransferHost(int userId);
public abstract Task ChangeSettings(MultiplayerRoomSettings settings);
public abstract Task ChangeState(MultiplayerUserState newState);
public abstract Task StartMatch();
Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state)
{
if (Room == null)
return Task.CompletedTask;
Schedule(() =>
{
if (Room == null)
return;
Debug.Assert(apiRoom != null);
Room.State = state;
switch (state)
{
case MultiplayerRoomState.Open:
apiRoom.Status.Value = new RoomStatusOpen();
break;
case MultiplayerRoomState.Playing:
apiRoom.Status.Value = new RoomStatusPlaying();
break;
case MultiplayerRoomState.Closed:
apiRoom.Status.Value = new RoomStatusEnded();
break;
}
RoomChanged?.Invoke();
});
return Task.CompletedTask;
}
async Task IMultiplayerClient.UserJoined(MultiplayerRoomUser user)
{
if (Room == null)
return;
await PopulateUser(user);
Schedule(() =>
{
if (Room == null)
return;
Room.Users.Add(user);
RoomChanged?.Invoke();
});
}
Task IMultiplayerClient.UserLeft(MultiplayerRoomUser user)
{
if (Room == null)
return Task.CompletedTask;
Schedule(() =>
{
if (Room == null)
return;
Room.Users.Remove(user);
PlayingUsers.Remove(user.UserID);
RoomChanged?.Invoke();
});
return Task.CompletedTask;
}
Task IMultiplayerClient.HostChanged(int userId)
{
if (Room == null)
return Task.CompletedTask;
Schedule(() =>
{
if (Room == null)
return;
Debug.Assert(apiRoom != null);
var user = Room.Users.FirstOrDefault(u => u.UserID == userId);
Room.Host = user;
apiRoom.Host.Value = user?.User;
RoomChanged?.Invoke();
});
return Task.CompletedTask;
}
Task IMultiplayerClient.SettingsChanged(MultiplayerRoomSettings newSettings)
{
updateLocalRoomSettings(newSettings);
return Task.CompletedTask;
}
Task IMultiplayerClient.UserStateChanged(int userId, MultiplayerUserState state)
{
if (Room == null)
return Task.CompletedTask;
Schedule(() =>
{
if (Room == null)
return;
Room.Users.Single(u => u.UserID == userId).State = state;
if (state != MultiplayerUserState.Playing)
PlayingUsers.Remove(userId);
RoomChanged?.Invoke();
});
return Task.CompletedTask;
}
Task IMultiplayerClient.LoadRequested()
{
if (Room == null)
return Task.CompletedTask;
Schedule(() =>
{
if (Room == null)
return;
LoadRequested?.Invoke();
});
return Task.CompletedTask;
}
Task IMultiplayerClient.MatchStarted()
{
if (Room == null)
return Task.CompletedTask;
Schedule(() =>
{
if (Room == null)
return;
PlayingUsers.AddRange(Room.Users.Where(u => u.State == MultiplayerUserState.Playing).Select(u => u.UserID));
MatchStarted?.Invoke();
});
return Task.CompletedTask;
}
Task IMultiplayerClient.ResultsReady()
{
if (Room == null)
return Task.CompletedTask;
Schedule(() =>
{
if (Room == null)
return;
ResultsReady?.Invoke();
});
return Task.CompletedTask;
}
/// <summary>
/// Populates the <see cref="User"/> for a given <see cref="MultiplayerRoomUser"/>.
/// </summary>
/// <param name="multiplayerUser">The <see cref="MultiplayerRoomUser"/> to populate.</param>
protected async Task PopulateUser(MultiplayerRoomUser multiplayerUser) => multiplayerUser.User ??= await userLookupCache.GetUserAsync(multiplayerUser.UserID);
/// <summary>
/// Updates the local room settings with the given <see cref="MultiplayerRoomSettings"/>.
/// </summary>
/// <remarks>
/// This updates both the joined <see cref="MultiplayerRoom"/> and the respective API <see cref="Room"/>.
/// </remarks>
/// <param name="settings">The new <see cref="MultiplayerRoomSettings"/> to update from.</param>
private void updateLocalRoomSettings(MultiplayerRoomSettings settings)
{
if (Room == null)
return;
// Update a few properties of the room instantaneously.
Schedule(() =>
{
if (Room == null)
return;
Debug.Assert(apiRoom != null);
Room.Settings = settings;
apiRoom.Name.Value = Room.Settings.Name;
// The playlist update is delayed until an online beatmap lookup (below) succeeds.
// In-order for the client to not display an outdated beatmap, the playlist is forcefully cleared here.
apiRoom.Playlist.Clear();
RoomChanged?.Invoke();
});
var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId);
req.Success += res => updatePlaylist(settings, res);
api.Queue(req);
}
private void updatePlaylist(MultiplayerRoomSettings settings, APIBeatmapSet onlineSet)
{
if (Room == null || !Room.Settings.Equals(settings))
return;
Debug.Assert(apiRoom != null);
var beatmapSet = onlineSet.ToBeatmapSet(rulesets);
var beatmap = beatmapSet.Beatmaps.Single(b => b.OnlineBeatmapID == settings.BeatmapID);
beatmap.MD5Hash = settings.BeatmapChecksum;
var ruleset = rulesets.GetRuleset(settings.RulesetID).CreateInstance();
var mods = settings.Mods.Select(m => m.ToMod(ruleset));
PlaylistItem playlistItem = new PlaylistItem
{
ID = playlistItemId,
Beatmap = { Value = beatmap },
Ruleset = { Value = ruleset.RulesetInfo },
};
playlistItem.RequiredMods.AddRange(mods);
apiRoom.Playlist.Clear(); // Clearing should be unnecessary, but here for sanity.
apiRoom.Playlist.Add(playlistItem);
}
}
}

View File

@ -11,28 +11,22 @@ using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
namespace osu.Game.Screens.Multi.Match.Components
namespace osu.Game.Screens.Multi.Components
{
public class ReadyButton : TriangleButton
public abstract class ReadyButton : TriangleButton
{
public readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
[Resolved(typeof(Room), nameof(Room.EndDate))]
private Bindable<DateTimeOffset> endDate { get; set; }
public new readonly BindableBool Enabled = new BindableBool();
[Resolved]
private IBindable<WorkingBeatmap> gameBeatmap { get; set; }
protected IBindable<WorkingBeatmap> GameBeatmap { get; private set; }
[Resolved]
private BeatmapManager beatmaps { get; set; }
private bool hasBeatmap;
public ReadyButton()
{
Text = "Start";
}
private IBindable<WeakReference<BeatmapSetInfo>> managerUpdated;
private IBindable<WeakReference<BeatmapSetInfo>> managerRemoved;
@ -45,10 +39,6 @@ namespace osu.Game.Screens.Multi.Match.Components
managerRemoved.BindValueChanged(beatmapRemoved);
SelectedItem.BindValueChanged(item => updateSelectedItem(item.NewValue), true);
BackgroundColour = colours.Green;
Triangles.ColourDark = colours.Green;
Triangles.ColourLight = colours.GreenLight;
}
private void updateSelectedItem(PlaylistItem item)
@ -94,15 +84,13 @@ namespace osu.Game.Screens.Multi.Match.Components
private void updateEnabledState()
{
if (gameBeatmap.Value == null || SelectedItem.Value == null)
if (GameBeatmap.Value == null || SelectedItem.Value == null)
{
Enabled.Value = false;
base.Enabled.Value = false;
return;
}
bool hasEnoughTime = DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(gameBeatmap.Value.Track.Length) < endDate.Value;
Enabled.Value = hasBeatmap && hasEnoughTime;
base.Enabled.Value = hasBeatmap && Enabled.Value;
}
}
}

View File

@ -28,6 +28,9 @@ namespace osu.Game.Screens.Multi.Components
public IBindableList<Room> Rooms => rooms;
protected IBindable<Room> JoinedRoom => joinedRoom;
private readonly Bindable<Room> joinedRoom = new Bindable<Room>();
[Resolved]
private RulesetStore rulesets { get; set; }
@ -37,8 +40,6 @@ namespace osu.Game.Screens.Multi.Components
[Resolved]
private IAPIProvider api { get; set; }
private Room joinedRoom;
protected RoomManager()
{
RelativeSizeAxes = Axes.Both;
@ -64,7 +65,7 @@ namespace osu.Game.Screens.Multi.Components
req.Success += result =>
{
joinedRoom = room;
joinedRoom.Value = room;
update(room, result);
addRoom(room);
@ -93,7 +94,7 @@ namespace osu.Game.Screens.Multi.Components
currentJoinRoomRequest.Success += () =>
{
joinedRoom = room;
joinedRoom.Value = room;
onSuccess?.Invoke(room);
};
@ -107,15 +108,15 @@ namespace osu.Game.Screens.Multi.Components
api.Queue(currentJoinRoomRequest);
}
public void PartRoom()
public virtual void PartRoom()
{
currentJoinRoomRequest?.Cancel();
if (joinedRoom == null)
if (JoinedRoom.Value == null)
return;
api.Queue(new PartRoomRequest(joinedRoom));
joinedRoom = null;
api.Queue(new PartRoomRequest(joinedRoom.Value));
joinedRoom.Value = null;
}
private readonly HashSet<int> ignoredRooms = new HashSet<int>();
@ -124,8 +125,7 @@ namespace osu.Game.Screens.Multi.Components
{
if (received == null)
{
rooms.Clear();
initialRoomsReceived.Value = false;
ClearRooms();
return;
}
@ -165,6 +165,14 @@ namespace osu.Game.Screens.Multi.Components
initialRoomsReceived.Value = true;
}
protected void RemoveRoom(Room room) => rooms.Remove(room);
protected void ClearRooms()
{
rooms.Clear();
initialRoomsReceived.Value = false;
}
/// <summary>
/// Updates a local <see cref="Room"/> with a remote copy.
/// </summary>

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens.Multi.Timeshift;
using osuTK;
namespace osu.Game.Screens.Multi.Match.Components
@ -31,7 +32,7 @@ namespace osu.Game.Screens.Multi.Match.Components
InternalChildren = new[]
{
background = new Box { RelativeSizeAxes = Axes.Both },
new ReadyButton
new TimeshiftReadyButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,

View File

@ -0,0 +1,189 @@
// 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.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Users;
using osu.Game.Users.Drawables;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants
{
public class ParticipantPanel : RealtimeRoomComposite, IHasContextMenu
{
public readonly MultiplayerRoomUser User;
[Resolved]
private IAPIProvider api { get; set; }
private ReadyMark readyMark;
private SpriteIcon crown;
public ParticipantPanel(MultiplayerRoomUser user)
{
User = user;
RelativeSizeAxes = Axes.X;
Height = 40;
}
[BackgroundDependencyLoader]
private void load()
{
Debug.Assert(User.User != null);
var backgroundColour = Color4Extensions.FromHex("#33413C");
InternalChildren = new Drawable[]
{
crown = new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Icon = FontAwesome.Solid.Crown,
Size = new Vector2(14),
Colour = Color4Extensions.FromHex("#F7E65D"),
Alpha = 0
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = 24 },
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 5,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = backgroundColour
},
new UserCoverBackground
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.Both,
Width = 0.75f,
User = User.User,
Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0), Color4.White.Opacity(0.25f))
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Spacing = new Vector2(10),
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
new UpdateableAvatar
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
User = User.User
},
new UpdateableFlag
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(30, 20),
Country = User.User.Country
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 18),
Text = User.User.Username
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: 14),
Text = User.User.CurrentModeRank != null ? $"#{User.User.CurrentModeRank}" : string.Empty
}
}
},
readyMark = new ReadyMark
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Margin = new MarginPadding { Right = 10 },
Alpha = 0
}
}
}
}
};
}
protected override void OnRoomChanged()
{
base.OnRoomChanged();
if (Room == null)
return;
const double fade_time = 50;
if (User.State == MultiplayerUserState.Ready)
readyMark.FadeIn(fade_time);
else
readyMark.FadeOut(fade_time);
if (Room.Host?.Equals(User) == true)
crown.FadeIn(fade_time);
else
crown.FadeOut(fade_time);
}
public MenuItem[] ContextMenuItems
{
get
{
if (Room == null)
return null;
// If the local user is targetted.
if (User.UserID == api.LocalUser.Value.Id)
return null;
// If the local user is not the host of the room.
if (Room.Host?.UserID != api.LocalUser.Value.Id)
return null;
int targetUser = User.UserID;
return new MenuItem[]
{
new OsuMenuItem("Give host", MenuItemType.Standard, () =>
{
// Ensure the local user is still host.
if (Room.Host?.UserID != api.LocalUser.Value.Id)
return;
Client.TransferHost(targetUser);
})
};
}
}
}
}

View File

@ -0,0 +1,56 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osuTK;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants
{
public class ParticipantsList : RealtimeRoomComposite
{
private FillFlowContainer<ParticipantPanel> panels;
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
Child = panels = new FillFlowContainer<ParticipantPanel>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 2)
}
}
};
}
protected override void OnRoomChanged()
{
base.OnRoomChanged();
if (Room == null)
panels.Clear();
else
{
// Remove panels for users no longer in the room.
panels.RemoveAll(p => !Room.Users.Contains(p.User));
// Add panels for all users new to the room.
foreach (var user in Room.Users.Except(panels.Select(p => p.User)))
panels.Add(new ParticipantPanel(user));
}
}
}
}

View File

@ -0,0 +1,51 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants
{
public class ReadyMark : CompositeDrawable
{
public ReadyMark()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(5),
Children = new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 12),
Text = "ready",
Colour = Color4Extensions.FromHex("#DDFFFF")
},
new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Icon = FontAwesome.Solid.CheckCircle,
Size = new Vector2(12),
Colour = Color4Extensions.FromHex("#AADD00")
}
}
};
}
}
}

View File

@ -0,0 +1,123 @@
// 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.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Screens.Multi.Components;
using osuTK;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer
{
public class RealtimeReadyButton : RealtimeRoomComposite
{
public Bindable<PlaylistItem> SelectedItem => button.SelectedItem;
[Resolved]
private IAPIProvider api { get; set; }
[CanBeNull]
private MultiplayerRoomUser localUser;
[Resolved]
private OsuColour colours { get; set; }
private readonly ButtonWithTrianglesExposed button;
public RealtimeReadyButton()
{
InternalChild = button = new ButtonWithTrianglesExposed
{
RelativeSizeAxes = Axes.Both,
Size = Vector2.One,
Enabled = { Value = true },
Action = onClick
};
}
protected override void OnRoomChanged()
{
base.OnRoomChanged();
localUser = Room?.Users.Single(u => u.User?.Id == api.LocalUser.Value.Id);
button.Enabled.Value = Client.Room?.State == MultiplayerRoomState.Open;
updateState();
}
private void updateState()
{
if (localUser == null)
return;
Debug.Assert(Room != null);
switch (localUser.State)
{
case MultiplayerUserState.Idle:
button.Text = "Ready";
updateButtonColour(true);
break;
case MultiplayerUserState.Ready:
if (Room?.Host?.Equals(localUser) == true)
{
int countReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready);
button.Text = $"Start match ({countReady} / {Room.Users.Count} ready)";
updateButtonColour(true);
}
else
{
button.Text = "Waiting for host...";
updateButtonColour(false);
}
break;
}
}
private void updateButtonColour(bool green)
{
if (green)
{
button.BackgroundColour = colours.Green;
button.Triangles.ColourDark = colours.Green;
button.Triangles.ColourLight = colours.GreenLight;
}
else
{
button.BackgroundColour = colours.YellowDark;
button.Triangles.ColourDark = colours.YellowDark;
button.Triangles.ColourLight = colours.Yellow;
}
}
private void onClick()
{
if (localUser == null)
return;
if (localUser.State == MultiplayerUserState.Idle)
Client.ChangeState(MultiplayerUserState.Ready);
else
{
if (Room?.Host?.Equals(localUser) == true)
Client.StartMatch();
else
Client.ChangeState(MultiplayerUserState.Idle);
}
}
private class ButtonWithTrianglesExposed : ReadyButton
{
public new Triangles Triangles => base.Triangles;
}
}
}

View File

@ -0,0 +1,38 @@
// 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 JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Game.Online.RealtimeMultiplayer;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer
{
public abstract class RealtimeRoomComposite : MultiplayerComposite
{
[CanBeNull]
protected MultiplayerRoom Room => Client.Room;
[Resolved]
protected StatefulMultiplayerClient Client { get; private set; }
protected override void LoadComplete()
{
base.LoadComplete();
Client.RoomChanged += OnRoomChanged;
OnRoomChanged();
}
protected virtual void OnRoomChanged()
{
}
protected override void Dispose(bool isDisposing)
{
if (Client != null)
Client.RoomChanged -= OnRoomChanged;
base.Dispose(isDisposing);
}
}
}

View File

@ -0,0 +1,144 @@
// 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.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Screens.Multi.Components;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer
{
public class RealtimeRoomManager : RoomManager
{
[Resolved]
private StatefulMultiplayerClient multiplayerClient { get; set; }
public readonly Bindable<double> TimeBetweenListingPolls = new Bindable<double>();
public readonly Bindable<double> TimeBetweenSelectionPolls = new Bindable<double>();
private readonly IBindable<bool> isConnected = new Bindable<bool>();
private readonly Bindable<bool> allowPolling = new Bindable<bool>();
private ListingPollingComponent listingPollingComponent;
protected override void LoadComplete()
{
base.LoadComplete();
isConnected.BindTo(multiplayerClient.IsConnected);
isConnected.BindValueChanged(_ => Schedule(updatePolling));
JoinedRoom.BindValueChanged(_ => updatePolling());
updatePolling();
}
public override void CreateRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
=> base.CreateRoom(room, r => joinMultiplayerRoom(r, onSuccess), onError);
public override void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
=> base.JoinRoom(room, r => joinMultiplayerRoom(r, onSuccess), onError);
public override void PartRoom()
{
if (JoinedRoom.Value == null)
return;
var joinedRoom = JoinedRoom.Value;
base.PartRoom();
multiplayerClient.LeaveRoom();
// Todo: This is not the way to do this. Basically when we're the only participant and the room closes, there's no way to know if this is actually the case.
// This is delayed one frame because upon exiting the match subscreen, multiplayer updates the polling rate and messes with polling.
Schedule(() =>
{
RemoveRoom(joinedRoom);
listingPollingComponent.PollImmediately();
});
}
private void joinMultiplayerRoom(Room room, Action<Room> onSuccess = null)
{
Debug.Assert(room.RoomID.Value != null);
var joinTask = multiplayerClient.JoinRoom(room);
joinTask.ContinueWith(_ => onSuccess?.Invoke(room), TaskContinuationOptions.OnlyOnRanToCompletion);
joinTask.ContinueWith(t =>
{
PartRoom();
if (t.Exception != null)
Logger.Error(t.Exception, "Failed to join multiplayer room.");
}, TaskContinuationOptions.NotOnRanToCompletion);
}
private void updatePolling()
{
if (!isConnected.Value)
ClearRooms();
// Don't poll when not connected or when a room has been joined.
allowPolling.Value = isConnected.Value && JoinedRoom.Value == null;
}
protected override IEnumerable<RoomPollingComponent> CreatePollingComponents() => new RoomPollingComponent[]
{
listingPollingComponent = new RealtimeListingPollingComponent
{
TimeBetweenPolls = { BindTarget = TimeBetweenListingPolls },
AllowPolling = { BindTarget = allowPolling }
},
new RealtimeSelectionPollingComponent
{
TimeBetweenPolls = { BindTarget = TimeBetweenSelectionPolls },
AllowPolling = { BindTarget = allowPolling }
}
};
private class RealtimeListingPollingComponent : ListingPollingComponent
{
public readonly IBindable<bool> AllowPolling = new Bindable<bool>();
protected override void LoadComplete()
{
base.LoadComplete();
AllowPolling.BindValueChanged(allowPolling =>
{
if (!allowPolling.NewValue)
return;
if (IsLoaded)
PollImmediately();
});
}
protected override Task Poll() => !AllowPolling.Value ? Task.CompletedTask : base.Poll();
}
private class RealtimeSelectionPollingComponent : SelectionPollingComponent
{
public readonly IBindable<bool> AllowPolling = new Bindable<bool>();
protected override void LoadComplete()
{
base.LoadComplete();
AllowPolling.BindValueChanged(allowPolling =>
{
if (!allowPolling.NewValue)
return;
if (IsLoaded)
PollImmediately();
});
}
protected override Task Poll() => !AllowPolling.Value ? Task.CompletedTask : base.Poll();
}
}
}

View File

@ -0,0 +1,38 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Graphics;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens.Multi.Components;
namespace osu.Game.Screens.Multi.Timeshift
{
public class TimeshiftReadyButton : ReadyButton
{
[Resolved(typeof(Room), nameof(Room.EndDate))]
private Bindable<DateTimeOffset> endDate { get; set; }
public TimeshiftReadyButton()
{
Text = "Start";
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
BackgroundColour = colours.Green;
Triangles.ColourDark = colours.Green;
Triangles.ColourLight = colours.GreenLight;
}
protected override void Update()
{
base.Update();
Enabled.Value = DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(GameBeatmap.Value.Track.Length) < endDate.Value;
}
}
}

View File

@ -0,0 +1,50 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Screens.Multi.Lounge.Components;
using osu.Game.Screens.Multi.RealtimeMultiplayer;
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
{
public class RealtimeMultiplayerTestScene : MultiplayerTestScene
{
[Cached(typeof(StatefulMultiplayerClient))]
public TestRealtimeMultiplayerClient Client { get; }
[Cached(typeof(RealtimeRoomManager))]
public TestRealtimeRoomManager RoomManager { get; }
[Cached]
public Bindable<FilterCriteria> Filter { get; }
protected override Container<Drawable> Content => content;
private readonly TestRealtimeRoomContainer content;
private readonly bool joinRoom;
public RealtimeMultiplayerTestScene(bool joinRoom = true)
{
this.joinRoom = joinRoom;
base.Content.Add(content = new TestRealtimeRoomContainer { RelativeSizeAxes = Axes.Both });
Client = content.Client;
RoomManager = content.RoomManager;
Filter = content.Filter;
}
[SetUp]
public new void Setup() => Schedule(() =>
{
RoomManager.Schedule(() => RoomManager.PartRoom());
if (joinRoom)
RoomManager.Schedule(() => RoomManager.CreateRoom(Room));
});
}
}

View File

@ -0,0 +1,119 @@
// 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.
#nullable enable
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Online.API;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
{
public class TestRealtimeMultiplayerClient : StatefulMultiplayerClient
{
public override IBindable<bool> IsConnected => isConnected;
private readonly Bindable<bool> isConnected = new Bindable<bool>(true);
[Resolved]
private IAPIProvider api { get; set; } = null!;
public void Connect() => isConnected.Value = true;
public void Disconnect() => isConnected.Value = false;
public void AddUser(User user) => ((IMultiplayerClient)this).UserJoined(new MultiplayerRoomUser(user.Id) { User = user });
public void RemoveUser(User user)
{
Debug.Assert(Room != null);
((IMultiplayerClient)this).UserLeft(Room.Users.Single(u => u.User == user));
Schedule(() =>
{
if (Room.Users.Any())
TransferHost(Room.Users.First().UserID);
});
}
public void ChangeUserState(int userId, MultiplayerUserState newState)
{
Debug.Assert(Room != null);
((IMultiplayerClient)this).UserStateChanged(userId, newState);
Schedule(() =>
{
switch (newState)
{
case MultiplayerUserState.Loaded:
if (Room.Users.All(u => u.State != MultiplayerUserState.WaitingForLoad))
{
foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.Loaded))
ChangeUserState(u.UserID, MultiplayerUserState.Playing);
((IMultiplayerClient)this).MatchStarted();
}
break;
case MultiplayerUserState.FinishedPlay:
if (Room.Users.All(u => u.State != MultiplayerUserState.Playing))
{
foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.FinishedPlay))
ChangeUserState(u.UserID, MultiplayerUserState.Results);
((IMultiplayerClient)this).ResultsReady();
}
break;
}
});
}
protected override Task<MultiplayerRoom> JoinRoom(long roomId)
{
var user = new MultiplayerRoomUser(api.LocalUser.Value.Id) { User = api.LocalUser.Value };
var room = new MultiplayerRoom(roomId);
room.Users.Add(user);
if (room.Users.Count == 1)
room.Host = user;
return Task.FromResult(room);
}
public override Task TransferHost(int userId) => ((IMultiplayerClient)this).HostChanged(userId);
public override async Task ChangeSettings(MultiplayerRoomSettings settings)
{
Debug.Assert(Room != null);
await ((IMultiplayerClient)this).SettingsChanged(settings);
foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready))
ChangeUserState(user.UserID, MultiplayerUserState.Idle);
}
public override Task ChangeState(MultiplayerUserState newState)
{
ChangeUserState(api.LocalUser.Value.Id, newState);
return Task.CompletedTask;
}
public override Task StartMatch()
{
Debug.Assert(Room != null);
foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready))
ChangeUserState(user.UserID, MultiplayerUserState.WaitingForLoad);
return ((IMultiplayerClient)this).LoadRequested();
}
}
}

View File

@ -0,0 +1,40 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Screens.Multi.Lounge.Components;
using osu.Game.Screens.Multi.RealtimeMultiplayer;
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
{
public class TestRealtimeRoomContainer : Container
{
protected override Container<Drawable> Content => content;
private readonly Container content;
[Cached(typeof(StatefulMultiplayerClient))]
public readonly TestRealtimeMultiplayerClient Client;
[Cached(typeof(RealtimeRoomManager))]
public readonly TestRealtimeRoomManager RoomManager;
[Cached]
public readonly Bindable<FilterCriteria> Filter = new Bindable<FilterCriteria>(new FilterCriteria());
public TestRealtimeRoomContainer()
{
RelativeSizeAxes = Axes.Both;
AddRangeInternal(new Drawable[]
{
Client = new TestRealtimeMultiplayerClient(),
RoomManager = new TestRealtimeRoomManager(),
content = new Container { RelativeSizeAxes = Axes.Both }
});
}
}
}

View File

@ -0,0 +1,116 @@
// 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.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Multi.Lounge.Components;
using osu.Game.Screens.Multi.RealtimeMultiplayer;
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
{
public class TestRealtimeRoomManager : RealtimeRoomManager
{
[Resolved]
private IAPIProvider api { get; set; }
[Resolved]
private OsuGameBase game { get; set; }
[Cached]
public readonly Bindable<FilterCriteria> Filter = new Bindable<FilterCriteria>(new FilterCriteria());
private readonly List<Room> rooms = new List<Room>();
protected override void LoadComplete()
{
base.LoadComplete();
int currentScoreId = 0;
int currentRoomId = 0;
((DummyAPIAccess)api).HandleRequest = req =>
{
switch (req)
{
case CreateRoomRequest createRoomRequest:
var createdRoom = new APICreatedRoom();
createdRoom.CopyFrom(createRoomRequest.Room);
createdRoom.RoomID.Value ??= currentRoomId++;
rooms.Add(createdRoom);
createRoomRequest.TriggerSuccess(createdRoom);
break;
case JoinRoomRequest joinRoomRequest:
joinRoomRequest.TriggerSuccess();
break;
case PartRoomRequest partRoomRequest:
partRoomRequest.TriggerSuccess();
break;
case GetRoomsRequest getRoomsRequest:
var roomsWithoutParticipants = new List<Room>();
foreach (var r in rooms)
{
var newRoom = new Room();
newRoom.CopyFrom(r);
newRoom.RecentParticipants.Clear();
roomsWithoutParticipants.Add(newRoom);
}
getRoomsRequest.TriggerSuccess(roomsWithoutParticipants);
break;
case GetRoomRequest getRoomRequest:
getRoomRequest.TriggerSuccess(rooms.Single(r => r.RoomID.Value == getRoomRequest.RoomId));
break;
case GetBeatmapSetRequest getBeatmapSetRequest:
var onlineReq = new GetBeatmapSetRequest(getBeatmapSetRequest.ID, getBeatmapSetRequest.Type);
onlineReq.Success += res => getBeatmapSetRequest.TriggerSuccess(res);
onlineReq.Failure += e => getBeatmapSetRequest.TriggerFailure(e);
// Get the online API from the game's dependencies.
game.Dependencies.Get<IAPIProvider>().Queue(onlineReq);
break;
case CreateRoomScoreRequest createRoomScoreRequest:
createRoomScoreRequest.TriggerSuccess(new APIScoreToken { ID = 1 });
break;
case SubmitRoomScoreRequest submitRoomScoreRequest:
submitRoomScoreRequest.TriggerSuccess(new MultiplayerScore
{
ID = currentScoreId++,
Accuracy = 1,
EndedAt = DateTimeOffset.Now,
Passed = true,
Rank = ScoreRank.S,
MaxCombo = 1000,
TotalScore = 1000000,
User = api.LocalUser.Value,
Statistics = new Dictionary<HitResult, int>()
});
break;
}
};
}
public new void ClearRooms() => base.ClearRooms();
public new void Schedule(Action action) => base.Schedule(action);
}
}