diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index a317ef252d..10aae70722 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty } } - protected override Skill[] CreateSkills(IBeatmap beatmap) + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) { halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f; @@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty return new Skill[] { - new Movement(halfCatcherWidth), + new Movement(mods, halfCatcherWidth), }; } diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index e679231638..9ad719be1a 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -5,6 +5,7 @@ using System; using osu.Game.Rulesets.Catch.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Catch.Difficulty.Skills { @@ -25,7 +26,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills private float lastDistanceMoved; private double lastStrainTime; - public Movement(float halfCatcherWidth) + public Movement(Mod[] mods, float halfCatcherWidth) + : base(mods) { HalfCatcherWidth = halfCatcherWidth; } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index ade830764d..8c0b9ed8b7 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -68,9 +68,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty // Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required. protected override IEnumerable SortObjects(IEnumerable input) => input; - protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[] { - new Strain(((ManiaBeatmap)beatmap).TotalColumns) + new Strain(mods, ((ManiaBeatmap)beatmap).TotalColumns) }; protected override Mod[] DifficultyAdjustmentMods diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs index 7ebc1ff752..d6ea58ee78 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs @@ -6,6 +6,7 @@ using osu.Framework.Utils; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mania.Difficulty.Preprocessing; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Mania.Difficulty.Skills @@ -24,7 +25,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills private double individualStrain; private double overallStrain; - public Strain(int totalColumns) + public Strain(Mod[] mods, int totalColumns) + : base(mods) { holdEndTimes = new double[totalColumns]; individualStrains = new double[totalColumns]; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 6a7d76151c..75d6786d95 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -79,10 +79,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty } } - protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[] { - new Aim(), - new Speed() + new Aim(mods), + new Speed(mods) }; protected override Mod[] DifficultyAdjustmentMods => new Mod[] diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index e74f4933b2..90cba13c7c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -4,6 +4,7 @@ using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Objects; @@ -17,6 +18,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private const double angle_bonus_begin = Math.PI / 3; private const double timing_threshold = 107; + public Aim(Mod[] mods) + : base(mods) + { + } + protected override double SkillMultiplier => 26.25; protected override double StrainDecayBase => 0.15; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 01f2fb8dc8..200bc7997d 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -4,6 +4,7 @@ using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Objects; @@ -27,6 +28,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private const double max_speed_bonus = 45; // ~330BPM private const double speed_balancing_factor = 40; + public Speed(Mod[] mods) + : base(mods) + { + } + protected override double StrainValueOf(DifficultyHitObject current) { if (current.BaseObject is Spinner) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index 32421ee00a..cc0738e252 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -5,6 +5,7 @@ using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Objects; @@ -39,6 +40,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// private int currentMonoLength; + public Colour(Mod[] mods) + : base(mods) + { + } + protected override double StrainValueOf(DifficultyHitObject current) { // changing from/to a drum roll or a swell does not constitute a colour change. diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index 5569b27ad5..f2b8309ac5 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -5,6 +5,7 @@ using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Objects; @@ -47,6 +48,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// private int notesSinceRhythmChange; + public Rhythm(Mod[] mods) + : base(mods) + { + } + protected override double StrainValueOf(DifficultyHitObject current) { // drum rolls and swells are exempt. diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 0b61eb9930..c34cce0cd6 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -5,6 +5,7 @@ using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Objects; @@ -48,8 +49,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// /// Creates a skill. /// + /// Mods for use in skill calculations. /// Whether this instance is performing calculations for the right hand. - public Stamina(bool rightHand) + public Stamina(Mod[] mods, bool rightHand) + : base(mods) { hand = rightHand ? 1 : 0; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index e5485db4df..fc198d2493 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -29,12 +29,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { } - protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[] { - new Colour(), - new Rhythm(), - new Stamina(true), - new Stamina(false), + new Colour(mods), + new Rhythm(mods), + new Stamina(mods, true), + new Stamina(mods, false), }; protected override Mod[] DifficultyAdjustmentMods => new Mod[] diff --git a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs index 5c7adb3f49..1c0bfd56dd 100644 --- a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs +++ b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs @@ -212,7 +212,7 @@ namespace osu.Game.Tests.NonVisual throw new NotImplementedException(); } - protected override Skill[] CreateSkills(IBeatmap beatmap) + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) { throw new NotImplementedException(); } diff --git a/osu.Game.Tests/OnlinePlay/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/OnlinePlay/StatefulMultiplayerClientTest.cs new file mode 100644 index 0000000000..82ce588c6f --- /dev/null +++ b/osu.Game.Tests/OnlinePlay/StatefulMultiplayerClientTest.cs @@ -0,0 +1,35 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Tests.Visual.Multiplayer; +using osu.Game.Users; + +namespace osu.Game.Tests.OnlinePlay +{ + [HeadlessTest] + public class StatefulMultiplayerClientTest : MultiplayerTestScene + { + [Test] + public void TestUserAddedOnJoin() + { + var user = new User { Id = 33 }; + + AddRepeatStep("add user multiple times", () => Client.AddUser(user), 3); + AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2); + } + + [Test] + public void TestUserRemovedOnLeave() + { + var user = new User { Id = 44 }; + + AddStep("add user", () => Client.AddUser(user)); + AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2); + + AddRepeatStep("remove user multiple times", () => Client.RemoveUser(user), 3); + AddAssert("room has 1 user", () => Client.Room?.Users.Count == 1); + } + } +} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 2e39471dc0..78bc51e47b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -1,46 +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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Game.Online.Multiplayer; using osu.Game.Screens.OnlinePlay.Components; -using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneMultiplayer : MultiplayerTestScene + public class TestSceneMultiplayer : ScreenTestScene { + private TestMultiplayer multiplayerScreen; + public TestSceneMultiplayer() { - var multi = new TestMultiplayer(); + AddStep("show", () => + { + multiplayerScreen = new TestMultiplayer(); - AddStep("show", () => LoadScreen(multi)); - AddUntilStep("wait for loaded", () => multi.IsLoaded); - } + // Needs to be added at a higher level since the multiplayer screen becomes non-current. + Child = multiplayerScreen.Client; - [Test] - public void TestOneUserJoinedMultipleTimes() - { - var user = new User { Id = 33 }; + LoadScreen(multiplayerScreen); + }); - AddRepeatStep("add user multiple times", () => Client.AddUser(user), 3); - - AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2); - } - - [Test] - public void TestOneUserLeftMultipleTimes() - { - var user = new User { Id = 44 }; - - AddStep("add user", () => Client.AddUser(user)); - AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2); - - AddRepeatStep("remove user multiple times", () => Client.RemoveUser(user), 3); - AddAssert("room has 1 user", () => Client.Room?.Users.Count == 1); + AddUntilStep("wait for loaded", () => multiplayerScreen.IsLoaded); } private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer { + [Cached(typeof(StatefulMultiplayerClient))] + public readonly TestMultiplayerClient Client; + + public TestMultiplayer() + { + Client = new TestMultiplayerClient((TestMultiplayerRoomManager)RoomManager); + } + protected override RoomManager CreateRoomManager() => new TestMultiplayerRoomManager(); } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 2344ebea0e..8869718fd1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -3,12 +3,10 @@ using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; -using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Tests.Beatmaps; @@ -20,9 +18,6 @@ namespace osu.Game.Tests.Visual.Multiplayer { private MultiplayerMatchSubScreen screen; - [Cached] - private OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker(); - public TestSceneMultiplayerMatchSubScreen() : base(false) { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs index 6de5704410..91c15de69f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs @@ -1,10 +1,13 @@ // 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 NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual.Multiplayer { @@ -21,15 +24,15 @@ namespace osu.Game.Tests.Visual.Multiplayer { createRoomManager().With(d => d.OnLoadComplete += _ => { - roomManager.CreateRoom(new Room { Name = { Value = "1" } }); + roomManager.CreateRoom(createRoom(r => r.Name.Value = "1")); roomManager.PartRoom(); - roomManager.CreateRoom(new Room { Name = { Value = "2" } }); + roomManager.CreateRoom(createRoom(r => r.Name.Value = "2")); roomManager.PartRoom(); roomManager.ClearRooms(); }); }); - AddAssert("manager polled for rooms", () => roomManager.Rooms.Count == 2); + AddAssert("manager polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 2); AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value); } @@ -40,16 +43,16 @@ namespace osu.Game.Tests.Visual.Multiplayer { createRoomManager().With(d => d.OnLoadComplete += _ => { - roomManager.CreateRoom(new Room()); + roomManager.CreateRoom(createRoom()); roomManager.PartRoom(); - roomManager.CreateRoom(new Room()); + roomManager.CreateRoom(createRoom()); roomManager.PartRoom(); }); }); AddStep("disconnect", () => roomContainer.Client.Disconnect()); - AddAssert("rooms cleared", () => roomManager.Rooms.Count == 0); + AddAssert("rooms cleared", () => ((RoomManager)roomManager).Rooms.Count == 0); AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value); } @@ -60,9 +63,9 @@ namespace osu.Game.Tests.Visual.Multiplayer { createRoomManager().With(d => d.OnLoadComplete += _ => { - roomManager.CreateRoom(new Room()); + roomManager.CreateRoom(createRoom()); roomManager.PartRoom(); - roomManager.CreateRoom(new Room()); + roomManager.CreateRoom(createRoom()); roomManager.PartRoom(); }); }); @@ -70,7 +73,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("disconnect", () => roomContainer.Client.Disconnect()); AddStep("connect", () => roomContainer.Client.Connect()); - AddAssert("manager polled for rooms", () => roomManager.Rooms.Count == 2); + AddAssert("manager polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 2); AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value); } @@ -81,12 +84,12 @@ namespace osu.Game.Tests.Visual.Multiplayer { createRoomManager().With(d => d.OnLoadComplete += _ => { - roomManager.CreateRoom(new Room()); + roomManager.CreateRoom(createRoom()); roomManager.ClearRooms(); }); }); - AddAssert("manager not polled for rooms", () => roomManager.Rooms.Count == 0); + AddAssert("manager not polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 0); AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value); } @@ -97,7 +100,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { createRoomManager().With(d => d.OnLoadComplete += _ => { - roomManager.CreateRoom(new Room()); + roomManager.CreateRoom(createRoom()); }); }); @@ -111,7 +114,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { createRoomManager().With(d => d.OnLoadComplete += _ => { - roomManager.CreateRoom(new Room()); + roomManager.CreateRoom(createRoom()); roomManager.PartRoom(); }); }); @@ -126,7 +129,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { createRoomManager().With(d => d.OnLoadComplete += _ => { - var r = new Room(); + var r = createRoom(); roomManager.CreateRoom(r); roomManager.PartRoom(); roomManager.JoinRoom(r); @@ -136,6 +139,21 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("multiplayer room joined", () => roomContainer.Client.Room != null); } + private Room createRoom(Action initFunc = null) + { + var room = new Room(); + + room.Name.Value = "test room"; + room.Playlist.Add(new PlaylistItem + { + Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, + Ruleset = { Value = Ruleset.Value } + }); + + initFunc?.Invoke(room); + return room; + } + private TestMultiplayerRoomManager createRoomManager() { Child = roomContainer = new TestMultiplayerRoomContainer diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs new file mode 100644 index 0000000000..8f1c17ed29 --- /dev/null +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs @@ -0,0 +1,43 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Overlays.Settings; + +namespace osu.Game.Tests.Visual.Settings +{ + [TestFixture] + public class TestSceneSettingsItem : OsuTestScene + { + [Test] + public void TestRestoreDefaultValueButtonVisibility() + { + TestSettingsTextBox textBox = null; + + AddStep("create settings item", () => Child = textBox = new TestSettingsTextBox + { + Current = new Bindable + { + Default = "test", + Value = "test" + } + }); + AddAssert("restore button hidden", () => textBox.RestoreDefaultValueButton.Alpha == 0); + + AddStep("change value from default", () => textBox.Current.Value = "non-default"); + AddUntilStep("restore button shown", () => textBox.RestoreDefaultValueButton.Alpha > 0); + + AddStep("restore default", () => textBox.Current.SetDefault()); + AddUntilStep("restore button hidden", () => textBox.RestoreDefaultValueButton.Alpha == 0); + } + + private class TestSettingsTextBox : SettingsTextBox + { + public new Drawable RestoreDefaultValueButton => this.ChildrenOfType().Single(); + } + } +} diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 95d76f384f..4529dfd0a7 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -9,7 +9,9 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Game.Beatmaps; using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; namespace osu.Game.Online.Multiplayer @@ -121,6 +123,29 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.StartMatch)); } + protected override Task GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default) + { + var tcs = new TaskCompletionSource(); + var req = new GetBeatmapSetRequest(beatmapId, BeatmapSetLookupType.BeatmapId); + + req.Success += res => + { + if (cancellationToken.IsCancellationRequested) + { + tcs.SetCanceled(); + return; + } + + tcs.SetResult(res.ToBeatmapSet(Rulesets)); + }; + + req.Failure += e => tcs.SetException(e); + + API.Queue(req); + + return tcs.Task; + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index bfd505fb19..73100be505 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -17,8 +17,6 @@ using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Rulesets; @@ -71,7 +69,7 @@ namespace osu.Game.Online.Multiplayer /// /// The corresponding to the local player, if available. /// - public MultiplayerRoomUser? LocalUser => Room?.Users.SingleOrDefault(u => u.User?.Id == api.LocalUser.Value.Id); + public MultiplayerRoomUser? LocalUser => Room?.Users.SingleOrDefault(u => u.User?.Id == API.LocalUser.Value.Id); /// /// Whether the is the host in . @@ -85,15 +83,15 @@ namespace osu.Game.Online.Multiplayer } } + [Resolved] + protected IAPIProvider API { get; private set; } = null!; + + [Resolved] + protected RulesetStore Rulesets { get; private set; } = null!; + [Resolved] private UserLookupCache userLookupCache { get; set; } = null!; - [Resolved] - private IAPIProvider api { get; set; } = null!; - - [Resolved] - private RulesetStore rulesets { get; set; } = null!; - // Only exists for compatibility with old osu-server-spectator build. // Todo: Can be removed on 2021/02/26. private long defaultPlaylistItemId; @@ -515,30 +513,26 @@ namespace osu.Game.Online.Multiplayer RoomUpdated?.Invoke(); - var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId); - req.Success += res => + GetOnlineBeatmapSet(settings.BeatmapID, cancellationToken).ContinueWith(set => Schedule(() => { if (cancellationToken.IsCancellationRequested) return; - updatePlaylist(settings, res); - }; - - api.Queue(req); + updatePlaylist(settings, set.Result); + }), TaskContinuationOptions.OnlyOnRanToCompletion); }, cancellationToken); - private void updatePlaylist(MultiplayerRoomSettings settings, APIBeatmapSet onlineSet) + private void updatePlaylist(MultiplayerRoomSettings settings, BeatmapSetInfo beatmapSet) { if (Room == null || !Room.Settings.Equals(settings)) return; Debug.Assert(apiRoom != null); - var beatmapSet = onlineSet.ToBeatmapSet(rulesets); var beatmap = beatmapSet.Beatmaps.Single(b => b.OnlineBeatmapID == settings.BeatmapID); beatmap.MD5Hash = settings.BeatmapChecksum; - var ruleset = rulesets.GetRuleset(settings.RulesetID).CreateInstance(); + var ruleset = Rulesets.GetRuleset(settings.RulesetID).CreateInstance(); var mods = settings.RequiredMods.Select(m => m.ToMod(ruleset)); var allowedMods = settings.AllowedMods.Select(m => m.ToMod(ruleset)); @@ -568,6 +562,14 @@ namespace osu.Game.Online.Multiplayer } } + /// + /// Retrieves a from an online source. + /// + /// The beatmap set ID. + /// A token to cancel the request. + /// The retrieval task. + protected abstract Task GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default); + /// /// For the provided user ID, update whether the user is included in . /// diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 771bcd2310..203cc458e0 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -361,14 +361,6 @@ namespace osu.Game PerformFromScreen(screen => { - // we might already be at song select, so a check is required before performing the load to solo. - if (screen is MainMenu) - menuScreen.LoadToSolo(); - - // we might even already be at the song - if (Beatmap.Value.BeatmapSetInfo.Hash == databasedSet.Hash && (difficultyCriteria?.Invoke(Beatmap.Value.BeatmapInfo) ?? true)) - return; - // Find beatmaps that match our predicate. var beatmaps = databasedSet.Beatmaps.Where(b => difficultyCriteria?.Invoke(b) ?? true).ToList(); @@ -381,9 +373,16 @@ namespace osu.Game ?? beatmaps.FirstOrDefault(b => b.Ruleset.Equals(Ruleset.Value)) ?? beatmaps.First(); - Ruleset.Value = selection.Ruleset; - Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection); - }, validScreens: new[] { typeof(SongSelect) }); + if (screen is IHandlePresentBeatmap presentableScreen) + { + presentableScreen.PresentBeatmap(BeatmapManager.GetWorkingBeatmap(selection), selection.Ruleset); + } + else + { + Ruleset.Value = selection.Ruleset; + Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection); + } + }, validScreens: new[] { typeof(SongSelect), typeof(IHandlePresentBeatmap) }); } /// @@ -881,13 +880,8 @@ namespace osu.Game switch (action) { case GlobalAction.ResetInputSettings: - var sensitivity = frameworkConfig.GetBindable(FrameworkSetting.CursorSensitivity); - - sensitivity.Disabled = false; - sensitivity.Value = 1; - sensitivity.Disabled = true; - - frameworkConfig.Set(FrameworkSetting.IgnoredInputHandlers, string.Empty); + frameworkConfig.GetBindable(FrameworkSetting.IgnoredInputHandlers).SetDefault(); + frameworkConfig.GetBindable(FrameworkSetting.CursorSensitivity).SetDefault(); frameworkConfig.GetBindable(FrameworkSetting.ConfineMouseMode).SetDefault(); return true; diff --git a/osu.Game/Overlays/Dialog/ConfirmDialog.cs b/osu.Game/Overlays/Dialog/ConfirmDialog.cs new file mode 100644 index 0000000000..a87c06ffdf --- /dev/null +++ b/osu.Game/Overlays/Dialog/ConfirmDialog.cs @@ -0,0 +1,42 @@ +// 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.Graphics.Sprites; + +namespace osu.Game.Overlays.Dialog +{ + /// + /// A dialog which confirms a user action. + /// + public class ConfirmDialog : PopupDialog + { + /// + /// Construct a new confirmation dialog. + /// + /// The description of the action to be displayed to the user. + /// An action to perform on confirmation. + /// An optional action to perform on cancel. + public ConfirmDialog(string message, Action onConfirm, Action onCancel = null) + { + HeaderText = message; + BodyText = "Last chance to turn back"; + + Icon = FontAwesome.Solid.ExclamationTriangle; + + Buttons = new PopupDialogButton[] + { + new PopupDialogOkButton + { + Text = @"Yes", + Action = onConfirm + }, + new PopupDialogCancelButton + { + Text = @"Cancel", + Action = onCancel + }, + }; + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index 768a18cca0..3a78cff890 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -17,7 +17,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input protected override string Header => "Mouse"; private readonly BindableBool rawInputToggle = new BindableBool(); - private Bindable sensitivityBindable = new BindableDouble(); + + private Bindable configSensitivity; + + private Bindable localSensitivity; + private Bindable ignoredInputHandlers; private Bindable windowMode; @@ -26,12 +30,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input [BackgroundDependencyLoader] private void load(OsuConfigManager osuConfig, FrameworkConfigManager config) { - var configSensitivity = config.GetBindable(FrameworkSetting.CursorSensitivity); - // use local bindable to avoid changing enabled state of game host's bindable. - sensitivityBindable = configSensitivity.GetUnboundCopy(); - configSensitivity.BindValueChanged(val => sensitivityBindable.Value = val.NewValue); - sensitivityBindable.BindValueChanged(val => configSensitivity.Value = val.NewValue); + configSensitivity = config.GetBindable(FrameworkSetting.CursorSensitivity); + localSensitivity = configSensitivity.GetUnboundCopy(); + + windowMode = config.GetBindable(FrameworkSetting.WindowMode); + ignoredInputHandlers = config.GetBindable(FrameworkSetting.IgnoredInputHandlers); Children = new Drawable[] { @@ -43,7 +47,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input new SensitivitySetting { LabelText = "Cursor sensitivity", - Current = sensitivityBindable + Current = localSensitivity }, new SettingsCheckbox { @@ -66,8 +70,23 @@ namespace osu.Game.Overlays.Settings.Sections.Input Current = osuConfig.GetBindable(OsuSetting.MouseDisableButtons) }, }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + configSensitivity.BindValueChanged(val => + { + var disabled = localSensitivity.Disabled; + + localSensitivity.Disabled = false; + localSensitivity.Value = val.NewValue; + localSensitivity.Disabled = disabled; + }, true); + + localSensitivity.BindValueChanged(val => configSensitivity.Value = val.NewValue); - windowMode = config.GetBindable(FrameworkSetting.WindowMode); windowMode.BindValueChanged(mode => { var isFullscreen = mode.NewValue == WindowMode.Fullscreen; @@ -87,7 +106,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows) { rawInputToggle.Disabled = true; - sensitivityBindable.Disabled = true; + localSensitivity.Disabled = true; } else { @@ -100,12 +119,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input ignoredInputHandlers.Value = enabled.NewValue ? standard_mouse_handlers : raw_mouse_handler; }; - ignoredInputHandlers = config.GetBindable(FrameworkSetting.IgnoredInputHandlers); ignoredInputHandlers.ValueChanged += handler => { bool raw = !handler.NewValue.Contains("Raw"); rawInputToggle.Value = raw; - sensitivityBindable.Disabled = !raw; + localSensitivity.Disabled = !raw; }; ignoredInputHandlers.TriggerChange(); diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index 4cb8d7f83c..8631b8ac7b 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -121,7 +121,7 @@ namespace osu.Game.Overlays.Settings labelText.Alpha = controlWithCurrent.Current.Disabled ? 0.3f : 1; } - private class RestoreDefaultValueButton : Container, IHasTooltip + protected internal class RestoreDefaultValueButton : Container, IHasTooltip { private Bindable bindable; @@ -147,6 +147,7 @@ namespace osu.Game.Overlays.Settings RelativeSizeAxes = Axes.Y; Width = SettingsPanel.CONTENT_MARGINS; Alpha = 0f; + AlwaysPresent = true; } [BackgroundDependencyLoader] diff --git a/osu.Game/PerformFromMenuRunner.cs b/osu.Game/PerformFromMenuRunner.cs index fe75a3a607..6f979b8dc8 100644 --- a/osu.Game/PerformFromMenuRunner.cs +++ b/osu.Game/PerformFromMenuRunner.cs @@ -5,11 +5,9 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Screens; using osu.Framework.Threading; -using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Notifications; @@ -30,9 +28,6 @@ namespace osu.Game [Resolved] private DialogOverlay dialogOverlay { get; set; } - [Resolved] - private IBindable beatmap { get; set; } - [Resolved(canBeNull: true)] private OsuGame game { get; set; } @@ -90,7 +85,7 @@ namespace osu.Game var type = current.GetType(); // check if we are already at a valid target screen. - if (validScreens.Any(t => t.IsAssignableFrom(type)) && !beatmap.Disabled) + if (validScreens.Any(t => t.IsAssignableFrom(type))) { finalAction(current); Cancel(); diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index f15e5e1df0..a25dc3e6db 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Difficulty private DifficultyAttributes calculate(IBeatmap beatmap, Mod[] mods, double clockRate) { - var skills = CreateSkills(beatmap); + var skills = CreateSkills(beatmap, mods); if (!beatmap.HitObjects.Any()) return CreateDifficultyAttributes(beatmap, mods, skills, clockRate); @@ -202,7 +202,8 @@ namespace osu.Game.Rulesets.Difficulty /// Creates the s to calculate the difficulty of an . /// /// The whose difficulty will be calculated. + /// Mods to calculate difficulty with. /// The s. - protected abstract Skill[] CreateSkills(IBeatmap beatmap); + protected abstract Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods); } } diff --git a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs index 1063a24b27..95117be073 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Difficulty.Skills { @@ -46,10 +47,22 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// protected double CurrentStrain { get; private set; } = 1; + /// + /// Mods for use in skill calculations. + /// + protected IReadOnlyList Mods => mods; + private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section. private readonly List strainPeaks = new List(); + private readonly Mod[] mods; + + protected Skill(Mod[] mods) + { + this.mods = mods; + } + /// /// Process a and update current strain values accordingly. /// diff --git a/osu.Game/Screens/IHandlePresentBeatmap.cs b/osu.Game/Screens/IHandlePresentBeatmap.cs new file mode 100644 index 0000000000..60801fb3eb --- /dev/null +++ b/osu.Game/Screens/IHandlePresentBeatmap.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.Beatmaps; +using osu.Game.Rulesets; + +namespace osu.Game.Screens +{ + /// + /// Denotes a screen which can handle beatmap / ruleset selection via local logic. + /// This is used in the flow to handle cases which require custom logic, + /// for instance, if a lease is held on the Beatmap. + /// + public interface IHandlePresentBeatmap + { + /// + /// Invoked with a requested beatmap / ruleset for selection. + /// + /// The beatmap to be selected. + /// The ruleset to be selected. + void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset); + } +} diff --git a/osu.Game/Screens/Menu/ConfirmExitDialog.cs b/osu.Game/Screens/Menu/ConfirmExitDialog.cs index d120eb21a8..6488a2fd63 100644 --- a/osu.Game/Screens/Menu/ConfirmExitDialog.cs +++ b/osu.Game/Screens/Menu/ConfirmExitDialog.cs @@ -9,10 +9,15 @@ namespace osu.Game.Screens.Menu { public class ConfirmExitDialog : PopupDialog { - public ConfirmExitDialog(Action confirm, Action cancel) + /// + /// Construct a new exit confirmation dialog. + /// + /// An action to perform on confirmation. + /// An optional action to perform on cancel. + public ConfirmExitDialog(Action onConfirm, Action onCancel = null) { - HeaderText = "Are you sure you want to exit?"; - BodyText = "Last chance to back out."; + HeaderText = "Are you sure you want to exit osu!?"; + BodyText = "Last chance to turn back"; Icon = FontAwesome.Solid.ExclamationTriangle; @@ -20,13 +25,13 @@ namespace osu.Game.Screens.Menu { new PopupDialogOkButton { - Text = @"Goodbye", - Action = confirm + Text = @"Let me out!", + Action = onConfirm }, new PopupDialogCancelButton { - Text = @"Just a little more", - Action = cancel + Text = @"Just a little more...", + Action = onCancel }, }; } diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 424e6d2cd5..baeb86c976 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -9,12 +9,14 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Framework.Screens; +using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.IO; using osu.Game.Online.API; using osu.Game.Overlays; +using osu.Game.Rulesets; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; using osu.Game.Screens.OnlinePlay.Multiplayer; @@ -23,7 +25,7 @@ using osu.Game.Screens.Select; namespace osu.Game.Screens.Menu { - public class MainMenu : OsuScreen + public class MainMenu : OsuScreen, IHandlePresentBeatmap { public const float FADE_IN_DURATION = 300; @@ -104,7 +106,7 @@ namespace osu.Game.Screens.Menu Beatmap.SetDefault(); this.Push(new Editor()); }, - OnSolo = onSolo, + OnSolo = loadSoloSongSelect, OnMultiplayer = () => this.Push(new Multiplayer()), OnPlaylists = () => this.Push(new Playlists()), OnExit = confirmAndExit, @@ -160,9 +162,7 @@ namespace osu.Game.Screens.Menu LoadComponentAsync(songSelect = new PlaySongSelect()); } - public void LoadToSolo() => Schedule(onSolo); - - private void onSolo() => this.Push(consumeSongSelect()); + private void loadSoloSongSelect() => this.Push(consumeSongSelect()); private Screen consumeSongSelect() { @@ -289,5 +289,13 @@ namespace osu.Game.Screens.Menu this.FadeOut(3000); return base.OnExiting(next); } + + public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset) + { + Beatmap.Value = beatmap; + Ruleset.Value = ruleset; + + Schedule(loadSoloSongSelect); + } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index f17d97c3fd..c9f0f6de90 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -4,9 +4,11 @@ using osu.Framework.Allocation; using osu.Framework.Logging; using osu.Framework.Screens; +using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select; @@ -19,6 +21,23 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private LoadingLayer loadingLayer; + /// + /// Construct a new instance of multiplayer song select. + /// + /// An optional initial beatmap selection to perform. + /// An optional initial ruleset selection to perform. + public MultiplayerMatchSongSelect(WorkingBeatmap beatmap = null, RulesetInfo ruleset = null) + { + if (beatmap != null || ruleset != null) + { + Schedule(() => + { + if (beatmap != null) Beatmap.Value = beatmap; + if (ruleset != null) Ruleset.Value = ruleset; + }); + } + } + [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 4fbea4e3be..ceeee67806 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -12,9 +12,13 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Framework.Threading; +using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Overlays.Dialog; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Match; @@ -29,7 +33,7 @@ using ParticipantsList = osu.Game.Screens.OnlinePlay.Multiplayer.Participants.Pa namespace osu.Game.Screens.OnlinePlay.Multiplayer { [Cached] - public class MultiplayerMatchSubScreen : RoomSubScreen + public class MultiplayerMatchSubScreen : RoomSubScreen, IHandlePresentBeatmap { public override string Title { get; } @@ -279,14 +283,36 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(ruleset)).Concat(SelectedItem.Value.RequiredMods).ToList(); } + [Resolved(canBeNull: true)] + private DialogOverlay dialogOverlay { get; set; } + + private bool exitConfirmed; + public override bool OnBackButton() { - if (client.Room != null && settingsOverlay.State.Value == Visibility.Visible) + if (client.Room == null) + { + // room has not been created yet; exit immediately. + return base.OnBackButton(); + } + + if (settingsOverlay.State.Value == Visibility.Visible) { settingsOverlay.Hide(); return true; } + if (!exitConfirmed && dialogOverlay != null) + { + dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave this multiplayer match?", () => + { + exitConfirmed = true; + this.Exit(); + })); + + return true; + } + return base.OnBackButton(); } @@ -394,5 +420,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer modSettingChangeTracker?.Dispose(); } + + public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset) + { + if (!this.IsCurrentScreen()) + return; + + if (!client.IsHost) + { + // todo: should handle this when the request queue is implemented. + // if we decide that the presentation should exit the user from the multiplayer game, the PresentBeatmap + // flow may need to change to support an "unable to present" return value. + return; + } + + this.Push(new MultiplayerMatchSongSelect(beatmap, ruleset)); + } } } diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index 2e8c834c65..7775c2bd24 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -7,8 +7,10 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Lounge.Components; +using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual.Multiplayer { @@ -48,7 +50,16 @@ namespace osu.Game.Tests.Visual.Multiplayer RoomManager.Schedule(() => RoomManager.PartRoom()); if (joinRoom) + { + Room.Name.Value = "test name"; + Room.Playlist.Add(new PlaylistItem + { + Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, + Ruleset = { Value = Ruleset.Value } + }); + RoomManager.Schedule(() => RoomManager.CreateRoom(Room)); + } }); public override void SetUpSteps() diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 379bb758c5..c03364a391 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -3,12 +3,15 @@ #nullable enable +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; @@ -25,6 +28,16 @@ namespace osu.Game.Tests.Visual.Multiplayer [Resolved] private IAPIProvider api { get; set; } = null!; + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; + + private readonly TestMultiplayerRoomManager roomManager; + + public TestMultiplayerClient(TestMultiplayerRoomManager roomManager) + { + this.roomManager = roomManager; + } + public void Connect() => isConnected.Value = true; public void Disconnect() => isConnected.Value = false; @@ -89,13 +102,28 @@ namespace osu.Game.Tests.Visual.Multiplayer protected override Task JoinRoom(long roomId) { - var user = new MultiplayerRoomUser(api.LocalUser.Value.Id) { User = api.LocalUser.Value }; + var apiRoom = roomManager.Rooms.Single(r => r.RoomID.Value == roomId); - var room = new MultiplayerRoom(roomId); - room.Users.Add(user); + var user = new MultiplayerRoomUser(api.LocalUser.Value.Id) + { + User = api.LocalUser.Value + }; - if (room.Users.Count == 1) - room.Host = user; + var room = new MultiplayerRoom(roomId) + { + Settings = + { + Name = apiRoom.Name.Value, + BeatmapID = apiRoom.Playlist.Last().BeatmapID, + RulesetID = apiRoom.Playlist.Last().RulesetID, + BeatmapChecksum = apiRoom.Playlist.Last().Beatmap.Value.MD5Hash, + RequiredMods = apiRoom.Playlist.Last().RequiredMods.Select(m => new APIMod(m)).ToArray(), + AllowedMods = apiRoom.Playlist.Last().AllowedMods.Select(m => new APIMod(m)).ToArray(), + PlaylistItemId = apiRoom.Playlist.Last().ID + }, + Users = { user }, + Host = user + }; return Task.FromResult(room); } @@ -150,5 +178,19 @@ namespace osu.Game.Tests.Visual.Multiplayer return ((IMultiplayerClient)this).LoadRequested(); } + + protected override Task GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default) + { + Debug.Assert(Room != null); + + var apiRoom = roomManager.Rooms.Single(r => r.RoomID.Value == Room.RoomID); + var set = apiRoom.Playlist.FirstOrDefault(p => p.BeatmapID == beatmapId)?.Beatmap.Value.BeatmapSet + ?? beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == beatmapId)?.BeatmapSet; + + if (set == null) + throw new InvalidOperationException("Beatmap not found."); + + return Task.FromResult(set); + } } } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs index 860caef071..e57411d04d 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs @@ -32,11 +32,15 @@ namespace osu.Game.Tests.Visual.Multiplayer { RelativeSizeAxes = Axes.Both; + RoomManager = new TestMultiplayerRoomManager(); + Client = new TestMultiplayerClient(RoomManager); + OngoingOperationTracker = new OngoingOperationTracker(); + AddRangeInternal(new Drawable[] { - Client = new TestMultiplayerClient(), - RoomManager = new TestMultiplayerRoomManager(), - OngoingOperationTracker = new OngoingOperationTracker(), + Client, + RoomManager, + OngoingOperationTracker, content = new Container { RelativeSizeAxes = Axes.Both } }); } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs index 5e12156f3c..7e824c4d7c 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Cached] public readonly Bindable Filter = new Bindable(new FilterCriteria()); - private readonly List rooms = new List(); + public new readonly List Rooms = new List(); protected override void LoadComplete() { @@ -35,6 +35,7 @@ namespace osu.Game.Tests.Visual.Multiplayer int currentScoreId = 0; int currentRoomId = 0; + int currentPlaylistItemId = 0; ((DummyAPIAccess)api).HandleRequest = req => { @@ -46,7 +47,10 @@ namespace osu.Game.Tests.Visual.Multiplayer createdRoom.CopyFrom(createRoomRequest.Room); createdRoom.RoomID.Value ??= currentRoomId++; - rooms.Add(createdRoom); + for (int i = 0; i < createdRoom.Playlist.Count; i++) + createdRoom.Playlist[i].ID = currentPlaylistItemId++; + + Rooms.Add(createdRoom); createRoomRequest.TriggerSuccess(createdRoom); break; @@ -61,7 +65,7 @@ namespace osu.Game.Tests.Visual.Multiplayer case GetRoomsRequest getRoomsRequest: var roomsWithoutParticipants = new List(); - foreach (var r in rooms) + foreach (var r in Rooms) { var newRoom = new Room(); @@ -75,7 +79,7 @@ namespace osu.Game.Tests.Visual.Multiplayer break; case GetRoomRequest getRoomRequest: - getRoomRequest.TriggerSuccess(rooms.Single(r => r.RoomID.Value == getRoomRequest.RoomId)); + getRoomRequest.TriggerSuccess(Rooms.Single(r => r.RoomID.Value == getRoomRequest.RoomId)); break; case GetBeatmapSetRequest getBeatmapSetRequest: