diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs index 15989dd47d..0e5e5b8aae 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs @@ -2,17 +2,20 @@ // 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.Testing; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect; using osu.Game.Tests.Visual.OnlinePlay; using osuTK; @@ -21,7 +24,7 @@ namespace osu.Game.Tests.Visual.Matchmaking { public partial class TestSceneBeatmapSelectGrid : OnlinePlayTestScene { - private MultiplayerPlaylistItem[] items = null!; + private MatchmakingPlaylistItem[] items = null!; private BeatmapSelectGrid grid = null!; @@ -36,24 +39,44 @@ namespace osu.Game.Tests.Visual.Matchmaking .Take(50) .ToArray(); + IEnumerable playlistItems; + if (beatmaps.Length > 0) { - items = Enumerable.Range(1, 50).Select(i => new MultiplayerPlaylistItem + playlistItems = Enumerable.Range(1, 50).Select(i => { - ID = i, - BeatmapID = beatmaps[i % beatmaps.Length].OnlineID, - StarRating = i / 10.0, - }).ToArray(); + var beatmap = beatmaps[i % beatmaps.Length]; + + return new MatchmakingPlaylistItem( + new MultiplayerPlaylistItem + { + ID = i, + BeatmapID = beatmap.OnlineID, + StarRating = i / 10.0, + }, + CreateAPIBeatmap(beatmap), + Array.Empty() + ); + }); } else { - items = Enumerable.Range(1, 50).Select(i => new MultiplayerPlaylistItem - { - ID = i, - BeatmapID = i, - StarRating = i / 10.0, - }).ToArray(); + playlistItems = Enumerable.Range(1, 50).Select(i => new MatchmakingPlaylistItem( + new MultiplayerPlaylistItem + { + ID = i, + BeatmapID = i, + StarRating = i / 10.0, + }, + CreateAPIBeatmap(), + Array.Empty() + )); } + + foreach (var item in playlistItems) + item.Beatmap.StarRating = item.PlaylistItem.StarRating; + + items = playlistItems.ToArray(); } public override void SetUpSteps() @@ -70,8 +93,7 @@ namespace osu.Game.Tests.Visual.Matchmaking AddStep("add items", () => { - foreach (var item in items) - grid.AddItem(item); + grid.AddItems(items); }); AddWaitStep("wait for panels", 3); @@ -85,17 +107,17 @@ namespace osu.Game.Tests.Visual.Matchmaking // test scene is weird. }); - AddStep("add selection 1", () => grid.ChildrenOfType().First().AddUser(new APIUser + AddStep("add selection 1", () => grid.ChildrenOfType().First().AddUser(new APIUser { Id = DummyAPIAccess.DUMMY_USER_ID, Username = "Maarvin", })); - AddStep("add selection 2", () => grid.ChildrenOfType().Skip(5).First().AddUser(new APIUser + AddStep("add selection 2", () => grid.ChildrenOfType().Skip(5).First().AddUser(new APIUser { Id = 2, Username = "peppy", })); - AddStep("add selection 3", () => grid.ChildrenOfType().Skip(10).First().AddUser(new APIUser + AddStep("add selection 3", () => grid.ChildrenOfType().Skip(10).First().AddUser(new APIUser { Id = 1040328, Username = "smoogipoo", @@ -180,7 +202,7 @@ namespace osu.Game.Tests.Visual.Matchmaking AddStep("display roll order", () => { - var panels = grid.ChildrenOfType().ToArray(); + var panels = grid.ChildrenOfType().ToArray(); for (int i = 0; i < panels.Length; i++) { @@ -211,7 +233,7 @@ namespace osu.Game.Tests.Visual.Matchmaking AddWaitStep("wait for animation", 5); - AddStep("reveal beatmap", () => grid.RevealRandomItem(new MultiplayerPlaylistItem())); + AddStep("reveal beatmap", () => grid.RevealRandomItem(items[RNG.Next(items.Length)].PlaylistItem)); } private (long[] candidateItems, long finalItem) pickRandomItems(int count) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs index 023b9b9743..9ac64288ed 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs @@ -1,14 +1,12 @@ // 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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics.Cursor; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -44,14 +42,14 @@ namespace osu.Game.Tests.Visual.Matchmaking [Test] public void TestBeatmapPanel() { - BeatmapSelectPanel? panel = null; + MatchmakingSelectPanel? panel = null; AddStep("add panel", () => { Child = new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, - Child = panel = new BeatmapSelectPanel(new MultiplayerPlaylistItem()) + Child = panel = new MatchmakingSelectPanelBeatmap(new MatchmakingPlaylistItem(new MultiplayerPlaylistItem(), CreateAPIBeatmap(), [])) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -81,53 +79,17 @@ namespace osu.Game.Tests.Visual.Matchmaking AddToggleStep("allow selection", value => panel!.AllowSelection = value); } - [Test] - public void TestFailedBeatmapLookup() - { - AddStep("setup request handle", () => - { - var api = (DummyAPIAccess)API; - var handler = api.HandleRequest; - api.HandleRequest = req => - { - switch (req) - { - case GetBeatmapRequest: - case GetBeatmapsRequest: - req.TriggerFailure(new InvalidOperationException()); - return false; - - default: - return handler?.Invoke(req) ?? false; - } - }; - }); - - AddStep("add panel", () => - { - Child = new OsuContextMenuContainer - { - RelativeSizeAxes = Axes.Both, - Child = new BeatmapSelectPanel(new MultiplayerPlaylistItem()) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - } - }; - }); - } - [Test] public void TestRandomPanel() { - BeatmapSelectPanel? panel = null; + MatchmakingSelectPanelRandom? panel = null; AddStep("add panel", () => { Child = new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, - Child = panel = new BeatmapSelectPanel(new MultiplayerPlaylistItem { ID = -1 }) + Child = panel = new MatchmakingSelectPanelRandom(new MultiplayerPlaylistItem { ID = -1 }) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -137,7 +99,7 @@ namespace osu.Game.Tests.Visual.Matchmaking AddToggleStep("allow selection", value => panel!.AllowSelection = value); - AddStep("reveal beatmap", () => panel!.DisplayItem(new MultiplayerPlaylistItem())); + AddStep("reveal beatmap", () => panel!.RevealBeatmap(CreateAPIBeatmap(), [])); } [Test] @@ -145,15 +107,12 @@ namespace osu.Game.Tests.Visual.Matchmaking { AddStep("add panel", () => { - BeatmapSelectPanel? panel; + MatchmakingSelectPanel? panel; Child = new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, - Child = panel = new BeatmapSelectPanel(new MultiplayerPlaylistItem - { - RequiredMods = [new APIMod(new OsuModHardRock()), new APIMod(new OsuModDoubleTime())] - }) + Child = panel = new MatchmakingSelectPanelBeatmap(new MatchmakingPlaylistItem(new MultiplayerPlaylistItem(), CreateAPIBeatmap(), [new OsuModHardRock(), new OsuModDoubleTime()])) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs deleted file mode 100644 index 96eb9dd0da..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs +++ /dev/null @@ -1,119 +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 System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Database; -using osu.Game.Graphics.Containers; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Rooms; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect -{ - public partial class BeatmapCardMatchmaking : OsuClickableContainer - { - public const float WIDTH = 345; - public const float HEIGHT = 80; - - [Resolved] - private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; - - [Resolved] - private RulesetStore rulesetStore { get; set; } = null!; - - private readonly List users = new List(); - - private Container contentContainer = null!; - private Drawable flashLayer = null!; - private BeatmapCardMatchmakingContent? content; - - public BeatmapCardMatchmaking() - { - Width = WIDTH; - Height = HEIGHT; - } - - [BackgroundDependencyLoader] - private void load() - { - Children = new[] - { - contentContainer = new Container - { - RelativeSizeAxes = Axes.Both - }, - flashLayer = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0 - } - }; - } - - public void AddUser(APIUser user) - { - users.Add(user); - content?.SelectionOverlay.AddUser(user); - } - - public void RemoveUser(APIUser user) - { - users.Remove(user); - content?.SelectionOverlay.RemoveUser(user.Id); - } - - public void DisplayItem(MultiplayerPlaylistItem item) - { - Ruleset? ruleset = rulesetStore.GetRuleset(item.RulesetID)?.CreateInstance(); - - if (ruleset == null) - return; - - Mod[] mods = item.RequiredMods.Select(m => m.ToMod(ruleset)).ToArray(); - - Task.Run(loadBeatmap); - - async Task loadBeatmap() - { - APIBeatmap? beatmap = await beatmapLookupCache.GetBeatmapAsync(item.BeatmapID).ConfigureAwait(false); - - beatmap ??= new APIBeatmap - { - BeatmapSet = new APIBeatmapSet - { - Title = "unknown beatmap", - TitleUnicode = "unknown beatmap", - Artist = "unknown artist", - ArtistUnicode = "unknown artist", - } - }; - - beatmap.StarRating = item.StarRating; - - loadContent(new BeatmapCardMatchmakingBeatmapContent(beatmap, mods)); - } - } - - public void DisplayRandom() => loadContent(new BeatmapCardMatchmakingRandomContent()); - - private void loadContent(BeatmapCardMatchmakingContent newContent) => Schedule(() => - { - bool flashNewContent = content != null; - - contentContainer.Child = content = newContent; - - foreach (var user in users) - newContent.SelectionOverlay.AddUser(user); - - if (flashNewContent) - flashLayer.FadeOutFromOne(1000, Easing.In); - }); - } -} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingBeatmapContent.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingBeatmapContent.cs deleted file mode 100644 index e6a2dfb055..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingBeatmapContent.cs +++ /dev/null @@ -1,378 +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 System.Collections.Generic; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Extensions.LocalisationExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input.Events; -using osu.Framework.Localisation; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Drawables; -using osu.Game.Beatmaps.Drawables.Cards; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Localisation; -using osu.Game.Online; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Overlays; -using osu.Game.Overlays.BeatmapSet; -using osu.Game.Resources.Localisation.Web; -using osu.Game.Rulesets.Mods; -using osu.Game.Screens.Play.HUD; -using osuTK; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect -{ - public partial class BeatmapCardMatchmakingBeatmapContent : BeatmapCardMatchmakingContent, IHasContextMenu - { - public override AvatarOverlay SelectionOverlay => selectionOverlay; - - [Resolved] - private OverlayColourProvider colourProvider { get; set; } = null!; - - [Resolved] - private BeatmapSetOverlay? beatmapSetOverlay { get; set; } - - private readonly IBindable downloadState = new Bindable(); - private readonly IBindableNumber downloadProgress = new BindableDouble(); - private readonly Bindable favouriteState = new Bindable(); - private readonly APIBeatmapSet beatmapSet; - private readonly APIBeatmap beatmap; - private readonly Mod[] mods; - - private BeatmapCardThumbnail thumbnail = null!; - private CollapsibleButtonContainer buttonContainer = null!; - private FillFlowContainer idleBottomContent = null!; - private BeatmapCardDownloadProgressBar downloadProgressBar = null!; - private AvatarOverlay selectionOverlay = null!; - - public BeatmapCardMatchmakingBeatmapContent(APIBeatmap beatmap, Mod[] mods) - { - this.beatmap = beatmap; - this.mods = mods; - - beatmapSet = beatmap.BeatmapSet!; - favouriteState.Value = new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - FillFlowContainer leftIconArea; - FillFlowContainer titleBadgeArea; - GridContainer artistContainer; - - InternalChildren = new Drawable[] - { - new BeatmapDownloadTracker(beatmap.BeatmapSet!) - { - State = { BindTarget = downloadState }, - Progress = { BindTarget = downloadProgress }, - }, - thumbnail = new BeatmapCardThumbnail(beatmapSet, beatmapSet, keepLoaded: true) - { - Name = @"Left (icon) area", - Size = new Vector2(BeatmapCardMatchmaking.HEIGHT), - Padding = new MarginPadding { Right = BeatmapCard.CORNER_RADIUS }, - Child = leftIconArea = new FillFlowContainer - { - Margin = new MarginPadding(4), - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(1) - } - }, - buttonContainer = new CollapsibleButtonContainer(beatmapSet, allowNavigationToBeatmap: false, keepBackgroundLoaded: true) - { - X = BeatmapCardMatchmaking.HEIGHT - BeatmapCard.CORNER_RADIUS, - Width = BeatmapCard.WIDTH - BeatmapCardMatchmaking.HEIGHT + BeatmapCard.CORNER_RADIUS, - FavouriteState = { BindTarget = favouriteState }, - ButtonsCollapsedWidth = 0, - ButtonsExpandedWidth = 24, - Children = new Drawable[] - { - new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new GridContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - ColumnDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize) - }, - Content = new[] - { - new Drawable[] - { - new TruncatingSpriteText - { - Text = new RomanisableString(beatmapSet.TitleUnicode, beatmapSet.Title), - Font = OsuFont.Default.With(size: 18f, weight: FontWeight.SemiBold), - RelativeSizeAxes = Axes.X, - }, - titleBadgeArea = new FillFlowContainer - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - } - } - } - }, - artistContainer = new GridContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - ColumnDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.AutoSize) - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize) - }, - Content = new[] - { - new[] - { - new TruncatingSpriteText - { - Text = BeatmapsetsStrings.ShowDetailsByArtist(new RomanisableString(beatmapSet.ArtistUnicode, beatmapSet.Artist)), - Font = OsuFont.Default.With(size: 14f, weight: FontWeight.SemiBold), - RelativeSizeAxes = Axes.X, - }, - Empty() - }, - } - }, - new LinkFlowContainer(s => - { - s.Shadow = false; - s.Font = OsuFont.GetFont(size: 11f, weight: FontWeight.SemiBold); - }).With(d => - { - d.AutoSizeAxes = Axes.Both; - d.Margin = new MarginPadding { Top = 1 }; - d.AddText("mapped by ", t => t.Colour = colourProvider.Content2); - d.AddUserLink(beatmapSet.Author); - }), - } - }, - new Container - { - Name = @"Bottom content", - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Children = new Drawable[] - { - idleBottomContent = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 2), - AlwaysPresent = true, - Children = new Drawable[] - { - new GridContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - ColumnDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.AutoSize) - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize) - }, - Content = new[] - { - new Drawable[] - { - new Container - { - Masking = true, - CornerRadius = BeatmapCard.CORNER_RADIUS, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] - { - new Box - { - Colour = colours.ForStarDifficulty(beatmap.StarRating).Darken(0.8f), - RelativeSizeAxes = Axes.Both, - }, - new FillFlowContainer - { - Padding = new MarginPadding(4), - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(6, 0), - Children = new Drawable[] - { - new StarRatingDisplay(new StarDifficulty(beatmap.StarRating, 0), StarRatingDisplaySize.Small, animated: true) - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Scale = new Vector2(0.9f), - }, - new TruncatingSpriteText - { - Text = beatmap.DifficultyName, - Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - } - }, - } - }, - new ModFlowDisplay - { - AutoSizeAxes = Axes.Both, - Scale = new Vector2(0.5f), - Margin = new MarginPadding { Left = 5 }, - Current = { Value = mods } - }, - }, - } - }, - } - }, - downloadProgressBar = new BeatmapCardDownloadProgressBar - { - RelativeSizeAxes = Axes.X, - Height = 5, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - State = { BindTarget = downloadState }, - Progress = { BindTarget = downloadProgress } - } - } - }, - selectionOverlay = new AvatarOverlay - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - } - } - } - }; - - if (beatmapSet.HasVideo) - leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) }); - - if (beatmapSet.HasStoryboard) - leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) }); - - if (beatmapSet.FeaturedInSpotlight) - { - titleBadgeArea.Add(new SpotlightBeatmapBadge - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Margin = new MarginPadding { Left = 4 } - }); - } - - if (beatmapSet.HasExplicitContent) - { - titleBadgeArea.Add(new ExplicitContentBeatmapBadge - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Margin = new MarginPadding { Left = 4 } - }); - } - - if (beatmapSet.TrackId != null) - { - artistContainer.Content[0][1] = new FeaturedArtistBeatmapBadge - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Margin = new MarginPadding { Left = 4 } - }; - } - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - downloadState.BindValueChanged(_ => updateState(), true); - - FinishTransforms(true); - } - - protected override bool OnHover(HoverEvent e) - { - updateState(); - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - updateState(); - base.OnHoverLost(e); - } - - private void updateState() - { - bool showDetails = IsHovered; - - buttonContainer.ShowDetails.Value = showDetails; - thumbnail.Dimmed.Value = showDetails; - - bool showProgress = downloadState.Value == DownloadState.Downloading || downloadState.Value == DownloadState.Importing; - - idleBottomContent.FadeTo(showProgress ? 0 : 1, 340, Easing.OutQuint); - downloadProgressBar.FadeTo(showProgress ? 1 : 0, 340, Easing.OutQuint); - } - - public MenuItem[] ContextMenuItems - { - get - { - List items = new List - { - new OsuMenuItem(ContextMenuStrings.ViewBeatmap, MenuItemType.Highlighted, () => beatmapSetOverlay?.FetchAndShowBeatmap(beatmap.OnlineID)) - }; - - foreach (var button in buttonContainer.Buttons) - { - if (button.Enabled.Value) - items.Add(new OsuMenuItem(button.TooltipText.ToSentence(), MenuItemType.Standard, () => button.TriggerClick())); - } - - return items.ToArray(); - } - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingContent.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingContent.cs deleted file mode 100644 index 8314174a4c..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingContent.cs +++ /dev/null @@ -1,153 +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 System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Sample; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests.Responses; -using osuTK; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect -{ - public abstract partial class BeatmapCardMatchmakingContent : CompositeDrawable - { - public abstract AvatarOverlay SelectionOverlay { get; } - - protected BeatmapCardMatchmakingContent() - { - RelativeSizeAxes = Axes.Both; - } - - public partial class AvatarOverlay : CompositeDrawable - { - private readonly Container avatars; - - private Sample? userAddedSample; - private double? lastSamplePlayback; - - [Resolved] - private IAPIProvider api { get; set; } = null!; - - public AvatarOverlay() - { - AutoSizeAxes = Axes.Both; - - InternalChild = avatars = new Container - { - AutoSizeAxes = Axes.X, - Height = SelectionAvatar.AVATAR_SIZE, - }; - - Padding = new MarginPadding { Vertical = 5 }; - } - - [BackgroundDependencyLoader] - private void load(AudioManager audio) - { - userAddedSample = audio.Samples.Get(@"Multiplayer/player-ready"); - } - - public bool AddUser(APIUser user) - { - if (avatars.Any(a => a.User.Id == user.Id)) - return false; - - var avatar = new SelectionAvatar(user, user.Equals(api.LocalUser.Value)); - - avatars.Add(avatar); - - if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME) - { - userAddedSample?.Play(); - lastSamplePlayback = Time.Current; - } - - updateAvatarLayout(); - - avatar.FinishTransforms(); - - return true; - } - - public bool RemoveUser(int id) - { - if (avatars.SingleOrDefault(a => a.User.Id == id) is not SelectionAvatar avatar) - return false; - - avatar.PopOutAndExpire(); - avatars.ChangeChildDepth(avatar, float.MaxValue); - - updateAvatarLayout(); - - return true; - } - - private void updateAvatarLayout() - { - const double stagger = 30; - const float spacing = 4; - - double delay = 0; - float x = 0; - - for (int i = avatars.Count - 1; i >= 0; i--) - { - var avatar = avatars[i]; - - if (avatar.Expired) - continue; - - avatar.Delay(delay).MoveToX(x, 500, Easing.OutElasticQuarter); - - x -= avatar.LayoutSize.X + spacing; - - delay += stagger; - } - } - - public partial class SelectionAvatar : CompositeDrawable - { - public const float AVATAR_SIZE = 30; - - public APIUser User { get; } - - public bool Expired { get; private set; } - - private readonly MatchmakingAvatar avatar; - - public SelectionAvatar(APIUser user, bool isOwnUser) - { - User = user; - Size = new Vector2(AVATAR_SIZE); - - InternalChild = avatar = new MatchmakingAvatar(user, isOwnUser) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - avatar.ScaleTo(0) - .ScaleTo(1, 500, Easing.OutElasticHalf) - .FadeIn(200); - } - - public void PopOutAndExpire() - { - avatar.ScaleTo(0, 400, Easing.OutExpo); - - this.FadeOut(100).Expire(); - Expired = true; - } - } - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingRandomContent.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingRandomContent.cs deleted file mode 100644 index 515456abe1..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmakingRandomContent.cs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics.Sprites; -using osu.Game.Overlays; -using osuTK; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect -{ - public partial class BeatmapCardMatchmakingRandomContent : BeatmapCardMatchmakingContent - { - public override AvatarOverlay SelectionOverlay => selectionOverlay; - - [Resolved] - private OverlayColourProvider colourProvider { get; set; } = null!; - - private AvatarOverlay selectionOverlay = null!; - - [BackgroundDependencyLoader] - private void load() - { - InternalChildren = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background2, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding - { - Horizontal = 10, - Vertical = 4 - }, - Children = new Drawable[] - { - new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Children = - [ - new SpriteIcon - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Size = new Vector2(32), - Icon = FontAwesome.Solid.Random, - }, - new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Text = "Random", - } - ] - }, - selectionOverlay = new AvatarOverlay - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - } - } - } - }; - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs index 967e2777b7..d27b0e3818 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; using Microsoft.Toolkit.HighPerformance; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -33,10 +34,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect public event Action? ItemSelected; - private readonly Dictionary panelLookup = new Dictionary(); + private readonly Dictionary panelLookup = new Dictionary(); + private readonly Dictionary playlistItems = new Dictionary(); + private MatchmakingSelectPanelRandom randomPanel = null!; private readonly PanelGridContainer panelGridContainer; - private readonly Container rollContainer; + private readonly Container rollContainer; private readonly OsuScrollContainer scroll; private bool allowSelection = true; @@ -64,15 +67,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect Spacing = new Vector2(panel_spacing) }, }, - rollContainer = new Container + rollContainer = new Container { RelativeSizeAxes = Axes.Both, Masking = true, }, }; - - // Special item denoting a random selection. - AddItem(new MultiplayerPlaylistItem { ID = -1 }); } [BackgroundDependencyLoader] @@ -86,9 +86,33 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect swooshSample = audio.Samples.Get(@"SongSelect/options-pop-out"); } - protected override void LoadComplete() + public void AddItems(IEnumerable items) { - base.LoadComplete(); + foreach (var item in items) + { + playlistItems[item.ID] = item; + + var panel = panelLookup[item.ID] = new MatchmakingSelectPanelBeatmap(item) + { + AllowSelection = allowSelection, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Action = i => ItemSelected?.Invoke(i), + }; + + panelGridContainer.Add(panel); + panelGridContainer.SetLayoutPosition(panel, (float)panel.Item.StarRating); + } + + panelLookup[-1] = randomPanel = new MatchmakingSelectPanelRandom(new MultiplayerPlaylistItem { ID = -1 }) + { + AllowSelection = allowSelection, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Action = i => ItemSelected?.Invoke(i), + }; + panelGridContainer.Add(randomPanel); + panelGridContainer.SetLayoutPosition(randomPanel, float.MinValue); const double enter_duration = 500; @@ -104,24 +128,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect panel.FadeInAndEnterFromBelow(duration: enter_duration, delay: delay); } + + panelsLoaded.SetResult(); }); } - public void AddItem(MultiplayerPlaylistItem item) - { - var panel = panelLookup[item.ID] = new BeatmapSelectPanel(item) - { - AllowSelection = allowSelection, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Action = i => ItemSelected?.Invoke(i), - }; - - panelGridContainer.Add(panel); - panelGridContainer.SetLayoutPosition(panel, (float)item.StarRating); - } - - public void SetUserSelection(APIUser user, long itemId, bool selected) + public void SetUserSelection(APIUser user, long itemId, bool selected) => whenPanelsLoaded(() => { if (!panelLookup.TryGetValue(itemId, out var panel)) return; @@ -130,18 +142,19 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect panel.AddUser(user); else panel.RemoveUser(user); - } + }); - public void RevealRandomItem(MultiplayerPlaylistItem item) + public void RevealRandomItem(MultiplayerPlaylistItem item) => whenPanelsLoaded(() => { - if (!panelLookup.TryGetValue(-1, out var panel)) - return; + playlistItems.TryGetValue(item.ID, out var playlistItem); + + Debug.Assert(playlistItem != null); - panel.DisplayItem(item); randomRevealSample?.Play(); - } + randomPanel.RevealBeatmap(playlistItem.Beatmap, playlistItem.Mods); + }); - public void RollAndDisplayFinalBeatmap(long[] candidateItemIds, long finalItemId) + public void RollAndDisplayFinalBeatmap(long[] candidateItemIds, long finalItemId) => whenPanelsLoaded(() => { Debug.Assert(candidateItemIds.Length >= 1); Debug.Assert(candidateItemIds.Contains(finalItemId)); @@ -168,7 +181,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect .Delay(roll_duration + present_beatmap_delay) .Schedule(() => PresentRolledBeatmap(finalItemId)); } - } + }); internal void TransferCandidatePanelsToRollContainer(long[] candidateItemIds, double duration = hide_duration) { @@ -177,7 +190,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect var rng = new Random(); - var remainingPanels = new List(); + var remainingPanels = new List(); foreach (var panel in panelGridContainer.Children.ToArray()) { @@ -217,7 +230,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { var panel = rollContainer.Children[i]; - var position = positions[i] * (BeatmapSelectPanel.SIZE + new Vector2(panel_spacing)); + var position = positions[i] * (MatchmakingSelectPanel.SIZE + new Vector2(panel_spacing)); panel.MoveTo(position, duration + stagger * i, new SplitEasingFunction(Easing.InCubic, Easing.OutExpo, 0.3f)); @@ -286,7 +299,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect while ((numSteps - 1) % rollContainer.Children.Count != finalItemIndex) numSteps++; - BeatmapSelectPanel? lastPanel = null; + MatchmakingSelectPanel? lastPanel = null; for (int i = 0; i < numSteps; i++) { @@ -347,7 +360,15 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect PresentRolledBeatmap(finalItem); } - private partial class PanelGridContainer : FillFlowContainer + private readonly TaskCompletionSource panelsLoaded = new TaskCompletionSource(); + + private void whenPanelsLoaded(Action action) => Task.Run(async () => + { + await panelsLoaded.Task.ConfigureAwait(false); + Schedule(action); + }); + + private partial class PanelGridContainer : FillFlowContainer { public bool LayoutDisabled; diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingPlaylistItem.cs new file mode 100644 index 0000000000..6b7fb9f21e --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingPlaylistItem.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect +{ + public record MatchmakingPlaylistItem(MultiplayerPlaylistItem PlaylistItem, APIBeatmap Beatmap, Mod[] Mods) + { + public long ID => PlaylistItem.ID; + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContent.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContent.cs new file mode 100644 index 0000000000..48c64f2f66 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContent.cs @@ -0,0 +1,156 @@ +// 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.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect +{ + public partial class MatchmakingSelectPanel + { + public abstract partial class CardContent : CompositeDrawable + { + public abstract AvatarOverlay SelectionOverlay { get; } + + protected CardContent() + { + RelativeSizeAxes = Axes.Both; + } + + public partial class AvatarOverlay : CompositeDrawable + { + private readonly Container avatars; + + private Sample? userAddedSample; + private double? lastSamplePlayback; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + public AvatarOverlay() + { + AutoSizeAxes = Axes.Both; + + InternalChild = avatars = new Container + { + AutoSizeAxes = Axes.X, + Height = SelectionAvatar.AVATAR_SIZE, + }; + + Padding = new MarginPadding { Vertical = 5 }; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + userAddedSample = audio.Samples.Get(@"Multiplayer/player-ready"); + } + + public bool AddUser(APIUser user) + { + if (avatars.Any(a => a.User.Id == user.Id)) + return false; + + var avatar = new SelectionAvatar(user, user.Equals(api.LocalUser.Value)); + + avatars.Add(avatar); + + if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME) + { + userAddedSample?.Play(); + lastSamplePlayback = Time.Current; + } + + updateAvatarLayout(); + + avatar.FinishTransforms(); + + return true; + } + + public bool RemoveUser(int id) + { + if (avatars.SingleOrDefault(a => a.User.Id == id) is not SelectionAvatar avatar) + return false; + + avatar.PopOutAndExpire(); + avatars.ChangeChildDepth(avatar, float.MaxValue); + + updateAvatarLayout(); + + return true; + } + + private void updateAvatarLayout() + { + const double stagger = 30; + const float spacing = 4; + + double delay = 0; + float x = 0; + + for (int i = avatars.Count - 1; i >= 0; i--) + { + var avatar = avatars[i]; + + if (avatar.Expired) + continue; + + avatar.Delay(delay).MoveToX(x, 500, Easing.OutElasticQuarter); + + x -= avatar.LayoutSize.X + spacing; + + delay += stagger; + } + } + + public partial class SelectionAvatar : CompositeDrawable + { + public const float AVATAR_SIZE = 30; + + public APIUser User { get; } + + public bool Expired { get; private set; } + + private readonly MatchmakingAvatar avatar; + + public SelectionAvatar(APIUser user, bool isOwnUser) + { + User = user; + Size = new Vector2(AVATAR_SIZE); + + InternalChild = avatar = new MatchmakingAvatar(user, isOwnUser) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + avatar.ScaleTo(0) + .ScaleTo(1, 500, Easing.OutElasticHalf) + .FadeIn(200); + } + + public void PopOutAndExpire() + { + avatar.ScaleTo(0, 400, Easing.OutExpo); + + this.FadeOut(100).Expire(); + Expired = true; + } + } + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentBeatmap.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentBeatmap.cs new file mode 100644 index 0000000000..b27ab2850b --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentBeatmap.cs @@ -0,0 +1,381 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; +using osu.Game.Online; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Overlays.BeatmapSet; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Play.HUD; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect +{ + public partial class MatchmakingSelectPanel + { + public partial class CardContentBeatmap : CardContent, IHasContextMenu + { + public override AvatarOverlay SelectionOverlay => selectionOverlay; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [Resolved] + private BeatmapSetOverlay? beatmapSetOverlay { get; set; } + + private readonly IBindable downloadState = new Bindable(); + private readonly IBindableNumber downloadProgress = new BindableDouble(); + private readonly Bindable favouriteState = new Bindable(); + private readonly APIBeatmapSet beatmapSet; + private readonly APIBeatmap beatmap; + private readonly Mod[] mods; + + private BeatmapCardThumbnail thumbnail = null!; + private CollapsibleButtonContainer buttonContainer = null!; + private FillFlowContainer idleBottomContent = null!; + private BeatmapCardDownloadProgressBar downloadProgressBar = null!; + private AvatarOverlay selectionOverlay = null!; + + public CardContentBeatmap(APIBeatmap beatmap, Mod[] mods) + { + this.beatmap = beatmap; + this.mods = mods; + + beatmapSet = beatmap.BeatmapSet!; + favouriteState.Value = new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + FillFlowContainer leftIconArea; + FillFlowContainer titleBadgeArea; + GridContainer artistContainer; + + InternalChildren = new Drawable[] + { + new BeatmapDownloadTracker(beatmap.BeatmapSet!) + { + State = { BindTarget = downloadState }, + Progress = { BindTarget = downloadProgress }, + }, + thumbnail = new BeatmapCardThumbnail(beatmapSet, beatmapSet, keepLoaded: true) + { + Name = @"Left (icon) area", + Size = new Vector2(MatchmakingSelectPanel.HEIGHT), + Padding = new MarginPadding { Right = BeatmapCard.CORNER_RADIUS }, + Child = leftIconArea = new FillFlowContainer + { + Margin = new MarginPadding(4), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(1) + } + }, + buttonContainer = new CollapsibleButtonContainer(beatmapSet, allowNavigationToBeatmap: false, keepBackgroundLoaded: true) + { + X = MatchmakingSelectPanel.HEIGHT - BeatmapCard.CORNER_RADIUS, + Width = BeatmapCard.WIDTH - MatchmakingSelectPanel.HEIGHT + BeatmapCard.CORNER_RADIUS, + FavouriteState = { BindTarget = favouriteState }, + ButtonsCollapsedWidth = 0, + ButtonsExpandedWidth = 24, + Children = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + new TruncatingSpriteText + { + Text = new RomanisableString(beatmapSet.TitleUnicode, beatmapSet.Title), + Font = OsuFont.Default.With(size: 18f, weight: FontWeight.SemiBold), + RelativeSizeAxes = Axes.X, + }, + titleBadgeArea = new FillFlowContainer + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + } + } + } + }, + artistContainer = new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new[] + { + new TruncatingSpriteText + { + Text = BeatmapsetsStrings.ShowDetailsByArtist(new RomanisableString(beatmapSet.ArtistUnicode, beatmapSet.Artist)), + Font = OsuFont.Default.With(size: 14f, weight: FontWeight.SemiBold), + RelativeSizeAxes = Axes.X, + }, + Empty() + }, + } + }, + new LinkFlowContainer(s => + { + s.Shadow = false; + s.Font = OsuFont.GetFont(size: 11f, weight: FontWeight.SemiBold); + }).With(d => + { + d.AutoSizeAxes = Axes.Both; + d.Margin = new MarginPadding { Top = 1 }; + d.AddText("mapped by ", t => t.Colour = colourProvider.Content2); + d.AddUserLink(beatmapSet.Author); + }), + } + }, + new Container + { + Name = @"Bottom content", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Children = new Drawable[] + { + idleBottomContent = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 2), + AlwaysPresent = true, + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + new Container + { + Masking = true, + CornerRadius = BeatmapCard.CORNER_RADIUS, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new Box + { + Colour = colours.ForStarDifficulty(beatmap.StarRating).Darken(0.8f), + RelativeSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + Padding = new MarginPadding(4), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(6, 0), + Children = new Drawable[] + { + new StarRatingDisplay(new StarDifficulty(beatmap.StarRating, 0), StarRatingDisplaySize.Small, animated: true) + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Scale = new Vector2(0.9f), + }, + new TruncatingSpriteText + { + Text = beatmap.DifficultyName, + Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + } + }, + } + }, + new ModFlowDisplay + { + AutoSizeAxes = Axes.Both, + Scale = new Vector2(0.5f), + Margin = new MarginPadding { Left = 5 }, + Current = { Value = mods } + }, + }, + } + }, + } + }, + downloadProgressBar = new BeatmapCardDownloadProgressBar + { + RelativeSizeAxes = Axes.X, + Height = 5, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + State = { BindTarget = downloadState }, + Progress = { BindTarget = downloadProgress } + } + } + }, + selectionOverlay = new AvatarOverlay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + } + } + } + }; + + if (beatmapSet.HasVideo) + leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) }); + + if (beatmapSet.HasStoryboard) + leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) }); + + if (beatmapSet.FeaturedInSpotlight) + { + titleBadgeArea.Add(new SpotlightBeatmapBadge + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Margin = new MarginPadding { Left = 4 } + }); + } + + if (beatmapSet.HasExplicitContent) + { + titleBadgeArea.Add(new ExplicitContentBeatmapBadge + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Margin = new MarginPadding { Left = 4 } + }); + } + + if (beatmapSet.TrackId != null) + { + artistContainer.Content[0][1] = new FeaturedArtistBeatmapBadge + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Margin = new MarginPadding { Left = 4 } + }; + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + downloadState.BindValueChanged(_ => updateState(), true); + + FinishTransforms(true); + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateState(); + base.OnHoverLost(e); + } + + private void updateState() + { + bool showDetails = IsHovered; + + buttonContainer.ShowDetails.Value = showDetails; + thumbnail.Dimmed.Value = showDetails; + + bool showProgress = downloadState.Value == DownloadState.Downloading || downloadState.Value == DownloadState.Importing; + + idleBottomContent.FadeTo(showProgress ? 0 : 1, 340, Easing.OutQuint); + downloadProgressBar.FadeTo(showProgress ? 1 : 0, 340, Easing.OutQuint); + } + + public MenuItem[] ContextMenuItems + { + get + { + List items = new List + { + new OsuMenuItem(ContextMenuStrings.ViewBeatmap, MenuItemType.Highlighted, () => beatmapSetOverlay?.FetchAndShowBeatmap(beatmap.OnlineID)) + }; + + foreach (var button in buttonContainer.Buttons) + { + if (button.Enabled.Value) + items.Add(new OsuMenuItem(button.TooltipText.ToSentence(), MenuItemType.Standard, () => button.TriggerClick())); + } + + return items.ToArray(); + } + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentRandom.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentRandom.cs new file mode 100644 index 0000000000..24422de1b5 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentRandom.cs @@ -0,0 +1,80 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect +{ + public partial class MatchmakingSelectPanel + { + public partial class CardContentRandom : CardContent + { + public override AvatarOverlay SelectionOverlay => selectionOverlay; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + private AvatarOverlay selectionOverlay = null!; + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background2, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Horizontal = 10, + Vertical = 4 + }, + Children = new Drawable[] + { + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = + [ + new SpriteIcon + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Size = new Vector2(32), + Icon = FontAwesome.Solid.Random, + }, + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = "Random", + } + ] + }, + selectionOverlay = new AvatarOverlay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + } + } + } + }; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.cs similarity index 56% rename from osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.cs index cbd8480da4..ca10133a36 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -19,9 +20,12 @@ using osuTK.Input; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { - public partial class BeatmapSelectPanel : Container + public abstract partial class MatchmakingSelectPanel : Container { - public static readonly Vector2 SIZE = new Vector2(BeatmapCard.WIDTH, BeatmapCardNormal.HEIGHT); + public const float WIDTH = 345; + public const float HEIGHT = 80; + + public static readonly Vector2 SIZE = new Vector2(WIDTH, HEIGHT); public bool AllowSelection { get; set; } @@ -29,14 +33,15 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect public Action? Action { private get; init; } + protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; + private const float border_width = 3; private Container scaleContainer = null!; private Drawable lighting = null!; private Container border = null!; - private BeatmapCardMatchmaking card = null!; - public BeatmapSelectPanel(MultiplayerPlaylistItem item) + protected MatchmakingSelectPanel(MultiplayerPlaylistItem item) { Item = item; Size = SIZE; @@ -45,88 +50,70 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { - InternalChild = scaleContainer = new Container + InternalChildren = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new[] + scaleContainer = new Container { - new Container + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new[] { - Masking = true, - CornerRadius = BeatmapCard.CORNER_RADIUS, - CornerExponent = 10, - RelativeSizeAxes = Axes.Both, - Children = new[] + new Container { - card = new BeatmapCardMatchmaking + Masking = true, + CornerRadius = BeatmapCard.CORNER_RADIUS, + CornerExponent = 10, + RelativeSizeAxes = Axes.Both, + Children = new[] { - Action = () => + Content, + lighting = new Box { - if (AllowSelection) - Action?.Invoke(Item); + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + Alpha = 0, }, - }, - lighting = new Box - { - Blending = BlendingParameters.Additive, - RelativeSizeAxes = Axes.Both, - Alpha = 0, - }, - } - }, - border = new Container - { - Alpha = 0, - Masking = true, - CornerRadius = BeatmapCard.CORNER_RADIUS, - CornerExponent = 10, - Blending = BlendingParameters.Additive, - RelativeSizeAxes = Axes.Both, - BorderThickness = border_width, - BorderColour = colourProvider.Light1, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Radius = 40, - Roundness = 300, - Colour = colourProvider.Light3.Opacity(0.1f), + } }, - Children = new Drawable[] + border = new Container { - new Box + Alpha = 0, + Masking = true, + CornerRadius = BeatmapCard.CORNER_RADIUS, + CornerExponent = 10, + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + BorderThickness = border_width, + BorderColour = colourProvider.Light1, + EdgeEffect = new EdgeEffectParameters { - AlwaysPresent = true, - Alpha = 0, - Colour = Color4.Black, - RelativeSizeAxes = Axes.Both, + Type = EdgeEffectType.Glow, + Radius = 40, + Roundness = 300, + Colour = colourProvider.Light3.Opacity(0.1f), }, - } - }, - } + Children = new Drawable[] + { + new Box + { + AlwaysPresent = true, + Alpha = 0, + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + }, + } + }, + } + }, + new HoverClickSounds(), }; - - if (Item.ID == -1) - card.DisplayRandom(); - else - card.DisplayItem(Item); } - public void AddUser(APIUser user) - { - card.AddUser(user); - } + // TODO: making these abstract for now but avatar overlay should really be owned by the top level class + public abstract void AddUser(APIUser user); - public void RemoveUser(APIUser user) - { - card.RemoveUser(user); - } - - public void DisplayItem(MultiplayerPlaylistItem item) - { - card.DisplayItem(item); - } + public abstract void RemoveUser(APIUser user); protected override bool OnHover(HoverEvent e) { @@ -171,10 +158,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect lighting.FadeTo(0.5f, 50) .Then() .FadeTo(0.1f, 400); + + Action?.Invoke(Item); } - // pass through to let the beatmap card handle actual click. - return false; + return true; } public void ShowChosenBorder() diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelBeatmap.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelBeatmap.cs new file mode 100644 index 0000000000..ec00ed3847 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelBeatmap.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 osu.Framework.Allocation; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect +{ + public partial class MatchmakingSelectPanelBeatmap : MatchmakingSelectPanel + { + private readonly APIBeatmap beatmap; + private readonly Mod[] mods; + + public MatchmakingSelectPanelBeatmap(MatchmakingPlaylistItem item) + : base(item.PlaylistItem) + { + beatmap = item.Beatmap; + mods = item.Mods; + } + + private CardContent content = null!; + + [BackgroundDependencyLoader] + private void load() + { + Add(content = new CardContentBeatmap(beatmap, mods)); + } + + public override void AddUser(APIUser user) + { + content.SelectionOverlay.AddUser(user); + } + + public override void RemoveUser(APIUser user) + { + content.SelectionOverlay.RemoveUser(user.Id); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs new file mode 100644 index 0000000000..0c818df06b --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanelRandom.cs @@ -0,0 +1,60 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect +{ + public partial class MatchmakingSelectPanelRandom : MatchmakingSelectPanel + { + public MatchmakingSelectPanelRandom(MultiplayerPlaylistItem item) + : base(item) + { + } + + private CardContent content = null!; + private readonly List users = new List(); + + [BackgroundDependencyLoader] + private void load() + { + Add(content = new CardContentRandom()); + } + + public void RevealBeatmap(APIBeatmap beatmap, Mod[] mods) + { + content.Expire(); + + var flashLayer = new Box { RelativeSizeAxes = Axes.Both }; + + AddRange(new Drawable[] + { + content = new CardContentBeatmap(beatmap, mods), + flashLayer, + }); + + foreach (var user in users) + content.SelectionOverlay.AddUser(user); + + flashLayer.FadeOutFromOne(1000, Easing.In); + } + + public override void AddUser(APIUser user) + { + users.Add(user); + content.SelectionOverlay.AddUser(user); + } + + public override void RemoveUser(APIUser user) + { + users.Remove(user); + content.SelectionOverlay.RemoveUser(user.Id); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs index e0db69783c..7951fc5448 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs @@ -1,14 +1,23 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Database; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Rooms; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osuTK; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { @@ -18,10 +27,17 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect public override Drawable PlayersDisplayArea { get; } private readonly BeatmapSelectGrid beatmapSelectGrid; + private readonly LoadingSpinner loadingSpinner; [Resolved] private MultiplayerClient client { get; set; } = null!; + [Resolved] + private RulesetStore rulesetStore { get; set; } = null!; + + [Resolved] + private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; + public SubScreenBeatmapSelect() { InternalChildren = new Drawable[] @@ -30,9 +46,19 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Horizontal = 200 }, - Child = beatmapSelectGrid = new BeatmapSelectGrid + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, + beatmapSelectGrid = new BeatmapSelectGrid + { + RelativeSizeAxes = Axes.Both, + }, + loadingSpinner = new LoadingSpinner + { + Size = new Vector2(64), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + State = { Value = Visibility.Visible } + } }, }, new Container @@ -50,25 +76,53 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { base.LoadComplete(); - client.ItemAdded += onItemAdded; - - foreach (var item in client.Room!.Playlist) - onItemAdded(item); - beatmapSelectGrid.ItemSelected += item => client.MatchmakingToggleSelection(item.ID); - client.MatchmakingItemSelected += onItemSelected; client.MatchmakingItemDeselected += onItemDeselected; client.SettingsChanged += onSettingsChanged; + + Debug.Assert(client.Room != null); + + loadItems(client.Room.Playlist.ToArray()).FireAndForget(); } - private void onItemAdded(MultiplayerPlaylistItem item) => Scheduler.Add(() => + private async Task loadItems(MultiplayerPlaylistItem[] items) { - if (item.Expired) - return; + var beatmaps = await beatmapLookupCache.GetBeatmapsAsync(items.Select(it => it.BeatmapID).ToArray()).ConfigureAwait(false); + var matchmakingItems = new List(); - beatmapSelectGrid.AddItem(item); - }); + foreach (var entry in items.Zip(beatmaps)) + { + var (item, beatmap) = entry; + + beatmap ??= new APIBeatmap + { + BeatmapSet = new APIBeatmapSet + { + Title = "unknown beatmap", + TitleUnicode = "unknown beatmap", + Artist = "unknown artist", + ArtistUnicode = "unknown artist", + } + }; + + beatmap.StarRating = item.StarRating; + + Ruleset? ruleset = rulesetStore.GetRuleset(item.RulesetID)?.CreateInstance(); + + Debug.Assert(ruleset != null); + + Mod[] mods = item.RequiredMods.Select(m => m.ToMod(ruleset)).ToArray(); + + matchmakingItems.Add(new MatchmakingPlaylistItem(item, beatmap, mods)); + } + + Scheduler.Add(() => + { + loadingSpinner.Hide(); + beatmapSelectGrid.AddItems(matchmakingItems); + }); + } private void onItemSelected(int userId, long itemId) { @@ -104,7 +158,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect if (client.IsNotNull()) { - client.ItemAdded -= onItemAdded; client.MatchmakingItemSelected -= onItemSelected; client.MatchmakingItemDeselected -= onItemDeselected; client.SettingsChanged -= onSettingsChanged;