diff --git a/osu.Android.props b/osu.Android.props index 25bde037db..494842f38f 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -54,6 +54,6 @@ - + diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index 8377b3786a..e2465d727e 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Catch.Mods { public class CatchModDifficultyAdjust : ModDifficultyAdjust { - [SettingSource("Fruit Size", "Override a beatmap's set CS.")] + [SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)] public BindableNumber CircleSize { get; } = new BindableFloat { Precision = 0.1f, @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Catch.Mods Value = 5, }; - [SettingSource("Approach Rate", "Override a beatmap's set AR.")] + [SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)] public BindableNumber ApproachRate { get; } = new BindableFloat { Precision = 0.1f, diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs index bcbc1ee527..7bbde400ea 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints { base.UpdatePosition(screenSpacePosition); - if (PlacementBegun) + if (PlacementActive) { var endTime = TimeAt(screenSpacePosition); diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index 362d6d40a8..a3657d3bb9 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints public override void UpdatePosition(Vector2 screenSpacePosition) { - if (!PlacementBegun) + if (!PlacementActive) Column = ColumnAt(screenSpacePosition); if (Column == null) return; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 75d05b9b6c..a780653796 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private void beginCurve() { - BeginPlacement(); + BeginPlacement(commitStart: true); setState(PlacementState.Body); } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs index 2c125aa7c3..74b563d922 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners if (e.Button != MouseButton.Left) return false; - BeginPlacement(); + BeginPlacement(commitStart: true); piece.FadeTo(1f, 150, Easing.OutQuint); isPlacingEnd = true; diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs index 22b4c3e82e..a8719e0aa8 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs @@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Osu.Edit if (existing == null) return; + hitObject.RemoveTransform(existing); + using (hitObject.BeginAbsoluteSequence(existing.StartTime)) hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire(); break; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index 7eee71be81..75de6896a3 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModDifficultyAdjust : ModDifficultyAdjust { - [SettingSource("Circle Size", "Override a beatmap's set CS.")] + [SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)] public BindableNumber CircleSize { get; } = new BindableFloat { Precision = 0.1f, @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods Value = 5, }; - [SettingSource("Approach Rate", "Override a beatmap's set AR.")] + [SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)] public BindableNumber ApproachRate { get; } = new BindableFloat { Precision = 0.1f, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs new file mode 100644 index 0000000000..c93d59acdf --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -0,0 +1,247 @@ +// 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.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Multiplayer; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens.Multi; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneDrawableRoomPlaylist : ManualInputManagerTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(DrawableRoomPlaylist), + typeof(DrawableRoomPlaylistItem) + }; + + [Resolved] + private BeatmapManager beatmapManager { get; set; } + + [Resolved] + private RulesetStore rulesetStore { get; set; } + + private DrawableRoomPlaylist playlist; + + [Test] + public void TestNonEditableNonSelectable() + { + createPlaylist(false, false); + + moveToItem(0); + assertHandleVisibility(0, false); + assertDeleteButtonVisibility(0, false); + + AddStep("click", () => InputManager.Click(MouseButton.Left)); + AddAssert("no item selected", () => playlist.SelectedItem.Value == null); + } + + [Test] + public void TestEditable() + { + createPlaylist(true, false); + + moveToItem(0); + assertHandleVisibility(0, true); + assertDeleteButtonVisibility(0, true); + + AddStep("click", () => InputManager.Click(MouseButton.Left)); + AddAssert("no item selected", () => playlist.SelectedItem.Value == null); + } + + [Test] + public void TestSelectable() + { + createPlaylist(false, true); + + moveToItem(0); + assertHandleVisibility(0, false); + assertDeleteButtonVisibility(0, false); + + AddStep("click", () => InputManager.Click(MouseButton.Left)); + + AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]); + } + + [Test] + public void TestEditableSelectable() + { + createPlaylist(true, true); + + moveToItem(0); + assertHandleVisibility(0, true); + assertDeleteButtonVisibility(0, true); + + AddStep("click", () => InputManager.Click(MouseButton.Left)); + + AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]); + } + + [Test] + public void TestSelectionNotLostAfterRearrangement() + { + createPlaylist(true, true); + + moveToItem(0); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + + moveToDragger(0); + AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left)); + moveToDragger(1, new Vector2(0, 5)); + AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left)); + + AddAssert("item 1 is selected", () => playlist.SelectedItem.Value == playlist.Items[1]); + } + + [Test] + public void TestItemRemovedOnDeletion() + { + PlaylistItem selectedItem = null; + + createPlaylist(true, true); + + moveToItem(0); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + AddStep("retrieve selection", () => selectedItem = playlist.SelectedItem.Value); + + moveToDeleteButton(0); + AddStep("click delete button", () => InputManager.Click(MouseButton.Left)); + + AddAssert("item removed", () => !playlist.Items.Contains(selectedItem)); + } + + [Test] + public void TestNextItemSelectedAfterDeletion() + { + createPlaylist(true, true); + + moveToItem(0); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + + moveToDeleteButton(0); + AddStep("click delete button", () => InputManager.Click(MouseButton.Left)); + + AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]); + } + + [Test] + public void TestLastItemSelectedAfterLastItemDeleted() + { + createPlaylist(true, true); + + AddWaitStep("wait for flow", 5); // Items may take 1 update frame to flow. A wait count of 5 is guaranteed to result in the flow being updated as desired. + AddStep("scroll to bottom", () => playlist.ChildrenOfType>().First().ScrollToEnd(false)); + + moveToItem(19); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + + moveToDeleteButton(19); + AddStep("click delete button", () => InputManager.Click(MouseButton.Left)); + + AddAssert("item 18 is selected", () => playlist.SelectedItem.Value == playlist.Items[18]); + } + + [Test] + public void TestSelectionResetWhenAllItemsDeleted() + { + createPlaylist(true, true); + + AddStep("remove all but one item", () => + { + playlist.Items.RemoveRange(1, playlist.Items.Count - 1); + }); + + moveToItem(0); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + moveToDeleteButton(0); + AddStep("click delete button", () => InputManager.Click(MouseButton.Left)); + + AddAssert("no item selected", () => playlist.SelectedItem.Value == null); + } + + // Todo: currently not possible due to bindable list shortcomings (https://github.com/ppy/osu-framework/issues/3081) + // [Test] + public void TestNextItemSelectedAfterExternalDeletion() + { + createPlaylist(true, true); + + moveToItem(0); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + AddStep("remove item 0", () => playlist.Items.RemoveAt(0)); + + AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]); + } + + private void moveToItem(int index, Vector2? offset = null) + => AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType>().ElementAt(index), offset)); + + private void moveToDragger(int index, Vector2? offset = null) => AddStep($"move mouse to dragger {index}", () => + { + var item = playlist.ChildrenOfType>().ElementAt(index); + InputManager.MoveMouseTo(item.ChildrenOfType.PlaylistItemHandle>().Single(), offset); + }); + + private void moveToDeleteButton(int index, Vector2? offset = null) => AddStep($"move mouse to delete button {index}", () => + { + var item = playlist.ChildrenOfType>().ElementAt(index); + InputManager.MoveMouseTo(item.ChildrenOfType().ElementAt(0), offset); + }); + + private void assertHandleVisibility(int index, bool visible) + => AddAssert($"handle {index} {(visible ? "is" : "is not")} visible", + () => (playlist.ChildrenOfType.PlaylistItemHandle>().ElementAt(index).Alpha > 0) == visible); + + private void assertDeleteButtonVisibility(int index, bool visible) + => AddAssert($"delete button {index} {(visible ? "is" : "is not")} visible", () => (playlist.ChildrenOfType().ElementAt(2 + index * 2).Alpha > 0) == visible); + + private void createPlaylist(bool allowEdit, bool allowSelection) => AddStep("create playlist", () => + { + Child = playlist = new DrawableRoomPlaylist(allowEdit, allowSelection) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(500, 300) + }; + + var beatmapSets = beatmapManager.GetAllUsableBeatmapSets(); + var rulesets = rulesetStore.AvailableRulesets.ToList(); + + for (int i = 0; i < 20; i++) + { + var set = beatmapSets[RNG.Next(0, beatmapSets.Count)]; + var beatmap = set.Beatmaps[RNG.Next(0, set.Beatmaps.Count)]; + + beatmap.BeatmapSet = set; + beatmap.Metadata = set.Metadata; + + playlist.Items.Add(new PlaylistItem + { + ID = i, + Beatmap = { Value = beatmap }, + Ruleset = { Value = rulesets[RNG.Next(0, rulesets.Count)] }, + RequiredMods = + { + new OsuModHardRock(), + new OsuModDoubleTime(), + new OsuModAutoplay() + } + }); + } + }); + } +} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index fe14a1ff0a..b5d946d049 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -121,10 +121,13 @@ namespace osu.Game.Tests.Visual.Multiplayer { room.Playlist.Add(new PlaylistItem { - Ruleset = ruleset, - Beatmap = new BeatmapInfo + Ruleset = { Value = ruleset }, + Beatmap = { - Metadata = new BeatmapMetadata() + Value = new BeatmapInfo + { + Metadata = new BeatmapMetadata() + } } }); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapPanel.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapPanel.cs index 1e3e06ce7a..f014b08325 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapPanel.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapPanel.cs @@ -7,7 +7,6 @@ using osu.Game.Beatmaps; using osu.Game.Online.Multiplayer; using osu.Game.Screens.Multi.Match.Components; using osu.Framework.Graphics; -using osu.Framework.Utils; using osu.Game.Audio; using osu.Framework.Allocation; @@ -32,22 +31,11 @@ namespace osu.Game.Tests.Visual.Multiplayer Origin = Anchor.Centre, }); - Room.Playlist.Add(new PlaylistItem { Beatmap = new BeatmapInfo { OnlineBeatmapID = 1763072 } }); - Room.Playlist.Add(new PlaylistItem { Beatmap = new BeatmapInfo { OnlineBeatmapID = 2101557 } }); - Room.Playlist.Add(new PlaylistItem { Beatmap = new BeatmapInfo { OnlineBeatmapID = 1973466 } }); - Room.Playlist.Add(new PlaylistItem { Beatmap = new BeatmapInfo { OnlineBeatmapID = 2109801 } }); - Room.Playlist.Add(new PlaylistItem { Beatmap = new BeatmapInfo { OnlineBeatmapID = 1922035 } }); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - AddStep("Select random beatmap", () => - { - Room.CurrentItem.Value = Room.Playlist[RNG.Next(Room.Playlist.Count)]; - previewTrackManager.StopAnyPlaying(this); - }); + Room.Playlist.Add(new PlaylistItem { Beatmap = { Value = new BeatmapInfo { OnlineBeatmapID = 1763072 } } }); + Room.Playlist.Add(new PlaylistItem { Beatmap = { Value = new BeatmapInfo { OnlineBeatmapID = 2101557 } } }); + Room.Playlist.Add(new PlaylistItem { Beatmap = { Value = new BeatmapInfo { OnlineBeatmapID = 1973466 } } }); + Room.Playlist.Add(new PlaylistItem { Beatmap = { Value = new BeatmapInfo { OnlineBeatmapID = 2109801 } } }); + Room.Playlist.Add(new PlaylistItem { Beatmap = { Value = new BeatmapInfo { OnlineBeatmapID = 1922035 } } }); } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs index e42042f2ea..7d7e7f85db 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs @@ -23,16 +23,19 @@ namespace osu.Game.Tests.Visual.Multiplayer { Room.Playlist.Add(new PlaylistItem { - Beatmap = new BeatmapInfo + Beatmap = { - Metadata = new BeatmapMetadata + Value = new BeatmapInfo { - Title = "Title", - Artist = "Artist", - AuthorString = "Author", - }, - Version = "Version", - Ruleset = new OsuRuleset().RulesetInfo + Metadata = new BeatmapMetadata + { + Title = "Title", + Artist = "Artist", + AuthorString = "Author", + }, + Version = "Version", + Ruleset = new OsuRuleset().RulesetInfo + } }, RequiredMods = { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchInfo.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchInfo.cs index a6c036a876..6ee9ceb2dd 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchInfo.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchInfo.cs @@ -37,16 +37,19 @@ namespace osu.Game.Tests.Visual.Multiplayer Room.Playlist.Clear(); Room.Playlist.Add(new PlaylistItem { - Beatmap = new BeatmapInfo + Beatmap = { - StarDifficulty = 2.4, - Ruleset = rulesets.GetRuleset(0), - Metadata = new BeatmapMetadata + Value = new BeatmapInfo { - Title = @"My Song", - Artist = @"VisualTests", - AuthorString = @"osu!lazer", - }, + StarDifficulty = 2.4, + Ruleset = rulesets.GetRuleset(0), + Metadata = new BeatmapMetadata + { + Title = @"My Song", + Artist = @"VisualTests", + AuthorString = @"osu!lazer", + }, + } } }); }); @@ -60,16 +63,19 @@ namespace osu.Game.Tests.Visual.Multiplayer Room.Playlist.Clear(); Room.Playlist.Add(new PlaylistItem { - Beatmap = new BeatmapInfo + Beatmap = { - StarDifficulty = 4.2, - Ruleset = rulesets.GetRuleset(3), - Metadata = new BeatmapMetadata + Value = new BeatmapInfo { - Title = @"Your Song", - Artist = @"Tester", - AuthorString = @"Someone", - }, + StarDifficulty = 4.2, + Ruleset = rulesets.GetRuleset(3), + Metadata = new BeatmapMetadata + { + Title = @"Your Song", + Artist = @"Tester", + AuthorString = @"Someone", + }, + } } }); }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchParticipants.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchParticipants.cs index 1ac914e27d..a6f47961e9 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchParticipants.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchParticipants.cs @@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Add(new Participants { RelativeSizeAxes = Axes.Both }); AddStep(@"set max to null", () => Room.MaxParticipants.Value = null); - AddStep(@"set users", () => Room.Participants.Value = new[] + AddStep(@"set users", () => Room.Participants.AddRange(new[] { new User { @@ -42,10 +42,10 @@ namespace osu.Game.Tests.Visual.Multiplayer CoverUrl = @"https://assets.ppy.sh/user-profile-covers/5287410/5cfeaa9dd41cbce038ecdc9d781396ed4b0108089170bf7f50492ef8eadeb368.jpeg", IsSupporter = true, }, - }); + })); AddStep(@"set max", () => Room.MaxParticipants.Value = 10); - AddStep(@"clear users", () => Room.Participants.Value = System.Array.Empty()); + AddStep(@"clear users", () => Room.Participants.Clear()); AddStep(@"set max to null", () => Room.MaxParticipants.Value = null); } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs index 8d842fc865..047e9d860d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs @@ -56,7 +56,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("set name", () => Room.Name.Value = "Room name"); AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value); - AddStep("set beatmap", () => Room.Playlist.Add(new PlaylistItem { Beatmap = CreateBeatmap(Ruleset.Value).BeatmapInfo })); + AddStep("set beatmap", () => Room.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } })); AddAssert("button enabled", () => settings.ApplyButton.Enabled.Value); AddStep("clear name", () => Room.Name.Value = ""); diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsHeader.cs index 898e461bde..1e711b3cd7 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneRankingsHeader.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsHeader.cs @@ -34,25 +34,7 @@ namespace osu.Game.Tests.Visual.Online { Current = { BindTarget = scope }, Country = { BindTarget = countryBindable }, - Ruleset = { BindTarget = ruleset }, - Spotlights = new[] - { - new Spotlight - { - Id = 1, - Text = "Spotlight 1" - }, - new Spotlight - { - Id = 2, - Text = "Spotlight 2" - }, - new Spotlight - { - Id = 3, - Text = "Spotlight 3" - } - } + Ruleset = { BindTarget = ruleset } }); var country = new Country diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsSpotlightSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsSpotlightSelector.cs index e46c8a4a71..f27ab1e775 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneRankingsSpotlightSelector.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsSpotlightSelector.cs @@ -35,6 +35,12 @@ namespace osu.Game.Tests.Visual.Online Add(selector = new SpotlightSelector()); } + [Test] + public void TestVisibility() + { + AddStep("Toggle Visibility", selector.ToggleVisibility); + } + [Test] public void TestLocalSpotlights() { diff --git a/osu.Game.Tests/Visual/Online/TestSceneSpotlightsLayout.cs b/osu.Game.Tests/Visual/Online/TestSceneSpotlightsLayout.cs new file mode 100644 index 0000000000..d025a8d7c2 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneSpotlightsLayout.cs @@ -0,0 +1,55 @@ +// 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.Generic; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Overlays; +using osu.Game.Overlays.Rankings; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Taiko; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneSpotlightsLayout : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(SpotlightsLayout), + typeof(SpotlightSelector), + }; + + protected override bool UseOnlineAPI => true; + + [Cached] + private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Green); + + public TestSceneSpotlightsLayout() + { + var ruleset = new Bindable(new OsuRuleset().RulesetInfo); + + Add(new BasicScrollContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Width = 0.8f, + Child = new SpotlightsLayout + { + Ruleset = { BindTarget = ruleset } + } + }); + + AddStep("Osu ruleset", () => ruleset.Value = new OsuRuleset().RulesetInfo); + AddStep("Mania ruleset", () => ruleset.Value = new ManiaRuleset().RulesetInfo); + AddStep("Taiko ruleset", () => ruleset.Value = new TaikoRuleset().RulesetInfo); + AddStep("Catch ruleset", () => ruleset.Value = new CatchRuleset().RulesetInfo); + } + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModDisplay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModDisplay.cs new file mode 100644 index 0000000000..8168faa106 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModDisplay.cs @@ -0,0 +1,40 @@ +// 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.Graphics; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens.Play.HUD; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneModDisplay : OsuTestScene + { + [TestCase(ExpansionMode.ExpandOnHover)] + [TestCase(ExpansionMode.AlwaysExpanded)] + [TestCase(ExpansionMode.AlwaysContracted)] + public void TestMode(ExpansionMode mode) + { + AddStep("create mod display", () => + { + Child = new ModDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + ExpansionMode = mode, + Current = + { + Value = new Mod[] + { + new OsuModHardRock(), + new OsuModDoubleTime(), + new OsuModDifficultyAdjust(), + new OsuModEasy(), + } + } + }; + }); + } + } +} diff --git a/osu.Game.Tests/WaveformTestBeatmap.cs b/osu.Game.Tests/WaveformTestBeatmap.cs index df6394ed34..53ce5def32 100644 --- a/osu.Game.Tests/WaveformTestBeatmap.cs +++ b/osu.Game.Tests/WaveformTestBeatmap.cs @@ -37,9 +37,9 @@ namespace osu.Game.Tests trackStore = audioManager.GetTrackStore(getZipReader()); } - protected override void Dispose(bool isDisposing) + ~WaveformTestBeatmap() { - base.Dispose(isDisposing); + // Remove the track store from the audio manager trackStore?.Dispose(); } diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index f9d71a2a6e..55c5175c5d 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -36,8 +36,9 @@ namespace osu.Game.Beatmaps using (var stream = new LineBufferedReader(store.GetStream(getPathForFile(BeatmapInfo.Path)))) return Decoder.GetDecoder(stream).Decode(stream); } - catch + catch (Exception e) { + Logger.Error(e, "Beatmap failed to load"); return null; } } @@ -59,8 +60,9 @@ namespace osu.Game.Beatmaps { return textureStore.Get(getPathForFile(Metadata.BackgroundFile)); } - catch + catch (Exception e) { + Logger.Error(e, "Background failed to load"); return null; } } @@ -74,8 +76,9 @@ namespace osu.Game.Beatmaps { return new VideoSprite(textureStore.GetStream(getPathForFile(Metadata.VideoFile))); } - catch + catch (Exception e) { + Logger.Error(e, "Video failed to load"); return null; } } @@ -86,8 +89,9 @@ namespace osu.Game.Beatmaps { return (trackStore ??= AudioManager.GetTrackStore(store)).Get(getPathForFile(Metadata.AudioFile)); } - catch + catch (Exception e) { + Logger.Error(e, "Track failed to load"); return null; } } @@ -115,8 +119,9 @@ namespace osu.Game.Beatmaps var trackData = store.GetStream(getPathForFile(Metadata.AudioFile)); return trackData == null ? null : new Waveform(trackData); } - catch + catch (Exception e) { + Logger.Error(e, "Waveform failed to load"); return null; } } diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 5dc483b61c..1e1ffad81e 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -17,10 +17,11 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.UI; using osu.Game.Skinning; using osu.Framework.Graphics.Video; +using osu.Framework.Logging; namespace osu.Game.Beatmaps { - public abstract class WorkingBeatmap : IWorkingBeatmap, IDisposable + public abstract class WorkingBeatmap : IWorkingBeatmap { public readonly BeatmapInfo BeatmapInfo; @@ -133,11 +134,29 @@ namespace osu.Game.Beatmaps return converted; } - public override string ToString() => BeatmapInfo.ToString(); + private CancellationTokenSource loadCancellation = new CancellationTokenSource(); - public bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false; + /// + /// Beings loading the contents of this asynchronously. + /// + public void BeginAsyncLoad() + { + loadBeatmapAsync(); + } - public Task LoadBeatmapAsync() => beatmapLoadTask ??= Task.Factory.StartNew(() => + /// + /// Cancels the asynchronous loading of the contents of this . + /// + public void CancelAsyncLoad() + { + loadCancellation?.Cancel(); + loadCancellation = new CancellationTokenSource(); + + if (beatmapLoadTask?.IsCompleted != true) + beatmapLoadTask = null; + } + + private Task loadBeatmapAsync() => beatmapLoadTask ??= Task.Factory.StartNew(() => { // Todo: Handle cancellation during beatmap parsing var b = GetBeatmap() ?? new Beatmap(); @@ -149,7 +168,11 @@ namespace osu.Game.Beatmaps b.BeatmapInfo = BeatmapInfo; return b; - }, beatmapCancellation.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); + }, loadCancellation.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); + + public override string ToString() => BeatmapInfo.ToString(); + + public bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false; public IBeatmap Beatmap { @@ -157,16 +180,25 @@ namespace osu.Game.Beatmaps { try { - return LoadBeatmapAsync().Result; + return loadBeatmapAsync().Result; } - catch (TaskCanceledException) + catch (AggregateException ae) { + // This is the exception that is generally expected here, which occurs via natural cancellation of the asynchronous load + if (ae.InnerExceptions.FirstOrDefault() is TaskCanceledException) + return null; + + Logger.Error(ae, "Beatmap failed to load"); + return null; + } + catch (Exception e) + { + Logger.Error(e, "Beatmap failed to load"); return null; } } } - private readonly CancellationTokenSource beatmapCancellation = new CancellationTokenSource(); protected abstract IBeatmap GetBeatmap(); private Task beatmapLoadTask; @@ -217,40 +249,11 @@ namespace osu.Game.Beatmaps /// public virtual void RecycleTrack() => track.Recycle(); - #region Disposal - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private bool isDisposed; - - protected virtual void Dispose(bool isDisposing) - { - if (isDisposed) - return; - - isDisposed = true; - - // recycling logic is not here for the time being, as components which use - // retrieved objects from WorkingBeatmap may not hold a reference to the WorkingBeatmap itself. - // this should be fine as each retrieved component do have their own finalizers. - - // cancelling the beatmap load is safe for now since the retrieval is a synchronous - // operation. if we add an async retrieval method this may need to be reconsidered. - beatmapCancellation?.Cancel(); - total_count.Value--; - } - ~WorkingBeatmap() { - Dispose(false); + total_count.Value--; } - #endregion - public class RecyclableLazy { private Lazy lazy; diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index a3788e4582..4bdbb5fc24 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using JetBrains.Annotations; using osu.Framework.Bindables; @@ -16,6 +17,10 @@ namespace osu.Game.Configuration /// An attribute to mark a bindable as being exposed to the user via settings controls. /// Can be used in conjunction with to automatically create UI controls. /// + /// + /// All controls with set will be placed first in ascending order. + /// All controls with no will come afterward in default order. + /// [MeansImplicitUse] [AttributeUsage(AttributeTargets.Property)] public class SettingSourceAttribute : Attribute @@ -24,18 +29,26 @@ namespace osu.Game.Configuration public string Description { get; } + public int? OrderPosition { get; } + public SettingSourceAttribute(string label, string description = null) { Label = label ?? string.Empty; Description = description ?? string.Empty; } + + public SettingSourceAttribute(string label, string description, int orderPosition) + : this(label, description) + { + OrderPosition = orderPosition; + } } public static class SettingSourceExtensions { public static IEnumerable CreateSettingsControls(this object obj) { - foreach (var (attr, property) in obj.GetSettingsSourceProperties()) + foreach (var (attr, property) in obj.GetOrderedSettingsSourceProperties()) { object value = property.GetValue(obj); @@ -116,5 +129,15 @@ namespace osu.Game.Configuration yield return (attr, property); } } + + public static IEnumerable<(SettingSourceAttribute, PropertyInfo)> GetOrderedSettingsSourceProperties(this object obj) + { + var original = obj.GetSettingsSourceProperties(); + + var orderedRelative = original.Where(attr => attr.Item1.OrderPosition != null).OrderBy(attr => attr.Item1.OrderPosition); + var unordered = original.Except(orderedRelative); + + return orderedRelative.Concat(unordered); + } } } diff --git a/osu.Game/Graphics/Containers/OsuRearrangeableListContainer.cs b/osu.Game/Graphics/Containers/OsuRearrangeableListContainer.cs new file mode 100644 index 0000000000..47aed1c500 --- /dev/null +++ b/osu.Game/Graphics/Containers/OsuRearrangeableListContainer.cs @@ -0,0 +1,26 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Graphics.Containers +{ + public abstract class OsuRearrangeableListContainer : RearrangeableListContainer + { + /// + /// Whether any item is currently being dragged. Used to hide other items' drag handles. + /// + private readonly BindableBool playlistDragActive = new BindableBool(); + + protected override ScrollContainer CreateScrollContainer() => new OsuScrollContainer(); + + protected sealed override RearrangeableListItem CreateDrawable(TModel item) => CreateOsuDrawable(item).With(d => + { + d.PlaylistDragActive.BindTo(playlistDragActive); + }); + + protected abstract OsuRearrangeableListItem CreateOsuDrawable(TModel item); + } +} diff --git a/osu.Game/Graphics/Containers/OsuRearrangeableListItem.cs b/osu.Game/Graphics/Containers/OsuRearrangeableListItem.cs new file mode 100644 index 0000000000..29553954fe --- /dev/null +++ b/osu.Game/Graphics/Containers/OsuRearrangeableListItem.cs @@ -0,0 +1,162 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Graphics.Containers +{ + public abstract class OsuRearrangeableListItem : RearrangeableListItem + { + public const float FADE_DURATION = 100; + + /// + /// Whether any item is currently being dragged. Used to hide other items' drag handles. + /// + public readonly BindableBool PlaylistDragActive = new BindableBool(); + + private Color4 handleColour = Color4.White; + + /// + /// The colour of the drag handle. + /// + protected Color4 HandleColour + { + get => handleColour; + set + { + if (handleColour == value) + return; + + handleColour = value; + + if (handle != null) + handle.Colour = value; + } + } + + /// + /// Whether the drag handle should be shown. + /// + protected virtual bool ShowDragHandle => true; + + private PlaylistItemHandle handle; + + protected OsuRearrangeableListItem(TModel item) + : base(item) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + } + + [BackgroundDependencyLoader] + private void load() + { + Container handleContainer; + + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Content = new[] + { + new[] + { + handleContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = 5 }, + Child = handle = new PlaylistItemHandle + { + Size = new Vector2(12), + Colour = HandleColour, + AlwaysPresent = true, + Alpha = 0 + } + }, + CreateContent() + } + }, + ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } + }; + + if (!ShowDragHandle) + handleContainer.Alpha = 0; + } + + protected override bool OnDragStart(DragStartEvent e) + { + if (!base.OnDragStart(e)) + return false; + + PlaylistDragActive.Value = true; + return true; + } + + protected override void OnDragEnd(DragEndEvent e) + { + PlaylistDragActive.Value = false; + base.OnDragEnd(e); + } + + protected override bool IsDraggableAt(Vector2 screenSpacePos) => handle.HandlingDrag; + + protected override bool OnHover(HoverEvent e) + { + handle.UpdateHoverState(IsDragged || !PlaylistDragActive.Value); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) => handle.UpdateHoverState(false); + + protected abstract Drawable CreateContent(); + + public class PlaylistItemHandle : SpriteIcon + { + public bool HandlingDrag { get; private set; } + private bool isHovering; + + public PlaylistItemHandle() + { + Icon = FontAwesome.Solid.Bars; + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + base.OnMouseDown(e); + + HandlingDrag = true; + UpdateHoverState(isHovering); + + return false; + } + + protected override void OnMouseUp(MouseUpEvent e) + { + base.OnMouseUp(e); + + HandlingDrag = false; + UpdateHoverState(isHovering); + } + + public void UpdateHoverState(bool hovering) + { + isHovering = hovering; + + if (isHovering || HandlingDrag) + this.FadeIn(FADE_DURATION); + else + this.FadeOut(FADE_DURATION); + } + } + } +} diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index c8298543a1..59dd823266 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Globalization; using osu.Game.Beatmaps; using osuTK.Graphics; @@ -14,41 +15,40 @@ namespace osu.Game.Graphics public static Color4 FromHex(string hex) { - if (hex[0] == '#') - hex = hex.Substring(1); + var hexSpan = hex[0] == '#' ? hex.AsSpan().Slice(1) : hex.AsSpan(); - switch (hex.Length) + switch (hexSpan.Length) { default: throw new ArgumentException(@"Invalid hex string length!"); case 3: return new Color4( - (byte)(Convert.ToByte(hex.Substring(0, 1), 16) * 17), - (byte)(Convert.ToByte(hex.Substring(1, 1), 16) * 17), - (byte)(Convert.ToByte(hex.Substring(2, 1), 16) * 17), + (byte)(byte.Parse(hexSpan.Slice(0, 1), NumberStyles.HexNumber) * 17), + (byte)(byte.Parse(hexSpan.Slice(1, 1), NumberStyles.HexNumber) * 17), + (byte)(byte.Parse(hexSpan.Slice(2, 1), NumberStyles.HexNumber) * 17), 255); case 6: return new Color4( - Convert.ToByte(hex.Substring(0, 2), 16), - Convert.ToByte(hex.Substring(2, 2), 16), - Convert.ToByte(hex.Substring(4, 2), 16), + byte.Parse(hexSpan.Slice(0, 2), NumberStyles.HexNumber), + byte.Parse(hexSpan.Slice(2, 2), NumberStyles.HexNumber), + byte.Parse(hexSpan.Slice(4, 2), NumberStyles.HexNumber), 255); case 4: return new Color4( - (byte)(Convert.ToByte(hex.Substring(0, 1), 16) * 17), - (byte)(Convert.ToByte(hex.Substring(1, 1), 16) * 17), - (byte)(Convert.ToByte(hex.Substring(2, 1), 16) * 17), - (byte)(Convert.ToByte(hex.Substring(3, 1), 16) * 17)); + (byte)(byte.Parse(hexSpan.Slice(0, 1), NumberStyles.HexNumber) * 17), + (byte)(byte.Parse(hexSpan.Slice(1, 1), NumberStyles.HexNumber) * 17), + (byte)(byte.Parse(hexSpan.Slice(0, 1), NumberStyles.HexNumber) * 17), + (byte)(byte.Parse(hexSpan.Slice(0, 1), NumberStyles.HexNumber) * 17)); case 8: return new Color4( - Convert.ToByte(hex.Substring(0, 2), 16), - Convert.ToByte(hex.Substring(2, 2), 16), - Convert.ToByte(hex.Substring(4, 2), 16), - Convert.ToByte(hex.Substring(6, 2), 16)); + byte.Parse(hexSpan.Slice(0, 2), NumberStyles.HexNumber), + byte.Parse(hexSpan.Slice(2, 2), NumberStyles.HexNumber), + byte.Parse(hexSpan.Slice(4, 2), NumberStyles.HexNumber), + byte.Parse(hexSpan.Slice(6, 2), NumberStyles.HexNumber)); } } diff --git a/osu.Game/Online/API/Requests/Responses/APISpotlight.cs b/osu.Game/Online/API/Requests/Responses/APISpotlight.cs index 3a002e57b2..4f63ebe3df 100644 --- a/osu.Game/Online/API/Requests/Responses/APISpotlight.cs +++ b/osu.Game/Online/API/Requests/Responses/APISpotlight.cs @@ -26,6 +26,9 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"end_date")] public DateTimeOffset EndDate; + [JsonProperty(@"participant_count")] + public int? Participants; + public override string ToString() => Name; } } diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index c9131883bb..1f52a4481b 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -55,7 +55,7 @@ namespace osu.Game.Online.Leaderboards private List statisticsLabels; - [Resolved] + [Resolved(CanBeNull = true)] private DialogOverlay dialogOverlay { get; set; } public LeaderboardScore(ScoreInfo score, int rank, bool allowHighlight = true) diff --git a/osu.Game/Online/Multiplayer/PlaylistItem.cs b/osu.Game/Online/Multiplayer/PlaylistItem.cs index 5f8edc607b..69e1f0db13 100644 --- a/osu.Game/Online/Multiplayer/PlaylistItem.cs +++ b/osu.Game/Online/Multiplayer/PlaylistItem.cs @@ -1,9 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; +using System; using System.Linq; using Newtonsoft.Json; +using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; @@ -12,7 +13,7 @@ using osu.Game.Rulesets.Mods; namespace osu.Game.Online.Multiplayer { - public class PlaylistItem + public class PlaylistItem : IEquatable { [JsonProperty("id")] public int ID { get; set; } @@ -24,24 +25,16 @@ namespace osu.Game.Online.Multiplayer public int RulesetID { get; set; } [JsonIgnore] - public BeatmapInfo Beatmap - { - get => beatmap; - set - { - beatmap = value; - BeatmapID = value?.OnlineBeatmapID ?? 0; - } - } + public readonly Bindable Beatmap = new Bindable(); [JsonIgnore] - public RulesetInfo Ruleset { get; set; } + public readonly Bindable Ruleset = new Bindable(); [JsonIgnore] - public readonly List AllowedMods = new List(); + public readonly BindableList AllowedMods = new BindableList(); [JsonIgnore] - public readonly List RequiredMods = new List(); + public readonly BindableList RequiredMods = new BindableList(); [JsonProperty("beatmap")] private APIBeatmap apiBeatmap { get; set; } @@ -64,16 +57,20 @@ namespace osu.Game.Online.Multiplayer set => requiredModsBacking = value; } - private BeatmapInfo beatmap; + public PlaylistItem() + { + Beatmap.BindValueChanged(beatmap => BeatmapID = beatmap.NewValue?.OnlineBeatmapID ?? 0); + Ruleset.BindValueChanged(ruleset => RulesetID = ruleset.NewValue?.ID ?? 0); + } public void MapObjects(BeatmapManager beatmaps, RulesetStore rulesets) { // If we don't have an api beatmap, the request occurred as a result of room creation, so we can query the local beatmap instead // Todo: Is this a bug? Room creation only returns the beatmap ID - Beatmap = apiBeatmap == null ? beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == BeatmapID) : apiBeatmap.ToBeatmap(rulesets); - Ruleset = rulesets.GetRuleset(RulesetID); + Beatmap.Value = apiBeatmap == null ? beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == BeatmapID) : apiBeatmap.ToBeatmap(rulesets); + Ruleset.Value = rulesets.GetRuleset(RulesetID); - Ruleset rulesetInstance = Ruleset.CreateInstance(); + Ruleset rulesetInstance = Ruleset.Value.CreateInstance(); if (allowedModsBacking != null) { @@ -94,5 +91,14 @@ namespace osu.Game.Online.Multiplayer public bool ShouldSerializeID() => false; public bool ShouldSerializeapiBeatmap() => false; + + public bool Equals(PlaylistItem other) => ID == other?.ID && BeatmapID == other.BeatmapID && RulesetID == other.RulesetID; + + public override int GetHashCode() + { + // ReSharper disable NonReadonlyMemberInGetHashCode + return HashCode.Combine(ID, BeatmapID, RulesetID); + // ReSharper restore NonReadonlyMemberInGetHashCode + } } } diff --git a/osu.Game/Online/Multiplayer/Room.cs b/osu.Game/Online/Multiplayer/Room.cs index 53089897f7..400afb39a1 100644 --- a/osu.Game/Online/Multiplayer/Room.cs +++ b/osu.Game/Online/Multiplayer/Room.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using osu.Framework.Allocation; @@ -31,10 +30,6 @@ namespace osu.Game.Online.Multiplayer [JsonProperty("playlist")] public BindableList Playlist { get; private set; } = new BindableList(); - [Cached] - [JsonIgnore] - public Bindable CurrentItem { get; private set; } = new Bindable(); - [Cached] [JsonProperty("channel_id")] public Bindable ChannelId { get; private set; } = new Bindable(); @@ -65,23 +60,11 @@ namespace osu.Game.Online.Multiplayer [Cached] [JsonIgnore] - public Bindable> Participants { get; private set; } = new Bindable>(Enumerable.Empty()); + public BindableList Participants { get; private set; } = new BindableList(); [Cached] public Bindable ParticipantCount { get; private set; } = new Bindable(); - public Room() - { - Playlist.ItemsAdded += updateCurrent; - Playlist.ItemsRemoved += updateCurrent; - updateCurrent(Playlist); - } - - private void updateCurrent(IEnumerable playlist) - { - CurrentItem.Value = playlist.FirstOrDefault(); - } - // todo: TEMPORARY [JsonProperty("participant_count")] private int? participantCount @@ -130,17 +113,18 @@ namespace osu.Game.Online.Multiplayer Type.Value = other.Type.Value; MaxParticipants.Value = other.MaxParticipants.Value; ParticipantCount.Value = other.ParticipantCount.Value; - Participants.Value = other.Participants.Value.ToArray(); EndDate.Value = other.EndDate.Value; if (DateTimeOffset.Now >= EndDate.Value) Status.Value = new RoomStatusEnded(); - // Todo: Temporary, should only remove/add new items (requires framework changes) - if (Playlist.Count == 0) - Playlist.AddRange(other.Playlist); - else if (other.Playlist.Count > 0) - Playlist.First().ID = other.Playlist.First().ID; + foreach (var removedItem in Playlist.Except(other.Playlist).ToArray()) + Playlist.Remove(removedItem); + Playlist.AddRange(other.Playlist.Except(Playlist).ToArray()); + + foreach (var removedItem in Participants.Except(other.Participants).ToArray()) + Participants.Remove(removedItem); + Participants.AddRange(other.Participants.Except(Participants).ToArray()); Position = other.Position; } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 79616ef97c..e7fffd49b4 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -401,15 +401,14 @@ namespace osu.Game if (nextBeatmap?.Track != null) nextBeatmap.Track.Completed += currentTrackCompleted; - using (var oldBeatmap = beatmap.OldValue) - { - if (oldBeatmap?.Track != null) - oldBeatmap.Track.Completed -= currentTrackCompleted; - } + var oldBeatmap = beatmap.OldValue; + if (oldBeatmap?.Track != null) + oldBeatmap.Track.Completed -= currentTrackCompleted; updateModDefaults(); - nextBeatmap?.LoadBeatmapAsync(); + oldBeatmap?.CancelAsyncLoad(); + nextBeatmap?.BeginAsyncLoad(); } private void modsChanged(ValueChangedEvent> mods) diff --git a/osu.Game/Overlays/Direct/DownloadProgressBar.cs b/osu.Game/Overlays/Direct/DownloadProgressBar.cs index a6cefaae84..9a8644efd2 100644 --- a/osu.Game/Overlays/Direct/DownloadProgressBar.cs +++ b/osu.Game/Overlays/Direct/DownloadProgressBar.cs @@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Direct public DownloadProgressBar(BeatmapSetInfo beatmapSet) : base(beatmapSet) { - AddInternal(progressBar = new ProgressBar + AddInternal(progressBar = new InteractionDisabledProgressBar { Height = 0, Alpha = 0, @@ -64,5 +64,11 @@ namespace osu.Game.Overlays.Direct } }, true); } + + private class InteractionDisabledProgressBar : ProgressBar + { + public override bool HandlePositionalInput => false; + public override bool HandleNonPositionalInput => false; + } } } diff --git a/osu.Game/Overlays/Direct/PanelDownloadButton.cs b/osu.Game/Overlays/Direct/PanelDownloadButton.cs index ed44f1e960..1b3657f010 100644 --- a/osu.Game/Overlays/Direct/PanelDownloadButton.cs +++ b/osu.Game/Overlays/Direct/PanelDownloadButton.cs @@ -45,7 +45,7 @@ namespace osu.Game.Overlays.Direct [BackgroundDependencyLoader(true)] private void load(OsuGame game, BeatmapManager beatmaps) { - if (BeatmapSet.Value.OnlineInfo.Availability?.DownloadDisabled ?? false) + if (BeatmapSet.Value?.OnlineInfo?.Availability?.DownloadDisabled ?? false) { button.Enabled.Value = false; button.TooltipText = "this beatmap is currently not available for download."; @@ -62,7 +62,7 @@ namespace osu.Game.Overlays.Direct break; case DownloadState.LocallyAvailable: - game.PresentBeatmap(BeatmapSet.Value); + game?.PresentBeatmap(BeatmapSet.Value); break; default: diff --git a/osu.Game/Overlays/Music/Playlist.cs b/osu.Game/Overlays/Music/Playlist.cs index 1ba568443d..621a533dd6 100644 --- a/osu.Game/Overlays/Music/Playlist.cs +++ b/osu.Game/Overlays/Music/Playlist.cs @@ -12,17 +12,12 @@ using osuTK; namespace osu.Game.Overlays.Music { - public class Playlist : RearrangeableListContainer + public class Playlist : OsuRearrangeableListContainer { public Action RequestSelection; public readonly Bindable SelectedSet = new Bindable(); - /// - /// Whether any item is currently being dragged. Used to hide other items' drag handles. - /// - private readonly BindableBool playlistDragActive = new BindableBool(); - public new MarginPadding Padding { get => base.Padding; @@ -33,15 +28,12 @@ namespace osu.Game.Overlays.Music public BeatmapSetInfo FirstVisibleSet => Items.FirstOrDefault(i => ((PlaylistItem)ItemMap[i]).MatchingFilter); - protected override RearrangeableListItem CreateDrawable(BeatmapSetInfo item) => new PlaylistItem(item) + protected override OsuRearrangeableListItem CreateOsuDrawable(BeatmapSetInfo item) => new PlaylistItem(item) { SelectedSet = { BindTarget = SelectedSet }, - PlaylistDragActive = { BindTarget = playlistDragActive }, RequestSelection = set => RequestSelection?.Invoke(set) }; - protected override ScrollContainer CreateScrollContainer() => new OsuScrollContainer(); - protected override FillFlowContainer> CreateListFillFlowContainer() => new SearchContainer> { Spacing = new Vector2(0, 3), diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 0569261867..8cafbc694a 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -14,36 +14,27 @@ using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osuTK; using osuTK.Graphics; namespace osu.Game.Overlays.Music { - public class PlaylistItem : RearrangeableListItem, IFilterable + public class PlaylistItem : OsuRearrangeableListItem, IFilterable { - private const float fade_duration = 100; - - public BindableBool PlaylistDragActive = new BindableBool(); - public readonly Bindable SelectedSet = new Bindable(); public Action RequestSelection; - private PlaylistItemHandle handle; private TextFlowContainer text; private IEnumerable titleSprites; private ILocalisedBindableString titleBind; private ILocalisedBindableString artistBind; - private Color4 hoverColour; + private Color4 selectedColour; private Color4 artistColour; public PlaylistItem(BeatmapSetInfo item) : base(item) { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - Padding = new MarginPadding { Left = 5 }; FilterTerms = item.Metadata.SearchableTerms; @@ -52,42 +43,12 @@ namespace osu.Game.Overlays.Music [BackgroundDependencyLoader] private void load(OsuColour colours, LocalisationManager localisation) { - hoverColour = colours.Yellow; + selectedColour = colours.Yellow; artistColour = colours.Gray9; - - InternalChild = new GridContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Content = new[] - { - new Drawable[] - { - handle = new PlaylistItemHandle - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Size = new Vector2(12), - Colour = colours.Gray5, - AlwaysPresent = true, - Alpha = 0 - }, - text = new OsuTextFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Left = 5 }, - }, - } - }, - ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, - RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } - }; + HandleColour = colours.Gray5; titleBind = localisation.GetLocalisedString(new LocalisedString((Model.Metadata.TitleUnicode, Model.Metadata.Title))); artistBind = localisation.GetLocalisedString(new LocalisedString((Model.Metadata.ArtistUnicode, Model.Metadata.Artist))); - - artistBind.BindValueChanged(_ => recreateText(), true); } protected override void LoadComplete() @@ -100,10 +61,18 @@ namespace osu.Game.Overlays.Music return; foreach (Drawable s in titleSprites) - s.FadeColour(set.NewValue == Model ? hoverColour : Color4.White, fade_duration); + s.FadeColour(set.NewValue == Model ? selectedColour : Color4.White, FADE_DURATION); }, true); + + artistBind.BindValueChanged(_ => recreateText(), true); } + protected override Drawable CreateContent() => text = new OsuTextFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }; + private void recreateText() { text.Clear(); @@ -125,31 +94,6 @@ namespace osu.Game.Overlays.Music return true; } - protected override bool OnDragStart(DragStartEvent e) - { - if (!base.OnDragStart(e)) - return false; - - PlaylistDragActive.Value = true; - return true; - } - - protected override void OnDragEnd(DragEndEvent e) - { - PlaylistDragActive.Value = false; - base.OnDragEnd(e); - } - - protected override bool IsDraggableAt(Vector2 screenSpacePos) => handle.HandlingDrag; - - protected override bool OnHover(HoverEvent e) - { - handle.UpdateHoverState(IsDragged || !PlaylistDragActive.Value); - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) => handle.UpdateHoverState(false); - public IEnumerable FilterTerms { get; } private bool matching = true; @@ -168,44 +112,5 @@ namespace osu.Game.Overlays.Music } public bool FilteringActive { get; set; } - - private class PlaylistItemHandle : SpriteIcon - { - public bool HandlingDrag { get; private set; } - private bool isHovering; - - public PlaylistItemHandle() - { - Icon = FontAwesome.Solid.Bars; - } - - protected override bool OnMouseDown(MouseDownEvent e) - { - base.OnMouseDown(e); - - HandlingDrag = true; - UpdateHoverState(isHovering); - - return false; - } - - protected override void OnMouseUp(MouseUpEvent e) - { - base.OnMouseUp(e); - - HandlingDrag = false; - UpdateHoverState(isHovering); - } - - public void UpdateHoverState(bool hovering) - { - isHovering = hovering; - - if (isHovering || HandlingDrag) - this.FadeIn(fade_duration); - else - this.FadeOut(fade_duration); - } - } } } diff --git a/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs b/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs index 94afe4e5a5..2674b3a81e 100644 --- a/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs +++ b/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs @@ -6,25 +6,14 @@ using osu.Framework.Bindables; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osu.Game.Users; -using System.Collections.Generic; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Allocation; namespace osu.Game.Overlays.Rankings { public class RankingsOverlayHeader : TabControlOverlayHeader { public readonly Bindable Ruleset = new Bindable(); - public readonly Bindable Spotlight = new Bindable(); public readonly Bindable Country = new Bindable(); - public IEnumerable Spotlights - { - get => spotlightsContainer.Spotlights; - set => spotlightsContainer.Spotlights = value; - } - protected override ScreenTitle CreateTitle() => new RankingsTitle { Scope = { BindTarget = Current } @@ -35,35 +24,11 @@ namespace osu.Game.Overlays.Rankings Current = Ruleset }; - private SpotlightsContainer spotlightsContainer; - - protected override Drawable CreateContent() => new FillFlowContainer + protected override Drawable CreateContent() => new CountryFilter { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new CountryFilter - { - Current = Country - }, - spotlightsContainer = new SpotlightsContainer - { - Spotlight = { BindTarget = Spotlight } - } - } + Current = Country }; - protected override void LoadComplete() - { - Current.BindValueChanged(onCurrentChanged, true); - base.LoadComplete(); - } - - private void onCurrentChanged(ValueChangedEvent scope) => - spotlightsContainer.FadeTo(scope.NewValue == RankingsScope.Spotlights ? 1 : 0, 200, Easing.OutQuint); - private class RankingsTitle : ScreenTitle { public readonly Bindable Scope = new Bindable(); @@ -81,48 +46,6 @@ namespace osu.Game.Overlays.Rankings protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/rankings"); } - - private class SpotlightsContainer : CompositeDrawable - { - public readonly Bindable Spotlight = new Bindable(); - - public IEnumerable Spotlights - { - get => dropdown.Items; - set => dropdown.Items = value; - } - - private readonly OsuDropdown dropdown; - private readonly Box background; - - public SpotlightsContainer() - { - Height = 100; - RelativeSizeAxes = Axes.X; - InternalChildren = new Drawable[] - { - background = new Box - { - RelativeSizeAxes = Axes.Both, - }, - dropdown = new OsuDropdown - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - Width = 0.8f, - Current = Spotlight, - Y = 20, - } - }; - } - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - background.Colour = colourProvider.Dark3; - } - } } public enum RankingsScope diff --git a/osu.Game/Overlays/Rankings/Spotlight.cs b/osu.Game/Overlays/Rankings/Spotlight.cs deleted file mode 100644 index e956b4f449..0000000000 --- a/osu.Game/Overlays/Rankings/Spotlight.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using Newtonsoft.Json; - -namespace osu.Game.Overlays.Rankings -{ - public class Spotlight - { - [JsonProperty("id")] - public int Id; - - [JsonProperty("text")] - public string Text; - - public override string ToString() => Text; - } -} diff --git a/osu.Game/Overlays/Rankings/SpotlightSelector.cs b/osu.Game/Overlays/Rankings/SpotlightSelector.cs index e34c01113e..f019b50ae8 100644 --- a/osu.Game/Overlays/Rankings/SpotlightSelector.cs +++ b/osu.Game/Overlays/Rankings/SpotlightSelector.cs @@ -14,11 +14,14 @@ using osuTK; using System; using System.Collections.Generic; using osu.Framework.Graphics.UserInterface; +using osu.Game.Online.API.Requests; namespace osu.Game.Overlays.Rankings { - public class SpotlightSelector : CompositeDrawable, IHasCurrentValue + public class SpotlightSelector : VisibilityContainer, IHasCurrentValue { + private const int duration = 300; + private readonly Box background; private readonly SpotlightsDropdown dropdown; @@ -36,50 +39,60 @@ namespace osu.Game.Overlays.Rankings set => dropdown.Items = value; } + protected override bool StartHidden => true; + private readonly InfoColumn startDateColumn; private readonly InfoColumn endDateColumn; + private readonly InfoColumn mapCountColumn; + private readonly InfoColumn participantsColumn; + private readonly Container content; public SpotlightSelector() { RelativeSizeAxes = Axes.X; Height = 100; - - InternalChildren = new Drawable[] + Add(content = new Container { - background = new Box + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN, Vertical = 10 }, - Children = new Drawable[] + background = new Box { - dropdown = new SpotlightsDropdown + RelativeSizeAxes = Axes.Both, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN, Vertical = 10 }, + Children = new Drawable[] { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - Current = Current, - Depth = -float.MaxValue - }, - new FillFlowContainer - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(15, 0), - Children = new Drawable[] + dropdown = new SpotlightsDropdown { - startDateColumn = new InfoColumn(@"Start Date"), - endDateColumn = new InfoColumn(@"End Date"), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + Current = Current, + Depth = -float.MaxValue + }, + new FillFlowContainer + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(15, 0), + Children = new Drawable[] + { + startDateColumn = new InfoColumn(@"Start Date"), + endDateColumn = new InfoColumn(@"End Date"), + mapCountColumn = new InfoColumn(@"Map Count"), + participantsColumn = new InfoColumn(@"Participants") + } } } } - }, - }; + } + }); } [BackgroundDependencyLoader] @@ -88,18 +101,17 @@ namespace osu.Game.Overlays.Rankings background.Colour = colourProvider.Dark3; } - protected override void LoadComplete() + public void ShowInfo(GetSpotlightRankingsResponse response) { - base.LoadComplete(); - - Current.BindValueChanged(onCurrentChanged); + startDateColumn.Value = dateToString(response.Spotlight.StartDate); + endDateColumn.Value = dateToString(response.Spotlight.EndDate); + mapCountColumn.Value = response.BeatmapSets.Count.ToString(); + participantsColumn.Value = response.Spotlight.Participants?.ToString("N0"); } - private void onCurrentChanged(ValueChangedEvent spotlight) - { - startDateColumn.Value = dateToString(spotlight.NewValue.StartDate); - endDateColumn.Value = dateToString(spotlight.NewValue.EndDate); - } + protected override void PopIn() => content.FadeIn(duration, Easing.OutQuint); + + protected override void PopOut() => content.FadeOut(duration, Easing.OutQuint); private string dateToString(DateTimeOffset date) => date.ToString("yyyy-MM-dd"); diff --git a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs new file mode 100644 index 0000000000..33811cc982 --- /dev/null +++ b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs @@ -0,0 +1,161 @@ +// 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.Graphics; +using osu.Framework.Bindables; +using osu.Game.Rulesets; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.API.Requests.Responses; +using osuTK; +using osu.Framework.Allocation; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Overlays.Rankings.Tables; +using System.Linq; +using osu.Game.Overlays.Direct; +using System.Threading; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Overlays.Rankings +{ + public class SpotlightsLayout : CompositeDrawable + { + public readonly Bindable Ruleset = new Bindable(); + + private readonly Bindable selectedSpotlight = new Bindable(); + + [Resolved] + private IAPIProvider api { get; set; } + + [Resolved] + private RulesetStore rulesets { get; set; } + + private CancellationTokenSource cancellationToken; + private GetSpotlightRankingsRequest getRankingsRequest; + private GetSpotlightsRequest spotlightsRequest; + + private SpotlightSelector selector; + private Container content; + private DimmedLoadingLayer loading; + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + InternalChild = new ReverseChildIDFillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + selector = new SpotlightSelector + { + Current = selectedSpotlight, + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + content = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Vertical = 10 } + }, + loading = new DimmedLoadingLayer() + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + selector.Show(); + + selectedSpotlight.BindValueChanged(onSpotlightChanged); + Ruleset.BindValueChanged(onRulesetChanged); + + getSpotlights(); + } + + private void getSpotlights() + { + spotlightsRequest = new GetSpotlightsRequest(); + spotlightsRequest.Success += response => selector.Spotlights = response.Spotlights; + api.Queue(spotlightsRequest); + } + + private void onRulesetChanged(ValueChangedEvent ruleset) + { + if (!selector.Spotlights.Any()) + return; + + selectedSpotlight.TriggerChange(); + } + + private void onSpotlightChanged(ValueChangedEvent spotlight) + { + loading.Show(); + + cancellationToken?.Cancel(); + getRankingsRequest?.Cancel(); + + getRankingsRequest = new GetSpotlightRankingsRequest(Ruleset.Value, spotlight.NewValue.Id); + getRankingsRequest.Success += onSuccess; + api.Queue(getRankingsRequest); + } + + private void onSuccess(GetSpotlightRankingsResponse response) + { + LoadComponentAsync(createContent(response), loaded => + { + selector.ShowInfo(response); + + content.Clear(); + content.Add(loaded); + + loading.Hide(); + }, (cancellationToken = new CancellationTokenSource()).Token); + } + + private Drawable createContent(GetSpotlightRankingsResponse response) => new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 20), + Children = new Drawable[] + { + new ScoresTable(1, response.Users), + new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Spacing = new Vector2(10), + Children = response.BeatmapSets.Select(b => new DirectGridPanel(b.ToBeatmapSet(rulesets)) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }).ToList() + } + } + }; + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + spotlightsRequest?.Cancel(); + getRankingsRequest?.Cancel(); + cancellationToken?.Cancel(); + } + } +} diff --git a/osu.Game/Overlays/RankingsOverlay.cs b/osu.Game/Overlays/RankingsOverlay.cs index 84470d9caa..f3215d07fa 100644 --- a/osu.Game/Overlays/RankingsOverlay.cs +++ b/osu.Game/Overlays/RankingsOverlay.cs @@ -24,7 +24,7 @@ namespace osu.Game.Overlays private readonly Bindable ruleset = new Bindable(); private readonly BasicScrollContainer scrollFlow; - private readonly Container tableContainer; + private readonly Container contentContainer; private readonly DimmedLoadingLayer loading; private readonly Box background; @@ -69,13 +69,13 @@ namespace osu.Game.Overlays AutoSizeAxes = Axes.Y, Children = new Drawable[] { - tableContainer = new Container + contentContainer = new Container { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, - Margin = new MarginPadding { Vertical = 10 } + Margin = new MarginPadding { Bottom = 10 } }, loading = new DimmedLoadingLayer(), } @@ -112,7 +112,13 @@ namespace osu.Game.Overlays Scheduler.AddOnce(loadNewContent); }, true); - ruleset.BindValueChanged(_ => Scheduler.AddOnce(loadNewContent), true); + ruleset.BindValueChanged(_ => + { + if (Scope.Value == RankingsScope.Spotlights) + return; + + Scheduler.AddOnce(loadNewContent); + }, true); base.LoadComplete(); } @@ -134,17 +140,26 @@ namespace osu.Game.Overlays cancellationToken?.Cancel(); lastRequest?.Cancel(); + if (Scope.Value == RankingsScope.Spotlights) + { + loadContent(new SpotlightsLayout + { + Ruleset = { BindTarget = ruleset } + }); + return; + } + var request = createScopedRequest(); lastRequest = request; if (request == null) { - loadTable(null); + loadContent(null); return; } - request.Success += () => loadTable(createTableFromResponse(request)); - request.Failure += _ => loadTable(null); + request.Success += () => loadContent(createTableFromResponse(request)); + request.Failure += _ => loadContent(null); api.Queue(request); } @@ -189,21 +204,21 @@ namespace osu.Game.Overlays return null; } - private void loadTable(Drawable table) + private void loadContent(Drawable content) { scrollFlow.ScrollToStart(); - if (table == null) + if (content == null) { - tableContainer.Clear(); + contentContainer.Clear(); loading.Hide(); return; } - LoadComponentAsync(table, t => + LoadComponentAsync(content, loaded => { loading.Hide(); - tableContainer.Child = table; + contentContainer.Child = loaded; }, (cancellationToken = new CancellationTokenSource()).Token); } } diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index 24fa96e1c5..ea77a6091a 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -20,17 +18,12 @@ namespace osu.Game.Rulesets.Edit /// /// A blueprint which governs the creation of a new to actualisation. /// - public abstract class PlacementBlueprint : CompositeDrawable, IStateful + public abstract class PlacementBlueprint : CompositeDrawable { /// - /// Invoked when has changed. + /// Whether the is currently mid-placement, but has not necessarily finished being placed. /// - public event Action StateChanged; - - /// - /// Whether the is currently being placed, but has not necessarily finished being placed. - /// - public bool PlacementBegun { get; private set; } + public bool PlacementActive { get; private set; } /// /// The that is being placed. @@ -53,8 +46,6 @@ namespace osu.Game.Rulesets.Edit // This is required to allow the blueprint's position to be updated via OnMouseMove/Handle // on the same frame it is made visible via a PlacementState change. AlwaysPresent = true; - - Alpha = 0; } [BackgroundDependencyLoader] @@ -67,48 +58,29 @@ namespace osu.Game.Rulesets.Edit ApplyDefaultsToHitObject(); } - private PlacementState state; - - public PlacementState State - { - get => state; - set - { - if (state == value) - return; - - state = value; - - if (state == PlacementState.Shown) - Show(); - else - Hide(); - - StateChanged?.Invoke(value); - } - } - /// /// Signals that the placement of has started. /// /// The start time of at the placement point. If null, the current clock time is used. - protected void BeginPlacement(double? startTime = null) + /// Whether this call is committing a value for HitObject.StartTime and continuing with further adjustments. + protected void BeginPlacement(double? startTime = null, bool commitStart = false) { HitObject.StartTime = startTime ?? EditorClock.CurrentTime; placementHandler.BeginPlacement(HitObject); - PlacementBegun = true; + PlacementActive |= commitStart; } /// /// Signals that the placement of has finished. - /// This will destroy this , and add the to the . + /// This will destroy this , and add the HitObject.StartTime to the . /// /// Whether the object should be committed. public void EndPlacement(bool commit) { - if (!PlacementBegun) + if (!PlacementActive) BeginPlacement(); placementHandler.EndPlacement(HitObject, commit); + PlacementActive = false; } /// @@ -142,10 +114,4 @@ namespace osu.Game.Rulesets.Edit } } } - - public enum PlacementState - { - Hidden, - Shown, - } } diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index d74e2ce2bc..2083671072 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -28,7 +28,11 @@ namespace osu.Game.Rulesets.Mods public override Type[] IncompatibleMods => new[] { typeof(ModEasy), typeof(ModHardRock) }; - [SettingSource("Drain Rate", "Override a beatmap's set HP.")] + protected const int FIRST_SETTING_ORDER = 1; + + protected const int LAST_SETTING_ORDER = 2; + + [SettingSource("HP Drain", "Override a beatmap's set HP.", FIRST_SETTING_ORDER)] public BindableNumber DrainRate { get; } = new BindableFloat { Precision = 0.1f, @@ -38,7 +42,7 @@ namespace osu.Game.Rulesets.Mods Value = 5, }; - [SettingSource("Overall Difficulty", "Override a beatmap's set OD.")] + [SettingSource("Accuracy", "Override a beatmap's set OD.", LAST_SETTING_ORDER)] public BindableNumber OverallDifficulty { get; } = new BindableFloat { Precision = 0.1f, diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index e391157b5b..d3a0b3450f 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Objects.Drawables HitObject.DefaultsApplied += onDefaultsApplied; startTimeBindable = HitObject.StartTimeBindable.GetBoundCopy(); - startTimeBindable.BindValueChanged(_ => updateState(ArmedState.Idle, true)); + startTimeBindable.BindValueChanged(_ => updateState(State.Value, true)); if (HitObject is IHasComboInformation combo) { @@ -250,8 +250,8 @@ namespace osu.Game.Rulesets.Objects.Drawables double transformTime = HitObject.StartTime - InitialLifetimeOffset; - base.ApplyTransformsAt(transformTime, true); - base.ClearTransformsAfter(transformTime, true); + base.ApplyTransformsAt(double.MinValue, true); + base.ClearTransformsAfter(double.MinValue, true); using (BeginAbsoluteSequence(transformTime, true)) { diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index b257688568..0eb77a8561 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -7,7 +7,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input; -using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Objects; @@ -62,20 +61,8 @@ namespace osu.Game.Screens.Edit.Compose.Components /// private void refreshTool() { - placementBlueprintContainer.Clear(); - - currentPlacement?.EndPlacement(false); - currentPlacement = null; - - var blueprint = CurrentTool?.CreatePlacementBlueprint(); - - if (blueprint != null) - { - placementBlueprintContainer.Child = currentPlacement = blueprint; - - // Fixes a 1-frame position discrepancy due to the first mouse move event happening in the next frame - updatePlacementPosition(inputManager.CurrentState.Mouse.Position); - } + removePlacement(); + createPlacement(); } private void updatePlacementPosition(Vector2 screenSpacePosition) @@ -88,33 +75,23 @@ namespace osu.Game.Screens.Edit.Compose.Components #endregion - protected override bool OnMouseMove(MouseMoveEvent e) - { - if (currentPlacement != null) - { - updatePlacementPosition(e.ScreenSpaceMousePosition); - return true; - } - - return base.OnMouseMove(e); - } - protected override void Update() { base.Update(); + if (composer.CursorInPlacementArea) + createPlacement(); + else if (currentPlacement?.PlacementActive == false) + removePlacement(); + if (currentPlacement != null) - { - if (composer.CursorInPlacementArea) - currentPlacement.State = PlacementState.Shown; - else if (currentPlacement?.PlacementBegun == false) - currentPlacement.State = PlacementState.Hidden; - } + updatePlacementPosition(inputManager.CurrentState.Mouse.Position); } protected sealed override SelectionBlueprint CreateBlueprintFor(HitObject hitObject) { var drawable = drawableHitObjects.FirstOrDefault(d => d.HitObject == hitObject); + if (drawable == null) return null; @@ -129,6 +106,30 @@ namespace osu.Game.Screens.Edit.Compose.Components base.AddBlueprintFor(hitObject); } + private void createPlacement() + { + if (currentPlacement != null) return; + + var blueprint = CurrentTool?.CreatePlacementBlueprint(); + + if (blueprint != null) + { + placementBlueprintContainer.Child = currentPlacement = blueprint; + + // Fixes a 1-frame position discrepancy due to the first mouse move event happening in the next frame + updatePlacementPosition(inputManager.CurrentState.Mouse.Position); + } + } + + private void removePlacement() + { + if (currentPlacement == null) return; + + currentPlacement.EndPlacement(false); + currentPlacement.Expire(); + currentPlacement = null; + } + private HitObjectCompositionTool currentTool; /// @@ -137,6 +138,7 @@ namespace osu.Game.Screens.Edit.Compose.Components public HitObjectCompositionTool CurrentTool { get => currentTool; + set { if (currentTool == value) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineButton.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineButton.cs index 8865bf31ea..5550c6a748 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineButton.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineButton.cs @@ -6,10 +6,13 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Framework.Threading; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osuTK; using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { @@ -52,6 +55,45 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline HoverColour = OsuColour.Gray(0.25f); FlashColour = OsuColour.Gray(0.5f); } + + private ScheduledDelegate repeatSchedule; + + /// + /// The initial delay before mouse down repeat begins. + /// + private const int repeat_initial_delay = 250; + + /// + /// The delay between mouse down repeats after the initial repeat. + /// + private const int repeat_tick_rate = 70; + + protected override bool OnClick(ClickEvent e) + { + // don't actuate a click since we are manually handling repeats. + return true; + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button == MouseButton.Left) + { + Action clickAction = () => base.OnClick(new ClickEvent(e.CurrentState, e.Button)); + + // run once for initial down + clickAction(); + + Scheduler.Add(repeatSchedule = new ScheduledDelegate(clickAction, Clock.CurrentTime + repeat_initial_delay, repeat_tick_rate)); + } + + return base.OnMouseDown(e); + } + + protected override void OnMouseUp(MouseUpEvent e) + { + repeatSchedule?.Cancel(); + base.OnMouseUp(e); + } } } } diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index 06ca161fed..dcc68296f6 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -95,7 +95,7 @@ namespace osu.Game.Screens.Menu private void updateAmplitudes() { var track = beatmap.Value.TrackLoaded ? beatmap.Value.Track : null; - var effect = beatmap.Value.BeatmapLoaded ? beatmap.Value.Beatmap.ControlPointInfo.EffectPointAt(track?.CurrentTime ?? Time.Current) : null; + var effect = beatmap.Value.BeatmapLoaded ? beatmap.Value.Beatmap?.ControlPointInfo.EffectPointAt(track?.CurrentTime ?? Time.Current) : null; float[] temporalAmplitudes = track?.CurrentAmplitudes.FrequencyAmplitudes; diff --git a/osu.Game/Screens/Multi/Components/BeatmapTitle.cs b/osu.Game/Screens/Multi/Components/BeatmapTitle.cs index b41b2d073e..baf11dfe0d 100644 --- a/osu.Game/Screens/Multi/Components/BeatmapTitle.cs +++ b/osu.Game/Screens/Multi/Components/BeatmapTitle.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; @@ -25,7 +26,10 @@ namespace osu.Game.Screens.Multi.Components [BackgroundDependencyLoader] private void load() { - CurrentItem.BindValueChanged(_ => updateText(), true); + Playlist.ItemsAdded += _ => updateText(); + Playlist.ItemsRemoved += _ => updateText(); + + updateText(); } private float textSize = OsuFont.DEFAULT_FONT_SIZE; @@ -54,7 +58,7 @@ namespace osu.Game.Screens.Multi.Components textFlow.Clear(); - var beatmap = CurrentItem.Value?.Beatmap; + var beatmap = Playlist.FirstOrDefault()?.Beatmap; if (beatmap == null) { @@ -70,7 +74,7 @@ namespace osu.Game.Screens.Multi.Components { new OsuSpriteText { - Text = new LocalisedString((beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist)), + Text = new LocalisedString((beatmap.Value.Metadata.ArtistUnicode, beatmap.Value.Metadata.Artist)), Font = OsuFont.GetFont(size: TextSize), }, new OsuSpriteText @@ -80,10 +84,10 @@ namespace osu.Game.Screens.Multi.Components }, new OsuSpriteText { - Text = new LocalisedString((beatmap.Metadata.TitleUnicode, beatmap.Metadata.Title)), + Text = new LocalisedString((beatmap.Value.Metadata.TitleUnicode, beatmap.Value.Metadata.Title)), Font = OsuFont.GetFont(size: TextSize), } - }, LinkAction.OpenBeatmap, beatmap.OnlineBeatmapID.ToString(), "Open beatmap"); + }, LinkAction.OpenBeatmap, beatmap.Value.OnlineBeatmapID.ToString(), "Open beatmap"); } } } diff --git a/osu.Game/Screens/Multi/Components/BeatmapTypeInfo.cs b/osu.Game/Screens/Multi/Components/BeatmapTypeInfo.cs index d63f2fecd2..a1334101b8 100644 --- a/osu.Game/Screens/Multi/Components/BeatmapTypeInfo.cs +++ b/osu.Game/Screens/Multi/Components/BeatmapTypeInfo.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -12,6 +13,8 @@ namespace osu.Game.Screens.Multi.Components { public class BeatmapTypeInfo : MultiplayerComposite { + private LinkFlowContainer beatmapAuthor; + public BeatmapTypeInfo() { AutoSizeAxes = Axes.Both; @@ -20,8 +23,6 @@ namespace osu.Game.Screens.Multi.Components [BackgroundDependencyLoader] private void load() { - LinkFlowContainer beatmapAuthor; - InternalChild = new FillFlowContainer { AutoSizeAxes = Axes.Both, @@ -50,18 +51,23 @@ namespace osu.Game.Screens.Multi.Components } }; - CurrentItem.BindValueChanged(item => + Playlist.ItemsAdded += _ => updateInfo(); + Playlist.ItemsRemoved += _ => updateInfo(); + + updateInfo(); + } + + private void updateInfo() + { + beatmapAuthor.Clear(); + + var beatmap = Playlist.FirstOrDefault()?.Beatmap; + + if (beatmap != null) { - beatmapAuthor.Clear(); - - var beatmap = item.NewValue?.Beatmap; - - if (beatmap != null) - { - beatmapAuthor.AddText("mapped by ", s => s.Colour = OsuColour.Gray(0.8f)); - beatmapAuthor.AddUserLink(beatmap.Metadata.Author); - } - }, true); + beatmapAuthor.AddText("mapped by ", s => s.Colour = OsuColour.Gray(0.8f)); + beatmapAuthor.AddUserLink(beatmap.Value.Metadata.Author); + } } } } diff --git a/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs b/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs index 6080458aec..258541bbd6 100644 --- a/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs +++ b/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs @@ -1,11 +1,11 @@ // 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps.Drawables; -using osu.Game.Online.Multiplayer; using osuTK; namespace osu.Game.Screens.Multi.Components @@ -46,17 +46,22 @@ namespace osu.Game.Screens.Multi.Components }, }; - CurrentItem.BindValueChanged(item => updateBeatmap(item.NewValue), true); - Type.BindValueChanged(type => gameTypeContainer.Child = new DrawableGameType(type.NewValue) { Size = new Vector2(height) }, true); + + Playlist.ItemsAdded += _ => updateBeatmap(); + Playlist.ItemsRemoved += _ => updateBeatmap(); + + updateBeatmap(); } - private void updateBeatmap(PlaylistItem item) + private void updateBeatmap() { + var item = Playlist.FirstOrDefault(); + if (item?.Beatmap != null) { drawableRuleset.FadeIn(transition_duration); - drawableRuleset.Child = new DifficultyIcon(item.Beatmap, item.Ruleset) { Size = new Vector2(height) }; + drawableRuleset.Child = new DifficultyIcon(item.Beatmap.Value, item.Ruleset.Value) { Size = new Vector2(height) }; } else drawableRuleset.FadeOut(transition_duration); diff --git a/osu.Game/Screens/Multi/Components/MultiplayerBackgroundSprite.cs b/osu.Game/Screens/Multi/Components/MultiplayerBackgroundSprite.cs index 968fa6e72e..5e2f2e530a 100644 --- a/osu.Game/Screens/Multi/Components/MultiplayerBackgroundSprite.cs +++ b/osu.Game/Screens/Multi/Components/MultiplayerBackgroundSprite.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps.Drawables; @@ -10,6 +11,7 @@ namespace osu.Game.Screens.Multi.Components public class MultiplayerBackgroundSprite : MultiplayerComposite { private readonly BeatmapSetCoverType beatmapSetCoverType; + private UpdateableBeatmapBackgroundSprite sprite; public MultiplayerBackgroundSprite(BeatmapSetCoverType beatmapSetCoverType = BeatmapSetCoverType.Cover) { @@ -19,11 +21,17 @@ namespace osu.Game.Screens.Multi.Components [BackgroundDependencyLoader] private void load() { - UpdateableBeatmapBackgroundSprite sprite; - InternalChild = sprite = CreateBackgroundSprite(); - CurrentItem.BindValueChanged(item => sprite.Beatmap.Value = item.NewValue?.Beatmap, true); + Playlist.ItemsAdded += _ => updateBeatmap(); + Playlist.ItemsRemoved += _ => updateBeatmap(); + + updateBeatmap(); + } + + private void updateBeatmap() + { + sprite.Beatmap.Value = Playlist.FirstOrDefault()?.Beatmap.Value; } protected virtual UpdateableBeatmapBackgroundSprite CreateBackgroundSprite() => new UpdateableBeatmapBackgroundSprite(beatmapSetCoverType) { RelativeSizeAxes = Axes.Both }; diff --git a/osu.Game/Screens/Multi/DrawableRoomPlaylist.cs b/osu.Game/Screens/Multi/DrawableRoomPlaylist.cs new file mode 100644 index 0000000000..b139c61166 --- /dev/null +++ b/osu.Game/Screens/Multi/DrawableRoomPlaylist.cs @@ -0,0 +1,72 @@ +// 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.Bindables; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Containers; +using osu.Game.Online.Multiplayer; +using osuTK; + +namespace osu.Game.Screens.Multi +{ + public class DrawableRoomPlaylist : OsuRearrangeableListContainer + { + public readonly Bindable SelectedItem = new Bindable(); + + private readonly bool allowEdit; + private readonly bool allowSelection; + + public DrawableRoomPlaylist(bool allowEdit, bool allowSelection) + { + this.allowEdit = allowEdit; + this.allowSelection = allowSelection; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + // Scheduled since items are removed and re-added upon rearrangement + Items.ItemsRemoved += items => Schedule(() => + { + if (!Items.Contains(SelectedItem.Value)) + SelectedItem.Value = null; + }); + } + + protected override ScrollContainer CreateScrollContainer() => base.CreateScrollContainer().With(d => + { + d.ScrollbarVisible = false; + }); + + protected override FillFlowContainer> CreateListFillFlowContainer() => new FillFlowContainer> + { + LayoutDuration = 200, + LayoutEasing = Easing.OutQuint, + Spacing = new Vector2(0, 2) + }; + + protected override OsuRearrangeableListItem CreateOsuDrawable(PlaylistItem item) => new DrawableRoomPlaylistItem(item, allowEdit, allowSelection) + { + SelectedItem = { BindTarget = SelectedItem }, + RequestDeletion = requestDeletion + }; + + private void requestSelection(PlaylistItem item) => SelectedItem.Value = item; + + private void requestDeletion(PlaylistItem item) + { + if (SelectedItem.Value == item) + { + if (Items.Count == 1) + SelectedItem.Value = null; + else + SelectedItem.Value = Items.GetNext(item) ?? Items[^2]; + } + + Items.Remove(item); + } + } +} diff --git a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs new file mode 100644 index 0000000000..b1e69e4c4f --- /dev/null +++ b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs @@ -0,0 +1,275 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Framework.Threading; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Chat; +using osu.Game.Online.Multiplayer; +using osu.Game.Overlays.Direct; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Play.HUD; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Multi +{ + public class DrawableRoomPlaylistItem : OsuRearrangeableListItem + { + public Action RequestDeletion; + + public readonly Bindable SelectedItem = new Bindable(); + + protected override bool ShowDragHandle => allowEdit; + + private Container maskingContainer; + private Container difficultyIconContainer; + private LinkFlowContainer beatmapText; + private LinkFlowContainer authorText; + private ModDisplay modDisplay; + + private readonly Bindable beatmap = new Bindable(); + private readonly Bindable ruleset = new Bindable(); + private readonly BindableList requiredMods = new BindableList(); + + private readonly PlaylistItem item; + private readonly bool allowEdit; + private readonly bool allowSelection; + + public DrawableRoomPlaylistItem(PlaylistItem item, bool allowEdit, bool allowSelection) + : base(item) + { + this.item = item; + this.allowEdit = allowEdit; + this.allowSelection = allowSelection; + + beatmap.BindTo(item.Beatmap); + ruleset.BindTo(item.Ruleset); + requiredMods.BindTo(item.RequiredMods); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + if (!allowEdit) + HandleColour = HandleColour.Opacity(0); + + maskingContainer.BorderColour = colours.Yellow; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + SelectedItem.BindValueChanged(selected => maskingContainer.BorderThickness = selected.NewValue == Model ? 5 : 0); + + beatmap.BindValueChanged(_ => scheduleRefresh()); + ruleset.BindValueChanged(_ => scheduleRefresh()); + + requiredMods.ItemsAdded += _ => scheduleRefresh(); + requiredMods.ItemsRemoved += _ => scheduleRefresh(); + + refresh(); + } + + private ScheduledDelegate scheduledRefresh; + + private void scheduleRefresh() + { + scheduledRefresh?.Cancel(); + scheduledRefresh = Schedule(refresh); + } + + private void refresh() + { + difficultyIconContainer.Child = new DifficultyIcon(beatmap.Value, ruleset.Value) { Size = new Vector2(32) }; + + beatmapText.Clear(); + beatmapText.AddLink(item.Beatmap.ToString(), LinkAction.OpenBeatmap, item.Beatmap.Value.OnlineBeatmapID.ToString()); + + authorText.Clear(); + + if (item.Beatmap?.Value?.Metadata?.Author != null) + { + authorText.AddText("mapped by "); + authorText.AddUserLink(item.Beatmap.Value?.Metadata.Author); + } + + modDisplay.Current.Value = requiredMods.ToArray(); + } + + protected override Drawable CreateContent() => maskingContainer = new Container + { + RelativeSizeAxes = Axes.X, + Height = 50, + Masking = true, + CornerRadius = 10, + Children = new Drawable[] + { + new Box // A transparent box that forces the border to be drawn if the panel background is opaque + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + }, + new PanelBackground + { + RelativeSizeAxes = Axes.Both, + Beatmap = { BindTarget = beatmap } + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 8 }, + Spacing = new Vector2(8, 0), + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + difficultyIconContainer = new Container + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + beatmapText = new LinkFlowContainer { AutoSizeAxes = Axes.Both }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new Drawable[] + { + authorText = new LinkFlowContainer { AutoSizeAxes = Axes.Both }, + modDisplay = new ModDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Scale = new Vector2(0.4f), + DisplayUnrankedText = false, + ExpansionMode = ExpansionMode.AlwaysExpanded + } + } + } + } + } + } + }, + new Container + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + X = -18, + Children = new Drawable[] + { + new IconButton + { + Icon = FontAwesome.Solid.MinusSquare, + Alpha = allowEdit ? 1 : 0, + Action = () => RequestDeletion?.Invoke(Model), + }, + new PanelDownloadButton(item.Beatmap.Value.BeatmapSet) + { + Size = new Vector2(50, 30), + Alpha = allowEdit ? 0 : 1 + } + } + } + } + }; + + protected override bool OnClick(ClickEvent e) + { + if (allowSelection) + SelectedItem.Value = Model; + return true; + } + + // For now, this is the same implementation as in PanelBackground, but supports a beatmap info rather than a working beatmap + private class PanelBackground : Container // todo: should be a buffered container (https://github.com/ppy/osu-framework/issues/3222) + { + public readonly Bindable Beatmap = new Bindable(); + + public PanelBackground() + { + InternalChildren = new Drawable[] + { + new UpdateableBeatmapBackgroundSprite + { + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fill, + Beatmap = { BindTarget = Beatmap } + }, + new Container + { + Depth = -1, + RelativeSizeAxes = Axes.Both, + // This makes the gradient not be perfectly horizontal, but diagonal at a ~40° angle + Shear = new Vector2(0.8f, 0), + Alpha = 0.5f, + Children = new[] + { + // The left half with no gradient applied + new Box + { + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.Both, + Colour = Color4.Black, + Width = 0.4f, + }, + // Piecewise-linear gradient with 3 segments to make it appear smoother + new Box + { + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(Color4.Black, new Color4(0f, 0f, 0f, 0.9f)), + Width = 0.05f, + X = 0.4f, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.9f), new Color4(0f, 0f, 0f, 0.1f)), + Width = 0.2f, + X = 0.45f, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.1f), new Color4(0, 0, 0, 0)), + Width = 0.05f, + X = 0.65f, + }, + } + } + }; + } + } + } +} diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs index 063957d816..64618a1d85 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components { bool matchingFilter = true; - matchingFilter &= r.Room.Playlist.Count == 0 || r.Room.Playlist.Any(i => i.Ruleset.Equals(criteria.Ruleset)); + matchingFilter &= r.Room.Playlist.Count == 0 || r.Room.Playlist.Any(i => i.Ruleset.Value.Equals(criteria.Ruleset)); if (!string.IsNullOrEmpty(criteria.SearchString)) matchingFilter &= r.FilterTerms.Any(term => term.IndexOf(criteria.SearchString, StringComparison.InvariantCultureIgnoreCase) >= 0); diff --git a/osu.Game/Screens/Multi/Match/Components/Header.cs b/osu.Game/Screens/Multi/Match/Components/Header.cs index a52d43acf4..cf1eb6b6ed 100644 --- a/osu.Game/Screens/Multi/Match/Components/Header.cs +++ b/osu.Game/Screens/Multi/Match/Components/Header.cs @@ -2,6 +2,7 @@ // 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.Extensions.Color4Extensions; @@ -32,6 +33,7 @@ namespace osu.Game.Screens.Multi.Match.Components public Action RequestBeatmapSelection; private MatchBeatmapPanel beatmapPanel; + private ModDisplay modDisplay; public Header() { @@ -43,7 +45,6 @@ namespace osu.Game.Screens.Multi.Match.Components private void load(OsuColour colours) { BeatmapSelectButton beatmapButton; - ModDisplay modDisplay; InternalChildren = new Drawable[] { @@ -119,9 +120,12 @@ namespace osu.Game.Screens.Multi.Match.Components }, }; - CurrentItem.BindValueChanged(item => modDisplay.Current.Value = item.NewValue?.RequiredMods?.ToArray() ?? Array.Empty(), true); - beatmapButton.Action = () => RequestBeatmapSelection?.Invoke(); + + Playlist.ItemsAdded += _ => updateMods(); + Playlist.ItemsRemoved += _ => updateMods(); + + updateMods(); } protected override void LoadComplete() @@ -130,6 +134,13 @@ namespace osu.Game.Screens.Multi.Match.Components ShowBeatmapPanel.BindValueChanged(value => beatmapPanel.FadeTo(value.NewValue ? 1 : 0, 200, Easing.OutQuint), true); } + private void updateMods() + { + var item = Playlist.FirstOrDefault(); + + modDisplay.Current.Value = item?.RequiredMods?.ToArray() ?? Array.Empty(); + } + private class BeatmapSelectButton : HeaderButton { [Resolved(typeof(Room), nameof(Room.RoomID))] diff --git a/osu.Game/Screens/Multi/Match/Components/Info.cs b/osu.Game/Screens/Multi/Match/Components/Info.cs index 74f000c21f..a320b08cc4 100644 --- a/osu.Game/Screens/Multi/Match/Components/Info.cs +++ b/osu.Game/Screens/Multi/Match/Components/Info.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -18,6 +19,8 @@ namespace osu.Game.Screens.Multi.Match.Components { public Action OnStart; + private ReadyButton readyButton; + public Info() { RelativeSizeAxes = Axes.X; @@ -27,7 +30,6 @@ namespace osu.Game.Screens.Multi.Match.Components [BackgroundDependencyLoader] private void load() { - ReadyButton readyButton; HostInfo hostInfo; InternalChildren = new Drawable[] @@ -89,9 +91,17 @@ namespace osu.Game.Screens.Multi.Match.Components }, }; - CurrentItem.BindValueChanged(item => readyButton.Beatmap.Value = item.NewValue?.Beatmap, true); - hostInfo.Host.BindTo(Host); + + Playlist.ItemsAdded += _ => updateBeatmap(); + Playlist.ItemsRemoved += _ => updateBeatmap(); + + updateBeatmap(); + } + + private void updateBeatmap() + { + readyButton.Beatmap.Value = Playlist.FirstOrDefault()?.Beatmap.Value; } } } diff --git a/osu.Game/Screens/Multi/Match/Components/MatchBeatmapPanel.cs b/osu.Game/Screens/Multi/Match/Components/MatchBeatmapPanel.cs index 7c1fe91393..c8de066caa 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchBeatmapPanel.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchBeatmapPanel.cs @@ -1,10 +1,10 @@ // 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 System.Threading; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Overlays.Direct; @@ -32,10 +32,13 @@ namespace osu.Game.Screens.Multi.Match.Components [BackgroundDependencyLoader] private void load() { - CurrentItem.BindValueChanged(item => loadNewPanel(item.NewValue?.Beatmap), true); + Playlist.ItemsAdded += _ => loadNewPanel(); + Playlist.ItemsRemoved += _ => loadNewPanel(); + + loadNewPanel(); } - private void loadNewPanel(BeatmapInfo beatmap) + private void loadNewPanel() { loadCancellation?.Cancel(); request?.Cancel(); @@ -44,6 +47,8 @@ namespace osu.Game.Screens.Multi.Match.Components panel?.Expire(); panel = null; + var beatmap = Playlist.FirstOrDefault()?.Beatmap.Value; + if (beatmap?.OnlineBeatmapID == null) return; diff --git a/osu.Game/Screens/Multi/Match/Components/Participants.cs b/osu.Game/Screens/Multi/Match/Components/Participants.cs index ad38ec6a99..00d2f3e150 100644 --- a/osu.Game/Screens/Multi/Match/Components/Participants.cs +++ b/osu.Game/Screens/Multi/Match/Components/Participants.cs @@ -51,9 +51,9 @@ namespace osu.Game.Screens.Multi.Match.Components }, }; - Participants.BindValueChanged(participants => + Participants.ItemsAdded += users => { - usersFlow.Children = participants.NewValue.Select(u => + usersFlow.AddRange(users.Select(u => { var panel = new UserPanel(u) { @@ -65,8 +65,13 @@ namespace osu.Game.Screens.Multi.Match.Components panel.OnLoadComplete += d => d.FadeInFromZero(60); return panel; - }).ToList(); - }, true); + }).ToList()); + }; + + Participants.ItemsRemoved += users => + { + usersFlow.RemoveAll(p => users.Contains(p.User)); + }; } } } diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs index c2bb7da6b5..890664e99b 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -2,6 +2,7 @@ // 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.Graphics; @@ -40,9 +41,6 @@ namespace osu.Game.Screens.Multi.Match [Resolved(typeof(Room))] protected BindableList Playlist { get; private set; } - [Resolved(typeof(Room))] - protected Bindable CurrentItem { get; private set; } - [Resolved] private BeatmapManager beatmapManager { get; set; } @@ -52,6 +50,7 @@ namespace osu.Game.Screens.Multi.Match [Resolved(CanBeNull = true)] private OsuGame game { get; set; } + private readonly Bindable selectedItem = new Bindable(); private MatchLeaderboard leaderboard; public MatchSubScreen(Room room) @@ -165,7 +164,16 @@ namespace osu.Game.Screens.Multi.Match { base.LoadComplete(); - CurrentItem.BindValueChanged(currentItemChanged, true); + Playlist.ItemsAdded += _ => updateSelectedItem(); + Playlist.ItemsRemoved += _ => updateSelectedItem(); + + updateSelectedItem(); + } + + private void updateSelectedItem() + { + selectedItem.Value = Playlist.FirstOrDefault(); + currentItemChanged(); } public override bool OnExiting(IScreen next) @@ -180,16 +188,18 @@ namespace osu.Game.Screens.Multi.Match /// /// Handles propagation of the current playlist item's content to game-wide mechanisms. /// - private void currentItemChanged(ValueChangedEvent e) + private void currentItemChanged() { + var item = selectedItem.Value; + // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info - var localBeatmap = e.NewValue?.Beatmap == null ? null : beatmapManager.QueryBeatmap(b => b.OnlineBeatmapID == e.NewValue.Beatmap.OnlineBeatmapID); + var localBeatmap = item?.Beatmap == null ? null : beatmapManager.QueryBeatmap(b => b.OnlineBeatmapID == item.Beatmap.Value.OnlineBeatmapID); Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); - Mods.Value = e.NewValue?.RequiredMods?.ToArray() ?? Array.Empty(); + Mods.Value = item?.RequiredMods?.ToArray() ?? Array.Empty(); - if (e.NewValue?.Ruleset != null) - Ruleset.Value = e.NewValue.Ruleset; + if (item?.Ruleset != null) + Ruleset.Value = item.Ruleset.Value; previewTrackManager.StopAnyPlaying(this); } @@ -202,11 +212,11 @@ namespace osu.Game.Screens.Multi.Match if (Beatmap.Value != beatmapManager.DefaultBeatmap) return; - if (CurrentItem.Value == null) + if (selectedItem.Value == null) return; // Try to retrieve the corresponding local beatmap - var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineBeatmapID == CurrentItem.Value.Beatmap.OnlineBeatmapID); + var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineBeatmapID == selectedItem.Value.Beatmap.Value.OnlineBeatmapID); if (localBeatmap != null) Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); @@ -223,7 +233,7 @@ namespace osu.Game.Screens.Multi.Match { default: case GameTypeTimeshift _: - multiplayer?.Start(() => new TimeshiftPlayer(CurrentItem.Value) + multiplayer?.Start(() => new TimeshiftPlayer(selectedItem.Value) { Exited = () => leaderboard.RefreshScores() }); diff --git a/osu.Game/Screens/Multi/MultiplayerComposite.cs b/osu.Game/Screens/Multi/MultiplayerComposite.cs index 8c09d576ff..3f048eceab 100644 --- a/osu.Game/Screens/Multi/MultiplayerComposite.cs +++ b/osu.Game/Screens/Multi/MultiplayerComposite.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; @@ -32,10 +31,7 @@ namespace osu.Game.Screens.Multi protected BindableList Playlist { get; private set; } [Resolved(typeof(Room))] - protected Bindable CurrentItem { get; private set; } - - [Resolved(typeof(Room))] - protected Bindable> Participants { get; private set; } + protected BindableList Participants { get; private set; } [Resolved(typeof(Room))] protected Bindable ParticipantCount { get; private set; } diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs index 88c6fc5e2e..3afacf2f31 100644 --- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs +++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs @@ -50,10 +50,10 @@ namespace osu.Game.Screens.Multi.Play bool failed = false; // Sanity checks to ensure that TimeshiftPlayer matches the settings for the current PlaylistItem - if (Beatmap.Value.BeatmapInfo.OnlineBeatmapID != playlistItem.Beatmap.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.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))) diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index 00edd4db99..336b03544f 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.Play.HUD public bool DisplayUnrankedText = true; - public bool AllowExpand = true; + public ExpansionMode ExpansionMode = ExpansionMode.ExpandOnHover; private readonly Bindable> current = new Bindable>(); @@ -110,11 +110,15 @@ namespace osu.Game.Screens.Play.HUD private void expand() { - if (AllowExpand) + if (ExpansionMode != ExpansionMode.AlwaysContracted) IconsContainer.TransformSpacingTo(new Vector2(5, 0), 500, Easing.OutQuint); } - private void contract() => IconsContainer.TransformSpacingTo(new Vector2(-25, 0), 500, Easing.OutQuint); + private void contract() + { + if (ExpansionMode != ExpansionMode.AlwaysExpanded) + IconsContainer.TransformSpacingTo(new Vector2(-25, 0), 500, Easing.OutQuint); + } protected override bool OnHover(HoverEvent e) { @@ -128,4 +132,22 @@ namespace osu.Game.Screens.Play.HUD base.OnHoverLost(e); } } + + public enum ExpansionMode + { + /// + /// The will expand only when hovered. + /// + ExpandOnHover, + + /// + /// The will always be expanded. + /// + AlwaysExpanded, + + /// + /// The will always be contracted. + /// + AlwaysContracted + } } diff --git a/osu.Game/Screens/Select/FooterButtonMods.cs b/osu.Game/Screens/Select/FooterButtonMods.cs index 4f2369847f..2411cf26f9 100644 --- a/osu.Game/Screens/Select/FooterButtonMods.cs +++ b/osu.Game/Screens/Select/FooterButtonMods.cs @@ -91,7 +91,7 @@ namespace osu.Game.Screens.Select public FooterModDisplay() { - AllowExpand = false; + ExpansionMode = ExpansionMode.AlwaysContracted; IconsContainer.Margin = new MarginPadding(); } } diff --git a/osu.Game/Screens/Select/MatchSongSelect.cs b/osu.Game/Screens/Select/MatchSongSelect.cs index fd7a8a539c..826677ee30 100644 --- a/osu.Game/Screens/Select/MatchSongSelect.cs +++ b/osu.Game/Screens/Select/MatchSongSelect.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using Humanizer; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -21,11 +22,11 @@ namespace osu.Game.Screens.Select public string ShortTitle => "song selection"; public override string Title => ShortTitle.Humanize(); - [Resolved(typeof(Room))] - protected Bindable CurrentItem { get; private set; } - public override bool AllowEditing => false; + [Resolved(typeof(Room), nameof(Room.Playlist))] + protected BindableList Playlist { get; private set; } + [Resolved] private BeatmapManager beatmaps { get; set; } @@ -40,8 +41,8 @@ namespace osu.Game.Screens.Select { var item = new PlaylistItem { - Beatmap = Beatmap.Value.BeatmapInfo, - Ruleset = Ruleset.Value, + Beatmap = { Value = Beatmap.Value.BeatmapInfo }, + Ruleset = { Value = Ruleset.Value }, RulesetID = Ruleset.Value.ID ?? 0 }; @@ -60,11 +61,13 @@ namespace osu.Game.Screens.Select if (base.OnExiting(next)) return true; - if (CurrentItem.Value != null) + var firstItem = Playlist.FirstOrDefault(); + + if (firstItem != null) { - Ruleset.Value = CurrentItem.Value.Ruleset; - Beatmap.Value = beatmaps.GetWorkingBeatmap(CurrentItem.Value.Beatmap); - Mods.Value = CurrentItem.Value.RequiredMods?.ToArray() ?? Array.Empty(); + Ruleset.Value = firstItem.Ruleset.Value; + Beatmap.Value = beatmaps.GetWorkingBeatmap(firstItem.Beatmap.Value); + Mods.Value = firstItem.RequiredMods?.ToArray() ?? Array.Empty(); } return false; diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 41ab7fce99..b203557fab 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -191,9 +191,9 @@ namespace osu.Game.Tests.Visual track = audio?.Tracks.GetVirtual(length); } - protected override void Dispose(bool isDisposing) + ~ClockBackedTestWorkingBeatmap() { - base.Dispose(isDisposing); + // Remove the track store from the audio manager store?.Dispose(); } diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 6ddbc13a06..6f34466e94 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -26,11 +26,12 @@ namespace osu.Game.Users { public class UserPanel : OsuClickableContainer, IHasContextMenu { - private readonly User user; private const float height = 100; private const float content_padding = 10; private const float status_height = 30; + public readonly User User; + [Resolved(canBeNull: true)] private OsuColour colours { get; set; } @@ -54,7 +55,7 @@ namespace osu.Game.Users if (user == null) throw new ArgumentNullException(nameof(user)); - this.user = user; + User = user; Height = height - status_height; } @@ -86,7 +87,7 @@ namespace osu.Game.Users RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, - User = user, + User = User, }, 300, 5000) { RelativeSizeAxes = Axes.Both, @@ -106,7 +107,7 @@ namespace osu.Game.Users new UpdateableAvatar { Size = new Vector2(height - status_height - content_padding * 2), - User = user, + User = User, Masking = true, CornerRadius = 5, OpenOnClick = { Value = false }, @@ -125,7 +126,7 @@ namespace osu.Game.Users { new OsuSpriteText { - Text = user.Username, + Text = User.Username, Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 18, italics: true), }, infoContainer = new FillFlowContainer @@ -138,7 +139,7 @@ namespace osu.Game.Users Spacing = new Vector2(5f, 0f), Children = new Drawable[] { - new UpdateableFlag(user.Country) + new UpdateableFlag(User.Country) { Width = 30f, RelativeSizeAxes = Axes.Y, @@ -191,12 +192,12 @@ namespace osu.Game.Users } }); - if (user.IsSupporter) + if (User.IsSupporter) { infoContainer.Add(new SupporterIcon { Height = 20f, - SupportLevel = user.SupportLevel + SupportLevel = User.SupportLevel }); } @@ -206,7 +207,7 @@ namespace osu.Game.Users base.Action = ViewProfile = () => { Action?.Invoke(); - profile?.ShowUser(user); + profile?.ShowUser(User); }; } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 389fbe8210..50d8c25b11 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 3ed25360c5..e56fc41b07 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -74,7 +74,7 @@ - + @@ -82,7 +82,7 @@ - +