From 638d959c5cc3fdcdb6d070eb976191e2b6f734ec Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 23 Dec 2024 20:12:25 +0900 Subject: [PATCH] Initial support for free style selection --- osu.Game/Online/Rooms/PlaylistItem.cs | 5 +- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 118 ++++++++++++++++-- .../MultiplayerMatchStyleSelect.cs | 84 +++++++++++++ .../Multiplayer/MultiplayerMatchSubScreen.cs | 75 +++++++---- .../Select/Carousel/CarouselBeatmap.cs | 3 + osu.Game/Screens/Select/FilterControl.cs | 2 +- osu.Game/Screens/Select/FilterCriteria.cs | 1 + osu.Game/Screens/Select/SongSelect.cs | 10 +- 8 files changed, 252 insertions(+), 46 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 3d829d1e4e..937bc40e9b 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -124,13 +124,14 @@ namespace osu.Game.Online.Rooms #endregion - public PlaylistItem With(Optional id = default, Optional beatmap = default, Optional playlistOrder = default) + public PlaylistItem With(Optional id = default, Optional beatmap = default, Optional playlistOrder = default, + Optional ruleset = default) { return new PlaylistItem(beatmap.GetOr(Beatmap)) { ID = id.GetOr(ID), OwnerID = OwnerID, - RulesetID = RulesetID, + RulesetID = ruleset.GetOr(RulesetID), Expired = Expired, PlaylistOrder = playlistOrder.GetOr(PlaylistOrder), PlayedAt = PlayedAt, diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 4ef31c02c3..c9e0cbc1e9 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -11,6 +11,7 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -36,6 +37,18 @@ namespace osu.Game.Screens.OnlinePlay.Match { public readonly Bindable SelectedItem = new Bindable(); + /// + /// When players are freely allowed to select their own gameplay style (selected item has a non-null beatmapset id), + /// a non-null value indicates a local beatmap selection from the same beatmapset as the selected item. + /// + public readonly Bindable DifficultyOverride = new Bindable(); + + /// + /// When players are freely allowed to select their own gameplay style (selected item has a non-null beatmapset id), + /// a non-null value indicates a local ruleset selection. + /// + public readonly Bindable RulesetOverride = new Bindable(); + public override bool? ApplyModTrackAdjustments => true; protected override BackgroundScreen CreateBackground() => new RoomBackgroundScreen(Room.Playlist.FirstOrDefault()) @@ -51,6 +64,17 @@ namespace osu.Game.Screens.OnlinePlay.Match /// protected Drawable? UserModsSection; + /// + /// A container that provides controls for selection of the user's difficulty override. + /// This will be shown/hidden automatically when applicable. + /// + protected Drawable? UserDifficultySection; + + /// + /// A container that will display the user's difficulty override. + /// + protected Container? UserStyleDisplayContainer; + private Sample? sampleStart; /// @@ -250,6 +274,8 @@ namespace osu.Game.Screens.OnlinePlay.Match SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged)); UserMods.BindValueChanged(_ => Scheduler.AddOnce(UpdateMods)); + DifficultyOverride.BindValueChanged(_ => Scheduler.AddOnce(updateStyleOverride)); + RulesetOverride.BindValueChanged(_ => Scheduler.AddOnce(updateStyleOverride)); beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateWorkingBeatmap()); @@ -383,7 +409,7 @@ namespace osu.Game.Screens.OnlinePlay.Match protected void StartPlay() { - if (SelectedItem.Value == null) + if (GetGameplayItem() is not PlaylistItem item) return; // User may be at song select or otherwise when the host starts gameplay. @@ -401,7 +427,7 @@ namespace osu.Game.Screens.OnlinePlay.Match // fallback is to allow this class to operate when there is no parent OnlineScreen (testing purposes). var targetScreen = (Screen?)ParentScreen ?? this; - targetScreen.Push(CreateGameplayScreen(SelectedItem.Value)); + targetScreen.Push(CreateGameplayScreen(item)); } /// @@ -413,11 +439,18 @@ namespace osu.Game.Screens.OnlinePlay.Match private void selectedItemChanged() { - updateWorkingBeatmap(); - if (SelectedItem.Value is not PlaylistItem selected) return; + if (selected.BeatmapSetId == null || selected.BeatmapSetId != DifficultyOverride.Value?.BeatmapSet.AsNonNull().OnlineID) + { + DifficultyOverride.Value = null; + RulesetOverride.Value = null; + } + + updateStyleOverride(); + updateWorkingBeatmap(); + var rulesetInstance = Rulesets.GetRuleset(selected.RulesetID)?.CreateInstance(); Debug.Assert(rulesetInstance != null); var allowedMods = selected.AllowedMods.Select(m => m.ToMod(rulesetInstance)); @@ -439,37 +472,96 @@ namespace osu.Game.Screens.OnlinePlay.Match UserModsSection?.Show(); UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); } + + if (selected.BeatmapSetId == null) + UserDifficultySection?.Hide(); + else + UserDifficultySection?.Show(); } private void updateWorkingBeatmap() { - if (SelectedItem.Value == null || !this.IsCurrentScreen()) + if (GetGameplayItem() is not PlaylistItem item || !this.IsCurrentScreen()) return; - var beatmap = SelectedItem.Value?.Beatmap; - // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info - var localBeatmap = beatmap == null ? null : beatmapManager.QueryBeatmap(b => b.OnlineID == beatmap.OnlineID); + var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == item.Beatmap.OnlineID); UserModsSelectOverlay.Beatmap.Value = Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); } protected virtual void UpdateMods() { - if (SelectedItem.Value == null || !this.IsCurrentScreen()) + if (GetGameplayItem() is not PlaylistItem item || !this.IsCurrentScreen()) return; - var rulesetInstance = Rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance(); + var rulesetInstance = Rulesets.GetRuleset(item.RulesetID)?.CreateInstance(); Debug.Assert(rulesetInstance != null); - Mods.Value = UserMods.Value.Concat(SelectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList(); + Mods.Value = UserMods.Value.Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList(); } - private void updateRuleset() + private void updateStyleOverride() { if (SelectedItem.Value == null || !this.IsCurrentScreen()) return; - Ruleset.Value = Rulesets.GetRuleset(SelectedItem.Value.RulesetID); + if (UserStyleDisplayContainer == null) + return; + + PlaylistItem gameplayItem = GetGameplayItem()!; + + if (UserStyleDisplayContainer.SingleOrDefault()?.Item.Equals(gameplayItem) == true) + return; + + UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem) + { + AllowReordering = false, + AllowEditing = true, + RequestEdit = openStyleSelection + }; + } + + protected PlaylistItem? GetGameplayItem() + { + PlaylistItem? selectedItemWithOverride = SelectedItem.Value; + + if (selectedItemWithOverride?.BeatmapSetId == null) + return selectedItemWithOverride; + + // Sanity check. + if (DifficultyOverride.Value?.BeatmapSet?.OnlineID != selectedItemWithOverride.BeatmapSetId) + return selectedItemWithOverride; + + if (DifficultyOverride.Value != null) + selectedItemWithOverride = selectedItemWithOverride.With(beatmap: DifficultyOverride.Value); + + if (RulesetOverride.Value != null) + selectedItemWithOverride = selectedItemWithOverride.With(ruleset: RulesetOverride.Value.OnlineID); + + return selectedItemWithOverride; + } + + private void openStyleSelection(PlaylistItem item) + { + if (!this.IsCurrentScreen()) + return; + + this.Push(new MultiplayerMatchStyleSelect(Room, item, (beatmap, ruleset) => + { + if (SelectedItem.Value?.BeatmapSetId == null || SelectedItem.Value.BeatmapSetId != beatmap.BeatmapSet?.OnlineID) + return; + + DifficultyOverride.Value = beatmap; + RulesetOverride.Value = ruleset; + })); + } + + private void updateRuleset() + { + if (GetGameplayItem() is not PlaylistItem item || !this.IsCurrentScreen()) + return; + + Ruleset.Value = Rulesets.GetRuleset(item.RulesetID); } private void beginHandlingTrack() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs new file mode 100644 index 0000000000..dc1393bf96 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs @@ -0,0 +1,84 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using Humanizer; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets; +using osu.Game.Screens.Select; +using osu.Game.Users; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer +{ + public partial class MultiplayerMatchStyleSelect : SongSelect, IOnlinePlaySubScreen + { + public string ShortTitle => "style selection"; + + public override string Title => ShortTitle.Humanize(); + + public override bool AllowEditing => false; + + protected override UserActivity InitialActivity => new UserActivity.InLobby(room); + + private readonly Room room; + private readonly PlaylistItem item; + private readonly Action onSelect; + + public MultiplayerMatchStyleSelect(Room room, PlaylistItem item, Action onSelect) + { + this.room = room; + this.item = item; + this.onSelect = onSelect; + + Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }; + } + + [BackgroundDependencyLoader] + private void load() + { + LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT }; + } + + protected override FilterControl CreateFilterControl() => new DifficultySelectFilterControl(item); + + protected override IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons() + { + // Required to create the drawable components. + base.CreateSongSelectFooterButtons(); + return Enumerable.Empty<(FooterButton, OverlayContainer?)>(); + } + + protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); + + protected override bool OnStart() + { + onSelect(Beatmap.Value.BeatmapInfo, Ruleset.Value); + this.Exit(); + return true; + } + + private partial class DifficultySelectFilterControl : FilterControl + { + private readonly PlaylistItem item; + + public DifficultySelectFilterControl(PlaylistItem item) + { + this.item = item; + } + + public override FilterCriteria CreateCriteria() + { + var criteria = base.CreateCriteria(); + criteria.BeatmapSetId = item.BeatmapSetId; + return criteria; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index edc45dbf7c..d807fe8177 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -145,43 +145,66 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer SelectedItem = SelectedItem } }, - new[] + new Drawable[] { - UserModsSection = new FillFlowContainer + new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Margin = new MarginPadding { Top = 10 }, - Alpha = 0, - Children = new Drawable[] + Children = new[] { - new OverlinedHeader("Extra mods"), - new FillFlowContainer + UserModsSection = new FillFlowContainer { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, Children = new Drawable[] { - new UserModSelectButton + new OverlinedHeader("Extra mods"), + new FillFlowContainer { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Width = 90, - Text = "Select", - Action = ShowUserModSelect, - }, - new ModDisplay - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Current = UserMods, - Scale = new Vector2(0.8f), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new Drawable[] + { + new UserModSelectButton + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 90, + Text = "Select", + Action = ShowUserModSelect, + }, + new ModDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Current = UserMods, + Scale = new Vector2(0.8f), + }, + } }, } }, + UserDifficultySection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, + Children = new Drawable[] + { + new OverlinedHeader("Difficulty"), + UserStyleDisplayContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } + } + }, } - }, + } }, }, RowDimensions = new[] @@ -240,14 +263,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override void UpdateMods() { - if (SelectedItem.Value == null || client.LocalUser == null || !this.IsCurrentScreen()) + if (GetGameplayItem() is not PlaylistItem item || client.LocalUser == null || !this.IsCurrentScreen()) return; // update local mods based on room's reported status for the local user (omitting the base call implementation). // this makes the server authoritative, and avoids the local user potentially setting mods that the server is not aware of (ie. if the match was started during the selection being changed). - var rulesetInstance = Rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance(); + var rulesetInstance = Rulesets.GetRuleset(item.RulesetID)?.CreateInstance(); Debug.Assert(rulesetInstance != null); - Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(rulesetInstance)).Concat(SelectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList(); + Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(rulesetInstance)).Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList(); } [Resolved(canBeNull: true)] diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index c007fa29ed..95186e98d8 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -90,6 +90,9 @@ namespace osu.Game.Screens.Select.Carousel if (match && criteria.RulesetCriteria != null) match &= criteria.RulesetCriteria.Matches(BeatmapInfo, criteria); + if (match && criteria.BeatmapSetId != null) + match &= criteria.BeatmapSetId == BeatmapInfo.BeatmapSet?.OnlineID; + return match; } diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index b221296ba8..488f63accb 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -57,7 +57,7 @@ namespace osu.Game.Screens.Select [CanBeNull] private FilterCriteria currentCriteria; - public FilterCriteria CreateCriteria() + public virtual FilterCriteria CreateCriteria() { string query = searchTextBox.Text; diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 76c0f769f0..63dbdfbed3 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -56,6 +56,7 @@ namespace osu.Game.Screens.Select public RulesetInfo? Ruleset; public IReadOnlyList? Mods; public bool AllowConvertedBeatmaps; + public int? BeatmapSetId; private string searchText = string.Empty; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 9ebd9c9846..c8d50436d9 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -216,11 +216,11 @@ namespace osu.Game.Screens.Select }, } }, - FilterControl = new FilterControl + FilterControl = CreateFilterControl().With(d => { - RelativeSizeAxes = Axes.X, - Height = FilterControl.HEIGHT, - }, + d.RelativeSizeAxes = Axes.X; + d.Height = FilterControl.HEIGHT; + }), new GridContainer // used for max width implementation { RelativeSizeAxes = Axes.Both, @@ -389,6 +389,8 @@ namespace osu.Game.Screens.Select SampleConfirm = audio.Samples.Get(@"SongSelect/confirm-selection"); } + protected virtual FilterControl CreateFilterControl() => new FilterControl(); + protected override void LoadComplete() { base.LoadComplete();