1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-25 01:00:00 +08:00

Split quickplay beatmap & "random" panel into separate classes (V2) (#35701)

* Load all beatmaps in bulk for SubScreenBeatmapSelect

* Fix tests no longer working due to drawable changes

* Remove test that no longer makes sense

* Split matchmaking panel into subclasses for each panel type

* Adjust tests to match new structure

* Add `ConfigureAwait`

* Display loading spinner while beatmaps are being fetched

* Fix test failure

* Load playlist items directly in `LoadComplete`

* Convert `MatchmakingSelectPanel` card content classes into nested classes

* Wait for panels to be loaded before operating on them

* Add ConfigureAwait()

---------

Co-authored-by: Dan Balasescu <smoogipoo@smgi.me>
This commit is contained in:
maarvin
2025-11-17 06:11:07 +01:00
committed by GitHub
Unverified
parent 45e8df7af2
commit 8b778e8106
15 changed files with 963 additions and 916 deletions
@@ -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<MatchmakingPlaylistItem> 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<Mod>()
);
});
}
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<Mod>()
));
}
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<BeatmapSelectPanel>().First().AddUser(new APIUser
AddStep("add selection 1", () => grid.ChildrenOfType<MatchmakingSelectPanel>().First().AddUser(new APIUser
{
Id = DummyAPIAccess.DUMMY_USER_ID,
Username = "Maarvin",
}));
AddStep("add selection 2", () => grid.ChildrenOfType<BeatmapSelectPanel>().Skip(5).First().AddUser(new APIUser
AddStep("add selection 2", () => grid.ChildrenOfType<MatchmakingSelectPanel>().Skip(5).First().AddUser(new APIUser
{
Id = 2,
Username = "peppy",
}));
AddStep("add selection 3", () => grid.ChildrenOfType<BeatmapSelectPanel>().Skip(10).First().AddUser(new APIUser
AddStep("add selection 3", () => grid.ChildrenOfType<MatchmakingSelectPanel>().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<BeatmapSelectPanel>().ToArray();
var panels = grid.ChildrenOfType<MatchmakingSelectPanel>().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)
@@ -1,14 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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,
@@ -1,119 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<APIUser> users = new List<APIUser>();
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);
});
}
}
@@ -1,378 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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> downloadState = new Bindable<DownloadState>();
private readonly IBindableNumber<double> downloadProgress = new BindableDouble();
private readonly Bindable<BeatmapSetFavouriteState> favouriteState = new Bindable<BeatmapSetFavouriteState>();
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<MenuItem> items = new List<MenuItem>
{
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();
}
}
}
}
@@ -1,153 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<SelectionAvatar> avatars;
private Sample? userAddedSample;
private double? lastSamplePlayback;
[Resolved]
private IAPIProvider api { get; set; } = null!;
public AvatarOverlay()
{
AutoSizeAxes = Axes.Both;
InternalChild = avatars = new Container<SelectionAvatar>
{
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;
}
}
}
}
}
@@ -1,77 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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,
}
}
}
};
}
}
}
@@ -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<MultiplayerPlaylistItem>? ItemSelected;
private readonly Dictionary<long, BeatmapSelectPanel> panelLookup = new Dictionary<long, BeatmapSelectPanel>();
private readonly Dictionary<long, MatchmakingSelectPanel> panelLookup = new Dictionary<long, MatchmakingSelectPanel>();
private readonly Dictionary<long, MatchmakingPlaylistItem> playlistItems = new Dictionary<long, MatchmakingPlaylistItem>();
private MatchmakingSelectPanelRandom randomPanel = null!;
private readonly PanelGridContainer panelGridContainer;
private readonly Container<BeatmapSelectPanel> rollContainer;
private readonly Container<MatchmakingSelectPanel> 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<BeatmapSelectPanel>
rollContainer = new Container<MatchmakingSelectPanel>
{
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<MatchmakingPlaylistItem> 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<BeatmapSelectPanel>();
var remainingPanels = new List<MatchmakingSelectPanel>();
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<BeatmapSelectPanel>
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<MatchmakingSelectPanel>
{
public bool LayoutDisabled;
@@ -0,0 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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;
}
}
@@ -0,0 +1,156 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<SelectionAvatar> avatars;
private Sample? userAddedSample;
private double? lastSamplePlayback;
[Resolved]
private IAPIProvider api { get; set; } = null!;
public AvatarOverlay()
{
AutoSizeAxes = Axes.Both;
InternalChild = avatars = new Container<SelectionAvatar>
{
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;
}
}
}
}
}
}
@@ -0,0 +1,381 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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> downloadState = new Bindable<DownloadState>();
private readonly IBindableNumber<double> downloadProgress = new BindableDouble();
private readonly Bindable<BeatmapSetFavouriteState> favouriteState = new Bindable<BeatmapSetFavouriteState>();
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<MenuItem> items = new List<MenuItem>
{
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();
}
}
}
}
}
@@ -0,0 +1,80 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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,
}
}
}
};
}
}
}
}
@@ -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<MultiplayerPlaylistItem>? Action { private get; init; }
protected override Container<Drawable> 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()
@@ -0,0 +1,40 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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);
}
}
}
@@ -0,0 +1,60 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<APIUser> users = new List<APIUser>();
[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);
}
}
}
@@ -1,14 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<MatchmakingPlaylistItem>();
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;