diff --git a/osu.Android.props b/osu.Android.props
index 2a08cb7867..fc01f9bf1d 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
-
+
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs
index 9684cbb167..5d662c18d3 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs
@@ -5,11 +5,11 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Utils;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
-using osuTK;
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
@@ -56,31 +56,30 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
(animation as IFramedAnimation)?.GotoFrame(0);
+ this.FadeInFromZero(20, Easing.Out)
+ .Then().Delay(160)
+ .FadeOutFromOne(40, Easing.In);
+
switch (result)
{
case HitResult.None:
break;
case HitResult.Miss:
- animation.ScaleTo(1.6f);
- animation.ScaleTo(1, 100, Easing.In);
-
- animation.MoveTo(Vector2.Zero);
- animation.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
+ animation.ScaleTo(1.2f).Then().ScaleTo(1, 100, Easing.Out);
animation.RotateTo(0);
- animation.RotateTo(40, 800, Easing.InQuint);
-
- this.FadeOutFromOne(800);
+ animation.RotateTo(RNG.NextSingle(-5.73f, 5.73f), 100, Easing.Out);
break;
default:
- animation.ScaleTo(0.8f);
- animation.ScaleTo(1, 250, Easing.OutElastic);
-
- animation.Delay(50).ScaleTo(0.75f, 250);
-
- this.Delay(50).FadeOut(200);
+ animation.ScaleTo(0.8f)
+ .Then().ScaleTo(1, 40)
+ // this is actually correct to match stable; there were overlapping transforms.
+ .Then().ScaleTo(0.85f)
+ .Then().ScaleTo(0.7f, 40)
+ .Then().Delay(100)
+ .Then().ScaleTo(0.4f, 40, Easing.In);
break;
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
index ec8c68005f..660e1844aa 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
@@ -157,10 +157,16 @@ namespace osu.Game.Rulesets.Osu.Edit
foreach (var h in hitObjects)
{
- h.Position = new Vector2(
- quad.TopLeft.X + (h.X - quad.TopLeft.X) / quad.Width * (quad.Width + scale.X),
- quad.TopLeft.Y + (h.Y - quad.TopLeft.Y) / quad.Height * (quad.Height + scale.Y)
- );
+ var newPosition = h.Position;
+
+ // guard against no-ops and NaN.
+ if (scale.X != 0 && quad.Width > 0)
+ newPosition.X = quad.TopLeft.X + (h.X - quad.TopLeft.X) / quad.Width * (quad.Width + scale.X);
+
+ if (scale.Y != 0 && quad.Height > 0)
+ newPosition.Y = quad.TopLeft.Y + (h.Y - quad.TopLeft.Y) / quad.Height * (quad.Height + scale.Y);
+
+ h.Position = newPosition;
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs
index 234374ee2b..1fcae9c709 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs
@@ -11,7 +11,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens.Multi;
-using osu.Game.Screens.Multi.Match.Components;
+using osu.Game.Screens.Multi.Timeshift;
namespace osu.Game.Tests.Visual.Multiplayer
{
@@ -109,14 +109,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("error not displayed", () => !settings.ErrorText.IsPresent);
}
- private class TestRoomSettings : MatchSettingsOverlay
+ private class TestRoomSettings : TimeshiftMatchSettingsOverlay
{
- public TriangleButton ApplyButton => Settings.ApplyButton;
+ public TriangleButton ApplyButton => ((MatchSettings)Settings).ApplyButton;
- public OsuTextBox NameField => Settings.NameField;
- public OsuDropdown DurationField => Settings.DurationField;
+ public OsuTextBox NameField => ((MatchSettings)Settings).NameField;
+ public OsuDropdown DurationField => ((MatchSettings)Settings).DurationField;
- public OsuSpriteText ErrorText => Settings.ErrorText;
+ public OsuSpriteText ErrorText => ((MatchSettings)Settings).ErrorText;
}
private class TestRoomManager : IRoomManager
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftLoungeSubScreen.cs
similarity index 92%
rename from osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeSubScreen.cs
rename to osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftLoungeSubScreen.cs
index 68987127d2..73afd65d6d 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeSubScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftLoungeSubScreen.cs
@@ -10,10 +10,11 @@ using osu.Framework.Testing;
using osu.Game.Graphics.Containers;
using osu.Game.Screens.Multi.Lounge;
using osu.Game.Screens.Multi.Lounge.Components;
+using osu.Game.Screens.Multi.Timeshift;
namespace osu.Game.Tests.Visual.Multiplayer
{
- public class TestSceneLoungeSubScreen : RoomManagerTestScene
+ public class TestSceneTimeshiftLoungeSubScreen : RoomManagerTestScene
{
private LoungeSubScreen loungeScreen;
@@ -26,7 +27,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
base.SetUpSteps();
- AddStep("push screen", () => LoadScreen(loungeScreen = new LoungeSubScreen
+ AddStep("push screen", () => LoadScreen(loungeScreen = new TimeshiftLoungeSubScreen
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftParticipantsList.cs
similarity index 92%
rename from osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs
rename to osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftParticipantsList.cs
index 7bbec7d30e..efc3be032c 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftParticipantsList.cs
@@ -8,7 +8,7 @@ using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
- public class TestSceneParticipantsList : MultiplayerTestScene
+ public class TestSceneTimeshiftParticipantsList : MultiplayerTestScene
{
[SetUp]
public new void Setup() => Schedule(() =>
@@ -20,6 +20,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Room.RecentParticipants.Add(new User
{
Username = "peppy",
+ CurrentModeRank = 1234,
Id = 2
});
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftRoomSubScreen.cs
similarity index 92%
rename from osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs
rename to osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftRoomSubScreen.cs
index bceb6efac1..bbd7d84081 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftRoomSubScreen.cs
@@ -16,15 +16,15 @@ using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Multi;
-using osu.Game.Screens.Multi.Match;
using osu.Game.Screens.Multi.Match.Components;
+using osu.Game.Screens.Multi.Timeshift;
using osu.Game.Tests.Beatmaps;
using osu.Game.Users;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
- public class TestSceneMatchSubScreen : MultiplayerTestScene
+ public class TestSceneTimeshiftRoomSubScreen : MultiplayerTestScene
{
protected override bool UseOnlineAPI => true;
@@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private BeatmapManager manager;
private RulesetStore rulesets;
- private TestMatchSubScreen match;
+ private TestTimeshiftRoomSubScreen match;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
@@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[SetUpSteps]
public void SetupSteps()
{
- AddStep("load match", () => LoadScreen(match = new TestMatchSubScreen(Room)));
+ AddStep("load match", () => LoadScreen(match = new TestTimeshiftRoomSubScreen(Room)));
AddUntilStep("wait for load", () => match.IsCurrentScreen());
}
@@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("create room", () =>
{
- InputManager.MoveMouseTo(match.ChildrenOfType().Single());
+ InputManager.MoveMouseTo(match.ChildrenOfType().Single());
InputManager.Click(MouseButton.Left);
});
@@ -131,13 +131,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("match has original beatmap", () => match.Beatmap.Value.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize != 1);
}
- private class TestMatchSubScreen : MatchSubScreen
+ private class TestTimeshiftRoomSubScreen : TimeshiftRoomSubScreen
{
public new Bindable SelectedItem => base.SelectedItem;
public new Bindable Beatmap => base.Beatmap;
- public TestMatchSubScreen(Room room)
+ public TestTimeshiftRoomSubScreen(Room room)
: base(room)
{
}
diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMatchSubScreen.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMatchSubScreen.cs
new file mode 100644
index 0000000000..a059bb1cc0
--- /dev/null
+++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMatchSubScreen.cs
@@ -0,0 +1,77 @@
+// Copyright (c) ppy Pty Ltd . 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.Screens;
+using osu.Framework.Testing;
+using osu.Game.Online.Multiplayer;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Screens.Multi.RealtimeMultiplayer;
+using osu.Game.Screens.Multi.RealtimeMultiplayer.Match;
+using osu.Game.Tests.Beatmaps;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.RealtimeMultiplayer
+{
+ public class TestSceneRealtimeMatchSubScreen : RealtimeMultiplayerTestScene
+ {
+ private RealtimeMatchSubScreen screen;
+
+ public TestSceneRealtimeMatchSubScreen()
+ : base(false)
+ {
+ }
+
+ [SetUp]
+ public new void Setup() => Schedule(() =>
+ {
+ Room.Name.Value = "Test Room";
+ });
+
+ [SetUpSteps]
+ public void SetupSteps()
+ {
+ AddStep("load match", () => LoadScreen(screen = new RealtimeMatchSubScreen(Room)));
+ AddUntilStep("wait for load", () => screen.IsCurrentScreen());
+ }
+
+ [Test]
+ public void TestSettingValidity()
+ {
+ AddAssert("create button not enabled", () => !this.ChildrenOfType().Single().Enabled.Value);
+
+ AddStep("set playlist", () =>
+ {
+ Room.Playlist.Add(new PlaylistItem
+ {
+ Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo },
+ });
+ });
+
+ AddAssert("create button enabled", () => this.ChildrenOfType().Single().Enabled.Value);
+ }
+
+ [Test]
+ public void TestCreatedRoom()
+ {
+ AddStep("set playlist", () =>
+ {
+ Room.Playlist.Add(new PlaylistItem
+ {
+ Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo },
+ });
+ });
+
+ AddStep("click create button", () =>
+ {
+ InputManager.MoveMouseTo(this.ChildrenOfType().Single());
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddWaitStep("wait", 10);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMultiplayer.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMultiplayer.cs
new file mode 100644
index 0000000000..80955ca380
--- /dev/null
+++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMultiplayer.cs
@@ -0,0 +1,23 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Screens.Multi.Components;
+
+namespace osu.Game.Tests.Visual.RealtimeMultiplayer
+{
+ public class TestSceneRealtimeMultiplayer : RealtimeMultiplayerTestScene
+ {
+ public TestSceneRealtimeMultiplayer()
+ {
+ var multi = new TestRealtimeMultiplayer();
+
+ AddStep("show", () => LoadScreen(multi));
+ AddUntilStep("wait for loaded", () => multi.IsLoaded);
+ }
+
+ private class TestRealtimeMultiplayer : Screens.Multi.RealtimeMultiplayer.RealtimeMultiplayer
+ {
+ protected override RoomManager CreateRoomManager() => new TestRealtimeRoomManager();
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMultiplayerParticipantsList.cs
similarity index 90%
rename from osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs
rename to osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMultiplayerParticipantsList.cs
index 8c997e9e32..4221821496 100644
--- a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs
+++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMultiplayerParticipantsList.cs
@@ -6,6 +6,7 @@ using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
+using osu.Framework.Utils;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Screens.Multi.RealtimeMultiplayer.Participants;
using osu.Game.Users;
@@ -13,7 +14,7 @@ using osuTK;
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
{
- public class TestSceneParticipantsList : RealtimeMultiplayerTestScene
+ public class TestSceneRealtimeMultiplayerParticipantsList : RealtimeMultiplayerTestScene
{
[SetUp]
public new void Setup() => Schedule(() =>
@@ -65,13 +66,13 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer
[Test]
public void TestToggleReadyState()
{
- AddAssert("ready mark invisible", () => !this.ChildrenOfType().Single().IsPresent);
+ AddAssert("ready mark invisible", () => !this.ChildrenOfType().Single().IsPresent);
AddStep("make user ready", () => Client.ChangeState(MultiplayerUserState.Ready));
- AddUntilStep("ready mark visible", () => this.ChildrenOfType().Single().IsPresent);
+ AddUntilStep("ready mark visible", () => this.ChildrenOfType().Single().IsPresent);
AddStep("make user idle", () => Client.ChangeState(MultiplayerUserState.Idle));
- AddUntilStep("ready mark invisible", () => !this.ChildrenOfType().Single().IsPresent);
+ AddUntilStep("ready mark invisible", () => !this.ChildrenOfType().Single().IsPresent);
}
[Test]
@@ -104,11 +105,11 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer
{
Id = i,
Username = $"User {i}",
+ CurrentModeRank = RNG.Next(1, 100000),
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
});
- if (i % 2 == 0)
- Client.ChangeUserState(i, MultiplayerUserState.Ready);
+ Client.ChangeUserState(i, (MultiplayerUserState)RNG.Next(0, (int)MultiplayerUserState.Results + 1));
}
});
}
diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs
index 1f863028af..b7cd81fb32 100644
--- a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs
+++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs
@@ -11,7 +11,7 @@ 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.Screens.Multi.RealtimeMultiplayer.Match;
using osu.Game.Tests.Resources;
using osu.Game.Users;
using osuTK;
diff --git a/osu.Game/Online/Multiplayer/PlaylistItem.cs b/osu.Game/Online/Multiplayer/PlaylistItem.cs
index 416091a1aa..4c4c071fc9 100644
--- a/osu.Game/Online/Multiplayer/PlaylistItem.cs
+++ b/osu.Game/Online/Multiplayer/PlaylistItem.cs
@@ -64,8 +64,8 @@ namespace osu.Game.Online.Multiplayer
public void MapObjects(BeatmapManager beatmaps, RulesetStore rulesets)
{
- Beatmap.Value = apiBeatmap.ToBeatmap(rulesets);
- Ruleset.Value = rulesets.GetRuleset(RulesetID);
+ Beatmap.Value ??= apiBeatmap.ToBeatmap(rulesets);
+ Ruleset.Value ??= rulesets.GetRuleset(RulesetID);
Ruleset rulesetInstance = Ruleset.Value.CreateInstance();
diff --git a/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs
new file mode 100644
index 0000000000..75bb578a29
--- /dev/null
+++ b/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs
@@ -0,0 +1,174 @@
+// Copyright (c) ppy Pty Ltd . 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.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.SignalR.Client;
+using Microsoft.Extensions.DependencyInjection;
+using Newtonsoft.Json;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Logging;
+using osu.Game.Online.API;
+
+namespace osu.Game.Online.RealtimeMultiplayer
+{
+ public class RealtimeMultiplayerClient : StatefulMultiplayerClient
+ {
+ private const string endpoint = "https://spectator.ppy.sh/multiplayer";
+
+ public override IBindable IsConnected => isConnected;
+
+ private readonly Bindable isConnected = new Bindable();
+ private readonly IBindable apiState = new Bindable();
+
+ [Resolved]
+ private IAPIProvider api { get; set; } = null!;
+
+ private HubConnection? connection;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ apiState.BindTo(api.State);
+ apiState.BindValueChanged(apiStateChanged, true);
+ }
+
+ private void apiStateChanged(ValueChangedEvent state)
+ {
+ switch (state.NewValue)
+ {
+ case APIState.Failing:
+ case APIState.Offline:
+ connection?.StopAsync();
+ connection = null;
+ break;
+
+ case APIState.Online:
+ Task.Run(Connect);
+ break;
+ }
+ }
+
+ protected virtual async Task Connect()
+ {
+ if (connection != null)
+ return;
+
+ connection = new HubConnectionBuilder()
+ .WithUrl(endpoint, options =>
+ {
+ options.Headers.Add("Authorization", $"Bearer {api.AccessToken}");
+ })
+ .AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; })
+ .Build();
+
+ // this is kind of SILLY
+ // https://github.com/dotnet/aspnetcore/issues/15198
+ connection.On(nameof(IMultiplayerClient.RoomStateChanged), ((IMultiplayerClient)this).RoomStateChanged);
+ connection.On(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined);
+ connection.On(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft);
+ connection.On(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged);
+ connection.On(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged);
+ connection.On(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged);
+ connection.On(nameof(IMultiplayerClient.LoadRequested), ((IMultiplayerClient)this).LoadRequested);
+ connection.On(nameof(IMultiplayerClient.MatchStarted), ((IMultiplayerClient)this).MatchStarted);
+ connection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady);
+
+ connection.Closed += async ex =>
+ {
+ isConnected.Value = false;
+
+ if (ex != null)
+ {
+ Logger.Log($"Multiplayer client lost connection: {ex}", LoggingTarget.Network);
+ await tryUntilConnected();
+ }
+ };
+
+ await tryUntilConnected();
+
+ async Task tryUntilConnected()
+ {
+ Logger.Log("Multiplayer client connecting...", LoggingTarget.Network);
+
+ while (api.State.Value == APIState.Online)
+ {
+ try
+ {
+ Debug.Assert(connection != null);
+
+ // reconnect on any failure
+ await connection.StartAsync();
+ Logger.Log("Multiplayer client connected!", LoggingTarget.Network);
+
+ // Success.
+ isConnected.Value = true;
+ break;
+ }
+ catch (Exception e)
+ {
+ Logger.Log($"Multiplayer client connection error: {e}", LoggingTarget.Network);
+ await Task.Delay(5000);
+ }
+ }
+ }
+ }
+
+ protected override Task JoinRoom(long roomId)
+ {
+ if (!isConnected.Value)
+ return Task.FromCanceled(CancellationToken.None);
+
+ return connection.InvokeAsync(nameof(IMultiplayerServer.JoinRoom), roomId);
+ }
+
+ public override async Task LeaveRoom()
+ {
+ if (!isConnected.Value)
+ return;
+
+ if (Room == null)
+ return;
+
+ await base.LeaveRoom();
+ await connection.InvokeAsync(nameof(IMultiplayerServer.LeaveRoom));
+ }
+
+ public override Task TransferHost(int userId)
+ {
+ if (!isConnected.Value)
+ return Task.CompletedTask;
+
+ return connection.InvokeAsync(nameof(IMultiplayerServer.TransferHost), userId);
+ }
+
+ public override Task ChangeSettings(MultiplayerRoomSettings settings)
+ {
+ if (!isConnected.Value)
+ return Task.CompletedTask;
+
+ return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeSettings), settings);
+ }
+
+ public override Task ChangeState(MultiplayerUserState newState)
+ {
+ if (!isConnected.Value)
+ return Task.CompletedTask;
+
+ return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeState), newState);
+ }
+
+ public override Task StartMatch()
+ {
+ if (!isConnected.Value)
+ return Task.CompletedTask;
+
+ return connection.InvokeAsync(nameof(IMultiplayerServer.StartMatch));
+ }
+ }
+}
diff --git a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs
index f56499f040..4ebd648689 100644
--- a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs
+++ b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs
@@ -127,10 +127,10 @@ namespace osu.Game.Online.RealtimeMultiplayer
///
/// The new room name, if any.
/// The new room playlist item, if any.
- public void ChangeSettings(Optional name = default, Optional item = default)
+ public Task ChangeSettings(Optional name = default, Optional item = default)
{
if (Room == null)
- return;
+ throw new InvalidOperationException("Must be joined to a match to change settings.");
// A dummy playlist item filled with the current room settings (except mods).
var existingPlaylistItem = new PlaylistItem
@@ -146,7 +146,7 @@ namespace osu.Game.Online.RealtimeMultiplayer
RulesetID = Room.Settings.RulesetID
};
- ChangeSettings(new MultiplayerRoomSettings
+ return ChangeSettings(new MultiplayerRoomSettings
{
Name = name.GetOr(Room.Settings.Name),
BeatmapID = item.GetOr(existingPlaylistItem).BeatmapID,
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 150569f1dd..eb27821d82 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -30,6 +30,7 @@ using osu.Game.Database;
using osu.Game.Input;
using osu.Game.Input.Bindings;
using osu.Game.IO;
+using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Online.Spectator;
using osu.Game.Overlays;
using osu.Game.Resources;
@@ -78,6 +79,7 @@ namespace osu.Game
protected IAPIProvider API;
private SpectatorStreamingClient spectatorStreaming;
+ private StatefulMultiplayerClient multiplayerClient;
protected MenuCursorContainer MenuCursorContainer;
@@ -211,6 +213,7 @@ namespace osu.Game
dependencies.CacheAs(API ??= new APIAccess(LocalConfig));
dependencies.CacheAs(spectatorStreaming = new SpectatorStreamingClient());
+ dependencies.CacheAs(multiplayerClient = new RealtimeMultiplayerClient());
var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures);
@@ -277,6 +280,7 @@ namespace osu.Game
if (API is APIAccess apiAccess)
AddInternal(apiAccess);
AddInternal(spectatorStreaming);
+ AddInternal(multiplayerClient);
AddInternal(RulesetConfigCache);
diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
index 62dc1dc806..3d3b543d70 100644
--- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
@@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System.Collections.Generic;
+using System;
using System.Drawing;
using System.Linq;
using osu.Framework.Allocation;
@@ -25,9 +25,13 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
private FillFlowContainer> scalingSettings;
+ private readonly IBindable currentDisplay = new Bindable();
+ private readonly IBindableList windowModes = new BindableList();
+
private Bindable scalingMode;
private Bindable sizeFullscreen;
- private readonly IBindableList windowModes = new BindableList();
+
+ private readonly BindableList resolutions = new BindableList(new[] { new Size(9999, 9999) });
[Resolved]
private OsuGameBase game { get; set; }
@@ -53,22 +57,25 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
scalingPositionY = osuConfig.GetBindable(OsuSetting.ScalingPositionY);
if (host.Window != null)
+ {
+ currentDisplay.BindTo(host.Window.CurrentDisplayBindable);
windowModes.BindTo(host.Window.SupportedWindowModes);
-
- Container resolutionSettingsContainer;
+ }
Children = new Drawable[]
{
windowModeDropdown = new SettingsDropdown
{
LabelText = "Screen mode",
- Current = config.GetBindable(FrameworkSetting.WindowMode),
ItemSource = windowModes,
+ Current = config.GetBindable(FrameworkSetting.WindowMode),
},
- resolutionSettingsContainer = new Container
+ resolutionDropdown = new ResolutionSettingsDropdown
{
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y
+ LabelText = "Resolution",
+ ShowsDefaultIndicator = false,
+ ItemSource = resolutions,
+ Current = sizeFullscreen
},
new SettingsSlider
{
@@ -126,31 +133,33 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
},
};
- scalingSettings.ForEach(s => bindPreviewEvent(s.Current));
-
- var resolutions = getResolutions();
-
- if (resolutions.Count > 1)
+ windowModes.BindCollectionChanged((sender, args) =>
{
- resolutionSettingsContainer.Child = resolutionDropdown = new ResolutionSettingsDropdown
- {
- LabelText = "Resolution",
- ShowsDefaultIndicator = false,
- Items = resolutions,
- Current = sizeFullscreen
- };
+ if (windowModes.Count > 1)
+ windowModeDropdown.Show();
+ else
+ windowModeDropdown.Hide();
+ }, true);
- windowModeDropdown.Current.BindValueChanged(mode =>
+ windowModeDropdown.Current.ValueChanged += _ => updateResolutionDropdown();
+
+ currentDisplay.BindValueChanged(display => Schedule(() =>
+ {
+ resolutions.RemoveRange(1, resolutions.Count - 1);
+
+ if (display.NewValue != null)
{
- if (mode.NewValue == WindowMode.Fullscreen)
- {
- resolutionDropdown.Show();
- sizeFullscreen.TriggerChange();
- }
- else
- resolutionDropdown.Hide();
- }, true);
- }
+ resolutions.AddRange(display.NewValue.DisplayModes
+ .Where(m => m.Size.Width >= 800 && m.Size.Height >= 600)
+ .OrderByDescending(m => Math.Max(m.Size.Height, m.Size.Width))
+ .Select(m => m.Size)
+ .Distinct());
+ }
+
+ updateResolutionDropdown();
+ }), true);
+
+ scalingSettings.ForEach(s => bindPreviewEvent(s.Current));
scalingMode.BindValueChanged(mode =>
{
@@ -163,17 +172,13 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
scalingSettings.ForEach(s => s.TransferValueOnCommit = mode.NewValue == ScalingMode.Everything);
}, true);
- windowModes.CollectionChanged += (sender, args) => windowModesChanged();
-
- windowModesChanged();
- }
-
- private void windowModesChanged()
- {
- if (windowModes.Count > 1)
- windowModeDropdown.Show();
- else
- windowModeDropdown.Hide();
+ void updateResolutionDropdown()
+ {
+ if (resolutions.Count > 1 && windowModeDropdown.Current.Value == WindowMode.Fullscreen)
+ resolutionDropdown.Show();
+ else
+ resolutionDropdown.Hide();
+ }
}
///
@@ -205,24 +210,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
preview.Expire();
}
- private IReadOnlyList getResolutions()
- {
- var resolutions = new List { new Size(9999, 9999) };
- var currentDisplay = game.Window?.CurrentDisplayBindable.Value;
-
- if (currentDisplay != null)
- {
- resolutions.AddRange(currentDisplay.DisplayModes
- .Where(m => m.Size.Width >= 800 && m.Size.Height >= 600)
- .OrderByDescending(m => m.Size.Width)
- .ThenByDescending(m => m.Size.Height)
- .Select(m => m.Size)
- .Distinct());
- }
-
- return resolutions;
- }
-
private class ScalingPreview : ScalingContainer
{
public ScalingPreview()
diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs
index 598b666642..95e2e9da30 100644
--- a/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
-using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Configuration;
@@ -28,23 +26,20 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
LabelText = "osu! music theme",
Current = config.GetBindable(OsuSetting.MenuMusic)
},
- new SettingsDropdown
+ new SettingsEnumDropdown
{
LabelText = "Intro sequence",
Current = config.GetBindable(OsuSetting.IntroSequence),
- Items = Enum.GetValues(typeof(IntroSequence)).Cast()
},
- new SettingsDropdown
+ new SettingsEnumDropdown
{
LabelText = "Background source",
Current = config.GetBindable(OsuSetting.MenuBackgroundSource),
- Items = Enum.GetValues(typeof(BackgroundSource)).Cast()
},
- new SettingsDropdown
+ new SettingsEnumDropdown
{
LabelText = "Seasonal backgrounds",
Current = config.GetBindable(OsuSetting.SeasonalBackgroundMode),
- Items = Enum.GetValues(typeof(SeasonalBackgroundMode)).Cast()
}
};
}
diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs
index 4becdd58cd..badfa3f693 100644
--- a/osu.Game/Screens/Menu/ButtonSystem.cs
+++ b/osu.Game/Screens/Menu/ButtonSystem.cs
@@ -42,8 +42,8 @@ namespace osu.Game.Screens.Menu
public Action OnBeatmapListing;
public Action OnSolo;
public Action OnSettings;
- public Action OnMulti;
- public Action OnChart;
+ public Action OnMultiplayer;
+ public Action OnTimeshift;
public const float BUTTON_WIDTH = 140f;
public const float WEDGE_WIDTH = 20;
@@ -124,8 +124,8 @@ namespace osu.Game.Screens.Menu
private void load(AudioManager audio, IdleTracker idleTracker, GameHost host)
{
buttonsPlay.Add(new Button(@"solo", @"button-solo-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P));
- buttonsPlay.Add(new Button(@"multi", @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMulti, 0, Key.M));
- buttonsPlay.Add(new Button(@"chart", @"button-generic-select", OsuIcon.Charts, new Color4(80, 53, 160, 255), () => OnChart?.Invoke()));
+ buttonsPlay.Add(new Button(@"multi", @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M));
+ buttonsPlay.Add(new Button(@"timeshift", @"button-generic-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onTimeshift, 0, Key.L));
buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play);
buttonsTopLevel.Add(new Button(@"play", @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P));
@@ -154,7 +154,7 @@ namespace osu.Game.Screens.Menu
sampleBack = audio.Samples.Get(@"Menu/button-back-select");
}
- private void onMulti()
+ private void onMultiplayer()
{
if (!api.IsLoggedIn)
{
@@ -172,7 +172,28 @@ namespace osu.Game.Screens.Menu
return;
}
- OnMulti?.Invoke();
+ OnMultiplayer?.Invoke();
+ }
+
+ private void onTimeshift()
+ {
+ if (!api.IsLoggedIn)
+ {
+ notifications?.Post(new SimpleNotification
+ {
+ Text = "You gotta be logged in to multi 'yo!",
+ Icon = FontAwesome.Solid.Globe,
+ Activated = () =>
+ {
+ loginOverlay?.Show();
+ return true;
+ }
+ });
+
+ return;
+ }
+
+ OnTimeshift?.Invoke();
}
private void updateIdleState(bool isIdle)
diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs
index b781c347f0..fa96ac9c51 100644
--- a/osu.Game/Screens/Menu/MainMenu.cs
+++ b/osu.Game/Screens/Menu/MainMenu.cs
@@ -17,6 +17,7 @@ using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Edit;
+using osu.Game.Screens.Multi.RealtimeMultiplayer;
using osu.Game.Screens.Multi.Timeshift;
using osu.Game.Screens.Select;
@@ -104,7 +105,8 @@ namespace osu.Game.Screens.Menu
this.Push(new Editor());
},
OnSolo = onSolo,
- OnMulti = delegate { this.Push(new TimeshiftMultiplayer()); },
+ OnMultiplayer = () => this.Push(new RealtimeMultiplayer()),
+ OnTimeshift = () => this.Push(new TimeshiftMultiplayer()),
OnExit = confirmAndExit,
}
}
@@ -136,7 +138,6 @@ namespace osu.Game.Screens.Menu
buttons.OnSettings = () => settings?.ToggleVisibility();
buttons.OnBeatmapListing = () => beatmapListing?.ToggleVisibility();
- buttons.OnChart = () => rankings?.ShowSpotlights();
LoadComponentAsync(background = new BackgroundScreenDefault());
preloadSongSelect();
diff --git a/osu.Game/Screens/Multi/Components/RoomManager.cs b/osu.Game/Screens/Multi/Components/RoomManager.cs
index 482ee5492c..f78d0d979e 100644
--- a/osu.Game/Screens/Multi/Components/RoomManager.cs
+++ b/osu.Game/Screens/Multi/Components/RoomManager.cs
@@ -76,10 +76,7 @@ namespace osu.Game.Screens.Multi.Components
req.Failure += exception =>
{
- if (req.Result != null)
- onError?.Invoke(req.Result.Error);
- else
- Logger.Log($"Failed to create the room: {exception}", level: LogLevel.Important);
+ onError?.Invoke(req.Result?.Error ?? exception.Message);
};
api.Queue(req);
diff --git a/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs
index 01a85382e4..56116b219a 100644
--- a/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs
+++ b/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs
@@ -242,7 +242,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components
{
new OsuMenuItem("Create copy", MenuItemType.Standard, () =>
{
- multiplayer?.CreateRoom(Room.CreateCopy());
+ multiplayer?.OpenNewRoom(Room.CreateCopy());
})
};
}
diff --git a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs
index 165a2b201c..44c893363b 100644
--- a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs
+++ b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs
@@ -19,16 +19,15 @@ using osu.Game.Users;
namespace osu.Game.Screens.Multi.Lounge
{
[Cached]
- public class LoungeSubScreen : MultiplayerSubScreen
+ public abstract class LoungeSubScreen : MultiplayerSubScreen
{
public override string Title => "Lounge";
- protected FilterControl Filter;
-
protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby();
private readonly IBindable initialRoomsReceived = new Bindable();
+ private FilterControl filter;
private Container content;
private LoadingLayer loadingLayer;
@@ -78,11 +77,11 @@ namespace osu.Game.Screens.Multi.Lounge
},
},
},
- Filter = new TimeshiftFilterControl
+ filter = CreateFilterControl().With(d =>
{
- RelativeSizeAxes = Axes.X,
- Height = 80,
- },
+ d.RelativeSizeAxes = Axes.X;
+ d.Height = 80;
+ })
};
// scroll selected room into view on selection.
@@ -108,7 +107,7 @@ namespace osu.Game.Screens.Multi.Lounge
content.Padding = new MarginPadding
{
- Top = Filter.DrawHeight,
+ Top = filter.DrawHeight,
Left = WaveOverlayContainer.WIDTH_PADDING - DrawableRoom.SELECTION_BORDER_WIDTH + HORIZONTAL_OVERFLOW_PADDING,
Right = WaveOverlayContainer.WIDTH_PADDING + HORIZONTAL_OVERFLOW_PADDING,
};
@@ -116,7 +115,7 @@ namespace osu.Game.Screens.Multi.Lounge
protected override void OnFocus(FocusEvent e)
{
- Filter.TakeFocus();
+ filter.TakeFocus();
}
public override void OnEntering(IScreen last)
@@ -140,19 +139,19 @@ namespace osu.Game.Screens.Multi.Lounge
private void onReturning()
{
- Filter.HoldFocus = true;
+ filter.HoldFocus = true;
}
public override bool OnExiting(IScreen next)
{
- Filter.HoldFocus = false;
+ filter.HoldFocus = false;
return base.OnExiting(next);
}
public override void OnSuspending(IScreen next)
{
base.OnSuspending(next);
- Filter.HoldFocus = false;
+ filter.HoldFocus = false;
}
private void joinRequested(Room room)
@@ -193,7 +192,11 @@ namespace osu.Game.Screens.Multi.Lounge
selectedRoom.Value = room;
- this.Push(new MatchSubScreen(room));
+ this.Push(CreateRoomSubScreen(room));
}
+
+ protected abstract FilterControl CreateFilterControl();
+
+ protected abstract RoomSubScreen CreateRoomSubScreen(Room room);
}
}
diff --git a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs
index 1859e8db8a..0bb56d0cdf 100644
--- a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs
+++ b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs
@@ -1,385 +1,41 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
-using System.Collections.Specialized;
-using Humanizer;
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
-using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
-using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
-using osu.Game.Online.Multiplayer;
-using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Multi.Match.Components
{
- public class MatchSettingsOverlay : FocusedOverlayContainer
+ public abstract class MatchSettingsOverlay : FocusedOverlayContainer
{
- private const float transition_duration = 350;
- private const float field_padding = 45;
+ protected const float TRANSITION_DURATION = 350;
+ protected const float FIELD_PADDING = 45;
- public Action EditPlaylist;
-
- protected MatchSettings Settings { get; private set; }
+ protected MultiplayerComposite Settings { get; set; }
[BackgroundDependencyLoader]
private void load()
{
Masking = true;
-
- Child = Settings = new MatchSettings
- {
- RelativeSizeAxes = Axes.Both,
- RelativePositionAxes = Axes.Y,
- EditPlaylist = () => EditPlaylist?.Invoke()
- };
}
protected override void PopIn()
{
- Settings.MoveToY(0, transition_duration, Easing.OutQuint);
+ Settings.MoveToY(0, TRANSITION_DURATION, Easing.OutQuint);
}
protected override void PopOut()
{
- Settings.MoveToY(-1, transition_duration, Easing.InSine);
+ Settings.MoveToY(-1, TRANSITION_DURATION, Easing.InSine);
}
- protected class MatchSettings : MultiplayerComposite
- {
- private const float disabled_alpha = 0.2f;
-
- public Action EditPlaylist;
-
- public OsuTextBox NameField, MaxParticipantsField;
- public OsuDropdown DurationField;
- public RoomAvailabilityPicker AvailabilityPicker;
- public GameTypePicker TypePicker;
- public TriangleButton ApplyButton;
-
- public OsuSpriteText ErrorText;
-
- private OsuSpriteText typeLabel;
- private LoadingLayer loadingLayer;
- private DrawableRoomPlaylist playlist;
- private OsuSpriteText playlistLength;
-
- [Resolved(CanBeNull = true)]
- private IRoomManager manager { get; set; }
-
- [Resolved]
- private Bindable currentRoom { get; set; }
-
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- Container dimContent;
-
- InternalChildren = new Drawable[]
- {
- dimContent = new Container
- {
- RelativeSizeAxes = Axes.Both,
- Children = new Drawable[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = Color4Extensions.FromHex(@"28242d"),
- },
- new GridContainer
- {
- RelativeSizeAxes = Axes.Both,
- RowDimensions = new[]
- {
- new Dimension(GridSizeMode.Distributed),
- new Dimension(GridSizeMode.AutoSize),
- },
- Content = new[]
- {
- new Drawable[]
- {
- new OsuScrollContainer
- {
- Padding = new MarginPadding
- {
- Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING,
- Vertical = 10
- },
- RelativeSizeAxes = Axes.Both,
- Children = new[]
- {
- new Container
- {
- Padding = new MarginPadding { Horizontal = WaveOverlayContainer.WIDTH_PADDING },
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Children = new Drawable[]
- {
- new SectionContainer
- {
- Padding = new MarginPadding { Right = field_padding / 2 },
- Children = new[]
- {
- new Section("Room name")
- {
- Child = NameField = new SettingsTextBox
- {
- RelativeSizeAxes = Axes.X,
- TabbableContentContainer = this,
- LengthLimit = 100
- },
- },
- new Section("Duration")
- {
- Child = DurationField = new DurationDropdown
- {
- RelativeSizeAxes = Axes.X,
- Items = new[]
- {
- TimeSpan.FromMinutes(30),
- TimeSpan.FromHours(1),
- TimeSpan.FromHours(2),
- TimeSpan.FromHours(4),
- TimeSpan.FromHours(8),
- TimeSpan.FromHours(12),
- //TimeSpan.FromHours(16),
- TimeSpan.FromHours(24),
- TimeSpan.FromDays(3),
- TimeSpan.FromDays(7)
- }
- }
- },
- new Section("Room visibility")
- {
- Alpha = disabled_alpha,
- Child = AvailabilityPicker = new RoomAvailabilityPicker
- {
- Enabled = { Value = false }
- },
- },
- new Section("Game type")
- {
- Alpha = disabled_alpha,
- Child = new FillFlowContainer
- {
- AutoSizeAxes = Axes.Y,
- RelativeSizeAxes = Axes.X,
- Direction = FillDirection.Vertical,
- Spacing = new Vector2(7),
- Children = new Drawable[]
- {
- TypePicker = new GameTypePicker
- {
- RelativeSizeAxes = Axes.X,
- Enabled = { Value = false }
- },
- typeLabel = new OsuSpriteText
- {
- Font = OsuFont.GetFont(size: 14),
- Colour = colours.Yellow
- },
- },
- },
- },
- new Section("Max participants")
- {
- Alpha = disabled_alpha,
- Child = MaxParticipantsField = new SettingsNumberTextBox
- {
- RelativeSizeAxes = Axes.X,
- TabbableContentContainer = this,
- ReadOnly = true,
- },
- },
- new Section("Password (optional)")
- {
- Alpha = disabled_alpha,
- Child = new SettingsPasswordTextBox
- {
- RelativeSizeAxes = Axes.X,
- TabbableContentContainer = this,
- ReadOnly = true,
- },
- },
- },
- },
- new SectionContainer
- {
- Anchor = Anchor.TopRight,
- Origin = Anchor.TopRight,
- Padding = new MarginPadding { Left = field_padding / 2 },
- Children = new[]
- {
- new Section("Playlist")
- {
- Child = new GridContainer
- {
- RelativeSizeAxes = Axes.X,
- Height = 300,
- Content = new[]
- {
- new Drawable[]
- {
- playlist = new DrawableRoomPlaylist(true, true) { RelativeSizeAxes = Axes.Both }
- },
- new Drawable[]
- {
- playlistLength = new OsuSpriteText
- {
- Margin = new MarginPadding { Vertical = 5 },
- Colour = colours.Yellow,
- Font = OsuFont.GetFont(size: 12),
- }
- },
- new Drawable[]
- {
- new PurpleTriangleButton
- {
- RelativeSizeAxes = Axes.X,
- Height = 40,
- Text = "Edit playlist",
- Action = () => EditPlaylist?.Invoke()
- }
- }
- },
- RowDimensions = new[]
- {
- new Dimension(),
- new Dimension(GridSizeMode.AutoSize),
- new Dimension(GridSizeMode.AutoSize),
- }
- }
- },
- },
- },
- },
- }
- },
- },
- },
- new Drawable[]
- {
- new Container
- {
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft,
- Y = 2,
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Children = new Drawable[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = Color4Extensions.FromHex(@"28242d").Darken(0.5f).Opacity(1f),
- },
- new FillFlowContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- Spacing = new Vector2(0, 20),
- Margin = new MarginPadding { Vertical = 20 },
- Padding = new MarginPadding { Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING },
- Children = new Drawable[]
- {
- ApplyButton = new CreateRoomButton
- {
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.BottomCentre,
- Size = new Vector2(230, 55),
- Enabled = { Value = false },
- Action = apply,
- },
- ErrorText = new OsuSpriteText
- {
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.BottomCentre,
- Alpha = 0,
- Depth = 1,
- Colour = colours.RedDark
- }
- }
- }
- }
- }
- }
- }
- },
- }
- },
- loadingLayer = new LoadingLayer(dimContent)
- };
-
- TypePicker.Current.BindValueChanged(type => typeLabel.Text = type.NewValue?.Name ?? string.Empty, true);
- RoomName.BindValueChanged(name => NameField.Text = name.NewValue, true);
- Availability.BindValueChanged(availability => AvailabilityPicker.Current.Value = availability.NewValue, true);
- Type.BindValueChanged(type => TypePicker.Current.Value = type.NewValue, true);
- MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true);
- Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue ?? TimeSpan.FromMinutes(30), true);
-
- playlist.Items.BindTo(Playlist);
- Playlist.BindCollectionChanged(onPlaylistChanged, true);
- }
-
- protected override void Update()
- {
- base.Update();
-
- ApplyButton.Enabled.Value = hasValidSettings;
- }
-
- private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) =>
- playlistLength.Text = $"Length: {Playlist.GetTotalDuration()}";
-
- private bool hasValidSettings => RoomID.Value == null && NameField.Text.Length > 0 && Playlist.Count > 0;
-
- private void apply()
- {
- if (!ApplyButton.Enabled.Value)
- return;
-
- hideError();
-
- RoomName.Value = NameField.Text;
- Availability.Value = AvailabilityPicker.Current.Value;
- Type.Value = TypePicker.Current.Value;
-
- if (int.TryParse(MaxParticipantsField.Text, out int max))
- MaxParticipants.Value = max;
- else
- MaxParticipants.Value = null;
-
- Duration.Value = DurationField.Current.Value;
-
- manager?.CreateRoom(currentRoom.Value, onSuccess, onError);
-
- loadingLayer.Show();
- }
-
- private void hideError() => ErrorText.FadeOut(50);
-
- private void onSuccess(Room room) => loadingLayer.Hide();
-
- private void onError(string text)
- {
- ErrorText.Text = text;
- ErrorText.FadeIn(50);
-
- loadingLayer.Hide();
- }
- }
-
- private class SettingsTextBox : OsuTextBox
+ protected class SettingsTextBox : OsuTextBox
{
[BackgroundDependencyLoader]
private void load()
@@ -389,12 +45,12 @@ namespace osu.Game.Screens.Multi.Match.Components
}
}
- private class SettingsNumberTextBox : SettingsTextBox
+ protected class SettingsNumberTextBox : SettingsTextBox
{
protected override bool CanAddCharacter(char character) => char.IsNumber(character);
}
- private class SettingsPasswordTextBox : OsuPasswordTextBox
+ protected class SettingsPasswordTextBox : OsuPasswordTextBox
{
[BackgroundDependencyLoader]
private void load()
@@ -404,7 +60,7 @@ namespace osu.Game.Screens.Multi.Match.Components
}
}
- private class SectionContainer : FillFlowContainer
+ protected class SectionContainer : FillFlowContainer
{
public SectionContainer()
{
@@ -412,11 +68,11 @@ namespace osu.Game.Screens.Multi.Match.Components
AutoSizeAxes = Axes.Y;
Width = 0.5f;
Direction = FillDirection.Vertical;
- Spacing = new Vector2(field_padding);
+ Spacing = new Vector2(FIELD_PADDING);
}
}
- private class Section : Container
+ protected class Section : Container
{
private readonly Container content;
@@ -449,31 +105,5 @@ namespace osu.Game.Screens.Multi.Match.Components
};
}
}
-
- public class CreateRoomButton : TriangleButton
- {
- public CreateRoomButton()
- {
- Text = "Create";
- }
-
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- BackgroundColour = colours.Yellow;
- Triangles.ColourLight = colours.YellowLight;
- Triangles.ColourDark = colours.YellowDark;
- }
- }
-
- private class DurationDropdown : OsuDropdown
- {
- public DurationDropdown()
- {
- Menu.MaxHeight = 100;
- }
-
- protected override string GenerateItemText(TimeSpan item) => item.Humanize();
- }
}
}
diff --git a/osu.Game/Screens/Multi/Match/RoomSubScreen.cs b/osu.Game/Screens/Multi/Match/RoomSubScreen.cs
new file mode 100644
index 0000000000..0cc9a4354e
--- /dev/null
+++ b/osu.Game/Screens/Multi/Match/RoomSubScreen.cs
@@ -0,0 +1,74 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Screens;
+using osu.Game.Audio;
+using osu.Game.Beatmaps;
+using osu.Game.Online.Multiplayer;
+using osu.Game.Rulesets.Mods;
+
+namespace osu.Game.Screens.Multi.Match
+{
+ [Cached(typeof(IPreviewTrackOwner))]
+ public abstract class RoomSubScreen : MultiplayerSubScreen, IPreviewTrackOwner
+ {
+ protected readonly Bindable SelectedItem = new Bindable();
+
+ public override bool DisallowExternalBeatmapRulesetChanges => true;
+
+ [Resolved(typeof(Room), nameof(Room.Playlist))]
+ protected BindableList Playlist { get; private set; }
+
+ [Resolved]
+ private BeatmapManager beatmapManager { get; set; }
+
+ private IBindable> managerUpdated;
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged));
+ SelectedItem.Value = Playlist.FirstOrDefault();
+
+ managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy();
+ managerUpdated.BindValueChanged(beatmapUpdated);
+ }
+
+ private void selectedItemChanged()
+ {
+ updateWorkingBeatmap();
+
+ var item = SelectedItem.Value;
+
+ Mods.Value = item?.RequiredMods?.ToArray() ?? Array.Empty();
+
+ if (item?.Ruleset != null)
+ Ruleset.Value = item.Ruleset.Value;
+ }
+
+ private void beatmapUpdated(ValueChangedEvent> weakSet) => Schedule(updateWorkingBeatmap);
+
+ private void updateWorkingBeatmap()
+ {
+ var beatmap = SelectedItem.Value?.Beatmap.Value;
+
+ // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
+ var localBeatmap = beatmap == null ? null : beatmapManager.QueryBeatmap(b => b.OnlineBeatmapID == beatmap.OnlineBeatmapID);
+
+ Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap);
+ }
+
+ public override bool OnExiting(IScreen next)
+ {
+ RoomManager?.PartRoom();
+ Mods.Value = Array.Empty();
+
+ return base.OnExiting(next);
+ }
+ }
+}
diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs
index 837ccdf2e9..eae779421d 100644
--- a/osu.Game/Screens/Multi/Multiplayer.cs
+++ b/osu.Game/Screens/Multi/Multiplayer.cs
@@ -134,7 +134,7 @@ namespace osu.Game.Screens.Multi
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
- Action = () => CreateRoom()
+ Action = () => OpenNewRoom()
},
RoomManager = CreateRoomManager()
}
@@ -143,7 +143,7 @@ namespace osu.Game.Screens.Multi
screenStack.ScreenPushed += screenPushed;
screenStack.ScreenExited += screenExited;
- screenStack.Push(loungeSubScreen = new LoungeSubScreen());
+ screenStack.Push(loungeSubScreen = CreateLounge());
}
private readonly IBindable apiState = new Bindable();
@@ -264,10 +264,16 @@ namespace osu.Game.Screens.Multi
}
///
- /// Create a new room.
+ /// Creates and opens the newly-created room.
///
/// An optional template to use when creating the room.
- public void CreateRoom(Room room = null) => loungeSubScreen.Open(room ?? new Room { Name = { Value = $"{api.LocalUser}'s awesome room" } });
+ public void OpenNewRoom(Room room = null) => loungeSubScreen.Open(room ?? CreateNewRoom());
+
+ ///
+ /// Creates a new room.
+ ///
+ /// The created .
+ protected virtual Room CreateNewRoom() => new Room { Name = { Value = $"{api.LocalUser}'s awesome room" } };
private void beginHandlingTrack()
{
@@ -302,7 +308,7 @@ namespace osu.Game.Screens.Multi
headerBackground.MoveToX(0, MultiplayerSubScreen.X_MOVE_DURATION, Easing.OutQuint);
break;
- case MatchSubScreen _:
+ case RoomSubScreen _:
header.ResizeHeightTo(135, MultiplayerSubScreen.APPEAR_DURATION, Easing.OutQuint);
headerBackground.MoveToX(-MultiplayerSubScreen.X_SHIFT, MultiplayerSubScreen.X_MOVE_DURATION, Easing.OutQuint);
break;
@@ -324,7 +330,7 @@ namespace osu.Game.Screens.Multi
private void updateTrack(ValueChangedEvent _ = null)
{
- if (screenStack.CurrentScreen is MatchSubScreen)
+ if (screenStack.CurrentScreen is RoomSubScreen)
{
var track = Beatmap.Value?.Track;
@@ -355,6 +361,8 @@ namespace osu.Game.Screens.Multi
protected abstract RoomManager CreateRoomManager();
+ protected abstract LoungeSubScreen CreateLounge();
+
private class MultiplayerWaveContainer : WaveContainer
{
protected override bool StartHidden => true;
diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs
index 41dcf61740..e8462088f1 100644
--- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs
+++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs
@@ -25,9 +25,11 @@ namespace osu.Game.Screens.Multi.Play
public Action Exited;
[Resolved(typeof(Room), nameof(Room.RoomID))]
- private Bindable roomId { get; set; }
+ protected Bindable RoomId { get; private set; }
- private readonly PlaylistItem playlistItem;
+ protected readonly PlaylistItem PlaylistItem;
+
+ protected int? Token { get; private set; }
[Resolved]
private IAPIProvider api { get; set; }
@@ -35,32 +37,31 @@ namespace osu.Game.Screens.Multi.Play
[Resolved]
private IBindable ruleset { get; set; }
- public TimeshiftPlayer(PlaylistItem playlistItem)
+ public TimeshiftPlayer(PlaylistItem playlistItem, bool allowPause = true)
+ : base(allowPause)
{
- this.playlistItem = playlistItem;
+ PlaylistItem = playlistItem;
}
- private int? token;
-
[BackgroundDependencyLoader]
private void load()
{
- token = null;
+ Token = null;
bool failed = false;
// Sanity checks to ensure that TimeshiftPlayer matches the settings for the current PlaylistItem
- if (Beatmap.Value.BeatmapInfo.OnlineBeatmapID != playlistItem.Beatmap.Value.OnlineBeatmapID)
+ if (Beatmap.Value.BeatmapInfo.OnlineBeatmapID != PlaylistItem.Beatmap.Value.OnlineBeatmapID)
throw new InvalidOperationException("Current Beatmap does not match PlaylistItem's Beatmap");
- if (ruleset.Value.ID != playlistItem.Ruleset.Value.ID)
+ if (ruleset.Value.ID != PlaylistItem.Ruleset.Value.ID)
throw new InvalidOperationException("Current Ruleset does not match PlaylistItem's Ruleset");
- if (!playlistItem.RequiredMods.All(m => Mods.Value.Any(m.Equals)))
+ if (!PlaylistItem.RequiredMods.All(m => Mods.Value.Any(m.Equals)))
throw new InvalidOperationException("Current Mods do not match PlaylistItem's RequiredMods");
- var req = new CreateRoomScoreRequest(roomId.Value ?? 0, playlistItem.ID, Game.VersionHash);
- req.Success += r => token = r.ID;
+ var req = new CreateRoomScoreRequest(RoomId.Value ?? 0, PlaylistItem.ID, Game.VersionHash);
+ req.Success += r => Token = r.ID;
req.Failure += e =>
{
failed = true;
@@ -76,7 +77,7 @@ namespace osu.Game.Screens.Multi.Play
api.Queue(req);
- while (!failed && !token.HasValue)
+ while (!failed && !Token.HasValue)
Thread.Sleep(1000);
}
@@ -92,8 +93,8 @@ namespace osu.Game.Screens.Multi.Play
protected override ResultsScreen CreateResults(ScoreInfo score)
{
- Debug.Assert(roomId.Value != null);
- return new TimeshiftResultsScreen(score, roomId.Value.Value, playlistItem, true);
+ Debug.Assert(RoomId.Value != null);
+ return new TimeshiftResultsScreen(score, RoomId.Value.Value, PlaylistItem, true);
}
protected override Score CreateScore()
@@ -107,10 +108,10 @@ namespace osu.Game.Screens.Multi.Play
{
await base.SubmitScore(score);
- Debug.Assert(token != null);
+ Debug.Assert(Token != null);
var tcs = new TaskCompletionSource();
- var request = new SubmitRoomScoreRequest(token.Value, roomId.Value ?? 0, playlistItem.ID, score.ScoreInfo);
+ var request = new SubmitRoomScoreRequest(Token.Value, RoomId.Value ?? 0, PlaylistItem.ID, score.ScoreInfo);
request.Success += s =>
{
diff --git a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs
index 3623208fa7..d3f1c19c7c 100644
--- a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs
+++ b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs
@@ -32,8 +32,8 @@ namespace osu.Game.Screens.Multi.Ranking
[Resolved]
private IAPIProvider api { get; set; }
- public TimeshiftResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry)
- : base(score, allowRetry)
+ public TimeshiftResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry, bool allowWatchingReplay = true)
+ : base(score, allowRetry, allowWatchingReplay)
{
this.roomId = roomId;
this.playlistItem = playlistItem;
diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/BeatmapSelectionControl.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/BeatmapSelectionControl.cs
new file mode 100644
index 0000000000..1939744916
--- /dev/null
+++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/BeatmapSelectionControl.cs
@@ -0,0 +1,81 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Specialized;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Screens;
+using osu.Game.Online.API;
+using osu.Game.Screens.Multi.Match.Components;
+
+namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match
+{
+ public class BeatmapSelectionControl : MultiplayerComposite
+ {
+ [Resolved]
+ private RealtimeMatchSubScreen matchSubScreen { get; set; }
+
+ [Resolved]
+ private IAPIProvider api { get; set; }
+
+ private Container beatmapPanelContainer;
+ private Button selectButton;
+
+ public BeatmapSelectionControl()
+ {
+ AutoSizeAxes = Axes.Y;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChild = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ beatmapPanelContainer = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y
+ },
+ selectButton = new PurpleTriangleButton
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 40,
+ Text = "Select beatmap",
+ Action = () => matchSubScreen.Push(new RealtimeMatchSongSelect()),
+ Alpha = 0
+ }
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Playlist.BindCollectionChanged(onPlaylistChanged, true);
+ Host.BindValueChanged(host =>
+ {
+ if (RoomID.Value == null || host.NewValue?.Equals(api.LocalUser.Value) == true)
+ selectButton.Show();
+ else
+ selectButton.Hide();
+ }, true);
+ }
+
+ private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ if (Playlist.Any())
+ beatmapPanelContainer.Child = new DrawableRoomPlaylistItem(Playlist.Single(), false, false);
+ else
+ beatmapPanelContainer.Clear();
+ }
+ }
+}
diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchFooter.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchFooter.cs
new file mode 100644
index 0000000000..31871729f6
--- /dev/null
+++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchFooter.cs
@@ -0,0 +1,48 @@
+// Copyright (c) ppy Pty Ltd . 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.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Online.Multiplayer;
+using osuTK;
+
+namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match
+{
+ public class RealtimeMatchFooter : CompositeDrawable
+ {
+ public const float HEIGHT = 50;
+
+ public readonly Bindable SelectedItem = new Bindable();
+
+ private readonly Drawable background;
+
+ public RealtimeMatchFooter()
+ {
+ RelativeSizeAxes = Axes.X;
+ Height = HEIGHT;
+
+ InternalChildren = new[]
+ {
+ background = new Box { RelativeSizeAxes = Axes.Both },
+ new RealtimeReadyButton
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(600, 50),
+ SelectedItem = { BindTarget = SelectedItem }
+ }
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ background.Colour = Color4Extensions.FromHex(@"28242d");
+ }
+ }
+}
diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchHeader.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchHeader.cs
new file mode 100644
index 0000000000..a9a10d1510
--- /dev/null
+++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchHeader.cs
@@ -0,0 +1,106 @@
+// Copyright (c) ppy Pty Ltd . 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.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.UserInterface;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Online.API;
+using osu.Game.Screens.Multi.Match.Components;
+using osu.Game.Users.Drawables;
+using osuTK;
+using FontWeight = osu.Game.Graphics.FontWeight;
+using OsuColour = osu.Game.Graphics.OsuColour;
+using OsuFont = osu.Game.Graphics.OsuFont;
+
+namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match
+{
+ public class RealtimeMatchHeader : MultiplayerComposite
+ {
+ public const float HEIGHT = 50;
+
+ public Action OpenSettings;
+
+ private UpdateableAvatar avatar;
+ private LinkFlowContainer hostText;
+ private Button openSettingsButton;
+
+ [Resolved]
+ private IAPIProvider api { get; set; }
+
+ public RealtimeMatchHeader()
+ {
+ RelativeSizeAxes = Axes.X;
+ Height = HEIGHT;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ InternalChildren = new Drawable[]
+ {
+ new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(10, 0),
+ Children = new Drawable[]
+ {
+ avatar = new UpdateableAvatar
+ {
+ Size = new Vector2(50),
+ Masking = true,
+ CornerRadius = 10,
+ },
+ new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ new OsuSpriteText
+ {
+ Font = OsuFont.GetFont(size: 30),
+ Current = { BindTarget = RoomName }
+ },
+ hostText = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(size: 20))
+ {
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ }
+ }
+ }
+ }
+ },
+ openSettingsButton = new PurpleTriangleButton
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ Size = new Vector2(150, HEIGHT),
+ Text = "Open settings",
+ Action = () => OpenSettings?.Invoke(),
+ Alpha = 0
+ }
+ };
+
+ Host.BindValueChanged(host =>
+ {
+ avatar.User = host.NewValue;
+
+ hostText.Clear();
+
+ if (host.NewValue != null)
+ {
+ hostText.AddText("hosted by ");
+ hostText.AddUserLink(host.NewValue, s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
+ }
+
+ openSettingsButton.Alpha = host.NewValue?.Equals(api.LocalUser.Value) == true ? 1 : 0;
+ }, true);
+ }
+ }
+}
diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchSettingsOverlay.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchSettingsOverlay.cs
new file mode 100644
index 0000000000..3e495b490f
--- /dev/null
+++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchSettingsOverlay.cs
@@ -0,0 +1,357 @@
+// Copyright (c) ppy Pty Ltd . 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.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Beatmaps;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.Multiplayer;
+using osu.Game.Online.RealtimeMultiplayer;
+using osu.Game.Overlays;
+using osu.Game.Rulesets;
+using osu.Game.Screens.Multi.Match.Components;
+using osuTK;
+
+namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match
+{
+ public class RealtimeMatchSettingsOverlay : MatchSettingsOverlay
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Child = Settings = new MatchSettings
+ {
+ RelativeSizeAxes = Axes.Both,
+ RelativePositionAxes = Axes.Y,
+ SettingsApplied = Hide
+ };
+ }
+
+ protected class MatchSettings : MultiplayerComposite
+ {
+ private const float disabled_alpha = 0.2f;
+
+ public Action SettingsApplied;
+
+ public OsuTextBox NameField, MaxParticipantsField;
+ public RoomAvailabilityPicker AvailabilityPicker;
+ public GameTypePicker TypePicker;
+ public TriangleButton ApplyButton;
+
+ public OsuSpriteText ErrorText;
+
+ private OsuSpriteText typeLabel;
+ private LoadingLayer loadingLayer;
+ private BeatmapSelectionControl initialBeatmapControl;
+
+ [Resolved]
+ private IRoomManager manager { get; set; }
+
+ [Resolved]
+ private StatefulMultiplayerClient client { get; set; }
+
+ [Resolved]
+ private Bindable currentRoom { get; set; }
+
+ [Resolved]
+ private Bindable beatmap { get; set; }
+
+ [Resolved]
+ private Bindable ruleset { get; set; }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ Container dimContent;
+
+ InternalChildren = new Drawable[]
+ {
+ dimContent = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4Extensions.FromHex(@"28242d"),
+ },
+ new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ RowDimensions = new[]
+ {
+ new Dimension(GridSizeMode.Distributed),
+ new Dimension(GridSizeMode.AutoSize),
+ },
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ new OsuScrollContainer
+ {
+ Padding = new MarginPadding
+ {
+ Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING,
+ Vertical = 10
+ },
+ RelativeSizeAxes = Axes.Both,
+ Children = new[]
+ {
+ new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 10),
+ Children = new Drawable[]
+ {
+ new Container
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Padding = new MarginPadding { Horizontal = WaveOverlayContainer.WIDTH_PADDING },
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Children = new Drawable[]
+ {
+ new SectionContainer
+ {
+ Padding = new MarginPadding { Right = FIELD_PADDING / 2 },
+ Children = new[]
+ {
+ new Section("Room name")
+ {
+ Child = NameField = new SettingsTextBox
+ {
+ RelativeSizeAxes = Axes.X,
+ TabbableContentContainer = this,
+ },
+ },
+ new Section("Room visibility")
+ {
+ Alpha = disabled_alpha,
+ Child = AvailabilityPicker = new RoomAvailabilityPicker
+ {
+ Enabled = { Value = false }
+ },
+ },
+ new Section("Game type")
+ {
+ Alpha = disabled_alpha,
+ Child = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Y,
+ RelativeSizeAxes = Axes.X,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(7),
+ Children = new Drawable[]
+ {
+ TypePicker = new GameTypePicker
+ {
+ RelativeSizeAxes = Axes.X,
+ Enabled = { Value = false }
+ },
+ typeLabel = new OsuSpriteText
+ {
+ Font = OsuFont.GetFont(size: 14),
+ Colour = colours.Yellow
+ },
+ },
+ },
+ },
+ },
+ },
+ new SectionContainer
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ Padding = new MarginPadding { Left = FIELD_PADDING / 2 },
+ Children = new[]
+ {
+ new Section("Max participants")
+ {
+ Alpha = disabled_alpha,
+ Child = MaxParticipantsField = new SettingsNumberTextBox
+ {
+ RelativeSizeAxes = Axes.X,
+ TabbableContentContainer = this,
+ ReadOnly = true,
+ },
+ },
+ new Section("Password (optional)")
+ {
+ Alpha = disabled_alpha,
+ Child = new SettingsPasswordTextBox
+ {
+ RelativeSizeAxes = Axes.X,
+ TabbableContentContainer = this,
+ ReadOnly = true,
+ },
+ },
+ }
+ }
+ },
+ },
+ initialBeatmapControl = new BeatmapSelectionControl
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ RelativeSizeAxes = Axes.X,
+ Width = 0.5f
+ }
+ }
+ }
+ },
+ },
+ },
+ new Drawable[]
+ {
+ new Container
+ {
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Y = 2,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4Extensions.FromHex(@"28242d").Darken(0.5f).Opacity(1f),
+ },
+ new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 20),
+ Margin = new MarginPadding { Vertical = 20 },
+ Padding = new MarginPadding { Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING },
+ Children = new Drawable[]
+ {
+ ApplyButton = new CreateOrUpdateButton
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ Size = new Vector2(230, 55),
+ Enabled = { Value = false },
+ Action = apply,
+ },
+ ErrorText = new OsuSpriteText
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ Alpha = 0,
+ Depth = 1,
+ Colour = colours.RedDark
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ }
+ },
+ loadingLayer = new LoadingLayer(dimContent)
+ };
+
+ TypePicker.Current.BindValueChanged(type => typeLabel.Text = type.NewValue?.Name ?? string.Empty, true);
+ RoomName.BindValueChanged(name => NameField.Text = name.NewValue, true);
+ Availability.BindValueChanged(availability => AvailabilityPicker.Current.Value = availability.NewValue, true);
+ 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);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ ApplyButton.Enabled.Value = Playlist.Count > 0 && NameField.Text.Length > 0;
+ }
+
+ private void apply()
+ {
+ if (!ApplyButton.Enabled.Value)
+ return;
+
+ hideError();
+ loadingLayer.Show();
+
+ // If the client is already in a room, update via the client.
+ // 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(() =>
+ {
+ if (t.IsCompletedSuccessfully)
+ onSuccess(currentRoom.Value);
+ else
+ onError(t.Exception?.Message ?? "Error changing settings.");
+ }));
+ }
+ else
+ {
+ currentRoom.Value.Name.Value = NameField.Text;
+ currentRoom.Value.Availability.Value = AvailabilityPicker.Current.Value;
+ currentRoom.Value.Type.Value = TypePicker.Current.Value;
+
+ if (int.TryParse(MaxParticipantsField.Text, out int max))
+ currentRoom.Value.MaxParticipants.Value = max;
+ else
+ currentRoom.Value.MaxParticipants.Value = null;
+
+ manager?.CreateRoom(currentRoom.Value, onSuccess, onError);
+ }
+ }
+
+ private void hideError() => ErrorText.FadeOut(50);
+
+ private void onSuccess(Room room)
+ {
+ loadingLayer.Hide();
+ SettingsApplied?.Invoke();
+ }
+
+ private void onError(string text)
+ {
+ ErrorText.Text = text;
+ ErrorText.FadeIn(50);
+
+ loadingLayer.Hide();
+ }
+ }
+
+ public class CreateOrUpdateButton : TriangleButton
+ {
+ [Resolved(typeof(Room), nameof(Room.RoomID))]
+ private Bindable roomId { get; set; }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ roomId.BindValueChanged(id => Text = id.NewValue == null ? "Create" : "Update", true);
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ BackgroundColour = colours.Yellow;
+ Triangles.ColourLight = colours.YellowLight;
+ Triangles.ColourDark = colours.YellowDark;
+ }
+ }
+ }
+}
diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeReadyButton.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeReadyButton.cs
similarity index 98%
rename from osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeReadyButton.cs
rename to osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeReadyButton.cs
index ea8fb04994..09487e9831 100644
--- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeReadyButton.cs
+++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeReadyButton.cs
@@ -15,7 +15,7 @@ using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Screens.Multi.Components;
using osuTK;
-namespace osu.Game.Screens.Multi.RealtimeMultiplayer
+namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match
{
public class RealtimeReadyButton : RealtimeRoomComposite
{
diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs
index 002849a275..a4ff2ce346 100644
--- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs
+++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants
[Resolved]
private IAPIProvider api { get; set; }
- private ReadyMark readyMark;
+ private StateDisplay userStateDisplay;
private SpriteIcon crown;
public ParticipantPanel(MultiplayerRoomUser user)
@@ -122,12 +122,11 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants
}
}
},
- readyMark = new ReadyMark
+ userStateDisplay = new StateDisplay
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Margin = new MarginPadding { Right = 10 },
- Alpha = 0
}
}
}
@@ -144,10 +143,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants
const double fade_time = 50;
- if (User.State == MultiplayerUserState.Ready)
- readyMark.FadeIn(fade_time);
- else
- readyMark.FadeOut(fade_time);
+ userStateDisplay.Status = User.State;
if (Room.Host?.Equals(User) == true)
crown.FadeIn(fade_time);
diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsListHeader.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsListHeader.cs
new file mode 100644
index 0000000000..0ca7d34005
--- /dev/null
+++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsListHeader.cs
@@ -0,0 +1,31 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Game.Online.RealtimeMultiplayer;
+using osu.Game.Screens.Multi.Components;
+
+namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants
+{
+ public class ParticipantsListHeader : OverlinedHeader
+ {
+ [Resolved]
+ private StatefulMultiplayerClient client { get; set; }
+
+ public ParticipantsListHeader()
+ : base("Participants")
+ {
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ var room = client.Room;
+ if (room == null)
+ return;
+
+ Details.Value = room.Users.Count.ToString();
+ }
+ }
+}
diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ReadyMark.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ReadyMark.cs
deleted file mode 100644
index df49d9342e..0000000000
--- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ReadyMark.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright (c) ppy Pty Ltd . 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")
- }
- }
- };
- }
- }
-}
diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/StateDisplay.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/StateDisplay.cs
new file mode 100644
index 0000000000..844f239363
--- /dev/null
+++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/StateDisplay.cs
@@ -0,0 +1,129 @@
+// Copyright (c) ppy Pty Ltd . 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 osu.Game.Online.RealtimeMultiplayer;
+using osuTK;
+
+namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants
+{
+ public class StateDisplay : CompositeDrawable
+ {
+ public StateDisplay()
+ {
+ AutoSizeAxes = Axes.Both;
+ Alpha = 0;
+ }
+
+ private MultiplayerUserState status;
+
+ private OsuSpriteText text;
+ private SpriteIcon icon;
+
+ private const double fade_time = 50;
+
+ public MultiplayerUserState Status
+ {
+ set
+ {
+ if (value == status)
+ return;
+
+ status = value;
+
+ if (IsLoaded)
+ updateStatus();
+ }
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChild = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Spacing = new Vector2(5),
+ Children = new Drawable[]
+ {
+ text = new OsuSpriteText
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 12),
+ Colour = Color4Extensions.FromHex("#DDFFFF")
+ },
+ icon = new SpriteIcon
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Icon = FontAwesome.Solid.CheckCircle,
+ Size = new Vector2(12),
+ }
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ updateStatus();
+ }
+
+ [Resolved]
+ private OsuColour colours { get; set; }
+
+ private void updateStatus()
+ {
+ switch (status)
+ {
+ default:
+ this.FadeOut(fade_time);
+ return;
+
+ case MultiplayerUserState.Ready:
+ text.Text = "ready";
+ icon.Icon = FontAwesome.Solid.CheckCircle;
+ icon.Colour = Color4Extensions.FromHex("#AADD00");
+ break;
+
+ case MultiplayerUserState.WaitingForLoad:
+ text.Text = "loading";
+ icon.Icon = FontAwesome.Solid.PauseCircle;
+ icon.Colour = colours.Yellow;
+ break;
+
+ case MultiplayerUserState.Loaded:
+ text.Text = "loaded";
+ icon.Icon = FontAwesome.Solid.DotCircle;
+ icon.Colour = colours.YellowLight;
+ break;
+
+ case MultiplayerUserState.Playing:
+ text.Text = "playing";
+ icon.Icon = FontAwesome.Solid.PlayCircle;
+ icon.Colour = colours.BlueLight;
+ break;
+
+ case MultiplayerUserState.FinishedPlay:
+ text.Text = "results pending";
+ icon.Icon = FontAwesome.Solid.ArrowAltCircleUp;
+ icon.Colour = colours.BlueLighter;
+ break;
+
+ case MultiplayerUserState.Results:
+ text.Text = "results";
+ icon.Icon = FontAwesome.Solid.ArrowAltCircleUp;
+ icon.Colour = colours.BlueLighter;
+ break;
+ }
+
+ this.FadeIn(fade_time);
+ }
+ }
+}
diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeFilterControl.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeFilterControl.cs
new file mode 100644
index 0000000000..acd9a057e3
--- /dev/null
+++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeFilterControl.cs
@@ -0,0 +1,17 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Screens.Multi.Lounge.Components;
+
+namespace osu.Game.Screens.Multi.RealtimeMultiplayer
+{
+ public class RealtimeFilterControl : FilterControl
+ {
+ protected override FilterCriteria CreateCriteria()
+ {
+ var criteria = base.CreateCriteria();
+ criteria.Category = "realtime";
+ return criteria;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeLoungeSubScreen.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeLoungeSubScreen.cs
new file mode 100644
index 0000000000..9fbf0c4654
--- /dev/null
+++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeLoungeSubScreen.cs
@@ -0,0 +1,17 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Online.Multiplayer;
+using osu.Game.Screens.Multi.Lounge;
+using osu.Game.Screens.Multi.Lounge.Components;
+using osu.Game.Screens.Multi.Match;
+
+namespace osu.Game.Screens.Multi.RealtimeMultiplayer
+{
+ public class RealtimeLoungeSubScreen : LoungeSubScreen
+ {
+ protected override FilterControl CreateFilterControl() => new RealtimeFilterControl();
+
+ protected override RoomSubScreen CreateRoomSubScreen(Room room) => new RealtimeMatchSubScreen(room);
+ }
+}
diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs
new file mode 100644
index 0000000000..f3dab93089
--- /dev/null
+++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs
@@ -0,0 +1,84 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using Humanizer;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Logging;
+using osu.Framework.Screens;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.Multiplayer;
+using osu.Game.Online.RealtimeMultiplayer;
+using osu.Game.Screens.Select;
+
+namespace osu.Game.Screens.Multi.RealtimeMultiplayer
+{
+ public class RealtimeMatchSongSelect : SongSelect, IMultiplayerSubScreen
+ {
+ public string ShortTitle => "song selection";
+
+ public override string Title => ShortTitle.Humanize();
+
+ [Resolved(typeof(Room), nameof(Room.Playlist))]
+ private BindableList playlist { get; set; }
+
+ [Resolved]
+ private StatefulMultiplayerClient client { get; set; }
+
+ private LoadingLayer loadingLayer;
+
+ public RealtimeMatchSongSelect()
+ {
+ Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddInternal(loadingLayer = new LoadingLayer(Carousel));
+ }
+
+ protected override bool OnStart()
+ {
+ var item = new PlaylistItem();
+
+ item.Beatmap.Value = Beatmap.Value.BeatmapInfo;
+ item.Ruleset.Value = Ruleset.Value;
+
+ item.RequiredMods.Clear();
+ item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy()));
+
+ // If the client is already in a room, update via the client.
+ // Otherwise, update the playlist directly in preparation for it to be submitted to the API on match creation.
+ if (client.Room != null)
+ {
+ loadingLayer.Show();
+
+ client.ChangeSettings(item: item).ContinueWith(t =>
+ {
+ return Schedule(() =>
+ {
+ loadingLayer.Hide();
+
+ if (t.IsCompletedSuccessfully)
+ this.Exit();
+ else
+ Logger.Log($"Could not use current beatmap ({t.Exception?.Message})", level: LogLevel.Important);
+ });
+ });
+ }
+ else
+ {
+ playlist.Clear();
+ playlist.Add(item);
+ this.Exit();
+ }
+
+ return true;
+ }
+
+ protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
+ }
+}
diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs
new file mode 100644
index 0000000000..cdab1435c0
--- /dev/null
+++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs
@@ -0,0 +1,201 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Specialized;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Screens;
+using osu.Game.Online.Multiplayer;
+using osu.Game.Online.RealtimeMultiplayer;
+using osu.Game.Screens.Multi.Components;
+using osu.Game.Screens.Multi.Match;
+using osu.Game.Screens.Multi.Match.Components;
+using osu.Game.Screens.Multi.RealtimeMultiplayer.Match;
+using osu.Game.Screens.Multi.RealtimeMultiplayer.Participants;
+using osu.Game.Screens.Play;
+using osu.Game.Users;
+
+namespace osu.Game.Screens.Multi.RealtimeMultiplayer
+{
+ [Cached]
+ public class RealtimeMatchSubScreen : RoomSubScreen
+ {
+ public override string Title { get; }
+
+ public override string ShortTitle => "match";
+
+ [Resolved(canBeNull: true)]
+ private Multiplayer multiplayer { get; set; }
+
+ [Resolved]
+ private StatefulMultiplayerClient client { get; set; }
+
+ private RealtimeMatchSettingsOverlay settingsOverlay;
+
+ public RealtimeMatchSubScreen(Room room)
+ {
+ Title = room.RoomID.Value == null ? "New match" : room.Name.Value;
+ Activity.Value = new UserActivity.InLobby(room);
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChildren = new Drawable[]
+ {
+ new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding
+ {
+ Horizontal = 105,
+ Vertical = 20
+ },
+ Child = new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ RowDimensions = new[]
+ {
+ new Dimension(GridSizeMode.AutoSize),
+ new Dimension(),
+ },
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ new RealtimeMatchHeader
+ {
+ OpenSettings = () => settingsOverlay.Show()
+ }
+ },
+ new Drawable[]
+ {
+ new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding { Horizontal = 5, Vertical = 10 },
+ Child = new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ RowDimensions = new[]
+ {
+ new Dimension(GridSizeMode.AutoSize)
+ },
+ Content = new[]
+ {
+ new Drawable[] { new ParticipantsListHeader() },
+ new Drawable[]
+ {
+ new Participants.ParticipantsList
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+ }
+ }
+ }
+ },
+ new FillFlowContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding { Horizontal = 5 },
+ Children = new Drawable[]
+ {
+ new OverlinedHeader("Beatmap"),
+ new BeatmapSelectionControl { RelativeSizeAxes = Axes.X }
+ }
+ }
+ }
+ }
+ }
+ },
+ new Drawable[]
+ {
+ new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ RowDimensions = new[]
+ {
+ new Dimension(GridSizeMode.AutoSize)
+ },
+ Content = new[]
+ {
+ new Drawable[] { new OverlinedHeader("Chat") },
+ new Drawable[] { new MatchChatDisplay { RelativeSizeAxes = Axes.Both } }
+ }
+ }
+ }
+ },
+ }
+ }
+ },
+ new Drawable[]
+ {
+ new RealtimeMatchFooter { SelectedItem = { BindTarget = SelectedItem } }
+ }
+ },
+ RowDimensions = new[]
+ {
+ new Dimension(),
+ new Dimension(GridSizeMode.AutoSize),
+ }
+ },
+ settingsOverlay = new RealtimeMatchSettingsOverlay
+ {
+ RelativeSizeAxes = Axes.Both,
+ State = { Value = client.Room == null ? Visibility.Visible : Visibility.Hidden }
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Playlist.BindCollectionChanged(onPlaylistChanged, true);
+
+ client.LoadRequested += onLoadRequested;
+ }
+
+ public override bool OnBackButton()
+ {
+ if (client.Room != null && settingsOverlay.State.Value == Visibility.Visible)
+ {
+ settingsOverlay.Hide();
+ return true;
+ }
+
+ return base.OnBackButton();
+ }
+
+ private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) => SelectedItem.Value = Playlist.FirstOrDefault();
+
+ private void onLoadRequested() => multiplayer?.Push(new PlayerLoader(() => new RealtimePlayer(SelectedItem.Value)));
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (client != null)
+ client.LoadRequested -= onLoadRequested;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs
new file mode 100644
index 0000000000..6455701d31
--- /dev/null
+++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs
@@ -0,0 +1,67 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Logging;
+using osu.Framework.Screens;
+using osu.Game.Online.Multiplayer;
+using osu.Game.Online.RealtimeMultiplayer;
+using osu.Game.Screens.Multi.Components;
+using osu.Game.Screens.Multi.Lounge;
+
+namespace osu.Game.Screens.Multi.RealtimeMultiplayer
+{
+ public class RealtimeMultiplayer : Multiplayer
+ {
+ [Resolved]
+ private StatefulMultiplayerClient client { get; set; }
+
+ public override void OnResuming(IScreen last)
+ {
+ base.OnResuming(last);
+
+ if (client.Room != null)
+ client.ChangeState(MultiplayerUserState.Idle);
+ }
+
+ protected override void UpdatePollingRate(bool isIdle)
+ {
+ var timeshiftManager = (RealtimeRoomManager)RoomManager;
+
+ if (!this.IsCurrentScreen())
+ {
+ timeshiftManager.TimeBetweenListingPolls.Value = 0;
+ timeshiftManager.TimeBetweenSelectionPolls.Value = 0;
+ }
+ else
+ {
+ switch (CurrentSubScreen)
+ {
+ case LoungeSubScreen _:
+ timeshiftManager.TimeBetweenListingPolls.Value = isIdle ? 120000 : 15000;
+ timeshiftManager.TimeBetweenSelectionPolls.Value = isIdle ? 120000 : 15000;
+ break;
+
+ // Don't poll inside the match or anywhere else.
+ default:
+ timeshiftManager.TimeBetweenListingPolls.Value = 0;
+ timeshiftManager.TimeBetweenSelectionPolls.Value = 0;
+ break;
+ }
+ }
+
+ Logger.Log($"Polling adjusted (listing: {timeshiftManager.TimeBetweenListingPolls.Value}, selection: {timeshiftManager.TimeBetweenSelectionPolls.Value})");
+ }
+
+ protected override Room CreateNewRoom()
+ {
+ var room = base.CreateNewRoom();
+ room.Category.Value = RoomCategory.Realtime;
+ return room;
+ }
+
+ protected override RoomManager CreateRoomManager() => new RealtimeRoomManager();
+
+ protected override LoungeSubScreen CreateLounge() => new RealtimeLoungeSubScreen();
+ }
+}
diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs
new file mode 100644
index 0000000000..c6d44686b5
--- /dev/null
+++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs
@@ -0,0 +1,92 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+using osu.Framework.Allocation;
+using osu.Framework.Logging;
+using osu.Framework.Screens;
+using osu.Game.Online.Multiplayer;
+using osu.Game.Online.RealtimeMultiplayer;
+using osu.Game.Scoring;
+using osu.Game.Screens.Multi.Play;
+using osu.Game.Screens.Ranking;
+
+namespace osu.Game.Screens.Multi.RealtimeMultiplayer
+{
+ // Todo: The "room" part of TimeshiftPlayer should be split out into an abstract player class to be inherited instead.
+ public class RealtimePlayer : TimeshiftPlayer
+ {
+ protected override bool PauseOnFocusLost => false;
+
+ // Disallow fails in multiplayer for now.
+ protected override bool CheckModsAllowFailure() => false;
+
+ [Resolved]
+ private StatefulMultiplayerClient client { get; set; }
+
+ private readonly TaskCompletionSource resultsReady = new TaskCompletionSource();
+ private readonly ManualResetEventSlim startedEvent = new ManualResetEventSlim();
+
+ public RealtimePlayer(PlaylistItem playlistItem)
+ : base(playlistItem, false)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ if (Token == null)
+ return; // Todo: Somehow handle token retrieval failure.
+
+ client.MatchStarted += onMatchStarted;
+ client.ResultsReady += onResultsReady;
+ client.ChangeState(MultiplayerUserState.Loaded);
+
+ if (!startedEvent.Wait(TimeSpan.FromSeconds(30)))
+ {
+ Logger.Log("Failed to start the multiplayer match in time.", LoggingTarget.Runtime, LogLevel.Important);
+
+ Schedule(() =>
+ {
+ ValidForResume = false;
+ this.Exit();
+ });
+ }
+ }
+
+ private void onMatchStarted() => startedEvent.Set();
+
+ private void onResultsReady() => resultsReady.SetResult(true);
+
+ protected override async Task SubmitScore(Score score)
+ {
+ await base.SubmitScore(score);
+
+ await client.ChangeState(MultiplayerUserState.FinishedPlay);
+
+ // Await up to 30 seconds for results to become available (3 api request timeouts).
+ // This is arbitrary just to not leave the player in an essentially deadlocked state if any connection issues occur.
+ await Task.WhenAny(resultsReady.Task, Task.Delay(TimeSpan.FromSeconds(30)));
+ }
+
+ protected override ResultsScreen CreateResults(ScoreInfo score)
+ {
+ Debug.Assert(RoomId.Value != null);
+ return new RealtimeResultsScreen(score, RoomId.Value.Value, PlaylistItem);
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (client != null)
+ {
+ client.MatchStarted -= onMatchStarted;
+ client.ResultsReady -= onResultsReady;
+ }
+ }
+ }
+}
diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeResultsScreen.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeResultsScreen.cs
new file mode 100644
index 0000000000..3964a87eb6
--- /dev/null
+++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeResultsScreen.cs
@@ -0,0 +1,17 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Online.Multiplayer;
+using osu.Game.Scoring;
+using osu.Game.Screens.Multi.Ranking;
+
+namespace osu.Game.Screens.Multi.RealtimeMultiplayer
+{
+ public class RealtimeResultsScreen : TimeshiftResultsScreen
+ {
+ public RealtimeResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem)
+ : base(score, roomId, playlistItem, false, false)
+ {
+ }
+ }
+}
diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs
index e0fca3ce4c..7ce031e0e9 100644
--- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs
+++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs
@@ -38,10 +38,10 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer
}
public override void CreateRoom(Room room, Action onSuccess = null, Action onError = null)
- => base.CreateRoom(room, r => joinMultiplayerRoom(r, onSuccess), onError);
+ => base.CreateRoom(room, r => joinMultiplayerRoom(r, onSuccess, onError), onError);
public override void JoinRoom(Room room, Action onSuccess = null, Action onError = null)
- => base.JoinRoom(room, r => joinMultiplayerRoom(r, onSuccess), onError);
+ => base.JoinRoom(room, r => joinMultiplayerRoom(r, onSuccess, onError), onError);
public override void PartRoom()
{
@@ -62,17 +62,18 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer
});
}
- private void joinMultiplayerRoom(Room room, Action onSuccess = null)
+ private void joinMultiplayerRoom(Room room, Action onSuccess = null, Action onError = null)
{
Debug.Assert(room.RoomID.Value != null);
var joinTask = multiplayerClient.JoinRoom(room);
- joinTask.ContinueWith(_ => onSuccess?.Invoke(room), TaskContinuationOptions.OnlyOnRanToCompletion);
+ joinTask.ContinueWith(_ => Schedule(() => onSuccess?.Invoke(room)), TaskContinuationOptions.OnlyOnRanToCompletion);
joinTask.ContinueWith(t =>
{
PartRoom();
if (t.Exception != null)
Logger.Error(t.Exception, "Failed to join multiplayer room.");
+ Schedule(() => onError?.Invoke(t.Exception?.ToString() ?? string.Empty));
}, TaskContinuationOptions.NotOnRanToCompletion);
}
diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftLoungeSubScreen.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftLoungeSubScreen.cs
new file mode 100644
index 0000000000..8e426ffbcc
--- /dev/null
+++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftLoungeSubScreen.cs
@@ -0,0 +1,17 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Online.Multiplayer;
+using osu.Game.Screens.Multi.Lounge;
+using osu.Game.Screens.Multi.Lounge.Components;
+using osu.Game.Screens.Multi.Match;
+
+namespace osu.Game.Screens.Multi.Timeshift
+{
+ public class TimeshiftLoungeSubScreen : LoungeSubScreen
+ {
+ protected override FilterControl CreateFilterControl() => new TimeshiftFilterControl();
+
+ protected override RoomSubScreen CreateRoomSubScreen(Room room) => new TimeshiftRoomSubScreen(room);
+ }
+}
diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftMatchSettingsOverlay.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftMatchSettingsOverlay.cs
new file mode 100644
index 0000000000..7e1e9894d8
--- /dev/null
+++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftMatchSettingsOverlay.cs
@@ -0,0 +1,391 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Specialized;
+using Humanizer;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.Multiplayer;
+using osu.Game.Overlays;
+using osu.Game.Screens.Multi.Match.Components;
+using osuTK;
+
+namespace osu.Game.Screens.Multi.Timeshift
+{
+ public class TimeshiftMatchSettingsOverlay : MatchSettingsOverlay
+ {
+ public Action EditPlaylist;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Child = Settings = new MatchSettings
+ {
+ RelativeSizeAxes = Axes.Both,
+ RelativePositionAxes = Axes.Y,
+ EditPlaylist = () => EditPlaylist?.Invoke()
+ };
+ }
+
+ protected class MatchSettings : MultiplayerComposite
+ {
+ private const float disabled_alpha = 0.2f;
+
+ public Action EditPlaylist;
+
+ public OsuTextBox NameField, MaxParticipantsField;
+ public OsuDropdown DurationField;
+ public RoomAvailabilityPicker AvailabilityPicker;
+ public GameTypePicker TypePicker;
+ public TriangleButton ApplyButton;
+
+ public OsuSpriteText ErrorText;
+
+ private OsuSpriteText typeLabel;
+ private LoadingLayer loadingLayer;
+ private DrawableRoomPlaylist playlist;
+ private OsuSpriteText playlistLength;
+
+ [Resolved(CanBeNull = true)]
+ private IRoomManager manager { get; set; }
+
+ [Resolved]
+ private Bindable currentRoom { get; set; }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ Container dimContent;
+
+ InternalChildren = new Drawable[]
+ {
+ dimContent = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4Extensions.FromHex(@"28242d"),
+ },
+ new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ RowDimensions = new[]
+ {
+ new Dimension(GridSizeMode.Distributed),
+ new Dimension(GridSizeMode.AutoSize),
+ },
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ new OsuScrollContainer
+ {
+ Padding = new MarginPadding
+ {
+ Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING,
+ Vertical = 10
+ },
+ RelativeSizeAxes = Axes.Both,
+ Children = new[]
+ {
+ new Container
+ {
+ Padding = new MarginPadding { Horizontal = WaveOverlayContainer.WIDTH_PADDING },
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Children = new Drawable[]
+ {
+ new SectionContainer
+ {
+ Padding = new MarginPadding { Right = FIELD_PADDING / 2 },
+ Children = new[]
+ {
+ new Section("Room name")
+ {
+ Child = NameField = new SettingsTextBox
+ {
+ RelativeSizeAxes = Axes.X,
+ TabbableContentContainer = this,
+ LengthLimit = 100
+ },
+ },
+ new Section("Duration")
+ {
+ Child = DurationField = new DurationDropdown
+ {
+ RelativeSizeAxes = Axes.X,
+ Items = new[]
+ {
+ TimeSpan.FromMinutes(30),
+ TimeSpan.FromHours(1),
+ TimeSpan.FromHours(2),
+ TimeSpan.FromHours(4),
+ TimeSpan.FromHours(8),
+ TimeSpan.FromHours(12),
+ //TimeSpan.FromHours(16),
+ TimeSpan.FromHours(24),
+ TimeSpan.FromDays(3),
+ TimeSpan.FromDays(7)
+ }
+ }
+ },
+ new Section("Room visibility")
+ {
+ Alpha = disabled_alpha,
+ Child = AvailabilityPicker = new RoomAvailabilityPicker
+ {
+ Enabled = { Value = false }
+ },
+ },
+ new Section("Game type")
+ {
+ Alpha = disabled_alpha,
+ Child = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Y,
+ RelativeSizeAxes = Axes.X,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(7),
+ Children = new Drawable[]
+ {
+ TypePicker = new GameTypePicker
+ {
+ RelativeSizeAxes = Axes.X,
+ Enabled = { Value = false }
+ },
+ typeLabel = new OsuSpriteText
+ {
+ Font = OsuFont.GetFont(size: 14),
+ Colour = colours.Yellow
+ },
+ },
+ },
+ },
+ new Section("Max participants")
+ {
+ Alpha = disabled_alpha,
+ Child = MaxParticipantsField = new SettingsNumberTextBox
+ {
+ RelativeSizeAxes = Axes.X,
+ TabbableContentContainer = this,
+ ReadOnly = true,
+ },
+ },
+ new Section("Password (optional)")
+ {
+ Alpha = disabled_alpha,
+ Child = new SettingsPasswordTextBox
+ {
+ RelativeSizeAxes = Axes.X,
+ TabbableContentContainer = this,
+ ReadOnly = true,
+ },
+ },
+ },
+ },
+ new SectionContainer
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ Padding = new MarginPadding { Left = FIELD_PADDING / 2 },
+ Children = new[]
+ {
+ new Section("Playlist")
+ {
+ Child = new GridContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 300,
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ playlist = new DrawableRoomPlaylist(true, true) { RelativeSizeAxes = Axes.Both }
+ },
+ new Drawable[]
+ {
+ playlistLength = new OsuSpriteText
+ {
+ Margin = new MarginPadding { Vertical = 5 },
+ Colour = colours.Yellow,
+ Font = OsuFont.GetFont(size: 12),
+ }
+ },
+ new Drawable[]
+ {
+ new PurpleTriangleButton
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 40,
+ Text = "Edit playlist",
+ Action = () => EditPlaylist?.Invoke()
+ }
+ }
+ },
+ RowDimensions = new[]
+ {
+ new Dimension(),
+ new Dimension(GridSizeMode.AutoSize),
+ new Dimension(GridSizeMode.AutoSize),
+ }
+ }
+ },
+ },
+ },
+ },
+ }
+ },
+ },
+ },
+ new Drawable[]
+ {
+ new Container
+ {
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Y = 2,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4Extensions.FromHex(@"28242d").Darken(0.5f).Opacity(1f),
+ },
+ new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 20),
+ Margin = new MarginPadding { Vertical = 20 },
+ Padding = new MarginPadding { Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING },
+ Children = new Drawable[]
+ {
+ ApplyButton = new CreateRoomButton
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ Size = new Vector2(230, 55),
+ Enabled = { Value = false },
+ Action = apply,
+ },
+ ErrorText = new OsuSpriteText
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ Alpha = 0,
+ Depth = 1,
+ Colour = colours.RedDark
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ }
+ },
+ loadingLayer = new LoadingLayer(dimContent)
+ };
+
+ TypePicker.Current.BindValueChanged(type => typeLabel.Text = type.NewValue?.Name ?? string.Empty, true);
+ RoomName.BindValueChanged(name => NameField.Text = name.NewValue, true);
+ Availability.BindValueChanged(availability => AvailabilityPicker.Current.Value = availability.NewValue, true);
+ Type.BindValueChanged(type => TypePicker.Current.Value = type.NewValue, true);
+ MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true);
+ Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue ?? TimeSpan.FromMinutes(30), true);
+
+ playlist.Items.BindTo(Playlist);
+ Playlist.BindCollectionChanged(onPlaylistChanged, true);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ ApplyButton.Enabled.Value = hasValidSettings;
+ }
+
+ private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) =>
+ playlistLength.Text = $"Length: {Playlist.GetTotalDuration()}";
+
+ private bool hasValidSettings => RoomID.Value == null && NameField.Text.Length > 0 && Playlist.Count > 0;
+
+ private void apply()
+ {
+ if (!ApplyButton.Enabled.Value)
+ return;
+
+ hideError();
+
+ RoomName.Value = NameField.Text;
+ Availability.Value = AvailabilityPicker.Current.Value;
+ Type.Value = TypePicker.Current.Value;
+
+ if (int.TryParse(MaxParticipantsField.Text, out int max))
+ MaxParticipants.Value = max;
+ else
+ MaxParticipants.Value = null;
+
+ Duration.Value = DurationField.Current.Value;
+
+ manager?.CreateRoom(currentRoom.Value, onSuccess, onError);
+
+ loadingLayer.Show();
+ }
+
+ private void hideError() => ErrorText.FadeOut(50);
+
+ private void onSuccess(Room room) => loadingLayer.Hide();
+
+ private void onError(string text)
+ {
+ ErrorText.Text = text;
+ ErrorText.FadeIn(50);
+
+ loadingLayer.Hide();
+ }
+ }
+
+ public class CreateRoomButton : TriangleButton
+ {
+ public CreateRoomButton()
+ {
+ Text = "Create";
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ BackgroundColour = colours.Yellow;
+ Triangles.ColourLight = colours.YellowLight;
+ Triangles.ColourDark = colours.YellowDark;
+ }
+ }
+
+ private class DurationDropdown : OsuDropdown
+ {
+ public DurationDropdown()
+ {
+ Menu.MaxHeight = 100;
+ }
+
+ protected override string GenerateItemText(TimeSpan item) => item.Humanize();
+ }
+ }
+}
diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs
index d2d6a35a2e..2ea4857799 100644
--- a/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs
+++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Screens.Multi.Timeshift
timeshiftManager.TimeBetweenSelectionPolls.Value = isIdle ? 120000 : 15000;
break;
- case MatchSubScreen _:
+ case RoomSubScreen _:
timeshiftManager.TimeBetweenListingPolls.Value = 0;
timeshiftManager.TimeBetweenSelectionPolls.Value = isIdle ? 30000 : 5000;
break;
@@ -45,5 +45,7 @@ namespace osu.Game.Screens.Multi.Timeshift
}
protected override RoomManager CreateRoomManager() => new TimeshiftRoomManager();
+
+ protected override LoungeSubScreen CreateLounge() => new TimeshiftLoungeSubScreen();
}
}
diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomSubScreen.cs
similarity index 79%
rename from osu.Game/Screens/Multi/Match/MatchSubScreen.cs
rename to osu.Game/Screens/Multi/Timeshift/TimeshiftRoomSubScreen.cs
index 2f8aad4e65..fa901179e9 100644
--- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs
+++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomSubScreen.cs
@@ -1,7 +1,6 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
@@ -9,13 +8,10 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
-using osu.Game.Audio;
-using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
-using osu.Game.Online.Multiplayer.GameTypes;
-using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Multi.Components;
+using osu.Game.Screens.Multi.Match;
using osu.Game.Screens.Multi.Match.Components;
using osu.Game.Screens.Multi.Play;
using osu.Game.Screens.Multi.Ranking;
@@ -24,13 +20,10 @@ using osu.Game.Screens.Select;
using osu.Game.Users;
using Footer = osu.Game.Screens.Multi.Match.Components.Footer;
-namespace osu.Game.Screens.Multi.Match
+namespace osu.Game.Screens.Multi.Timeshift
{
- [Cached(typeof(IPreviewTrackOwner))]
- public class MatchSubScreen : MultiplayerSubScreen, IPreviewTrackOwner
+ public class TimeshiftRoomSubScreen : RoomSubScreen
{
- public override bool DisallowExternalBeatmapRulesetChanges => true;
-
public override string Title { get; }
public override string ShortTitle => "room";
@@ -38,27 +31,15 @@ namespace osu.Game.Screens.Multi.Match
[Resolved(typeof(Room), nameof(Room.RoomID))]
private Bindable roomId { get; set; }
- [Resolved(typeof(Room), nameof(Room.Type))]
- private Bindable type { get; set; }
-
- [Resolved(typeof(Room), nameof(Room.Playlist))]
- private BindableList playlist { get; set; }
-
- [Resolved]
- private BeatmapManager beatmapManager { get; set; }
-
[Resolved(canBeNull: true)]
private Multiplayer multiplayer { get; set; }
- protected readonly Bindable SelectedItem = new Bindable();
-
private MatchSettingsOverlay settingsOverlay;
private MatchLeaderboard leaderboard;
- private IBindable> managerUpdated;
private OverlinedHeader participantsHeader;
- public MatchSubScreen(Room room)
+ public TimeshiftRoomSubScreen(Room room)
{
Title = room.RoomID.Value == null ? "New room" : room.Name.Value;
Activity.Value = new UserActivity.InLobby(room);
@@ -96,7 +77,7 @@ namespace osu.Game.Screens.Multi.Match
},
Content = new[]
{
- new Drawable[] { new Components.Header() },
+ new Drawable[] { new Match.Components.Header() },
new Drawable[]
{
participantsHeader = new OverlinedHeader("Participants")
@@ -141,7 +122,7 @@ namespace osu.Game.Screens.Multi.Match
new DrawableRoomPlaylistWithResults
{
RelativeSizeAxes = Axes.Both,
- Items = { BindTarget = playlist },
+ Items = { BindTarget = Playlist },
SelectedItem = { BindTarget = SelectedItem },
RequestShowResults = item =>
{
@@ -208,7 +189,7 @@ namespace osu.Game.Screens.Multi.Match
new Dimension(GridSizeMode.AutoSize),
}
},
- settingsOverlay = new MatchSettingsOverlay
+ settingsOverlay = new TimeshiftMatchSettingsOverlay
{
RelativeSizeAxes = Axes.Both,
EditPlaylist = () => this.Push(new MatchSongSelect()),
@@ -234,61 +215,17 @@ namespace osu.Game.Screens.Multi.Match
// Set the first playlist item.
// This is scheduled since updating the room and playlist may happen in an arbitrary order (via Room.CopyFrom()).
- Schedule(() => SelectedItem.Value = playlist.FirstOrDefault());
+ Schedule(() => SelectedItem.Value = Playlist.FirstOrDefault());
}
}, true);
-
- SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged));
- SelectedItem.Value = playlist.FirstOrDefault();
-
- managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy();
- managerUpdated.BindValueChanged(beatmapUpdated);
- }
-
- public override bool OnExiting(IScreen next)
- {
- RoomManager?.PartRoom();
- Mods.Value = Array.Empty();
-
- return base.OnExiting(next);
- }
-
- private void selectedItemChanged()
- {
- updateWorkingBeatmap();
-
- var item = SelectedItem.Value;
-
- Mods.Value = item?.RequiredMods?.ToArray() ?? Array.Empty();
-
- if (item?.Ruleset != null)
- Ruleset.Value = item.Ruleset.Value;
- }
-
- private void beatmapUpdated(ValueChangedEvent> weakSet) => Schedule(updateWorkingBeatmap);
-
- private void updateWorkingBeatmap()
- {
- var beatmap = SelectedItem.Value?.Beatmap.Value;
-
- // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
- var localBeatmap = beatmap == null ? null : beatmapManager.QueryBeatmap(b => b.OnlineBeatmapID == beatmap.OnlineBeatmapID);
-
- Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap);
}
private void onStart()
{
- switch (type.Value)
+ multiplayer?.Push(new PlayerLoader(() => new TimeshiftPlayer(SelectedItem.Value)
{
- default:
- case GameTypeTimeshift _:
- multiplayer?.Push(new PlayerLoader(() => new TimeshiftPlayer(SelectedItem.Value)
- {
- Exited = () => leaderboard.RefreshScores()
- }));
- break;
- }
+ Exited = () => leaderboard.RefreshScores()
+ }));
}
}
}
diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs
index 887e7ec8a9..528a1842af 100644
--- a/osu.Game/Screens/Ranking/ResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/ResultsScreen.cs
@@ -57,11 +57,13 @@ namespace osu.Game.Screens.Ranking
private APIRequest nextPageRequest;
private readonly bool allowRetry;
+ private readonly bool allowWatchingReplay;
- protected ResultsScreen(ScoreInfo score, bool allowRetry)
+ protected ResultsScreen(ScoreInfo score, bool allowRetry, bool allowWatchingReplay = true)
{
Score = score;
this.allowRetry = allowRetry;
+ this.allowWatchingReplay = allowWatchingReplay;
SelectedScore.Value = score;
}
@@ -128,15 +130,7 @@ namespace osu.Game.Screens.Ranking
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(5),
- Direction = FillDirection.Horizontal,
- Children = new Drawable[]
- {
- new ReplayDownloadButton(null)
- {
- Score = { BindTarget = SelectedScore },
- Width = 300
- },
- }
+ Direction = FillDirection.Horizontal
}
}
}
@@ -157,6 +151,15 @@ namespace osu.Game.Screens.Ranking
ScorePanelList.AddScore(Score, shouldFlair);
}
+ if (allowWatchingReplay)
+ {
+ buttons.Add(new ReplayDownloadButton(null)
+ {
+ Score = { BindTarget = SelectedScore },
+ Width = 300
+ });
+ }
+
if (player != null && allowRetry)
{
buttons.Add(new RetryButton { Width = 300 });
diff --git a/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs b/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs
index aec70d8be4..30bd3ebc32 100644
--- a/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs
+++ b/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs
@@ -7,8 +7,8 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.RealtimeMultiplayer;
+using osu.Game.Screens.Multi;
using osu.Game.Screens.Multi.Lounge.Components;
-using osu.Game.Screens.Multi.RealtimeMultiplayer;
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
{
@@ -17,7 +17,7 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer
[Cached(typeof(StatefulMultiplayerClient))]
public TestRealtimeMultiplayerClient Client { get; }
- [Cached(typeof(RealtimeRoomManager))]
+ [Cached(typeof(IRoomManager))]
public TestRealtimeRoomManager RoomManager { get; }
[Cached]
diff --git a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomContainer.cs b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomContainer.cs
index aa75968cca..3565d6ac5d 100644
--- a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomContainer.cs
+++ b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomContainer.cs
@@ -6,8 +6,8 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.RealtimeMultiplayer;
+using osu.Game.Screens.Multi;
using osu.Game.Screens.Multi.Lounge.Components;
-using osu.Game.Screens.Multi.RealtimeMultiplayer;
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
{
@@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer
[Cached(typeof(StatefulMultiplayerClient))]
public readonly TestRealtimeMultiplayerClient Client;
- [Cached(typeof(RealtimeRoomManager))]
+ [Cached(typeof(IRoomManager))]
public readonly TestRealtimeRoomManager RoomManager;
[Cached]
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 960959f367..cbf9f6f1bd 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -26,7 +26,7 @@
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index a5bcb91c74..adbcc0ef1c 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
-
+
@@ -88,7 +88,7 @@
-
+