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/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/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/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/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/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]