From 63e0768207752f674a44ada84acfc61be63f508c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Mar 2023 14:44:52 +0900 Subject: [PATCH 001/801] Show count of beatmaps in collections in manage dialog --- .../Collections/DrawableCollectionListItem.cs | 65 +++++++++++++++++-- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 23156b1ad5..efeb066869 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -13,6 +14,7 @@ using osu.Framework.Input.Events; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; @@ -25,7 +27,7 @@ namespace osu.Game.Collections /// public partial class DrawableCollectionListItem : OsuRearrangeableListItem> { - private const float item_height = 35; + private const float item_height = 45; private const float button_width = item_height * 0.75f; /// @@ -81,12 +83,10 @@ namespace osu.Game.Collections Padding = new MarginPadding { Right = collection.IsManaged ? button_width : 0 }, Children = new Drawable[] { - textBox = new ItemTextBox + textBox = new ItemTextBox(collection) { - RelativeSizeAxes = Axes.Both, - Size = Vector2.One, - CornerRadius = item_height / 2, - PlaceholderText = collection.IsManaged ? string.Empty : "Create a new collection" + RelativeSizeAxes = Axes.X, + Height = item_height }, } }, @@ -117,11 +117,64 @@ namespace osu.Game.Collections { protected override float LeftRightPadding => item_height / 2; + private const float count_text_size = 12; + + [Resolved] + private RealmAccess realm { get; set; } = null!; + + private readonly Live collection; + + private OsuSpriteText countText = null!; + + private IDisposable? itemCountSubscription; + + public ItemTextBox(Live collection) + { + this.collection = collection; + + CornerRadius = item_height / 2; + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { BackgroundUnfocused = colours.GreySeaFoamDarker.Darken(0.5f); BackgroundFocused = colours.GreySeaFoam; + + if (collection.IsManaged) + { + TextContainer.Height *= (Height - count_text_size) / Height; + TextContainer.Margin = new MarginPadding { Bottom = count_text_size }; + + TextContainer.Add(countText = new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.TopLeft, + Depth = float.MinValue, + Font = OsuFont.Default.With(size: count_text_size, weight: FontWeight.SemiBold), + Margin = new MarginPadding { Top = 2, Left = 2 }, + Colour = colours.Yellow + }); + + itemCountSubscription = realm.SubscribeToPropertyChanged>(r => r.Find(collection.ID), c => c.BeatmapMD5Hashes, items => + { + countText.Text = items.Count == 1 + // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 + // but also in this case we want support for formatting a number within a string). + ? $"{items.Count:#,0} beatmap" + : $"{items.Count:#,0} beatmaps"; + }); + } + else + { + PlaceholderText = "Create a new collection"; + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + itemCountSubscription?.Dispose(); } } From 954be126922a63458dff577917ebf46e3ac72b75 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Mar 2023 14:46:13 +0900 Subject: [PATCH 002/801] Debounce updates to ensure event isn't fired too often after much collection management --- .../Collections/DrawableCollectionListItem.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index efeb066869..87cc14ecb9 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -156,14 +156,17 @@ namespace osu.Game.Collections Colour = colours.Yellow }); - itemCountSubscription = realm.SubscribeToPropertyChanged>(r => r.Find(collection.ID), c => c.BeatmapMD5Hashes, items => - { - countText.Text = items.Count == 1 - // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 - // but also in this case we want support for formatting a number within a string). - ? $"{items.Count:#,0} beatmap" - : $"{items.Count:#,0} beatmaps"; - }); + itemCountSubscription = realm.SubscribeToPropertyChanged>(r => r.Find(collection.ID), c => c.BeatmapMD5Hashes, _ => + Scheduler.AddOnce(() => + { + int count = collection.PerformRead(c => c.BeatmapMD5Hashes.Count); + + countText.Text = count == 1 + // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 + // but also in this case we want support for formatting a number within a string). + ? $"{count:#,0} beatmap" + : $"{count:#,0} beatmaps"; + })); } else { From 256789193f7d99d6e1dd5ca00f23a82830d195a2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Apr 2023 15:28:01 +0900 Subject: [PATCH 003/801] Remove redundant type specification --- osu.Game/Collections/DrawableCollectionListItem.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 87cc14ecb9..31b127ef2a 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -156,7 +155,7 @@ namespace osu.Game.Collections Colour = colours.Yellow }); - itemCountSubscription = realm.SubscribeToPropertyChanged>(r => r.Find(collection.ID), c => c.BeatmapMD5Hashes, _ => + itemCountSubscription = realm.SubscribeToPropertyChanged(r => r.Find(collection.ID), c => c.BeatmapMD5Hashes, _ => Scheduler.AddOnce(() => { int count = collection.PerformRead(c => c.BeatmapMD5Hashes.Count); From a99a992ceba30b3ff0208de4873eacd41719b65e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 9 Dec 2024 13:48:05 +0900 Subject: [PATCH 004/801] Adjust test to load song select during setup --- .../Multiplayer/TestSceneMultiplayerMatchSongSelect.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 2a5f16d091..a266b1d95e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -60,14 +60,15 @@ namespace osu.Game.Tests.Visual.Multiplayer private void setUp() { - AddStep("reset", () => + AddStep("create song select", () => { Ruleset.Value = new OsuRuleset().RulesetInfo; Beatmap.SetDefault(); SelectedMods.SetDefault(); + + LoadScreen(songSelect = new TestMultiplayerMatchSongSelect(SelectedRoom.Value!)); }); - AddStep("create song select", () => LoadScreen(songSelect = new TestMultiplayerMatchSongSelect(SelectedRoom.Value!))); AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded); } From 2bae93d7add0d6d24758040b46d3542200a40480 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 9 Dec 2024 01:59:16 -0500 Subject: [PATCH 005/801] Add special handling for file import button on iOS --- .../Sections/Maintenance/GeneralSettings.cs | 20 ++++++-- .../Maintenance/SystemFileImportComponent.cs | 51 +++++++++++++++++++ 2 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index f75fc2c8bc..ed3e72adbe 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Localisation; +using osu.Framework.Platform; using osu.Framework.Screens; using osu.Game.Localisation; using osu.Game.Screens; @@ -15,22 +17,32 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { protected override LocalisableString Header => CommonStrings.General; + private SystemFileImportComponent systemFileImport = null!; + [BackgroundDependencyLoader] - private void load(IPerformFromScreenRunner? performer) + private void load(OsuGame game, GameHost host, IPerformFromScreenRunner? performer) { - Children = new[] + Add(systemFileImport = new SystemFileImportComponent(game, host)); + + AddRange(new Drawable[] { new SettingsButton { Text = DebugSettingsStrings.ImportFiles, - Action = () => performer?.PerformFromScreen(menu => menu.Push(new FileImportScreen())) + Action = () => + { + if (systemFileImport.PresentIfAvailable()) + return; + + performer?.PerformFromScreen(menu => menu.Push(new FileImportScreen())); + }, }, new SettingsButton { Text = DebugSettingsStrings.RunLatencyCertifier, Action = () => performer?.PerformFromScreen(menu => menu.Push(new LatencyCertifierScreen())) } - }; + }); } } } diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs new file mode 100644 index 0000000000..9827872702 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs @@ -0,0 +1,51 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Graphics; +using osu.Framework.Platform; + +namespace osu.Game.Overlays.Settings.Sections.Maintenance +{ + public partial class SystemFileImportComponent : Component + { + private readonly OsuGame game; + private readonly GameHost host; + + private ISystemFileSelector? selector; + + public SystemFileImportComponent(OsuGame game, GameHost host) + { + this.game = game; + this.host = host; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + selector = host.CreateSystemFileSelector(game.HandledExtensions.ToArray()); + + if (selector != null) + selector.Selected += f => Schedule(() => startImport(f.FullName)); + } + + public bool PresentIfAvailable() + { + if (selector == null) + return false; + + selector.Present(); + return true; + } + + private void startImport(string path) + { + Task.Factory.StartNew(async () => + { + await game.Import(path).ConfigureAwait(false); + }, TaskCreationOptions.LongRunning); + } + } +} From 9abb92a8d659982b76d0ece4ac45c7bb98132020 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 10 Dec 2024 15:46:28 +0900 Subject: [PATCH 006/801] Add BeatmapSetId to playlist items --- osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs | 3 +++ osu.Game/Online/Rooms/PlaylistItem.cs | 6 ++++++ .../OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs | 1 + 3 files changed, 10 insertions(+) diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index 8be703e620..027d5b4a17 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -56,6 +56,9 @@ namespace osu.Game.Online.Rooms [Key(10)] public double StarRating { get; set; } + [Key(11)] + public int? BeatmapSetID { get; set; } + [SerializationConstructor] public MultiplayerPlaylistItem() { diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 47d4e163bf..3d829d1e4e 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -67,6 +67,9 @@ namespace osu.Game.Online.Rooms set => Beatmap = new APIBeatmap { OnlineID = value }; } + [JsonProperty("beatmapset_id")] + public int? BeatmapSetId { get; set; } + /// /// A beatmap representing this playlist item. /// In many cases, this will *not* contain any usable information apart from OnlineID. @@ -101,6 +104,7 @@ namespace osu.Game.Online.Rooms PlayedAt = item.PlayedAt; RequiredMods = item.RequiredMods.ToArray(); AllowedMods = item.AllowedMods.ToArray(); + BeatmapSetId = item.BeatmapSetID; } public void MarkInvalid() => valid.Value = false; @@ -133,12 +137,14 @@ namespace osu.Game.Online.Rooms AllowedMods = AllowedMods, RequiredMods = RequiredMods, valid = { Value = Valid.Value }, + BeatmapSetId = BeatmapSetId }; } public bool Equals(PlaylistItem? other) => ID == other?.ID && Beatmap.OnlineID == other.Beatmap.OnlineID + && BeatmapSetId == other.BeatmapSetId && RulesetID == other.RulesetID && Expired == other.Expired && PlaylistOrder == other.PlaylistOrder diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 4e03c19095..9f9e6349a6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -83,6 +83,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { ID = itemToEdit?.ID ?? 0, BeatmapID = item.Beatmap.OnlineID, + BeatmapSetID = item.BeatmapSetId, BeatmapChecksum = item.Beatmap.MD5Hash, RulesetID = item.RulesetID, RequiredMods = item.RequiredMods.ToArray(), From 0fb75233ffe501b51ca5cf605f3390c87695dcb9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 10 Dec 2024 23:02:26 +0900 Subject: [PATCH 007/801] Add "freeplay" button to multiplayer song select --- .../OnlinePlay/FooterButtonFreePlay.cs | 94 +++++++++++++++++++ .../OnlinePlay/OnlinePlaySongSelect.cs | 55 ++++++++--- .../Playlists/PlaylistsSongSelect.cs | 3 +- osu.Game/Screens/Select/SongSelect.cs | 7 +- 4 files changed, 146 insertions(+), 13 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs new file mode 100644 index 0000000000..367857e780 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs @@ -0,0 +1,94 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Screens.Select; + +namespace osu.Game.Screens.OnlinePlay +{ + public class FooterButtonFreePlay : FooterButton, IHasCurrentValue + { + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private OsuSpriteText text = null!; + private Circle circle = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + ButtonContentContainer.AddRange(new[] + { + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + circle = new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = colours.YellowDark, + RelativeSizeAxes = Axes.Both, + }, + text = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding(5), + UseFullGlyphHeight = false, + } + } + } + }); + + SelectedColour = colours.Yellow; + DeselectedColour = SelectedColour.Opacity(0.5f); + Text = @"freeplay"; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(_ => updateDisplay(), true); + + // Overwrite any external behaviour as we delegate the main toggle action to a sub-button. + Action = () => current.Value = !current.Value; + } + + private void updateDisplay() + { + if (current.Value) + { + text.Text = "on"; + text.FadeColour(colours.Gray2, 200, Easing.OutQuint); + circle.FadeColour(colours.Yellow, 200, Easing.OutQuint); + } + else + { + text.Text = "off"; + text.FadeColour(colours.GrayF, 200, Easing.OutQuint); + circle.FadeColour(colours.Gray4, 200, Easing.OutQuint); + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index f6b6dfd3ab..1f1d259d0a 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -41,10 +41,12 @@ namespace osu.Game.Screens.OnlinePlay protected override UserActivity InitialActivity => new UserActivity.InLobby(room); protected readonly Bindable> FreeMods = new Bindable>(Array.Empty()); + protected readonly Bindable FreePlay = new Bindable(); private readonly Room room; private readonly PlaylistItem? initialItem; - private readonly FreeModSelectOverlay freeModSelectOverlay; + private readonly FreeModSelectOverlay freeModSelect; + private FooterButton freeModsFooterButton = null!; private IDisposable? freeModSelectOverlayRegistration; @@ -61,7 +63,7 @@ namespace osu.Game.Screens.OnlinePlay Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }; - freeModSelectOverlay = new FreeModSelectOverlay + freeModSelect = new FreeModSelectOverlay { SelectedMods = { BindTarget = FreeMods }, IsValidMod = IsValidFreeMod, @@ -72,7 +74,7 @@ namespace osu.Game.Screens.OnlinePlay private void load() { LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT }; - LoadComponent(freeModSelectOverlay); + LoadComponent(freeModSelect); } protected override void LoadComplete() @@ -108,12 +110,36 @@ namespace osu.Game.Screens.OnlinePlay Mods.Value = initialItem.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); FreeMods.Value = initialItem.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); } + + if (initialItem.BeatmapSetId != null) + FreePlay.Value = true; } Mods.BindValueChanged(onModsChanged); Ruleset.BindValueChanged(onRulesetChanged); + FreePlay.BindValueChanged(onFreePlayChanged, true); - freeModSelectOverlayRegistration = OverlayManager?.RegisterBlockingOverlay(freeModSelectOverlay); + freeModSelectOverlayRegistration = OverlayManager?.RegisterBlockingOverlay(freeModSelect); + } + + private void onFreePlayChanged(ValueChangedEvent enabled) + { + if (enabled.NewValue) + { + freeModsFooterButton.Enabled.Value = false; + ModsFooterButton.Enabled.Value = false; + + ModSelect.Hide(); + freeModSelect.Hide(); + + Mods.Value = []; + FreeMods.Value = []; + } + else + { + freeModsFooterButton.Enabled.Value = true; + ModsFooterButton.Enabled.Value = true; + } } private void onModsChanged(ValueChangedEvent> mods) @@ -121,7 +147,7 @@ namespace osu.Game.Screens.OnlinePlay FreeMods.Value = FreeMods.Value.Where(checkCompatibleFreeMod).ToList(); // Reset the validity delegate to update the overlay's display. - freeModSelectOverlay.IsValidMod = IsValidFreeMod; + freeModSelect.IsValidMod = IsValidFreeMod; } private void onRulesetChanged(ValueChangedEvent ruleset) @@ -135,7 +161,8 @@ namespace osu.Game.Screens.OnlinePlay { RulesetID = Ruleset.Value.OnlineID, RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), - AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray() + AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(), + BeatmapSetId = FreePlay.Value ? Beatmap.Value.BeatmapSetInfo.OnlineID : null }; return SelectItem(item); @@ -150,9 +177,9 @@ namespace osu.Game.Screens.OnlinePlay public override bool OnBackButton() { - if (freeModSelectOverlay.State.Value == Visibility.Visible) + if (freeModSelect.State.Value == Visibility.Visible) { - freeModSelectOverlay.Hide(); + freeModSelect.Hide(); return true; } @@ -161,7 +188,7 @@ namespace osu.Game.Screens.OnlinePlay public override bool OnExiting(ScreenExitEvent e) { - freeModSelectOverlay.Hide(); + freeModSelect.Hide(); return base.OnExiting(e); } @@ -173,9 +200,15 @@ namespace osu.Game.Screens.OnlinePlay protected override IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons() { var baseButtons = base.CreateSongSelectFooterButtons().ToList(); - var freeModsButton = new FooterButtonFreeMods(freeModSelectOverlay) { Current = FreeMods }; - baseButtons.Insert(baseButtons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, (freeModsButton, freeModSelectOverlay)); + freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) { Current = FreeMods }; + var freePlayButton = new FooterButtonFreePlay { Current = FreePlay }; + + baseButtons.InsertRange(baseButtons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[] + { + (freeModsFooterButton, freeModSelect), + (freePlayButton, null) + }); return baseButtons; } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs index 23824b6a73..f9e014a727 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs @@ -37,9 +37,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private PlaylistItem createNewItem() => new PlaylistItem(Beatmap.Value.BeatmapInfo) { ID = room.Playlist.Count == 0 ? 0 : room.Playlist.Max(p => p.ID) + 1, + BeatmapSetId = FreePlay.Value ? Beatmap.Value.BeatmapSetInfo.OnlineID : null, RulesetID = Ruleset.Value.OnlineID, RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), - AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray() + AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(), }; } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 9f7a2c02ff..9ebd9c9846 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -82,6 +82,11 @@ namespace osu.Game.Screens.Select /// protected Container FooterPanels { get; private set; } = null!; + /// + /// The that opens the mod select dialog. + /// + protected FooterButton ModsFooterButton { get; private set; } = null!; + /// /// Whether entering editor mode should be allowed. /// @@ -407,7 +412,7 @@ namespace osu.Game.Screens.Select /// A set of and an optional which the button opens when pressed. protected virtual IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons() => new (FooterButton, OverlayContainer?)[] { - (new FooterButtonMods { Current = Mods }, ModSelect), + (ModsFooterButton = new FooterButtonMods { Current = Mods }, ModSelect), (new FooterButtonRandom { NextRandom = () => Carousel.SelectNextRandom(), From 5a2cae89ff8a9035ca17af7e76a8b1ac7325a060 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 10 Dec 2024 23:02:35 +0900 Subject: [PATCH 008/801] Fix free mod button overriding enabled state --- osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs index dd6536cf26..952b15a873 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs @@ -36,8 +36,9 @@ namespace osu.Game.Screens.OnlinePlay } } - private OsuSpriteText count = null!; + public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); } + private OsuSpriteText count = null!; private Circle circle = null!; private readonly FreeModSelectOverlay freeModSelectOverlay; @@ -45,6 +46,9 @@ namespace osu.Game.Screens.OnlinePlay public FooterButtonFreeMods(FreeModSelectOverlay freeModSelectOverlay) { this.freeModSelectOverlay = freeModSelectOverlay; + + // Overwrite any external behaviour as we delegate the main toggle action to a sub-button. + base.Action = toggleAllFreeMods; } [Resolved] @@ -98,9 +102,6 @@ namespace osu.Game.Screens.OnlinePlay base.LoadComplete(); Current.BindValueChanged(_ => updateModDisplay(), true); - - // Overwrite any external behaviour as we delegate the main toggle action to a sub-button. - Action = toggleAllFreeMods; } /// From 159f6025b8a80a4d666506c47833190c0fcdcb71 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 18 Dec 2024 23:19:14 +0900 Subject: [PATCH 009/801] Fix incorrect behaviour --- osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs index 367857e780..bcc7bb787d 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -24,12 +25,20 @@ namespace osu.Game.Screens.OnlinePlay set => current.Current = value; } + public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); } + private OsuSpriteText text = null!; private Circle circle = null!; [Resolved] private OsuColour colours { get; set; } = null!; + public FooterButtonFreePlay() + { + // Overwrite any external behaviour as we delegate the main toggle action to a sub-button. + base.Action = () => current.Value = !current.Value; + } + [BackgroundDependencyLoader] private void load() { @@ -70,9 +79,6 @@ namespace osu.Game.Screens.OnlinePlay base.LoadComplete(); Current.BindValueChanged(_ => updateDisplay(), true); - - // Overwrite any external behaviour as we delegate the main toggle action to a sub-button. - Action = () => current.Value = !current.Value; } private void updateDisplay() From 79a3afe06feffe9db9aa60760a1509b01bfee3ba Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Thu, 19 Dec 2024 01:16:27 +1000 Subject: [PATCH 010/801] Implement considerations for Relax within osu!taiko diffcalc (#30591) --- .../Difficulty/TaikoDifficultyCalculator.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 7f2558c406..b3efb7f46d 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -77,6 +77,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (beatmap.HitObjects.Count == 0) return new TaikoDifficultyAttributes { Mods = mods }; + bool isRelax = mods.Any(h => h is TaikoModRelax); + Colour colour = (Colour)skills.First(x => x is Colour); Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm); Stamina stamina = (Stamina)skills.First(x => x is Stamina); @@ -88,15 +90,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double monoStaminaRating = singleColourStamina.DifficultyValue() * stamina_skill_multiplier; double monoStaminaFactor = staminaRating == 0 ? 1 : Math.Pow(monoStaminaRating / staminaRating, 5); - double combinedRating = combinedDifficultyValue(rhythm, colour, stamina); + double combinedRating = combinedDifficultyValue(rhythm, colour, stamina, isRelax); double starRating = rescale(combinedRating * 1.4); // TODO: This is temporary measure as we don't detect abuse of multiple-input playstyles of converts within the current system. if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0) { starRating *= 0.925; - // For maps with low colour variance and high stamina requirement, multiple inputs are more likely to be abused. - if (colourRating < 2 && staminaRating > 8) + + // For maps with either relax or low colour variance and high stamina requirement, multiple inputs are more likely to be abused. + if (isRelax) + starRating *= 0.60; + else if (colourRating < 2 && staminaRating > 8) starRating *= 0.80; } @@ -138,7 +143,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// For each section, the peak strains of all separate skills are combined into a single peak strain for the section. /// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more). /// - private double combinedDifficultyValue(Rhythm rhythm, Colour colour, Stamina stamina) + private double combinedDifficultyValue(Rhythm rhythm, Colour colour, Stamina stamina, bool isRelax) { List peaks = new List(); @@ -152,6 +157,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier; double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier; + if (isRelax) + { + colourPeak = 0; // There is no colour difficulty in relax. + staminaPeak /= 1.5; // Stamina difficulty is decreased with an increased available finger count. + } + double peak = norm(1.5, colourPeak, staminaPeak); peak = norm(2, peak, rhythmPeak); From 0f2f25db532418ff5f8deba221ef14ed7a4867e7 Mon Sep 17 00:00:00 2001 From: YaniFR <58740803+YaniFR@users.noreply.github.com> Date: Wed, 18 Dec 2024 19:11:51 +0100 Subject: [PATCH 011/801] Adjust `DifficultyValue` curve to avoid lower star rating of osu!taiko being too inflated (#31067) * low sr * merge two line * update decimal * fix formatting --------- Co-authored-by: StanR --- .../Difficulty/TaikoPerformanceCalculator.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index c672b7a1d9..ed7d41bf72 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -73,7 +73,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes) { - double difficultyValue = Math.Pow(5 * Math.Max(1.0, attributes.StarRating / 0.115) - 4.0, 2.25) / 1150.0; + double baseDifficulty = 5 * Math.Max(1.0, attributes.StarRating / 0.115) - 4.0; + double difficultyValue = Math.Min(Math.Pow(baseDifficulty, 3) / 69052.51, Math.Pow(baseDifficulty, 2.25) / 1150.0); double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0); difficultyValue *= lengthBonus; From 4ca88ae2d66dd417a8380144b5fe5010821ad9ec Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Thu, 19 Dec 2024 21:32:59 +1000 Subject: [PATCH 012/801] Refactor `TaikoDifficultyCalculator` and add `DifficultStrain` attributes (#31191) * refactor + countdifficultstrain * norm in utils * adjust scaling shift * fix comment * revert all value changes * add the else back * remove cds comments --- .../Difficulty/TaikoDifficultyAttributes.cs | 13 ++-- .../Difficulty/TaikoDifficultyCalculator.cs | 75 ++++++++++--------- .../Utils/DifficultyCalculationUtils.cs | 9 +++ 3 files changed, 58 insertions(+), 39 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index c8f0448767..4a35c30e60 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -34,11 +34,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("colour_difficulty")] public double ColourDifficulty { get; set; } - /// - /// The difficulty corresponding to the hardest parts of the map. - /// - [JsonProperty("peak_difficulty")] - public double PeakDifficulty { get; set; } + [JsonProperty("rhythm_difficult_strains")] + public double RhythmTopStrains { get; set; } + + [JsonProperty("colour_difficult_strains")] + public double ColourTopStrains { get; set; } + + [JsonProperty("stamina_difficult_strains")] + public double StaminaTopStrains { get; set; } /// /// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc). diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index b3efb7f46d..05081d471e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -8,6 +8,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; @@ -53,18 +54,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { - List difficultyHitObjects = new List(); - List centreObjects = new List(); - List rimObjects = new List(); - List noteObjects = new List(); + var difficultyHitObjects = new List(); + var centreObjects = new List(); + var rimObjects = new List(); + var noteObjects = new List(); + // Generate TaikoDifficultyHitObjects from the beatmap's hit objects. for (int i = 2; i < beatmap.HitObjects.Count; i++) { - difficultyHitObjects.Add( - new TaikoDifficultyHitObject( - beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, difficultyHitObjects, - centreObjects, rimObjects, noteObjects, difficultyHitObjects.Count) - ); + difficultyHitObjects.Add(new TaikoDifficultyHitObject( + beatmap.HitObjects[i], + beatmap.HitObjects[i - 1], + beatmap.HitObjects[i - 2], + clockRate, + difficultyHitObjects, + centreObjects, + rimObjects, + noteObjects, + difficultyHitObjects.Count + )); } TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects); @@ -79,28 +87,33 @@ namespace osu.Game.Rulesets.Taiko.Difficulty bool isRelax = mods.Any(h => h is TaikoModRelax); - Colour colour = (Colour)skills.First(x => x is Colour); Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm); + Colour colour = (Colour)skills.First(x => x is Colour); Stamina stamina = (Stamina)skills.First(x => x is Stamina); Stamina singleColourStamina = (Stamina)skills.Last(x => x is Stamina); - double colourRating = colour.DifficultyValue() * colour_skill_multiplier; double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier; + double colourRating = colour.DifficultyValue() * colour_skill_multiplier; double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier; double monoStaminaRating = singleColourStamina.DifficultyValue() * stamina_skill_multiplier; double monoStaminaFactor = staminaRating == 0 ? 1 : Math.Pow(monoStaminaRating / staminaRating, 5); + double rhythmDifficultStrains = rhythm.CountTopWeightedStrains(); + double colourDifficultStrains = colour.CountTopWeightedStrains(); + double staminaDifficultStrains = stamina.CountTopWeightedStrains(); + double combinedRating = combinedDifficultyValue(rhythm, colour, stamina, isRelax); double starRating = rescale(combinedRating * 1.4); - // TODO: This is temporary measure as we don't detect abuse of multiple-input playstyles of converts within the current system. + // Converts are penalised outside the scope of difficulty calculation, as our assumptions surrounding standard play-styles becomes out-of-scope. if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0) { starRating *= 0.925; - // For maps with either relax or low colour variance and high stamina requirement, multiple inputs are more likely to be abused. + // For maps with relax, multiple inputs are more likely to be abused. if (isRelax) starRating *= 0.60; + // For maps with either relax or low colour variance and high stamina requirement, multiple inputs are more likely to be abused. else if (colourRating < 2 && staminaRating > 8) starRating *= 0.80; } @@ -112,11 +125,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { StarRating = starRating, Mods = mods, - StaminaDifficulty = staminaRating, - MonoStaminaFactor = monoStaminaFactor, RhythmDifficulty = rhythmRating, ColourDifficulty = colourRating, - PeakDifficulty = combinedRating, + StaminaDifficulty = staminaRating, + MonoStaminaFactor = monoStaminaFactor, + StaminaTopStrains = staminaDifficultStrains, + RhythmTopStrains = rhythmDifficultStrains, + ColourTopStrains = colourDifficultStrains, GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate, OkHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate, MaxCombo = beatmap.GetMaxCombo(), @@ -125,17 +140,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty return attributes; } - /// - /// Applies a final re-scaling of the star rating. - /// - /// The raw star rating value before re-scaling. - private double rescale(double sr) - { - if (sr < 0) return sr; - - return 10.43 * Math.Log(sr / 8 + 1); - } - /// /// Returns the combined star rating of the beatmap, calculated using peak strains from all sections of the map. /// @@ -153,8 +157,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty for (int i = 0; i < colourPeaks.Count; i++) { - double colourPeak = colourPeaks[i] * colour_skill_multiplier; double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier; + double colourPeak = colourPeaks[i] * colour_skill_multiplier; double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier; if (isRelax) @@ -163,8 +167,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty staminaPeak /= 1.5; // Stamina difficulty is decreased with an increased available finger count. } - double peak = norm(1.5, colourPeak, staminaPeak); - peak = norm(2, peak, rhythmPeak); + double peak = DifficultyCalculationUtils.Norm(2, DifficultyCalculationUtils.Norm(1.5, colourPeak, staminaPeak), rhythmPeak); // Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). // These sections will not contribute to the difficulty. @@ -185,10 +188,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty } /// - /// Returns the p-norm of an n-dimensional vector. + /// Applies a final re-scaling of the star rating. /// - /// The value of p to calculate the norm for. - /// The coefficients of the vector. - private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p); + /// The raw star rating value before re-scaling. + private double rescale(double sr) + { + if (sr < 0) return sr; + + return 10.43 * Math.Log(sr / 8 + 1); + } } } diff --git a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs index b9efcd683d..df2d84d6f2 100644 --- a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; namespace osu.Game.Rulesets.Difficulty.Utils { @@ -46,5 +47,13 @@ namespace osu.Game.Rulesets.Difficulty.Utils /// Exponent /// The output of logistic function public static double Logistic(double exponent, double maxValue = 1) => maxValue / (1 + Math.Exp(exponent)); + + /// + /// Returns the p-norm of an n-dimensional vector (https://en.wikipedia.org/wiki/Norm_(mathematics)) + /// + /// The value of p to calculate the norm for. + /// The coefficients of the vector. + /// The p-norm of the vector. + public static double Norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p); } } From ecd6b4192816391591ca8e96b77d80fe7c1fa948 Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Fri, 20 Dec 2024 00:45:11 +1000 Subject: [PATCH 013/801] Increase `accscalingshift` and include `countok` in hit proportion (#31195) * revert acc scaling shift to previous values * increase variance in accuracy values across od * move return values, move nullcheck into return --------- Co-authored-by: James Wilson --- .../Difficulty/TaikoPerformanceCalculator.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index ed7d41bf72..a93f4c66ab 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty // Scale accuracy more harshly on nearly-completely mono (single coloured) speed maps. double accScalingExponent = 2 + attributes.MonoStaminaFactor; - double accScalingShift = 300 - 100 * attributes.MonoStaminaFactor; + double accScalingShift = 400 - 100 * attributes.MonoStaminaFactor; return difficultyValue * Math.Pow(SpecialFunctions.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent); } @@ -134,6 +134,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed). + double? deviationGreatWindow = calcDeviationGreatWindow(); + double? deviationGoodWindow = calcDeviationGoodWindow(); + + return deviationGreatWindow is null ? deviationGoodWindow : Math.Min(deviationGreatWindow.Value, deviationGoodWindow!.Value); + // The upper bound on deviation, calculated with the ratio of 300s to objects, and the great hit window. double? calcDeviationGreatWindow() { @@ -160,7 +165,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double n = totalHits; // Proportion of greats + goods hit. - double p = totalSuccessfulHits / n; + double p = Math.Max(0, totalSuccessfulHits - 0.0005 * countOk) / n; // We can be 99% confident that p is at least this value. double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4); @@ -168,14 +173,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty // We can be 99% confident that the deviation is not higher than: return h100 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); } - - double? deviationGreatWindow = calcDeviationGreatWindow(); - double? deviationGoodWindow = calcDeviationGoodWindow(); - - if (deviationGreatWindow is null) - return deviationGoodWindow; - - return Math.Min(deviationGreatWindow.Value, deviationGoodWindow!.Value); } private int totalHits => countGreat + countOk + countMeh + countMiss; From d8c3d899ebb4660b97301f9a5d07902bb4598cbe Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Fri, 20 Dec 2024 03:22:16 +1000 Subject: [PATCH 014/801] remove particular condition on convert nerf (#31196) Co-authored-by: James Wilson --- .../Difficulty/TaikoDifficultyCalculator.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 05081d471e..8f725d4f94 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -113,9 +113,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty // For maps with relax, multiple inputs are more likely to be abused. if (isRelax) starRating *= 0.60; - // For maps with either relax or low colour variance and high stamina requirement, multiple inputs are more likely to be abused. - else if (colourRating < 2 && staminaRating > 8) - starRating *= 0.80; } HitWindows hitWindows = new TaikoHitWindows(); From f722f94f26f0055f7a68bb867b60600aff6bac81 Mon Sep 17 00:00:00 2001 From: StanR Date: Sat, 21 Dec 2024 04:32:51 +0500 Subject: [PATCH 015/801] Simplify osu! high-bpm acute angle jumps bonus (#30902) * Simplify osu! high-bpm acute angle jumps bonus * Add aim wiggle bonus * Add hitwindow-based aim velocity decrease * Revert "Add hitwindow-based aim velocity decrease" This reverts commit bcebe9662cfcb7a72805e48712525ef54ec9820e. * Move wiggle multiplier to a const, slightly decrease acute bonus multiplier * Make sure the previous object in the wiggle bonus is also part of the wiggle * Scale the wiggle bonus multiplayer down * Increase the acute angle jump bonus multiplier * Make wiggle bonus only apply on >150 bpm streams, make repetitive angle penalty * Reduce wiggle bonus multiplier to not break velocity>difficulty relation * Adjust wiggle falloff function to fix stability issues * Adjust wiggle consts * Update tests --- .../OsuDifficultyCalculatorTest.cs | 6 ++-- .../Difficulty/Evaluators/AimEvaluator.cs | 33 ++++++++++++------- .../Utils/DifficultyCalculationUtils.cs | 24 ++++++++++++++ 3 files changed, 48 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index efda3fa369..9798611488 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,20 +15,20 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests"; - [TestCase(6.7171144000821119d, 239, "diffcalc-test")] + [TestCase(6.718709884850683d, 239, "diffcalc-test")] [TestCase(1.4485749025771304d, 54, "zero-length-sliders")] [TestCase(0.42630400627180914d, 4, "very-fast-slider")] [TestCase(0.14143808967817237d, 2, "nan-slider")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(8.9825709931204205d, 239, "diffcalc-test")] + [TestCase(9.4310274277499619d, 239, "diffcalc-test")] [TestCase(1.7550169162648608d, 54, "zero-length-sliders")] [TestCase(0.55231632896800109d, 4, "very-fast-slider")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); - [TestCase(6.7171144000821119d, 239, "diffcalc-test")] + [TestCase(6.718709884850683d, 239, "diffcalc-test")] [TestCase(1.4485749025771304d, 54, "zero-length-sliders")] [TestCase(0.42630400627180914d, 4, "very-fast-slider")] public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index 9816f6d0a4..c3270f25f8 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -12,9 +12,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators public static class AimEvaluator { private const double wide_angle_multiplier = 1.5; - private const double acute_angle_multiplier = 1.95; + private const double acute_angle_multiplier = 2.35; private const double slider_multiplier = 1.35; private const double velocity_change_multiplier = 0.75; + private const double wiggle_multiplier = 1.02; /// /// Evaluates the difficulty of aiming the current object, based on: @@ -64,6 +65,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double acuteAngleBonus = 0; double sliderBonus = 0; double velocityChangeBonus = 0; + double wiggleBonus = 0; double aimStrain = currVelocity; // Start strain with regular velocity. @@ -79,22 +81,27 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double angleBonus = Math.Min(currVelocity, prevVelocity); wideAngleBonus = calcWideAngleBonus(currAngle); - acuteAngleBonus = calcAcuteAngleBonus(currAngle); - if (DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2) < 300) // Only buff deltaTime exceeding 300 bpm 1/2. - acuteAngleBonus = 0; - else - { - acuteAngleBonus *= calcAcuteAngleBonus(lastAngle) // Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern. - * Math.Min(angleBonus, diameter * 1.25 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime - * Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, (100 - osuCurrObj.StrainTime) / 25)), 2) // scale buff from 150 bpm 1/4 to 200 bpm 1/4 - * Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.LazyJumpDistance, radius, diameter) - radius) / radius), 2); // Buff distance exceeding radius up to diameter. - } + // Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter + acuteAngleBonus = calcAcuteAngleBonus(currAngle) * + angleBonus * + DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2), 300, 400) * + DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, diameter, diameter * 2); // Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute. wideAngleBonus *= angleBonus * (1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3))); // Penalize acute angles if they're repeated, reducing the penalty as the lastLastAngle gets more obtuse. - acuteAngleBonus *= 0.5 + 0.5 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastLastAngle), 3))); + acuteAngleBonus *= 0.03 + 0.97 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastLastAngle), 3))); + + // Apply wiggle bonus for jumps that are [radius, 2*diameter] in distance, with < 110 angle and bpm > 150 + // https://www.desmos.com/calculator/iis7lgbppe + wiggleBonus = angleBonus + * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, radius, diameter) + * Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuCurrObj.LazyJumpDistance, diameter * 3, diameter), 1.8) + * DifficultyCalculationUtils.Smootherstep(currAngle, double.DegreesToRadians(110), double.DegreesToRadians(60)) + * DifficultyCalculationUtils.Smootherstep(osuLastObj.LazyJumpDistance, radius, diameter) + * Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuLastObj.LazyJumpDistance, diameter * 3, diameter), 1.8) + * DifficultyCalculationUtils.Smootherstep(lastAngle, double.DegreesToRadians(110), double.DegreesToRadians(60)); } } @@ -122,6 +129,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime; } + aimStrain += wiggleBonus * wiggle_multiplier; + // Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger. aimStrain += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier + velocityChangeBonus * velocity_change_multiplier); diff --git a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs index df2d84d6f2..055d8a458b 100644 --- a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs @@ -55,5 +55,29 @@ namespace osu.Game.Rulesets.Difficulty.Utils /// The coefficients of the vector. /// The p-norm of the vector. public static double Norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p); + + /// + /// Smootherstep function (https://en.wikipedia.org/wiki/Smoothstep#Variations) + /// + /// Value to calculate the function for + /// Value at which function returns 0 + /// Value at which function returns 1 + public static double Smootherstep(double x, double start, double end) + { + x = Math.Clamp((x - start) / (end - start), 0.0, 1.0); + + return x * x * x * (x * (6.0 * x - 15.0) + 10.0); + } + + /// + /// Reverse linear interpolation function (https://en.wikipedia.org/wiki/Linear_interpolation) + /// + /// Value to calculate the function for + /// Value at which function returns 0 + /// Value at which function returns 1 + public static double ReverseLerp(double x, double start, double end) + { + return Math.Clamp((x - start) / (end - start), 0.0, 1.0); + } } } From f6a36f7b2e1427f858b087052bfe7f3dc50b2ab2 Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Sat, 21 Dec 2024 20:19:14 +1000 Subject: [PATCH 016/801] Implement `Reading` Skill into osu!taiko (#31208) --- .../Difficulty/Evaluators/ReadingEvaluator.cs | 43 ++++++++++++++++ .../Preprocessing/Reading/EffectiveBPM.cs | 50 +++++++++++++++++++ .../Preprocessing/TaikoDifficultyHitObject.cs | 10 ++++ .../Difficulty/Skills/Reading.cs | 44 ++++++++++++++++ .../Difficulty/TaikoDifficultyAttributes.cs | 6 +++ .../Difficulty/TaikoDifficultyCalculator.cs | 22 +++++--- .../Difficulty/TaikoPerformanceCalculator.cs | 3 -- 7 files changed, 169 insertions(+), 9 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs new file mode 100644 index 0000000000..a6a1513842 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators +{ + public static class ReadingEvaluator + { + private readonly struct VelocityRange + { + public double Min { get; } + public double Max { get; } + public double Center => (Max + Min) / 2; + public double Range => Max - Min; + + public VelocityRange(double min, double max) + { + Min = min; + Max = max; + } + } + + /// + /// Calculates the influence of higher slider velocities on hitobject difficulty. + /// The bonus is determined based on the EffectiveBPM, shifting within a defined range + /// between the upper and lower boundaries to reflect how increased slider velocity impacts difficulty. + /// + /// The hit object to evaluate. + /// The reading difficulty value for the given hit object. + public static double EvaluateDifficultyOf(TaikoDifficultyHitObject noteObject) + { + double effectiveBPM = noteObject.EffectiveBPM; + + var highVelocity = new VelocityRange(480, 640); + var midVelocity = new VelocityRange(360, 480); + + return 1.0 * DifficultyCalculationUtils.Logistic(effectiveBPM, highVelocity.Center, 1.0 / (highVelocity.Range / 10)) + + 0.5 * DifficultyCalculationUtils.Logistic(effectiveBPM, midVelocity.Center, 1.0 / (midVelocity.Range / 10)); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs new file mode 100644 index 0000000000..17e05d5fbf --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs @@ -0,0 +1,50 @@ +// 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.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Reading +{ + public class EffectiveBPMPreprocessor + { + private readonly IList noteObjects; + private readonly double globalSliderVelocity; + + public EffectiveBPMPreprocessor(IBeatmap beatmap, List noteObjects) + { + this.noteObjects = noteObjects; + globalSliderVelocity = beatmap.Difficulty.SliderMultiplier; + } + + /// + /// Calculates and sets the effective BPM and slider velocity for each note object, considering clock rate and scroll speed. + /// + public void ProcessEffectiveBPM(ControlPointInfo controlPointInfo, double clockRate) + { + foreach (var currentNoteObject in noteObjects) + { + double startTime = currentNoteObject.StartTime * clockRate; + + // Retrieve the timing point at the note's start time + TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(startTime); + + // Calculate the slider velocity at the note's start time. + double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, startTime, clockRate); + currentNoteObject.CurrentSliderVelocity = currentSliderVelocity; + + currentNoteObject.EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity; + } + } + + /// + /// Calculates the slider velocity based on control point info and clock rate. + /// + private double calculateSliderVelocity(ControlPointInfo controlPointInfo, double startTime, double clockRate) + { + var activeEffectControlPoint = controlPointInfo.EffectPointAt(startTime); + return globalSliderVelocity * (activeEffectControlPoint.ScrollSpeed) * clockRate; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 4aaee50c18..e741e4c9e7 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -48,6 +48,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public readonly TaikoDifficultyHitObjectColour Colour; + /// + /// The adjusted BPM of this hit object, based on its slider velocity and scroll speed. + /// + public double EffectiveBPM; + + /// + /// The current slider velocity of this hit object. + /// + public double CurrentSliderVelocity; + /// /// Creates a new difficulty hit object. /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs new file mode 100644 index 0000000000..9de058f289 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs @@ -0,0 +1,44 @@ +// 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.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Objects; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Skills +{ + /// + /// Calculates the reading coefficient of taiko difficulty. + /// + public class Reading : StrainDecaySkill + { + protected override double SkillMultiplier => 1.0; + protected override double StrainDecayBase => 0.4; + + private double currentStrain; + + public Reading(Mod[] mods) + : base(mods) + { + } + + protected override double StrainValueOf(DifficultyHitObject current) + { + // Drum Rolls and Swells are exempt. + if (current.BaseObject is not Hit) + { + return 0.0; + } + + var taikoObject = (TaikoDifficultyHitObject)current; + + currentStrain *= StrainDecayBase; + currentStrain += ReadingEvaluator.EvaluateDifficultyOf(taikoObject) * SkillMultiplier; + + return currentStrain; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index 4a35c30e60..d3cdb379d5 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -28,6 +28,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("rhythm_difficulty")] public double RhythmDifficulty { get; set; } + /// + /// The difficulty corresponding to the reading skill. + /// + [JsonProperty("reading_difficulty")] + public double ReadingDifficulty { get; set; } + /// /// The difficulty corresponding to the colour skill. /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 8f725d4f94..0d6ecb8d3e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Reading; using osu.Game.Rulesets.Taiko.Difficulty.Skills; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Scoring; @@ -22,7 +23,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty public class TaikoDifficultyCalculator : DifficultyCalculator { private const double difficulty_multiplier = 0.084375; - private const double rhythm_skill_multiplier = 0.2 * difficulty_multiplier; + private const double rhythm_skill_multiplier = 0.200 * difficulty_multiplier; + private const double reading_skill_multiplier = 0.100 * difficulty_multiplier; private const double colour_skill_multiplier = 0.375 * difficulty_multiplier; private const double stamina_skill_multiplier = 0.375 * difficulty_multiplier; @@ -38,6 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty return new Skill[] { new Rhythm(mods), + new Reading(mods), new Colour(mods), new Stamina(mods, false), new Stamina(mods, true) @@ -58,6 +61,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty var centreObjects = new List(); var rimObjects = new List(); var noteObjects = new List(); + EffectiveBPMPreprocessor bpmLoader = new EffectiveBPMPreprocessor(beatmap, noteObjects); // Generate TaikoDifficultyHitObjects from the beatmap's hit objects. for (int i = 2; i < beatmap.HitObjects.Count; i++) @@ -76,6 +80,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty } TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects); + bpmLoader.ProcessEffectiveBPM(beatmap.ControlPointInfo, clockRate); return difficultyHitObjects; } @@ -88,11 +93,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty bool isRelax = mods.Any(h => h is TaikoModRelax); Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm); + Reading reading = (Reading)skills.First(x => x is Reading); Colour colour = (Colour)skills.First(x => x is Colour); Stamina stamina = (Stamina)skills.First(x => x is Stamina); Stamina singleColourStamina = (Stamina)skills.Last(x => x is Stamina); double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier; + double readingRating = reading.DifficultyValue() * reading_skill_multiplier; double colourRating = colour.DifficultyValue() * colour_skill_multiplier; double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier; double monoStaminaRating = singleColourStamina.DifficultyValue() * stamina_skill_multiplier; @@ -102,13 +109,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double colourDifficultStrains = colour.CountTopWeightedStrains(); double staminaDifficultStrains = stamina.CountTopWeightedStrains(); - double combinedRating = combinedDifficultyValue(rhythm, colour, stamina, isRelax); + double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax); double starRating = rescale(combinedRating * 1.4); // Converts are penalised outside the scope of difficulty calculation, as our assumptions surrounding standard play-styles becomes out-of-scope. if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0) { - starRating *= 0.925; + starRating *= 0.825; // For maps with relax, multiple inputs are more likely to be abused. if (isRelax) @@ -123,6 +130,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty StarRating = starRating, Mods = mods, RhythmDifficulty = rhythmRating, + ReadingDifficulty = readingRating, ColourDifficulty = colourRating, StaminaDifficulty = staminaRating, MonoStaminaFactor = monoStaminaFactor, @@ -144,17 +152,19 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// For each section, the peak strains of all separate skills are combined into a single peak strain for the section. /// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more). /// - private double combinedDifficultyValue(Rhythm rhythm, Colour colour, Stamina stamina, bool isRelax) + private double combinedDifficultyValue(Rhythm rhythm, Reading reading, Colour colour, Stamina stamina, bool isRelax) { List peaks = new List(); - var colourPeaks = colour.GetCurrentStrainPeaks().ToList(); var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList(); + var readingPeaks = reading.GetCurrentStrainPeaks().ToList(); + var colourPeaks = colour.GetCurrentStrainPeaks().ToList(); var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList(); for (int i = 0; i < colourPeaks.Count; i++) { double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier; + double readingPeak = readingPeaks[i] * reading_skill_multiplier; double colourPeak = colourPeaks[i] * colour_skill_multiplier; double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier; @@ -164,7 +174,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty staminaPeak /= 1.5; // Stamina difficulty is decreased with an increased available finger count. } - double peak = DifficultyCalculationUtils.Norm(2, DifficultyCalculationUtils.Norm(1.5, colourPeak, staminaPeak), rhythmPeak); + double peak = DifficultyCalculationUtils.Norm(2, DifficultyCalculationUtils.Norm(1.5, colourPeak, staminaPeak), rhythmPeak, readingPeak); // Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). // These sections will not contribute to the difficulty. diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index a93f4c66ab..5da18e7963 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -87,9 +87,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (score.Mods.Any(m => m is ModHidden)) difficultyValue *= 1.025; - if (score.Mods.Any(m => m is ModHardRock)) - difficultyValue *= 1.10; - if (score.Mods.Any(m => m is ModFlashlight)) difficultyValue *= Math.Max(1, 1.050 - Math.Min(attributes.MonoStaminaFactor / 50, 1) * lengthBonus); From 6808a5a77cffdb5800fc6443823bcad80283f549 Mon Sep 17 00:00:00 2001 From: StanR Date: Sun, 22 Dec 2024 04:45:29 +0500 Subject: [PATCH 017/801] Change slider drop penalty to use actual number of difficult sliders, fix slider drop penalty being too lenient (#31055) * Change slider drop penalty to use actual number of difficult sliders, fix slider nerf being too lenient * Move cubing to performance calculation * Add separate list for slider strains * Rename difficulty atttribute * Rename attribute in perfcalc * Check if AimDifficultSliderCount is more than 0, code quality fixes * Add `AimDifficultSliderCount` to the list of databased attributes * Code quality --------- Co-authored-by: James Wilson --- .../Difficulty/OsuDifficultyAttributes.cs | 9 ++++ .../Difficulty/OsuDifficultyCalculator.cs | 3 +- .../Difficulty/OsuPerformanceCalculator.cs | 49 +++++++++---------- .../Difficulty/Skills/Aim.cs | 24 +++++++++ .../Difficulty/DifficultyAttributes.cs | 1 + 5 files changed, 60 insertions(+), 26 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index a3c0209a08..3b9a23df23 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -8,6 +8,7 @@ using Newtonsoft.Json; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Difficulty { @@ -19,6 +20,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty [JsonProperty("aim_difficulty")] public double AimDifficulty { get; set; } + /// + /// The number of s weighted by difficulty. + /// + [JsonProperty("aim_difficult_slider_count")] + public double AimDifficultSliderCount { get; set; } + /// /// The difficulty corresponding to the speed skill. /// @@ -109,6 +116,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty yield return (ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT, AimDifficultStrainCount); yield return (ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount); yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount); + yield return (ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT, AimDifficultSliderCount); } public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) @@ -125,6 +133,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty AimDifficultStrainCount = values[ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT]; SpeedDifficultStrainCount = values[ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT]; SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT]; + AimDifficultSliderCount = values[ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT]; DrainRate = onlineInfo.DrainRate; HitCircleCount = onlineInfo.CircleCount; SliderCount = onlineInfo.SliderCount; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 575e03051c..ffdd4673e3 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double aimRatingNoSliders = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier; double speedRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier; double speedNotes = ((Speed)skills[2]).RelevantNoteCount(); - + double difficultSliders = ((Aim)skills[0]).GetDifficultSliders(); double flashlightRating = 0.0; if (mods.Any(h => h is OsuModFlashlight)) @@ -99,6 +99,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty StarRating = starRating, Mods = mods, AimDifficulty = aimRating, + AimDifficultSliderCount = difficultSliders, SpeedDifficulty = speedRating, SpeedNoteCount = speedNotes, FlashlightDifficulty = flashlightRating, diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 31b00dba2b..3610845533 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -135,7 +135,30 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes) { - double aimValue = OsuStrainSkill.DifficultyToPerformance(attributes.AimDifficulty); + double aimDifficulty = attributes.AimDifficulty; + + if (attributes.SliderCount > 0 && attributes.AimDifficultSliderCount > 0) + { + double estimateImproperlyFollowedDifficultSliders; + + if (usingClassicSliderAccuracy) + { + // When the score is considered classic (regardless if it was made on old client or not) we consider all missing combo to be dropped difficult sliders + int maximumPossibleDroppedSliders = totalImperfectHits; + estimateImproperlyFollowedDifficultSliders = Math.Clamp(Math.Min(maximumPossibleDroppedSliders, attributes.MaxCombo - scoreMaxCombo), 0, attributes.AimDifficultSliderCount); + } + else + { + // We add tick misses here since they too mean that the player didn't follow the slider properly + // We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly + estimateImproperlyFollowedDifficultSliders = Math.Min(countSliderEndsDropped + countSliderTickMiss, attributes.AimDifficultSliderCount); + } + + double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / attributes.AimDifficultSliderCount, 3) + attributes.SliderFactor; + aimDifficulty *= sliderNerfFactor; + } + + double aimValue = OsuStrainSkill.DifficultyToPerformance(aimDifficulty); double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); @@ -163,30 +186,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); } - // We assume 15% of sliders in a map are difficult since there's no way to tell from the performance calculator. - double estimateDifficultSliders = attributes.SliderCount * 0.15; - - if (attributes.SliderCount > 0) - { - double estimateImproperlyFollowedDifficultSliders; - - if (usingClassicSliderAccuracy) - { - // When the score is considered classic (regardless if it was made on old client or not) we consider all missing combo to be dropped difficult sliders - int maximumPossibleDroppedSliders = totalImperfectHits; - estimateImproperlyFollowedDifficultSliders = Math.Clamp(Math.Min(maximumPossibleDroppedSliders, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders); - } - else - { - // We add tick misses here since they too mean that the player didn't follow the slider properly - // We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly - estimateImproperlyFollowedDifficultSliders = Math.Clamp(countSliderEndsDropped + countSliderTickMiss, 0, estimateDifficultSliders); - } - - double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / estimateDifficultSliders, 3) + attributes.SliderFactor; - aimValue *= sliderNerfFactor; - } - aimValue *= accuracy; // It is important to consider accuracy difficulty when scaling with accuracy. aimValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index faf91e4652..400bc97fbc 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -2,9 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Difficulty.Evaluators; +using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Difficulty.Skills { @@ -26,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double skillMultiplier => 25.18; private double strainDecayBase => 0.15; + private readonly List sliderStrains = new List(); + private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => currentStrain * strainDecay(time - current.Previous(0).StartTime); @@ -35,7 +40,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills currentStrain *= strainDecay(current.DeltaTime); currentStrain += AimEvaluator.EvaluateDifficultyOf(current, withSliders) * skillMultiplier; + if (current.BaseObject is Slider) + { + sliderStrains.Add(currentStrain); + } + return currentStrain; } + + public double GetDifficultSliders() + { + if (sliderStrains.Count == 0) + return 0; + + double[] sortedStrains = sliderStrains.OrderDescending().ToArray(); + + double maxSliderStrain = sortedStrains.Max(); + if (maxSliderStrain == 0) + return 0; + + return sortedStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxSliderStrain * 12.0 - 6.0)))); + } } } diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index 7b6bc37a61..f5ed5a180b 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -30,6 +30,7 @@ namespace osu.Game.Rulesets.Difficulty protected const int ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT = 25; protected const int ATTRIB_ID_OK_HIT_WINDOW = 27; protected const int ATTRIB_ID_MONO_STAMINA_FACTOR = 29; + protected const int ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT = 31; /// /// The mods which were applied to the beatmap. From fa5922337da11583469482d26b8b4043badc8574 Mon Sep 17 00:00:00 2001 From: Plextora <71889427+Plextora@users.noreply.github.com> Date: Sat, 21 Dec 2024 21:17:03 -0500 Subject: [PATCH 018/801] Fail on slider tail miss option in Sudden Death --- osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs index e661610fe7..f781bf0b90 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs @@ -3,7 +3,12 @@ using System; using System.Linq; +using osu.Framework.Bindables; +using osu.Game.Configuration; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Mods { @@ -13,5 +18,16 @@ namespace osu.Game.Rulesets.Osu.Mods { typeof(OsuModTargetPractice), }).ToArray(); + + [SettingSource("Fail on slider tail miss", "Fail when missing on the end of a slider")] + public BindableBool SliderTailMiss { get; } = new BindableBool(); + + protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) + { + if (SliderTailMiss.Value && result.HitObject is SliderTailCircle && result.Type == HitResult.IgnoreMiss) + return true; + + return result.Type.AffectsCombo() && !result.IsHit; + } } } From 87697a72e333d1468a35d4a3fec388319cc16e2a Mon Sep 17 00:00:00 2001 From: Plextora <71889427+Plextora@users.noreply.github.com> Date: Sat, 21 Dec 2024 21:32:09 -0500 Subject: [PATCH 019/801] Rename to PFC mode --- osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs index f781bf0b90..3a65ba3b10 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods typeof(OsuModTargetPractice), }).ToArray(); - [SettingSource("Fail on slider tail miss", "Fail when missing on the end of a slider")] + [SettingSource("PFC mode", "Fail when missing on a slider tail")] public BindableBool SliderTailMiss { get; } = new BindableBool(); protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) From 420c5577d3a8aef97af82158110211c72cf5f8aa Mon Sep 17 00:00:00 2001 From: Plextora <71889427+Plextora@users.noreply.github.com> Date: Sat, 21 Dec 2024 22:55:30 -0500 Subject: [PATCH 020/801] Rename option (again) --- osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs index 3a65ba3b10..fb587a94ca 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods typeof(OsuModTargetPractice), }).ToArray(); - [SettingSource("PFC mode", "Fail when missing on a slider tail")] + [SettingSource("Fail when missing on a slider tail")] public BindableBool SliderTailMiss { get; } = new BindableBool(); protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) From 5c9278ee2f5c044e0b6565973497b559f620ab5e Mon Sep 17 00:00:00 2001 From: Plextora <71889427+Plextora@users.noreply.github.com> Date: Sat, 21 Dec 2024 22:56:42 -0500 Subject: [PATCH 021/801] One line return for FailCondition --- osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs index fb587a94ca..a90d44c473 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs @@ -22,12 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods [SettingSource("Fail when missing on a slider tail")] public BindableBool SliderTailMiss { get; } = new BindableBool(); - protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) - { - if (SliderTailMiss.Value && result.HitObject is SliderTailCircle && result.Type == HitResult.IgnoreMiss) - return true; - - return result.Type.AffectsCombo() && !result.IsHit; - } + protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) => ( + SliderTailMiss.Value && result.HitObject is SliderTailCircle && result.Type == HitResult.IgnoreMiss) || (result.Type.AffectsCombo() && !result.IsHit); } } From 047c448741a6c2ab038a04085ebab97048e8473d Mon Sep 17 00:00:00 2001 From: Plextora <71889427+Plextora@users.noreply.github.com> Date: Sun, 22 Dec 2024 12:09:27 -0500 Subject: [PATCH 022/801] Return base for default FailCondition --- osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs index a90d44c473..ea32b4868a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods [SettingSource("Fail when missing on a slider tail")] public BindableBool SliderTailMiss { get; } = new BindableBool(); - protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) => ( - SliderTailMiss.Value && result.HitObject is SliderTailCircle && result.Type == HitResult.IgnoreMiss) || (result.Type.AffectsCombo() && !result.IsHit); + protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) => + (SliderTailMiss.Value && result.HitObject is SliderTailCircle && result.Type == HitResult.IgnoreMiss) || base.FailCondition(healthProcessor, result); } } From fd1cc34e3fd01747eb132c04b831a22429be7c99 Mon Sep 17 00:00:00 2001 From: Plextora <71889427+Plextora@users.noreply.github.com> Date: Sun, 22 Dec 2024 17:46:01 -0500 Subject: [PATCH 023/801] No more one line return for FailCondition --- osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs index ea32b4868a..73d0403e3f 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs @@ -22,7 +22,12 @@ namespace osu.Game.Rulesets.Osu.Mods [SettingSource("Fail when missing on a slider tail")] public BindableBool SliderTailMiss { get; } = new BindableBool(); - protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) => - (SliderTailMiss.Value && result.HitObject is SliderTailCircle && result.Type == HitResult.IgnoreMiss) || base.FailCondition(healthProcessor, result); + protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) + { + if (SliderTailMiss.Value && result.HitObject is SliderTailCircle && result.Type == HitResult.IgnoreMiss) + return true; + + return base.FailCondition(healthProcessor, result); + } } } From 638d959c5cc3fdcdb6d070eb976191e2b6f734ec Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 23 Dec 2024 20:12:25 +0900 Subject: [PATCH 024/801] 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(); From 7777c447754a0bcfd64036681175712528c5d454 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 24 Dec 2024 17:57:59 +0900 Subject: [PATCH 025/801] Only allow selecting beatmaps within 30s length --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 8 ++++---- .../Multiplayer/MultiplayerMatchStyleSelect.cs | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index c9e0cbc1e9..49144f9de5 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -517,7 +517,7 @@ namespace osu.Game.Screens.OnlinePlay.Match { AllowReordering = false, AllowEditing = true, - RequestEdit = openStyleSelection + RequestEdit = _ => openStyleSelection() }; } @@ -541,12 +541,12 @@ namespace osu.Game.Screens.OnlinePlay.Match return selectedItemWithOverride; } - private void openStyleSelection(PlaylistItem item) + private void openStyleSelection() { - if (!this.IsCurrentScreen()) + if (SelectedItem.Value == null || !this.IsCurrentScreen()) return; - this.Push(new MultiplayerMatchStyleSelect(Room, item, (beatmap, ruleset) => + this.Push(new MultiplayerMatchStyleSelect(Room, SelectedItem.Value, (beatmap, ruleset) => { if (SelectedItem.Value?.BeatmapSetId == null || SelectedItem.Value.BeatmapSetId != beatmap.BeatmapSet?.OnlineID) return; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs index dc1393bf96..19d8b96f2b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Screens.Select; @@ -67,16 +68,33 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private partial class DifficultySelectFilterControl : FilterControl { private readonly PlaylistItem item; + private double itemLength; public DifficultySelectFilterControl(PlaylistItem item) { this.item = item; } + [BackgroundDependencyLoader] + private void load(RealmAccess realm) + { + int beatmapId = item.Beatmap.OnlineID; + itemLength = realm.Run(r => r.All().FirstOrDefault(b => b.OnlineID == beatmapId)?.Length ?? 0); + } + public override FilterCriteria CreateCriteria() { var criteria = base.CreateCriteria(); + + // Must be from the same set as the playlist item. criteria.BeatmapSetId = item.BeatmapSetId; + + // Must be within 30s of the playlist item. + criteria.Length.Min = itemLength - 30000; + criteria.Length.Max = itemLength + 30000; + criteria.Length.IsLowerInclusive = true; + criteria.Length.IsUpperInclusive = true; + return criteria; } } From 40486c4f38bfd60099c30fc3d20fcd148123c605 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 24 Dec 2024 18:04:36 +0900 Subject: [PATCH 026/801] Block beatmap presents in style select screen --- .../OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs index 19d8b96f2b..867579171d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs @@ -18,7 +18,7 @@ using osu.Game.Users; namespace osu.Game.Screens.OnlinePlay.Multiplayer { - public partial class MultiplayerMatchStyleSelect : SongSelect, IOnlinePlaySubScreen + public partial class MultiplayerMatchStyleSelect : SongSelect, IOnlinePlaySubScreen, IHandlePresentBeatmap { public string ShortTitle => "style selection"; @@ -65,6 +65,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return true; } + public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset) + { + // This screen cannot present beatmaps. + } + private partial class DifficultySelectFilterControl : FilterControl { private readonly PlaylistItem item; From 3ddeaf8460476e8e8c8386e584addd8f9594d0d1 Mon Sep 17 00:00:00 2001 From: James Wilson Date: Tue, 24 Dec 2024 09:43:44 +0000 Subject: [PATCH 027/801] Use `lastAngle` when nerfing repeated angles on acute bonus (#31245) * Use `lastAngle` when nerfing repeated angles on acute bonus * Bump acute multiplier * Correct outdated wiggle bonus comment * Update test --------- Co-authored-by: StanR --- .../OsuDifficultyCalculatorTest.cs | 2 +- .../Difficulty/Evaluators/AimEvaluator.cs | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index 9798611488..c0a6d3a755 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Tests public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(9.4310274277499619d, 239, "diffcalc-test")] + [TestCase(9.6343245007055653d, 239, "diffcalc-test")] [TestCase(1.7550169162648608d, 54, "zero-length-sliders")] [TestCase(0.55231632896800109d, 4, "very-fast-slider")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index c3270f25f8..fdf94719ed 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators public static class AimEvaluator { private const double wide_angle_multiplier = 1.5; - private const double acute_angle_multiplier = 2.35; + private const double acute_angle_multiplier = 2.7; private const double slider_multiplier = 1.35; private const double velocity_change_multiplier = 0.75; private const double wiggle_multiplier = 1.02; @@ -75,7 +75,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators { double currAngle = osuCurrObj.Angle.Value; double lastAngle = osuLastObj.Angle.Value; - double lastLastAngle = osuLastLastObj.Angle.Value; // Rewarding angles, take the smaller velocity as base. double angleBonus = Math.Min(currVelocity, prevVelocity); @@ -90,11 +89,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute. wideAngleBonus *= angleBonus * (1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3))); - // Penalize acute angles if they're repeated, reducing the penalty as the lastLastAngle gets more obtuse. - acuteAngleBonus *= 0.03 + 0.97 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastLastAngle), 3))); + // Penalize acute angles if they're repeated, reducing the penalty as the lastAngle gets more obtuse. + acuteAngleBonus *= 0.03 + 0.97 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); - // Apply wiggle bonus for jumps that are [radius, 2*diameter] in distance, with < 110 angle and bpm > 150 - // https://www.desmos.com/calculator/iis7lgbppe + // Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle + // https://www.desmos.com/calculator/dp0v0nvowc wiggleBonus = angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, radius, diameter) * Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuCurrObj.LazyJumpDistance, diameter * 3, diameter), 1.8) From 971ccb6a4e6a93b44e8bc17eb1ad577e334e6e6c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 24 Dec 2024 19:05:50 +0900 Subject: [PATCH 028/801] Adjust namings --- ...rButtonFreePlay.cs => FooterButtonFreeStyle.cs} | 6 +++--- .../Screens/OnlinePlay/OnlinePlaySongSelect.cs | 14 +++++++------- .../OnlinePlay/Playlists/PlaylistsSongSelect.cs | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) rename osu.Game/Screens/OnlinePlay/{FooterButtonFreePlay.cs => FooterButtonFreeStyle.cs} (95%) diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs similarity index 95% rename from osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs rename to osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs index bcc7bb787d..5edcddcb78 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs @@ -15,7 +15,7 @@ using osu.Game.Screens.Select; namespace osu.Game.Screens.OnlinePlay { - public class FooterButtonFreePlay : FooterButton, IHasCurrentValue + public class FooterButtonFreeStyle : FooterButton, IHasCurrentValue { private readonly BindableWithCurrent current = new BindableWithCurrent(); @@ -33,7 +33,7 @@ namespace osu.Game.Screens.OnlinePlay [Resolved] private OsuColour colours { get; set; } = null!; - public FooterButtonFreePlay() + public FooterButtonFreeStyle() { // Overwrite any external behaviour as we delegate the main toggle action to a sub-button. base.Action = () => current.Value = !current.Value; @@ -71,7 +71,7 @@ namespace osu.Game.Screens.OnlinePlay SelectedColour = colours.Yellow; DeselectedColour = SelectedColour.Opacity(0.5f); - Text = @"freeplay"; + Text = @"freestyle"; } protected override void LoadComplete() diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 1f1d259d0a..02f8c619a7 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.OnlinePlay protected override UserActivity InitialActivity => new UserActivity.InLobby(room); protected readonly Bindable> FreeMods = new Bindable>(Array.Empty()); - protected readonly Bindable FreePlay = new Bindable(); + protected readonly Bindable FreeStyle = new Bindable(); private readonly Room room; private readonly PlaylistItem? initialItem; @@ -112,17 +112,17 @@ namespace osu.Game.Screens.OnlinePlay } if (initialItem.BeatmapSetId != null) - FreePlay.Value = true; + FreeStyle.Value = true; } Mods.BindValueChanged(onModsChanged); Ruleset.BindValueChanged(onRulesetChanged); - FreePlay.BindValueChanged(onFreePlayChanged, true); + FreeStyle.BindValueChanged(onFreeStyleChanged, true); freeModSelectOverlayRegistration = OverlayManager?.RegisterBlockingOverlay(freeModSelect); } - private void onFreePlayChanged(ValueChangedEvent enabled) + private void onFreeStyleChanged(ValueChangedEvent enabled) { if (enabled.NewValue) { @@ -162,7 +162,7 @@ namespace osu.Game.Screens.OnlinePlay RulesetID = Ruleset.Value.OnlineID, RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(), - BeatmapSetId = FreePlay.Value ? Beatmap.Value.BeatmapSetInfo.OnlineID : null + BeatmapSetId = FreeStyle.Value ? Beatmap.Value.BeatmapSetInfo.OnlineID : null }; return SelectItem(item); @@ -202,12 +202,12 @@ namespace osu.Game.Screens.OnlinePlay var baseButtons = base.CreateSongSelectFooterButtons().ToList(); freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) { Current = FreeMods }; - var freePlayButton = new FooterButtonFreePlay { Current = FreePlay }; + var freeStyleButton = new FooterButtonFreeStyle { Current = FreeStyle }; baseButtons.InsertRange(baseButtons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[] { (freeModsFooterButton, freeModSelect), - (freePlayButton, null) + (freeStyleButton, null) }); return baseButtons; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs index f9e014a727..a3b8a1575e 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs @@ -37,7 +37,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private PlaylistItem createNewItem() => new PlaylistItem(Beatmap.Value.BeatmapInfo) { ID = room.Playlist.Count == 0 ? 0 : room.Playlist.Max(p => p.ID) + 1, - BeatmapSetId = FreePlay.Value ? Beatmap.Value.BeatmapSetInfo.OnlineID : null, + BeatmapSetId = FreeStyle.Value ? Beatmap.Value.BeatmapSetInfo.OnlineID : null, RulesetID = Ruleset.Value.OnlineID, RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(), From ac738f109ad4eb6ebf1790a26d031e3d8a738d85 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 24 Dec 2024 19:28:09 +0900 Subject: [PATCH 029/801] Add style selection to playlists screen --- .../Playlists/PlaylistsRoomSubScreen.cs | 70 +++++++++++++------ 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 9573155f5a..98667c16fb 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -171,39 +171,63 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { new[] { - UserModsSection = new FillFlowContainer + new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Alpha = 0, Margin = new MarginPadding { Bottom = 10 }, - 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, + Margin = new MarginPadding { Bottom = 10 }, 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 + } + } + }, } }, }, From d8ff5bcacbb4460de8d51ff674b16f6a9aeba3b7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 24 Dec 2024 19:39:56 +0900 Subject: [PATCH 030/801] Fix freemods button opening overlay unexpectedly --- osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 02f8c619a7..a91f43635b 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -206,7 +206,7 @@ namespace osu.Game.Screens.OnlinePlay baseButtons.InsertRange(baseButtons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[] { - (freeModsFooterButton, freeModSelect), + (freeModsFooterButton, null), (freeStyleButton, null) }); From c88e906cb69bbc17c826fc1c9c0860cb64adc069 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 24 Dec 2024 19:40:06 +0900 Subject: [PATCH 031/801] Add some comments --- osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs | 4 ++++ osu.Game/Online/Rooms/PlaylistItem.cs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index 027d5b4a17..4a15fd9690 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -56,6 +56,10 @@ namespace osu.Game.Online.Rooms [Key(10)] public double StarRating { get; set; } + /// + /// A non-null value indicates "freestyle" mode where players are able to individually select + /// their own choice of beatmap (from the respective beatmap set) and ruleset to play in the room. + /// [Key(11)] public int? BeatmapSetID { get; set; } diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 937bc40e9b..16c252befc 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -67,6 +67,10 @@ namespace osu.Game.Online.Rooms set => Beatmap = new APIBeatmap { OnlineID = value }; } + /// + /// A non-null value indicates "freestyle" mode where players are able to individually select + /// their own choice of beatmap (from the respective beatmap set) and ruleset to play in the room. + /// [JsonProperty("beatmapset_id")] public int? BeatmapSetId { get; set; } From b4f35f330ce215cd9aa7049d3ceb9e5e75fb2b8f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 24 Dec 2024 20:13:35 +0900 Subject: [PATCH 032/801] Use online ruleset_id to build local score models --- osu.Game/Online/Rooms/MultiplayerScore.cs | 11 +++++++---- .../DailyChallenge/DailyChallengeLeaderboard.cs | 4 ++-- .../OnlinePlay/Playlists/PlaylistItemResultsScreen.cs | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game/Online/Rooms/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs index faa66c571d..2adee26da3 100644 --- a/osu.Game/Online/Rooms/MultiplayerScore.cs +++ b/osu.Game/Online/Rooms/MultiplayerScore.cs @@ -77,11 +77,14 @@ namespace osu.Game.Online.Rooms [CanBeNull] public MultiplayerScoresAround ScoresAround { get; set; } - public ScoreInfo CreateScoreInfo(ScoreManager scoreManager, RulesetStore rulesets, PlaylistItem playlistItem, [NotNull] BeatmapInfo beatmap) + [JsonProperty("ruleset_id")] + public int RulesetId { get; set; } + + public ScoreInfo CreateScoreInfo(ScoreManager scoreManager, RulesetStore rulesets, [NotNull] BeatmapInfo beatmap) { - var ruleset = rulesets.GetRuleset(playlistItem.RulesetID); + var ruleset = rulesets.GetRuleset(RulesetId); if (ruleset == null) - throw new InvalidOperationException($"Couldn't create score with unknown ruleset: {playlistItem.RulesetID}"); + throw new InvalidOperationException($"Couldn't create score with unknown ruleset: {RulesetId}"); var rulesetInstance = ruleset.CreateInstance(); @@ -91,7 +94,7 @@ namespace osu.Game.Online.Rooms TotalScore = TotalScore, MaxCombo = MaxCombo, BeatmapInfo = beatmap, - Ruleset = rulesets.GetRuleset(playlistItem.RulesetID) ?? throw new InvalidOperationException($"Ruleset with ID of {playlistItem.RulesetID} not found locally"), + Ruleset = ruleset, Passed = Passed, Statistics = Statistics, MaximumStatistics = MaximumStatistics, diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs index 9fe2b70a5a..4736ba28db 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs @@ -142,10 +142,10 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge request.Success += req => Schedule(() => { - var best = req.Scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, playlistItem, beatmap.Value.BeatmapInfo)).ToArray(); + var best = req.Scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, beatmap.Value.BeatmapInfo)).ToArray(); userBestScore.Value = req.UserScore; - var userBest = userBestScore.Value?.CreateScoreInfo(scoreManager, rulesets, playlistItem, beatmap.Value.BeatmapInfo); + var userBest = userBestScore.Value?.CreateScoreInfo(scoreManager, rulesets, beatmap.Value.BeatmapInfo); cancellationTokenSource?.Cancel(); cancellationTokenSource = null; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index 81ae51bd1b..13ef5d6f64 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -189,7 +189,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// An optional pivot around which the scores were retrieved. protected virtual ScoreInfo[] PerformSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null) { - var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, PlaylistItem, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray(); + var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray(); // Invoke callback to add the scores. Exclude the score provided to this screen since it's added already. callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID)); From a2dc16f8dffab2521b83d154cdcecb8d6baa48c1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 24 Dec 2024 20:22:16 +0900 Subject: [PATCH 033/801] Fix inspection --- osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs index 5edcddcb78..cdfb73cee1 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs @@ -15,7 +15,7 @@ using osu.Game.Screens.Select; namespace osu.Game.Screens.OnlinePlay { - public class FooterButtonFreeStyle : FooterButton, IHasCurrentValue + public partial class FooterButtonFreeStyle : FooterButton, IHasCurrentValue { private readonly BindableWithCurrent current = new BindableWithCurrent(); From a407e3f3e04d5765d8678970c83e4fb13b04f513 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Dec 2024 16:46:02 +0900 Subject: [PATCH 034/801] Fix co-variant array conversion --- osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 98667c16fb..48d50d727b 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -169,7 +169,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists RelativeSizeAxes = Axes.Both, Content = new[] { - new[] + new Drawable[] { new Container { From 95fe8d67e4fb899eec812e28a30528f145617caf Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Dec 2024 16:51:50 +0900 Subject: [PATCH 035/801] Fix test --- .../Multiplayer/TestSceneMultiplayerMatchSubScreen.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 8ea52f8099..e95209f993 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -30,6 +30,7 @@ using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; +using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist; using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; @@ -271,7 +272,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("last playlist item selected", () => { - var lastItem = this.ChildrenOfType().Single(p => p.Item.ID == MultiplayerClient.ServerAPIRoom?.Playlist.Last().ID); + var lastItem = this.ChildrenOfType() + .Single() + .ChildrenOfType() + .Single(p => p.Item.ID == MultiplayerClient.ServerAPIRoom?.Playlist.Last().ID); return lastItem.IsSelectedItem; }); } From 0093af8f5595bb28b8f39fc5faa2b96bf658ea5f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Dec 2024 22:24:21 +0900 Subject: [PATCH 036/801] Rewrite everything to better support spectator server messaging --- .../Online/Multiplayer/IMultiplayerClient.cs | 8 + .../Multiplayer/IMultiplayerRoomServer.cs | 7 + .../Online/Multiplayer/MultiplayerClient.cs | 21 ++ .../Online/Multiplayer/MultiplayerRoomUser.cs | 18 +- .../Multiplayer/OnlineMultiplayerClient.cs | 11 + .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 228 +++++++++--------- .../MultiplayerMatchStyleSelect.cs | 130 +++++----- .../Multiplayer/MultiplayerMatchSubScreen.cs | 37 ++- .../OnlinePlay/OnlinePlayStyleSelect.cs | 98 ++++++++ .../Playlists/PlaylistsRoomStyleSelect.cs | 30 +++ .../Playlists/PlaylistsRoomSubScreen.cs | 14 +- .../Multiplayer/TestMultiplayerClient.cs | 17 ++ 12 files changed, 417 insertions(+), 202 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs create mode 100644 osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs index 0452d8b79c..adb9b92614 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs @@ -95,6 +95,14 @@ namespace osu.Game.Online.Multiplayer /// The new beatmap availability state of the user. Task UserBeatmapAvailabilityChanged(int userId, BeatmapAvailability beatmapAvailability); + /// + /// Signals that a user in this room changed their style. + /// + /// The ID of the user whose style changed. + /// The user's beatmap. + /// The user's ruleset. + Task UserStyleChanged(int userId, int? beatmapId, int? rulesetId); + /// /// Signals that a user in this room changed their local mods. /// diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index 55f00b447f..490973faa2 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -57,6 +57,13 @@ namespace osu.Game.Online.Multiplayer /// The proposed new beatmap availability state. Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability); + /// + /// Change the local user's style in the currently joined room. + /// + /// The beatmap. + /// The ruleset. + Task ChangeUserStyle(int? beatmapId, int? rulesetId); + /// /// Change the local user's mods in the currently joined room. /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 998a34931d..a588ec4441 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -359,6 +359,8 @@ namespace osu.Game.Online.Multiplayer public abstract Task DisconnectInternal(); + public abstract Task ChangeUserStyle(int? beatmapId, int? rulesetId); + /// /// Change the local user's mods in the currently joined room. /// @@ -652,6 +654,25 @@ namespace osu.Game.Online.Multiplayer return Task.CompletedTask; } + public Task UserStyleChanged(int userId, int? beatmapId, int? rulesetId) + { + Scheduler.Add(() => + { + var user = Room?.Users.SingleOrDefault(u => u.UserID == userId); + + // errors here are not critical - user style is mostly for display. + if (user == null) + return; + + user.BeatmapId = beatmapId; + user.RulesetId = rulesetId; + + RoomUpdated?.Invoke(); + }, false); + + return Task.CompletedTask; + } + public Task UserModsChanged(int userId, IEnumerable mods) { Scheduler.Add(() => diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs index f769b4c805..8142873fd5 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs @@ -22,9 +22,6 @@ namespace osu.Game.Online.Multiplayer [Key(1)] public MultiplayerUserState State { get; set; } = MultiplayerUserState.Idle; - [Key(4)] - public MatchUserState? MatchState { get; set; } - /// /// The availability state of the current beatmap. /// @@ -37,6 +34,21 @@ namespace osu.Game.Online.Multiplayer [Key(3)] public IEnumerable Mods { get; set; } = Enumerable.Empty(); + [Key(4)] + public MatchUserState? MatchState { get; set; } + + /// + /// Any ruleset applicable only to the local user. + /// + [Key(5)] + public int? RulesetId; + + /// + /// Any beatmap applicable only to the local user. + /// + [Key(6)] + public int? BeatmapId; + [IgnoreMember] public APIUser? User { get; set; } diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 40436d730e..2660cd94e4 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -60,6 +60,7 @@ namespace osu.Game.Online.Multiplayer connection.On(nameof(IMultiplayerClient.GameplayStarted), ((IMultiplayerClient)this).GameplayStarted); connection.On(nameof(IMultiplayerClient.GameplayAborted), ((IMultiplayerClient)this).GameplayAborted); connection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady); + connection.On(nameof(IMultiplayerClient.UserStyleChanged), ((IMultiplayerClient)this).UserStyleChanged); connection.On>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged); connection.On(nameof(IMultiplayerClient.UserBeatmapAvailabilityChanged), ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged); connection.On(nameof(IMultiplayerClient.MatchRoomStateChanged), ((IMultiplayerClient)this).MatchRoomStateChanged); @@ -186,6 +187,16 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeBeatmapAvailability), newBeatmapAvailability); } + public override Task ChangeUserStyle(int? beatmapId, int? rulesetId) + { + if (!IsConnected.Value) + return Task.CompletedTask; + + Debug.Assert(connection != null); + + return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeUserStyle), beatmapId, rulesetId); + } + public override Task ChangeUserMods(IEnumerable newMods) { if (!IsConnected.Value) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 49144f9de5..b51679ded6 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -4,14 +4,12 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; 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; @@ -28,6 +26,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Screens.Menu; using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Utils; using Container = osu.Framework.Graphics.Containers.Container; namespace osu.Game.Screens.OnlinePlay.Match @@ -37,18 +36,6 @@ 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()) @@ -65,13 +52,13 @@ namespace osu.Game.Screens.OnlinePlay.Match protected Drawable? UserModsSection; /// - /// A container that provides controls for selection of the user's difficulty override. + /// A container that provides controls for selection of the user style. /// This will be shown/hidden automatically when applicable. /// - protected Drawable? UserDifficultySection; + protected Drawable? UserStyleSection; /// - /// A container that will display the user's difficulty override. + /// A container that will display the user's style. /// protected Container? UserStyleDisplayContainer; @@ -82,6 +69,18 @@ namespace osu.Game.Screens.OnlinePlay.Match /// protected readonly Bindable> UserMods = new Bindable>(Array.Empty()); + /// + /// 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 UserBeatmap = 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 UserRuleset = new Bindable(); + [Resolved(CanBeNull = true)] private IOverlayManager? overlayManager { get; set; } @@ -272,13 +271,25 @@ namespace osu.Game.Screens.OnlinePlay.Match { base.LoadComplete(); - SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged)); - UserMods.BindValueChanged(_ => Scheduler.AddOnce(UpdateMods)); - DifficultyOverride.BindValueChanged(_ => Scheduler.AddOnce(updateStyleOverride)); - RulesetOverride.BindValueChanged(_ => Scheduler.AddOnce(updateStyleOverride)); + SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); + + UserMods.BindValueChanged(_ => Scheduler.AddOnce(updateMods)); + + UserBeatmap.BindValueChanged(_ => Scheduler.AddOnce(() => + { + updateBeatmap(); + updateUserStyle(); + })); + + UserRuleset.BindValueChanged(_ => Scheduler.AddOnce(() => + { + updateUserMods(); + updateRuleset(); + updateUserStyle(); + })); beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); - beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateWorkingBeatmap()); + beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateBeatmap()); userModsSelectOverlayRegistration = overlayManager?.RegisterBlockingOverlay(UserModsSelectOverlay); @@ -347,7 +358,7 @@ namespace osu.Game.Screens.OnlinePlay.Match public override void OnSuspending(ScreenTransitionEvent e) { // Should be a noop in most cases, but let's ensure beyond doubt that the beatmap is in a correct state. - updateWorkingBeatmap(); + updateBeatmap(); onLeaving(); base.OnSuspending(e); @@ -356,10 +367,11 @@ namespace osu.Game.Screens.OnlinePlay.Match public override void OnResuming(ScreenTransitionEvent e) { base.OnResuming(e); - updateWorkingBeatmap(); + updateBeatmap(); beginHandlingTrack(); - Scheduler.AddOnce(UpdateMods); + Scheduler.AddOnce(updateMods); Scheduler.AddOnce(updateRuleset); + Scheduler.AddOnce(updateUserStyle); } protected bool ExitConfirmed { get; private set; } @@ -409,9 +421,13 @@ namespace osu.Game.Screens.OnlinePlay.Match protected void StartPlay() { - if (GetGameplayItem() is not PlaylistItem item) + if (SelectedItem.Value is not PlaylistItem item) return; + item = item.With( + ruleset: GetGameplayRuleset().OnlineID, + beatmap: new Optional(GetGameplayBeatmap())); + // User may be at song select or otherwise when the host starts gameplay. // Ensure that they first return to this screen, else global bindables (beatmap etc.) may be in a bad state. if (!this.IsCurrentScreen()) @@ -437,31 +453,26 @@ namespace osu.Game.Screens.OnlinePlay.Match /// The screen to enter. protected abstract Screen CreateGameplayScreen(PlaylistItem selectedItem); - private void selectedItemChanged() + protected void OnSelectedItemChanged() { - if (SelectedItem.Value is not PlaylistItem selected) + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) return; - if (selected.BeatmapSetId == null || selected.BeatmapSetId != DifficultyOverride.Value?.BeatmapSet.AsNonNull().OnlineID) + // Reset user style if no longer valid. + // Todo: In the future this can be made more lenient, such as allowing a non-null ruleset as the set changes. + if (item.BeatmapSetId == null || item.BeatmapSetId != UserBeatmap.Value?.BeatmapSet!.OnlineID) { - DifficultyOverride.Value = null; - RulesetOverride.Value = null; + UserBeatmap.Value = null; + UserRuleset.Value = null; } - updateStyleOverride(); - updateWorkingBeatmap(); - - var rulesetInstance = Rulesets.GetRuleset(selected.RulesetID)?.CreateInstance(); - Debug.Assert(rulesetInstance != null); - var allowedMods = selected.AllowedMods.Select(m => m.ToMod(rulesetInstance)); - - // Remove any user mods that are no longer allowed. - UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList(); - - UpdateMods(); + updateUserMods(); + updateBeatmap(); + updateMods(); updateRuleset(); + updateUserStyle(); - if (!selected.AllowedMods.Any()) + if (!item.AllowedMods.Any()) { UserModsSection?.Hide(); UserModsSelectOverlay.Hide(); @@ -470,100 +481,89 @@ namespace osu.Game.Screens.OnlinePlay.Match else { UserModsSection?.Show(); + + var rulesetInstance = GetGameplayRuleset().CreateInstance(); + var allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance)); UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); } - if (selected.BeatmapSetId == null) - UserDifficultySection?.Hide(); + if (item.BeatmapSetId == null) + UserStyleSection?.Hide(); else - UserDifficultySection?.Show(); + UserStyleSection?.Show(); } - private void updateWorkingBeatmap() + private void updateUserMods() { - if (GetGameplayItem() is not PlaylistItem item || !this.IsCurrentScreen()) + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) + return; + + // Remove any user mods that are no longer allowed. + var rulesetInstance = GetGameplayRuleset().CreateInstance(); + var allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance)); + UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList(); + } + + private void updateBeatmap() + { + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) return; // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info - var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == item.Beatmap.OnlineID); - - UserModsSelectOverlay.Beatmap.Value = Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); + int beatmapId = GetGameplayBeatmap().OnlineID; + var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == beatmapId); + Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); + UserModsSelectOverlay.Beatmap.Value = Beatmap.Value; } - protected virtual void UpdateMods() + private void updateMods() { - if (GetGameplayItem() is not PlaylistItem item || !this.IsCurrentScreen()) + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) return; - var rulesetInstance = Rulesets.GetRuleset(item.RulesetID)?.CreateInstance(); - Debug.Assert(rulesetInstance != null); - Mods.Value = UserMods.Value.Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList(); - } - - private void updateStyleOverride() - { - if (SelectedItem.Value == null || !this.IsCurrentScreen()) - return; - - 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() - { - if (SelectedItem.Value == null || !this.IsCurrentScreen()) - return; - - this.Push(new MultiplayerMatchStyleSelect(Room, SelectedItem.Value, (beatmap, ruleset) => - { - if (SelectedItem.Value?.BeatmapSetId == null || SelectedItem.Value.BeatmapSetId != beatmap.BeatmapSet?.OnlineID) - return; - - DifficultyOverride.Value = beatmap; - RulesetOverride.Value = ruleset; - })); + var rulesetInstance = GetGameplayRuleset().CreateInstance(); + Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray(); } private void updateRuleset() { - if (GetGameplayItem() is not PlaylistItem item || !this.IsCurrentScreen()) + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) return; - Ruleset.Value = Rulesets.GetRuleset(item.RulesetID); + Ruleset.Value = GetGameplayRuleset(); } + private void updateUserStyle() + { + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) + return; + + if (UserStyleDisplayContainer != null) + { + PlaylistItem gameplayItem = SelectedItem.Value.With( + ruleset: GetGameplayRuleset().OnlineID, + beatmap: new Optional(GetGameplayBeatmap())); + + UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem) + { + AllowReordering = false, + AllowEditing = true, + RequestEdit = _ => OpenStyleSelection() + }; + } + } + + protected virtual APIMod[] GetGameplayMods() + => UserMods.Value.Select(m => new APIMod(m)).Concat(SelectedItem.Value!.RequiredMods).ToArray(); + + protected virtual RulesetInfo GetGameplayRuleset() + => Rulesets.GetRuleset(UserRuleset.Value?.OnlineID ?? SelectedItem.Value!.RulesetID)!; + + protected virtual IBeatmapInfo GetGameplayBeatmap() + => UserBeatmap.Value ?? SelectedItem.Value!.Beatmap; + + protected abstract void OpenStyleSelection(); + private void beginHandlingTrack() { Beatmap.BindValueChanged(applyLoopingToTrack, true); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs index 867579171d..3fe4926052 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs @@ -2,106 +2,88 @@ // 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.Bindables; +using osu.Framework.Logging; using osu.Framework.Screens; -using osu.Game.Beatmaps; -using osu.Game.Database; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Multiplayer; 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, IHandlePresentBeatmap + public partial class MultiplayerMatchStyleSelect : OnlinePlayStyleSelect { - public string ShortTitle => "style selection"; + [Resolved] + private MultiplayerClient client { get; set; } = null!; - public override string Title => ShortTitle.Humanize(); + [Resolved] + private OngoingOperationTracker operationTracker { get; set; } = null!; - public override bool AllowEditing => false; + private readonly IBindable operationInProgress = new Bindable(); - protected override UserActivity InitialActivity => new UserActivity.InLobby(room); + private LoadingLayer loadingLayer = null!; + private IDisposable? selectionOperation; - private readonly Room room; - private readonly PlaylistItem item; - private readonly Action onSelect; - - public MultiplayerMatchStyleSelect(Room room, PlaylistItem item, Action onSelect) + public MultiplayerMatchStyleSelect(Room room, PlaylistItem item) + : base(room, item) { - 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 }; + AddInternal(loadingLayer = new LoadingLayer(true)); } - protected override FilterControl CreateFilterControl() => new DifficultySelectFilterControl(item); - - protected override IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons() + protected override void LoadComplete() { - // Required to create the drawable components. - base.CreateSongSelectFooterButtons(); - return Enumerable.Empty<(FooterButton, OverlayContainer?)>(); + base.LoadComplete(); + + operationInProgress.BindTo(operationTracker.InProgress); + operationInProgress.BindValueChanged(_ => updateLoadingLayer(), true); } - protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); + private void updateLoadingLayer() + { + if (operationInProgress.Value) + loadingLayer.Show(); + else + loadingLayer.Hide(); + } protected override bool OnStart() { - onSelect(Beatmap.Value.BeatmapInfo, Ruleset.Value); - this.Exit(); + if (operationInProgress.Value) + { + Logger.Log($"{nameof(OnStart)} aborted due to {nameof(operationInProgress)}"); + return false; + } + + selectionOperation = operationTracker.BeginOperation(); + + client.ChangeUserStyle(Beatmap.Value.BeatmapInfo.OnlineID, Ruleset.Value.OnlineID) + .FireAndForget(onSuccess: () => + { + selectionOperation.Dispose(); + + Schedule(() => + { + // If an error or server side trigger occurred this screen may have already exited by external means. + if (this.IsCurrentScreen()) + this.Exit(); + }); + }, onError: _ => + { + selectionOperation.Dispose(); + + Schedule(() => + { + Carousel.AllowSelection = true; + }); + }); + return true; } - - public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset) - { - // This screen cannot present beatmaps. - } - - private partial class DifficultySelectFilterControl : FilterControl - { - private readonly PlaylistItem item; - private double itemLength; - - public DifficultySelectFilterControl(PlaylistItem item) - { - this.item = item; - } - - [BackgroundDependencyLoader] - private void load(RealmAccess realm) - { - int beatmapId = item.Beatmap.OnlineID; - itemLength = realm.Run(r => r.All().FirstOrDefault(b => b.OnlineID == beatmapId)?.Length ?? 0); - } - - public override FilterCriteria CreateCriteria() - { - var criteria = base.CreateCriteria(); - - // Must be from the same set as the playlist item. - criteria.BeatmapSetId = item.BeatmapSetId; - - // Must be within 30s of the playlist item. - criteria.Length.Min = itemLength - 30000; - criteria.Length.Max = itemLength + 30000; - criteria.Length.IsLowerInclusive = true; - criteria.Length.IsUpperInclusive = true; - - return criteria; - } - } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index d807fe8177..edfb059c77 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -16,6 +16,8 @@ using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics.Cursor; using osu.Game.Online; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -188,7 +190,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer }, } }, - UserDifficultySection = new FillFlowContainer + UserStyleSection = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -251,6 +253,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer this.Push(new MultiplayerMatchSongSelect(Room, itemToEdit)); } + protected override void OpenStyleSelection() + { + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) + return; + + this.Push(new MultiplayerMatchStyleSelect(Room, item)); + } + protected override Drawable CreateFooter() => new MultiplayerMatchFooter { SelectedItem = SelectedItem @@ -261,16 +271,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer SelectedItem = SelectedItem }; - protected override void UpdateMods() + protected override APIMod[] GetGameplayMods() { - if (GetGameplayItem() is not PlaylistItem item || client.LocalUser == null || !this.IsCurrentScreen()) - return; + // Using the room's reported status makes the server authoritative. + return client.LocalUser?.Mods.Concat(SelectedItem.Value!.RequiredMods).ToArray()!; + } - // 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(item.RulesetID)?.CreateInstance(); - Debug.Assert(rulesetInstance != null); - Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(rulesetInstance)).Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList(); + protected override RulesetInfo GetGameplayRuleset() + { + // Using the room's reported status makes the server authoritative. + return client.LocalUser?.RulesetId != null ? Rulesets.GetRuleset(client.LocalUser.RulesetId.Value)! : base.GetGameplayRuleset(); + } + + protected override IBeatmapInfo GetGameplayBeatmap() + { + // Using the room's reported status makes the server authoritative. + return client.LocalUser?.BeatmapId != null ? new APIBeatmap { OnlineID = client.LocalUser.BeatmapId.Value } : base.GetGameplayBeatmap(); } [Resolved(canBeNull: true)] @@ -376,7 +392,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer addItemButton.Alpha = localUserCanAddItem ? 1 : 0; - Scheduler.AddOnce(UpdateMods); + // Forcefully update the selected item so that the user state is applied. + Scheduler.AddOnce(OnSelectedItemChanged); Activity.Value = new UserActivity.InLobby(Room); } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs new file mode 100644 index 0000000000..89f2ffc883 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs @@ -0,0 +1,98 @@ +// 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.Database; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets; +using osu.Game.Screens.Select; +using osu.Game.Users; + +namespace osu.Game.Screens.OnlinePlay +{ + public abstract partial class OnlinePlayStyleSelect : SongSelect, IOnlinePlaySubScreen, IHandlePresentBeatmap + { + 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; + + protected OnlinePlayStyleSelect(Room room, PlaylistItem item) + { + this.room = room; + this.item = item; + + 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(); + + public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset) + { + // This screen cannot present beatmaps. + } + + private partial class DifficultySelectFilterControl : FilterControl + { + private readonly PlaylistItem item; + private double itemLength; + + public DifficultySelectFilterControl(PlaylistItem item) + { + this.item = item; + } + + [BackgroundDependencyLoader] + private void load(RealmAccess realm) + { + int beatmapId = item.Beatmap.OnlineID; + itemLength = realm.Run(r => r.All().FirstOrDefault(b => b.OnlineID == beatmapId)?.Length ?? 0); + } + + public override FilterCriteria CreateCriteria() + { + var criteria = base.CreateCriteria(); + + // Must be from the same set as the playlist item. + criteria.BeatmapSetId = item.BeatmapSetId; + + // Must be within 30s of the playlist item. + criteria.Length.Min = itemLength - 30000; + criteria.Length.Max = itemLength + 30000; + criteria.Length.IsLowerInclusive = true; + criteria.Length.IsUpperInclusive = true; + + return criteria; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs new file mode 100644 index 0000000000..f3d868b0de --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets; + +namespace osu.Game.Screens.OnlinePlay.Playlists +{ + public partial class PlaylistsRoomStyleSelect : OnlinePlayStyleSelect + { + public new readonly Bindable Beatmap = new Bindable(); + public new readonly Bindable Ruleset = new Bindable(); + + public PlaylistsRoomStyleSelect(Room room, PlaylistItem item) + : base(room, item) + { + } + + protected override bool OnStart() + { + Beatmap.Value = base.Beatmap.Value.BeatmapInfo; + Ruleset.Value = base.Ruleset.Value; + this.Exit(); + return true; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 48d50d727b..b941bbd290 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -213,7 +213,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } } }, - UserDifficultySection = new FillFlowContainer + UserStyleSection = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -299,6 +299,18 @@ namespace osu.Game.Screens.OnlinePlay.Playlists }, }; + protected override void OpenStyleSelection() + { + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) + return; + + this.Push(new PlaylistsRoomStyleSelect(Room, item) + { + Beatmap = { BindTarget = UserBeatmap }, + Ruleset = { BindTarget = UserRuleset } + }); + } + private void updatePollingRate() { selectionPollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 30000 : 5000; diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 4d812abf11..3abef523cd 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -335,6 +335,23 @@ namespace osu.Game.Tests.Visual.Multiplayer return Task.CompletedTask; } + public override Task ChangeUserStyle(int? beatmapId, int? rulesetId) + { + ChangeUserStyle(api.LocalUser.Value.Id, beatmapId, rulesetId); + return Task.CompletedTask; + } + + public void ChangeUserStyle(int userId, int? beatmapId, int? rulesetId) + { + Debug.Assert(ServerRoom != null); + + var user = ServerRoom.Users.Single(u => u.UserID == userId); + user.BeatmapId = beatmapId; + user.RulesetId = rulesetId; + + ((IMultiplayerClient)this).UserStyleChanged(userId, beatmapId, rulesetId); + } + public void ChangeUserMods(int userId, IEnumerable newMods) => ChangeUserMods(userId, newMods.Select(m => new APIMod(m))); From c3aa9d6f8a495f4ef592767ddab579f8c232ce5b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Dec 2024 23:30:24 +0900 Subject: [PATCH 037/801] Display user style in participant panel --- .../TestSceneMultiplayerParticipantsList.cs | 27 +++++ .../Participants/ParticipantPanel.cs | 105 +++++++++++++++++- 2 files changed, 128 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index d88741ec0c..238a716f91 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -308,6 +308,33 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("set state: locally available", () => MultiplayerClient.ChangeUserBeatmapAvailability(0, BeatmapAvailability.LocallyAvailable())); } + [Test] + public void TestUserWithStyle() + { + AddStep("add users", () => + { + MultiplayerClient.AddUser(new APIUser + { + Id = 0, + Username = "User 0", + RulesetsStatistics = new Dictionary + { + { + Ruleset.Value.ShortName, + new UserStatistics { GlobalRank = RNG.Next(1, 100000), } + } + }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }); + + MultiplayerClient.ChangeUserStyle(0, 259, 2); + }); + + AddStep("set beatmap locally available", () => MultiplayerClient.ChangeUserBeatmapAvailability(0, BeatmapAvailability.LocallyAvailable())); + AddStep("change user style to beatmap: 258, ruleset: 1", () => MultiplayerClient.ChangeUserStyle(0, 258, 1)); + AddStep("change user style to beatmap: null, ruleset: null", () => MultiplayerClient.ChangeUserStyle(0, null, null)); + } + [Test] public void TestModOverlap() { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 7e42b18240..64c4648125 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.ObjectExtensions; @@ -14,6 +16,9 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Logging; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -47,6 +52,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants private SpriteIcon crown = null!; private OsuSpriteText userRankText = null!; + private StyleDisplayIcon userStyleDisplay = null!; private ModDisplay userModsDisplay = null!; private StateDisplay userStateDisplay = null!; @@ -149,16 +155,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants } } }, - new Container + new FillFlowContainer { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, AutoSizeAxes = Axes.Both, Margin = new MarginPadding { Right = 70 }, - Child = userModsDisplay = new ModDisplay + Children = new Drawable[] { - Scale = new Vector2(0.5f), - ExpansionMode = ExpansionMode.AlwaysContracted, + userStyleDisplay = new StyleDisplayIcon(), + userModsDisplay = new ModDisplay + { + Scale = new Vector2(0.5f), + ExpansionMode = ExpansionMode.AlwaysContracted, + } } }, userStateDisplay = new StateDisplay @@ -208,9 +218,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability); if ((User.BeatmapAvailability.State == DownloadState.LocallyAvailable) && (User.State != MultiplayerUserState.Spectating)) + { userModsDisplay.FadeIn(fade_time); + userStyleDisplay.FadeIn(fade_time); + } else + { userModsDisplay.FadeOut(fade_time); + userStyleDisplay.FadeOut(fade_time); + } + + if (User.BeatmapId == null && User.RulesetId == null) + userStyleDisplay.Style = null; + else + userStyleDisplay.Style = (User.BeatmapId ?? currentItem?.BeatmapID ?? 0, User.RulesetId ?? currentItem?.RulesetID ?? 0); kickButton.Alpha = client.IsHost && !User.Equals(client.LocalUser) ? 1 : 0; crown.Alpha = client.Room.Host?.Equals(User) == true ? 1 : 0; @@ -284,5 +305,81 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants IconHoverColour = colours.Red; } } + + private partial class StyleDisplayIcon : CompositeComponent + { + [Resolved] + private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + public StyleDisplayIcon() + { + AutoSizeAxes = Axes.Both; + } + + private (int beatmap, int ruleset)? style; + + public (int beatmap, int ruleset)? Style + { + get => style; + set + { + if (style == value) + return; + + style = value; + Scheduler.Add(refresh); + } + } + + private CancellationTokenSource? cancellationSource; + + private void refresh() + { + cancellationSource?.Cancel(); + cancellationSource?.Dispose(); + cancellationSource = null; + + if (Style == null) + { + ClearInternal(); + return; + } + + cancellationSource = new CancellationTokenSource(); + CancellationToken token = cancellationSource.Token; + + int localBeatmap = Style.Value.beatmap; + int localRuleset = Style.Value.ruleset; + + Task.Run(async () => + { + try + { + var beatmap = await beatmapLookupCache.GetBeatmapAsync(localBeatmap, token).ConfigureAwait(false); + if (beatmap == null) + return; + + Schedule(() => + { + if (token.IsCancellationRequested) + return; + + InternalChild = new DifficultyIcon(beatmap, rulesets.GetRuleset(localRuleset)) + { + Size = new Vector2(20), + TooltipType = DifficultyIconTooltipType.Extended, + }; + }); + } + catch (Exception e) + { + Logger.Log($"Error while populating participant style icon {e}"); + } + }, token); + } + } } } From e7c272b8b9278e706baf9305c8ff92548c22ff32 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Dec 2024 23:39:01 +0900 Subject: [PATCH 038/801] Don't display on matching beatmap/ruleset --- .../OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 64c4648125..a2657019a3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -228,7 +228,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants userStyleDisplay.FadeOut(fade_time); } - if (User.BeatmapId == null && User.RulesetId == null) + if ((User.BeatmapId == null && User.RulesetId == null) || (User.BeatmapId == currentItem?.BeatmapID && User.RulesetId == currentItem?.RulesetID)) userStyleDisplay.Style = null; else userStyleDisplay.Style = (User.BeatmapId ?? currentItem?.BeatmapID ?? 0, User.RulesetId ?? currentItem?.RulesetID ?? 0); From 6579b055618f375e06437f05ff70f612316e72a6 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Dec 2024 23:45:36 +0900 Subject: [PATCH 039/801] Remove unused usings --- osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs index 89f2ffc883..029ca68e36 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.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.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.Database; using osu.Game.Online.Rooms; From 824497d82c6f86eebf6421b1cdcf25beaf39f881 Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Fri, 27 Dec 2024 23:30:30 +1000 Subject: [PATCH 040/801] Rewrite of the `Rhythm` Skill within osu!taiko (#31284) * implement bell curve into diffcalcutils * remove unneeded attributes * implement new rhythm skill * change dho variables * update dho rhythm * interval interface * implement rhythmevaluator * evenhitobjects * evenpatterns * evenrhythm * change attribute ordering * initial balancing * change naming to Same instead of Even * remove attribute bump for display * Fix diffcalc tests --------- Co-authored-by: StanR --- .../TaikoDifficultyCalculatorTest.cs | 8 +- .../Difficulty/Evaluators/RhythmEvaluator.cs | 149 +++++++++++++++++ .../Preprocessing/Rhythm/Data/SamePatterns.cs | 55 ++++++ .../Preprocessing/Rhythm/Data/SameRhythm.cs | 73 ++++++++ .../Rhythm/Data/SameRhythmHitObjects.cs | 94 +++++++++++ .../Preprocessing/Rhythm/IHasInterval.cs | 13 ++ .../Rhythm/TaikoDifficultyHitObjectRhythm.cs | 79 ++++++++- .../Preprocessing/TaikoDifficultyHitObject.cs | 51 ++---- .../Difficulty/Skills/Rhythm.cs | 157 ++---------------- .../Difficulty/TaikoDifficultyAttributes.cs | 28 ++-- .../Difficulty/TaikoDifficultyCalculator.cs | 20 ++- .../Utils/DifficultyCalculationUtils.cs | 10 ++ 12 files changed, 520 insertions(+), 217 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index 09d6540f72..ba247c68d4 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(3.0920212594351191d, 200, "diffcalc-test")] - [TestCase(3.0920212594351191d, 200, "diffcalc-test-strong")] + [TestCase(3.0950934814938953d, 200, "diffcalc-test")] + [TestCase(3.0950934814938953d, 200, "diffcalc-test-strong")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(4.0789820318081444d, 200, "diffcalc-test")] - [TestCase(4.0789820318081444d, 200, "diffcalc-test-strong")] + [TestCase(4.0839365008715403d, 200, "diffcalc-test")] + [TestCase(4.0839365008715403d, 200, "diffcalc-test-strong")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime()); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs new file mode 100644 index 0000000000..3a294f7123 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs @@ -0,0 +1,149 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators +{ + public class RhythmEvaluator + { + /// + /// Multiplier for a given denominator term. + /// + private static double termPenalty(double ratio, int denominator, double power, double multiplier) + { + return -multiplier * Math.Pow(Math.Cos(denominator * Math.PI * ratio), power); + } + + /// + /// Calculates the difficulty of a given ratio using a combination of periodic penalties and bonuses. + /// + private static double ratioDifficulty(double ratio, int terms = 8) + { + double difficulty = 0; + + for (int i = 1; i <= terms; ++i) + { + difficulty += termPenalty(ratio, i, 2, 1); + } + + difficulty += terms; + + // Give bonus to near-1 ratios + difficulty += DifficultyCalculationUtils.BellCurve(ratio, 1, 0.7); + + // Penalize ratios that are VERY near 1 + difficulty -= DifficultyCalculationUtils.BellCurve(ratio, 1, 0.5); + + return difficulty / Math.Sqrt(8); + } + + /// + /// Determines if the changes in hit object intervals is consistent based on a given threshold. + /// + private static double repeatedIntervalPenalty(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow, double threshold = 0.1) + { + double longIntervalPenalty = sameInterval(sameRhythmHitObjects, 3); + + double shortIntervalPenalty = sameRhythmHitObjects.Children.Count < 6 + ? sameInterval(sameRhythmHitObjects, 4) + : 1.0; // Returns a non-penalty if there are 6 or more notes within an interval. + + // Scale penalties dynamically based on hit object duration relative to hitWindow. + double penaltyScaling = Math.Max(1 - sameRhythmHitObjects.Duration / (hitWindow * 2), 0.5); + + return Math.Min(longIntervalPenalty, shortIntervalPenalty) * penaltyScaling; + + double sameInterval(SameRhythmHitObjects startObject, int intervalCount) + { + List intervals = new List(); + var currentObject = startObject; + + for (int i = 0; i < intervalCount && currentObject != null; i++) + { + intervals.Add(currentObject.HitObjectInterval); + currentObject = currentObject.Previous; + } + + intervals.RemoveAll(interval => interval == null); + + if (intervals.Count < intervalCount) + return 1.0; // No penalty if there aren't enough valid intervals. + + for (int i = 0; i < intervals.Count; i++) + { + for (int j = i + 1; j < intervals.Count; j++) + { + double ratio = intervals[i]!.Value / intervals[j]!.Value; + if (Math.Abs(1 - ratio) <= threshold) // If any two intervals are similar, apply a penalty. + return 0.3; + } + } + + return 1.0; // No penalty if all intervals are different. + } + } + + private static double evaluateDifficultyOf(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow) + { + double intervalDifficulty = ratioDifficulty(sameRhythmHitObjects.HitObjectIntervalRatio); + double? previousInterval = sameRhythmHitObjects.Previous?.HitObjectInterval; + + // If a previous interval exists and there are multiple hit objects in the sequence: + if (previousInterval != null && sameRhythmHitObjects.Children.Count > 1) + { + double expectedDurationFromPrevious = (double)previousInterval * sameRhythmHitObjects.Children.Count; + double durationDifference = sameRhythmHitObjects.Duration - expectedDurationFromPrevious; + + if (durationDifference > 0) + { + intervalDifficulty *= DifficultyCalculationUtils.Logistic( + durationDifference / hitWindow, + midpointOffset: 0.7, + multiplier: 1.5, + maxValue: 1); + } + } + + // Apply consistency penalty. + intervalDifficulty *= repeatedIntervalPenalty(sameRhythmHitObjects, hitWindow); + + // Penalise patterns that can be hit within a single hit window. + intervalDifficulty *= DifficultyCalculationUtils.Logistic( + sameRhythmHitObjects.Duration / hitWindow, + midpointOffset: 0.6, + multiplier: 1, + maxValue: 1); + + return Math.Pow(intervalDifficulty, 0.75); + } + + private static double evaluateDifficultyOf(SamePatterns samePatterns) + { + return ratioDifficulty(samePatterns.IntervalRatio); + } + + /// + /// Evaluate the difficulty of a hitobject considering its interval change. + /// + public static double EvaluateDifficultyOf(DifficultyHitObject hitObject, double hitWindow) + { + TaikoDifficultyHitObjectRhythm rhythm = ((TaikoDifficultyHitObject)hitObject).Rhythm; + double difficulty = 0.0d; + + if (rhythm.SameRhythmHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmHitObjects + difficulty += evaluateDifficultyOf(rhythm.SameRhythmHitObjects, hitWindow); + + if (rhythm.SamePatterns?.FirstHitObject == hitObject) // Difficulty for SamePatterns + difficulty += 0.5 * evaluateDifficultyOf(rhythm.SamePatterns); + + return difficulty; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs new file mode 100644 index 0000000000..50839c4561 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs @@ -0,0 +1,55 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data +{ + /// + /// Represents grouped by their 's interval. + /// + public class SamePatterns : SameRhythm + { + public SamePatterns? Previous { get; private set; } + + /// + /// The between children within this group. + /// If there is only one child, this will have the value of the first child's . + /// + public double ChildrenInterval => Children.Count > 1 ? Children[1].Interval : Children[0].Interval; + + /// + /// The ratio of between this and the previous . In the + /// case where there is no previous , this will have a value of 1. + /// + public double IntervalRatio => ChildrenInterval / Previous?.ChildrenInterval ?? 1.0d; + + public TaikoDifficultyHitObject FirstHitObject => Children[0].FirstHitObject; + + public IEnumerable AllHitObjects => Children.SelectMany(child => child.Children); + + private SamePatterns(SamePatterns? previous, List data, ref int i) + : base(data, ref i, 5) + { + Previous = previous; + + foreach (TaikoDifficultyHitObject hitObject in AllHitObjects) + { + hitObject.Rhythm.SamePatterns = this; + } + } + + public static void GroupPatterns(List data) + { + List samePatterns = new List(); + + // Index does not need to be incremented, as it is handled within the SameRhythm constructor. + for (int i = 0; i < data.Count;) + { + SamePatterns? previous = samePatterns.Count > 0 ? samePatterns[^1] : null; + samePatterns.Add(new SamePatterns(previous, data, ref i)); + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs new file mode 100644 index 0000000000..b1ca22595b --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs @@ -0,0 +1,73 @@ +// 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; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data +{ + /// + /// A base class for grouping s by their interval. In edges where an interval change + /// occurs, the is added to the group with the smaller interval. + /// + public abstract class SameRhythm + where ChildType : IHasInterval + { + public IReadOnlyList Children { get; private set; } + + /// + /// Determines if the intervals between two child objects are within a specified margin of error, + /// indicating that the intervals are effectively "flat" or consistent. + /// + private bool isFlat(ChildType current, ChildType previous, double marginOfError) + { + return Math.Abs(current.Interval - previous.Interval) <= marginOfError; + } + + /// + /// Create a new from a list of s, and add + /// them to the list until the end of the group. + /// + /// The list of s. + /// + /// Index in to start adding children. This will be modified and should be passed into + /// the next 's constructor. + /// + /// + /// The margin of error for the interval, within of which no interval change is considered to have occured. + /// + protected SameRhythm(List data, ref int i, double marginOfError) + { + List children = new List(); + Children = children; + children.Add(data[i]); + i++; + + for (; i < data.Count - 1; i++) + { + // An interval change occured, add the current data if the next interval is larger. + if (!isFlat(data[i], data[i + 1], marginOfError)) + { + if (data[i + 1].Interval > data[i].Interval + marginOfError) + { + children.Add(data[i]); + i++; + } + + return; + } + + // No interval change occured + children.Add(data[i]); + } + + // Check if the last two objects in the data form a "flat" rhythm pattern within the specified margin of error. + // If true, add the current object to the group and increment the index to process the next object. + if (data.Count > 2 && isFlat(data[^1], data[^2], marginOfError)) + { + children.Add(data[i]); + i++; + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs new file mode 100644 index 0000000000..0ccc6da026 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs @@ -0,0 +1,94 @@ +// 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.Game.Rulesets.Difficulty.Preprocessing; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data +{ + /// + /// Represents a group of s with no rhythm variation. + /// + public class SameRhythmHitObjects : SameRhythm, IHasInterval + { + public TaikoDifficultyHitObject FirstHitObject => Children[0]; + + public SameRhythmHitObjects? Previous; + + /// + /// of the first hit object. + /// + public double StartTime => Children[0].StartTime; + + /// + /// The interval between the first and final hit object within this group. + /// + public double Duration => Children[^1].StartTime - Children[0].StartTime; + + /// + /// The interval in ms of each hit object in this . This is only defined if there is + /// more than two hit objects in this . + /// + public double? HitObjectInterval; + + /// + /// The ratio of between this and the previous . In the + /// case where one or both of the is undefined, this will have a value of 1. + /// + public double HitObjectIntervalRatio = 1; + + /// + /// The interval between the of this and the previous . + /// + public double Interval { get; private set; } = double.PositiveInfinity; + + public SameRhythmHitObjects(SameRhythmHitObjects? previous, List data, ref int i) + : base(data, ref i, 5) + { + Previous = previous; + + foreach (var hitObject in Children) + { + hitObject.Rhythm.SameRhythmHitObjects = this; + + // Pass the HitObjectInterval to each child. + hitObject.HitObjectInterval = HitObjectInterval; + } + + calculateIntervals(); + } + + public static List GroupHitObjects(List data) + { + List flatPatterns = new List(); + + // Index does not need to be incremented, as it is handled within SameRhythm's constructor. + for (int i = 0; i < data.Count;) + { + SameRhythmHitObjects? previous = flatPatterns.Count > 0 ? flatPatterns[^1] : null; + flatPatterns.Add(new SameRhythmHitObjects(previous, data, ref i)); + } + + return flatPatterns; + } + + private void calculateIntervals() + { + // Calculate the average interval between hitobjects, or null if there are fewer than two. + HitObjectInterval = Children.Count < 2 ? null : (Children[^1].StartTime - Children[0].StartTime) / (Children.Count - 1); + + // If both the current and previous intervals are available, calculate the ratio. + if (Previous?.HitObjectInterval != null && HitObjectInterval != null) + { + HitObjectIntervalRatio = HitObjectInterval.Value / Previous.HitObjectInterval.Value; + } + + if (Previous == null) + { + return; + } + + Interval = StartTime - Previous.StartTime; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs new file mode 100644 index 0000000000..8f3917cbde --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm +{ + /// + /// The interface for hitobjects that provide an interval value. + /// + public interface IHasInterval + { + double Interval { get; } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs index a273d7e2ea..beb7bfe5f6 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs @@ -1,35 +1,98 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Linq; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data; + namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm { /// - /// Represents a rhythm change in a taiko map. + /// Stores rhythm data for a . /// public class TaikoDifficultyHitObjectRhythm { /// - /// The difficulty multiplier associated with this rhythm change. + /// The group of hit objects with consistent rhythm that this object belongs to. /// - public readonly double Difficulty; + public SameRhythmHitObjects? SameRhythmHitObjects; /// - /// The ratio of current - /// to previous for the rhythm change. + /// The larger pattern of rhythm groups that this object is part of. + /// + public SamePatterns? SamePatterns; + + /// + /// The ratio of current + /// to previous for the rhythm change. /// A above 1 indicates a slow-down; a below 1 indicates a speed-up. /// public readonly double Ratio; + /// + /// List of most common rhythm changes in taiko maps. Based on how each object's interval compares to the previous object. + /// + /// + /// The general guidelines for the values are: + /// + /// rhythm changes with ratio closer to 1 (that are not 1) are harder to play, + /// speeding up is generally harder than slowing down (with exceptions of rhythm changes requiring a hand switch). + /// + /// + private static readonly TaikoDifficultyHitObjectRhythm[] common_rhythms = + { + new TaikoDifficultyHitObjectRhythm(1, 1), + new TaikoDifficultyHitObjectRhythm(2, 1), + new TaikoDifficultyHitObjectRhythm(1, 2), + new TaikoDifficultyHitObjectRhythm(3, 1), + new TaikoDifficultyHitObjectRhythm(1, 3), + new TaikoDifficultyHitObjectRhythm(3, 2), + new TaikoDifficultyHitObjectRhythm(2, 3), + new TaikoDifficultyHitObjectRhythm(5, 4), + new TaikoDifficultyHitObjectRhythm(4, 5) + }; + + /// + /// Initialises a new instance of s, + /// calculating the closest rhythm change and its associated difficulty for the current hit object. + /// + /// The current being processed. + public TaikoDifficultyHitObjectRhythm(TaikoDifficultyHitObject current) + { + var previous = current.Previous(0); + + if (previous == null) + { + Ratio = 1; + return; + } + + TaikoDifficultyHitObjectRhythm closestRhythm = getClosestRhythm(current.DeltaTime, previous.DeltaTime); + Ratio = closestRhythm.Ratio; + } + /// /// Creates an object representing a rhythm change. /// /// The numerator for . /// The denominator for - /// The difficulty multiplier associated with this rhythm change. - public TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty) + private TaikoDifficultyHitObjectRhythm(int numerator, int denominator) { Ratio = numerator / (double)denominator; - Difficulty = difficulty; + } + + /// + /// Determines the closest rhythm change from that matches the timing ratio + /// between the current and previous intervals. + /// + /// The time difference between the current hit object and the previous one. + /// The time difference between the previous hit object and the one before it. + /// The closest matching rhythm from . + private TaikoDifficultyHitObjectRhythm getClosestRhythm(double currentDeltaTime, double previousDeltaTime) + { + double ratio = currentDeltaTime / previousDeltaTime; + return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First(); } } } + diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index e741e4c9e7..dfcd08ed94 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; @@ -15,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// /// Represents a single hit object in taiko difficulty calculation. /// - public class TaikoDifficultyHitObject : DifficultyHitObject + public class TaikoDifficultyHitObject : DifficultyHitObject, IHasInterval { /// /// The list of all of the same colour as this in the beatmap. @@ -42,6 +41,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public readonly TaikoDifficultyHitObjectRhythm Rhythm; + /// + /// The interval between this hit object and the surrounding hit objects in its rhythm group. + /// + public double? HitObjectInterval { get; set; } + /// /// Colour data for this hit object. This is used by colour evaluator to calculate colour difficulty, but can be used /// by other skills in the future. @@ -58,6 +62,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public double CurrentSliderVelocity; + public double Interval => DeltaTime; + /// /// Creates a new difficulty hit object. /// @@ -81,7 +87,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing // Create the Colour object, its properties should be filled in by TaikoDifficultyPreprocessor Colour = new TaikoDifficultyHitObjectColour(); - Rhythm = getClosestRhythm(lastObject, lastLastObject, clockRate); + + // Create a Rhythm object, its properties are filled in by TaikoDifficultyHitObjectRhythm + Rhythm = new TaikoDifficultyHitObjectRhythm(this); switch ((hitObject as Hit)?.Type) { @@ -105,43 +113,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing } } - /// - /// List of most common rhythm changes in taiko maps. - /// - /// - /// The general guidelines for the values are: - /// - /// rhythm changes with ratio closer to 1 (that are not 1) are harder to play, - /// speeding up is generally harder than slowing down (with exceptions of rhythm changes requiring a hand switch). - /// - /// - private static readonly TaikoDifficultyHitObjectRhythm[] common_rhythms = - { - new TaikoDifficultyHitObjectRhythm(1, 1, 0.0), - new TaikoDifficultyHitObjectRhythm(2, 1, 0.3), - new TaikoDifficultyHitObjectRhythm(1, 2, 0.5), - new TaikoDifficultyHitObjectRhythm(3, 1, 0.3), - new TaikoDifficultyHitObjectRhythm(1, 3, 0.35), - new TaikoDifficultyHitObjectRhythm(3, 2, 0.6), // purposefully higher (requires hand switch in full alternating gameplay style) - new TaikoDifficultyHitObjectRhythm(2, 3, 0.4), - new TaikoDifficultyHitObjectRhythm(5, 4, 0.5), - new TaikoDifficultyHitObjectRhythm(4, 5, 0.7) - }; - - /// - /// Returns the closest rhythm change from required to hit this object. - /// - /// The gameplay preceding this one. - /// The gameplay preceding . - /// The rate of the gameplay clock. - private TaikoDifficultyHitObjectRhythm getClosestRhythm(HitObject lastObject, HitObject lastLastObject, double clockRate) - { - double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate; - double ratio = DeltaTime / prevLength; - - return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First(); - } - public TaikoDifficultyHitObject? PreviousMono(int backwardsIndex) => monoDifficultyHitObjects?.ElementAtOrDefault(MonoIndex - (backwardsIndex + 1)); public TaikoDifficultyHitObject? NextMono(int forwardsIndex) => monoDifficultyHitObjects?.ElementAtOrDefault(MonoIndex + (forwardsIndex + 1)); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index e76af13686..4fe1ea693e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -1,13 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; -using osu.Game.Rulesets.Taiko.Objects; -using osu.Game.Utils; +using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { @@ -16,158 +14,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// public class Rhythm : StrainDecaySkill { - protected override double SkillMultiplier => 10; - protected override double StrainDecayBase => 0; + protected override double SkillMultiplier => 1.0; + protected override double StrainDecayBase => 0.4; - /// - /// The note-based decay for rhythm strain. - /// - /// - /// is not used here, as it's time- and not note-based. - /// - private const double strain_decay = 0.96; + private readonly double greatHitWindow; - /// - /// Maximum number of entries in . - /// - private const int rhythm_history_max_length = 8; - - /// - /// Contains the last changes in note sequence rhythms. - /// - private readonly LimitedCapacityQueue rhythmHistory = new LimitedCapacityQueue(rhythm_history_max_length); - - /// - /// Contains the rolling rhythm strain. - /// Used to apply per-note decay. - /// - private double currentStrain; - - /// - /// Number of notes since the last rhythm change has taken place. - /// - private int notesSinceRhythmChange; - - public Rhythm(Mod[] mods) + public Rhythm(Mod[] mods, double greatHitWindow) : base(mods) { + this.greatHitWindow = greatHitWindow; } protected override double StrainValueOf(DifficultyHitObject current) { - // drum rolls and swells are exempt. - if (!(current.BaseObject is Hit)) - { - resetRhythmAndStrain(); - return 0.0; - } + double difficulty = RhythmEvaluator.EvaluateDifficultyOf(current, greatHitWindow); - currentStrain *= strain_decay; + // To prevent abuse of exceedingly long intervals between awkward rhythms, we penalise its difficulty. + difficulty *= DifficultyCalculationUtils.Logistic(current.DeltaTime, 350, -1 / 25.0, 0.5) + 0.5; - TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current; - notesSinceRhythmChange += 1; - - // rhythm difficulty zero (due to rhythm not changing) => no rhythm strain. - if (hitObject.Rhythm.Difficulty == 0.0) - { - return 0.0; - } - - double objectStrain = hitObject.Rhythm.Difficulty; - - objectStrain *= repetitionPenalties(hitObject); - objectStrain *= patternLengthPenalty(notesSinceRhythmChange); - objectStrain *= speedPenalty(hitObject.DeltaTime); - - // careful - needs to be done here since calls above read this value - notesSinceRhythmChange = 0; - - currentStrain += objectStrain; - return currentStrain; - } - - /// - /// Returns a penalty to apply to the current hit object caused by repeating rhythm changes. - /// - /// - /// Repetitions of more recent patterns are associated with a higher penalty. - /// - /// The current hit object being considered. - private double repetitionPenalties(TaikoDifficultyHitObject hitObject) - { - double penalty = 1; - - rhythmHistory.Enqueue(hitObject); - - for (int mostRecentPatternsToCompare = 2; mostRecentPatternsToCompare <= rhythm_history_max_length / 2; mostRecentPatternsToCompare++) - { - for (int start = rhythmHistory.Count - mostRecentPatternsToCompare - 1; start >= 0; start--) - { - if (!samePattern(start, mostRecentPatternsToCompare)) - continue; - - int notesSince = hitObject.Index - rhythmHistory[start].Index; - penalty *= repetitionPenalty(notesSince); - break; - } - } - - return penalty; - } - - /// - /// Determines whether the rhythm change pattern starting at is a repeat of any of the - /// . - /// - private bool samePattern(int start, int mostRecentPatternsToCompare) - { - for (int i = 0; i < mostRecentPatternsToCompare; i++) - { - if (rhythmHistory[start + i].Rhythm != rhythmHistory[rhythmHistory.Count - mostRecentPatternsToCompare + i].Rhythm) - return false; - } - - return true; - } - - /// - /// Calculates a single rhythm repetition penalty. - /// - /// Number of notes since the last repetition of a rhythm change. - private static double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince); - - /// - /// Calculates a penalty based on the number of notes since the last rhythm change. - /// Both rare and frequent rhythm changes are penalised. - /// - /// Number of notes since the last rhythm change. - private static double patternLengthPenalty(int patternLength) - { - double shortPatternPenalty = Math.Min(0.15 * patternLength, 1.0); - double longPatternPenalty = Math.Clamp(2.5 - 0.15 * patternLength, 0.0, 1.0); - return Math.Min(shortPatternPenalty, longPatternPenalty); - } - - /// - /// Calculates a penalty for objects that do not require alternating hands. - /// - /// Time (in milliseconds) since the last hit object. - private double speedPenalty(double deltaTime) - { - if (deltaTime < 80) return 1; - if (deltaTime < 210) return Math.Max(0, 1.4 - 0.005 * deltaTime); - - resetRhythmAndStrain(); - return 0.0; - } - - /// - /// Resets the rolling strain value and counter. - /// - private void resetRhythmAndStrain() - { - currentStrain = 0.0; - notesSinceRhythmChange = 0; + return difficulty; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index d3cdb379d5..ef729e1f07 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -10,18 +10,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { public class TaikoDifficultyAttributes : DifficultyAttributes { - /// - /// The difficulty corresponding to the stamina skill. - /// - [JsonProperty("stamina_difficulty")] - public double StaminaDifficulty { get; set; } - - /// - /// The ratio of stamina difficulty from mono-color (single colour) streams to total stamina difficulty. - /// - [JsonProperty("mono_stamina_factor")] - public double MonoStaminaFactor { get; set; } - /// /// The difficulty corresponding to the rhythm skill. /// @@ -40,8 +28,20 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("colour_difficulty")] public double ColourDifficulty { get; set; } - [JsonProperty("rhythm_difficult_strains")] - public double RhythmTopStrains { get; set; } + /// + /// The difficulty corresponding to the stamina skill. + /// + [JsonProperty("stamina_difficulty")] + public double StaminaDifficulty { get; set; } + + /// + /// The ratio of stamina difficulty from mono-color (single colour) streams to total stamina difficulty. + /// + [JsonProperty("mono_stamina_factor")] + public double MonoStaminaFactor { get; set; } + + [JsonProperty("reading_difficult_strains")] + public double ReadingTopStrains { get; set; } [JsonProperty("colour_difficult_strains")] public double ColourTopStrains { get; set; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 0d6ecb8d3e..f8ff6f6065 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -14,6 +14,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Reading; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data; using osu.Game.Rulesets.Taiko.Difficulty.Skills; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Scoring; @@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty public class TaikoDifficultyCalculator : DifficultyCalculator { private const double difficulty_multiplier = 0.084375; - private const double rhythm_skill_multiplier = 0.200 * difficulty_multiplier; + private const double rhythm_skill_multiplier = 1.24 * difficulty_multiplier; private const double reading_skill_multiplier = 0.100 * difficulty_multiplier; private const double colour_skill_multiplier = 0.375 * difficulty_multiplier; private const double stamina_skill_multiplier = 0.375 * difficulty_multiplier; @@ -37,9 +38,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) { + HitWindows hitWindows = new HitWindows(); + hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); + return new Skill[] { - new Rhythm(mods), + new Rhythm(mods, hitWindows.WindowFor(HitResult.Great) / clockRate), new Reading(mods), new Colour(mods), new Stamina(mods, false), @@ -57,6 +61,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { + var hitWindows = new HitWindows(); + hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); + var difficultyHitObjects = new List(); var centreObjects = new List(); var rimObjects = new List(); @@ -79,7 +86,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty )); } + var groupedHitObjects = SameRhythmHitObjects.GroupHitObjects(noteObjects); + TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects); + SamePatterns.GroupPatterns(groupedHitObjects); bpmLoader.ProcessEffectiveBPM(beatmap.ControlPointInfo, clockRate); return difficultyHitObjects; @@ -105,8 +115,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double monoStaminaRating = singleColourStamina.DifficultyValue() * stamina_skill_multiplier; double monoStaminaFactor = staminaRating == 0 ? 1 : Math.Pow(monoStaminaRating / staminaRating, 5); - double rhythmDifficultStrains = rhythm.CountTopWeightedStrains(); double colourDifficultStrains = colour.CountTopWeightedStrains(); + double readingDifficultStrains = reading.CountTopWeightedStrains(); double staminaDifficultStrains = stamina.CountTopWeightedStrains(); double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax); @@ -134,9 +144,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty ColourDifficulty = colourRating, StaminaDifficulty = staminaRating, MonoStaminaFactor = monoStaminaFactor, - StaminaTopStrains = staminaDifficultStrains, - RhythmTopStrains = rhythmDifficultStrains, + ReadingTopStrains = readingDifficultStrains, ColourTopStrains = colourDifficultStrains, + StaminaTopStrains = staminaDifficultStrains, GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate, OkHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate, MaxCombo = beatmap.GetMaxCombo(), diff --git a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs index 055d8a458b..497a1f8234 100644 --- a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs @@ -56,6 +56,16 @@ namespace osu.Game.Rulesets.Difficulty.Utils /// The p-norm of the vector. public static double Norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p); + /// + /// Calculates a Gaussian-based bell curve function (https://en.wikipedia.org/wiki/Gaussian_function) + /// + /// Value to calculate the function for + /// The mean (center) of the bell curve + /// The width (spread) of the curve + /// Multiplier to adjust the curve's height + /// The output of the bell curve function of + public static double BellCurve(double x, double mean, double width, double multiplier = 1.0) => multiplier * Math.Exp(Math.E * -(Math.Pow(x - mean, 2) / Math.Pow(width, 2))); + /// /// Smootherstep function (https://en.wikipedia.org/wiki/Smoothstep#Variations) /// From 988ed374ae82528991f37516ee40098d2adf1af4 Mon Sep 17 00:00:00 2001 From: James Wilson Date: Sun, 29 Dec 2024 19:29:57 +0000 Subject: [PATCH 041/801] Add basic difficulty & performance calculation for Autopilot mod on osu! ruleset (#21211) * Set speed distance to 0 * Reduce speed & flashlight, remove aim * Remove speed AR bonus * cleanup autopilot mod check in `SpeedEvaluator` * further decrease speed rating for extra hand availability * Pass all mods to the speed evaluator, zero out distance bonus instead of distance --------- Co-authored-by: tsunyoku Co-authored-by: StanR --- .../Difficulty/Evaluators/SpeedEvaluator.cs | 9 ++++++++- .../Difficulty/OsuDifficultyCalculator.cs | 6 ++++++ .../Difficulty/OsuPerformanceCalculator.cs | 6 ++++++ osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 2 +- 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs index a5f6468f17..e5e9769081 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs @@ -2,9 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators @@ -24,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators /// and how easily they can be cheesed. /// /// - public static double EvaluateDifficultyOf(DifficultyHitObject current) + public static double EvaluateDifficultyOf(DifficultyHitObject current, IReadOnlyList mods) { if (current.BaseObject is Spinner) return 0; @@ -56,6 +60,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold double distanceBonus = Math.Pow(distance / single_spacing_threshold, 3.95) * distance_multiplier; + if (mods.OfType().Any()) + distanceBonus = 0; + // Base difficulty with all bonuses double difficulty = (1 + speedBonus + distanceBonus) * 1000 / strainTime; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index ffdd4673e3..d0f23735c3 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -63,6 +63,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedRating = 0.0; flashlightRating *= 0.7; } + else if (mods.Any(h => h is OsuModAutopilot)) + { + speedRating *= 0.5; + aimRating = 0.0; + flashlightRating *= 0.4; + } double baseAimPerformance = OsuStrainSkill.DifficultyToPerformance(aimRating); double baseSpeedPerformance = OsuStrainSkill.DifficultyToPerformance(speedRating); diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 3610845533..df418fb3f8 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -135,6 +135,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes) { + if (score.Mods.Any(h => h is OsuModAutopilot)) + return 0.0; + double aimDifficulty = attributes.AimDifficulty; if (attributes.SliderCount > 0 && attributes.AimDifficultSliderCount > 0) @@ -211,6 +214,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (attributes.ApproachRate > 10.33) approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33); + if (score.Mods.Any(h => h is OsuModAutopilot)) + approachRateFactor = 0.0; + speedValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR. if (score.Mods.Any(m => m is OsuModBlinds)) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index d2c4bbb618..5dae9a9fc5 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills protected override double StrainValueAt(DifficultyHitObject current) { currentStrain *= strainDecay(((OsuDifficultyHitObject)current).StrainTime); - currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current) * skillMultiplier; + currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current, Mods) * skillMultiplier; currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current); From 7563a18c7fdcc40c33a1ef0e0ab5342ba8e879d1 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 24 Dec 2024 09:23:52 -0500 Subject: [PATCH 042/801] Allow locking orientation on iOS in certain circumstances --- osu.Game/OsuGame.cs | 12 ++++ osu.Game/Rulesets/UI/DrawableRuleset.cs | 5 ++ osu.Game/Screens/IOsuScreen.cs | 10 ++++ osu.Game/Screens/OsuScreen.cs | 2 + osu.Game/Screens/Play/Player.cs | 2 + osu.iOS/AppDelegate.cs | 49 +++++++++++++++- osu.iOS/IOSOrientationHandler.cs | 76 +++++++++++++++++++++++++ osu.iOS/OsuGameIOS.cs | 12 ++++ 8 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 osu.iOS/IOSOrientationHandler.cs diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 06e30e3fab..4352eb2a71 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -174,6 +174,16 @@ namespace osu.Game /// public readonly IBindable OverlayActivationMode = new Bindable(); + /// + /// On mobile devices, this specifies whether the device should be set and locked to portrait orientation. + /// + /// + /// Implementations can be viewed in mobile projects. + /// + public IBindable RequiresPortraitOrientation => requiresPortraitOrientation; + + private readonly Bindable requiresPortraitOrientation = new BindableBool(); + /// /// Whether the back button is currently displayed. /// @@ -1623,6 +1633,8 @@ namespace osu.Game GlobalCursorDisplay.MenuCursor.HideCursorOnNonMouseInput = newOsuScreen.HideMenuCursorOnNonMouseInput; + requiresPortraitOrientation.Value = newOsuScreen.RequiresPortraitOrientation; + if (newOsuScreen.HideOverlaysOnEnter) CloseAllOverlays(); else diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index ebd84fd91b..13d4b67132 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -577,6 +577,11 @@ namespace osu.Game.Rulesets.UI /// public virtual bool AllowGameplayOverlays => true; + /// + /// On mobile devices, this specifies whether this ruleset requires the device to be in portrait orientation. + /// + public virtual bool RequiresPortraitOrientation => false; + /// /// Sets a replay to be used, overriding local input. /// diff --git a/osu.Game/Screens/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs index 9e474ed0c6..8b3ff4306f 100644 --- a/osu.Game/Screens/IOsuScreen.cs +++ b/osu.Game/Screens/IOsuScreen.cs @@ -61,6 +61,16 @@ namespace osu.Game.Screens /// bool HideMenuCursorOnNonMouseInput { get; } + /// + /// On mobile devices, this specifies whether this requires the device to be in portrait orientation. + /// + /// + /// By default, all screens in the game display in landscape orientation. + /// Setting this to true will display this screen in portrait orientation instead, + /// and switch back to landscape when transitioning back to a regular non-portrait screen. + /// + bool RequiresPortraitOrientation { get; } + /// /// Whether overlays should be able to be opened when this screen is current. /// diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index ab66241a77..e1d1ac38da 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -47,6 +47,8 @@ namespace osu.Game.Screens public virtual bool HideMenuCursorOnNonMouseInput => false; + public virtual bool RequiresPortraitOrientation => false; + /// /// The initial overlay activation mode to use when this screen is entered for the first time. /// diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 228b77b780..e50f97f912 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -68,6 +68,8 @@ namespace osu.Game.Screens.Play public override bool HideMenuCursorOnNonMouseInput => true; + public override bool RequiresPortraitOrientation => DrawableRuleset.RequiresPortraitOrientation; + protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered; // We are managing our own adjustments (see OnEntering/OnExiting). diff --git a/osu.iOS/AppDelegate.cs b/osu.iOS/AppDelegate.cs index e88b39f710..5d309f2fc1 100644 --- a/osu.iOS/AppDelegate.cs +++ b/osu.iOS/AppDelegate.cs @@ -1,14 +1,61 @@ // 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 Foundation; using osu.Framework.iOS; +using UIKit; namespace osu.iOS { [Register("AppDelegate")] public class AppDelegate : GameApplicationDelegate { - protected override Framework.Game CreateGame() => new OsuGameIOS(); + private UIInterfaceOrientationMask? defaultOrientationsMask; + private UIInterfaceOrientationMask? orientations; + + /// + /// The current orientation the game is displayed in. + /// + public UIInterfaceOrientation CurrentOrientation => Host.Window.UIWindow.WindowScene!.InterfaceOrientation; + + /// + /// Controls the orientations allowed for the device to rotate to, overriding the default allowed orientations. + /// + public UIInterfaceOrientationMask? Orientations + { + get => orientations; + set + { + if (orientations == value) + return; + + orientations = value; + + if (OperatingSystem.IsIOSVersionAtLeast(16)) + Host.Window.ViewController.SetNeedsUpdateOfSupportedInterfaceOrientations(); + else + UIViewController.AttemptRotationToDeviceOrientation(); + } + } + + protected override Framework.Game CreateGame() => new OsuGameIOS(this); + + public override UIInterfaceOrientationMask GetSupportedInterfaceOrientations(UIApplication application, UIWindow forWindow) + { + if (orientations != null) + return orientations.Value; + + if (defaultOrientationsMask == null) + { + defaultOrientationsMask = 0; + var defaultOrientations = (NSArray)NSBundle.MainBundle.ObjectForInfoDictionary("UISupportedInterfaceOrientations"); + + foreach (var value in defaultOrientations.ToArray()) + defaultOrientationsMask |= Enum.Parse(value.ToString().Replace("UIInterfaceOrientation", string.Empty)); + } + + return defaultOrientationsMask.Value; + } } } diff --git a/osu.iOS/IOSOrientationHandler.cs b/osu.iOS/IOSOrientationHandler.cs new file mode 100644 index 0000000000..9b60497be8 --- /dev/null +++ b/osu.iOS/IOSOrientationHandler.cs @@ -0,0 +1,76 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game; +using osu.Game.Screens.Play; +using UIKit; + +namespace osu.iOS +{ + public partial class IOSOrientationHandler : Component + { + private readonly AppDelegate appDelegate; + + [Resolved] + private OsuGame game { get; set; } = null!; + + [Resolved] + private ILocalUserPlayInfo localUserPlayInfo { get; set; } = null!; + + private IBindable requiresPortraitOrientation = null!; + private IBindable localUserPlaying = null!; + + public IOSOrientationHandler(AppDelegate appDelegate) + { + this.appDelegate = appDelegate; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + requiresPortraitOrientation = game.RequiresPortraitOrientation.GetBoundCopy(); + requiresPortraitOrientation.BindValueChanged(_ => updateOrientations()); + + localUserPlaying = localUserPlayInfo.PlayingState.GetBoundCopy(); + localUserPlaying.BindValueChanged(_ => updateOrientations()); + + updateOrientations(); + } + + private void updateOrientations() + { + UIInterfaceOrientation currentOrientation = appDelegate.CurrentOrientation; + bool lockCurrentOrientation = localUserPlaying.Value == LocalUserPlayingState.Playing; + bool lockToPortrait = requiresPortraitOrientation.Value; + bool isPhone = UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Phone; + + if (lockCurrentOrientation) + { + if (lockToPortrait && !currentOrientation.IsPortrait()) + currentOrientation = UIInterfaceOrientation.Portrait; + else if (!lockToPortrait && currentOrientation.IsPortrait() && isPhone) + currentOrientation = UIInterfaceOrientation.LandscapeRight; + + appDelegate.Orientations = (UIInterfaceOrientationMask)(1 << (int)currentOrientation); + return; + } + + if (lockToPortrait) + { + UIInterfaceOrientationMask portraitOrientations = UIInterfaceOrientationMask.Portrait; + + if (!isPhone) + portraitOrientations |= UIInterfaceOrientationMask.PortraitUpsideDown; + + appDelegate.Orientations = portraitOrientations; + return; + } + + appDelegate.Orientations = null; + } + } +} diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index a9ca1778a0..6a3d0d0ba4 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -15,10 +15,22 @@ namespace osu.iOS { public partial class OsuGameIOS : OsuGame { + private readonly AppDelegate appDelegate; public override Version AssemblyVersion => new Version(NSBundle.MainBundle.InfoDictionary["CFBundleVersion"].ToString()); public override bool HideUnlicensedContent => true; + public OsuGameIOS(AppDelegate appDelegate) + { + this.appDelegate = appDelegate; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Add(new IOSOrientationHandler(appDelegate)); + } + protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier(); protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo(); From 9d08bc2b50d9e5b80f38f0ebad2b72c6f3855361 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 28 Dec 2024 19:22:45 -0500 Subject: [PATCH 043/801] Improve osu!mania gameplay scaling on portrait orientation --- .../UI/DrawableManiaRuleset.cs | 2 + .../UI/ManiaPlayfieldAdjustmentContainer.cs | 51 ++++++++++++++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index d173ae4143..136b172a59 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -51,6 +51,8 @@ namespace osu.Game.Rulesets.Mania.UI public IEnumerable BarLines; + public override bool RequiresPortraitOrientation => Beatmap.Stages.Count == 1; + protected override bool RelativeScaleBeatLengths => true; protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config; diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs index 1183b616f5..d7cb211d4a 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs @@ -1,17 +1,64 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.UI; +using osuTK; namespace osu.Game.Rulesets.Mania.UI { public partial class ManiaPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer { + protected override Container Content { get; } + + private readonly DrawSizePreservingFillContainer scalingContainer; + public ManiaPlayfieldAdjustmentContainer() { - Anchor = Anchor.Centre; - Origin = Anchor.Centre; + InternalChild = scalingContainer = new DrawSizePreservingFillContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Child = Content = new Container + { + RelativeSizeAxes = Axes.Both, + } + }; + } + + [Resolved] + private DrawableManiaRuleset drawableManiaRuleset { get; set; } = null!; + + protected override void Update() + { + base.Update(); + + float aspectRatio = DrawWidth / DrawHeight; + bool isPortrait = aspectRatio < 4 / 3f; + + if (isPortrait && drawableManiaRuleset.Beatmap.Stages.Count == 1) + { + // Scale playfield up by 25% to become playable on mobile devices, + // and leave a 10% horizontal gap if the playfield is scaled down due to being too wide. + const float base_scale = 1.25f; + const float base_width = 768f / base_scale; + const float side_gap = 0.9f; + + scalingContainer.Strategy = DrawSizePreservationStrategy.Maximum; + float stageWidth = drawableManiaRuleset.Playfield.Stages[0].DrawWidth; + scalingContainer.TargetDrawSize = new Vector2(1024, base_width * Math.Max(stageWidth / aspectRatio / (base_width * side_gap), 1f)); + } + else + { + scalingContainer.Strategy = DrawSizePreservationStrategy.Minimum; + scalingContainer.Scale = new Vector2(1f); + scalingContainer.Size = new Vector2(1f); + scalingContainer.TargetDrawSize = new Vector2(1024, 768); + } } } } From d7e4038f4ae75645a6f074e7c49c9265ac9f04e2 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 29 Dec 2024 23:54:04 -0500 Subject: [PATCH 044/801] Keep game in portrait mode when restarting --- osu.Game/Screens/Play/PlayerLoader.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 837974a8f2..b258de0e9e 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -54,6 +54,9 @@ namespace osu.Game.Screens.Play public override bool? AllowGlobalTrackControl => false; + // this makes the game stay in portrait mode when restarting gameplay rather than switching back to landscape. + public override bool RequiresPortraitOrientation => CurrentPlayer?.RequiresPortraitOrientation == true; + public override float BackgroundParallaxAmount => quickRestart ? 0 : 1; // Here because IsHovered will not update unless we do so. From 0cd7f1b2d4f138443260042cb04ca6cbf2988184 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 30 Dec 2024 15:04:21 -0500 Subject: [PATCH 045/801] Abstractify orientation handling and add Android support --- osu.Android/AndroidOrientationManager.cs | 39 ++++++++++ osu.Android/GameplayScreenRotationLocker.cs | 34 --------- osu.Android/OsuGameActivity.cs | 6 +- osu.Android/OsuGameAndroid.cs | 2 +- osu.Game/Mobile/GameOrientation.cs | 34 +++++++++ osu.Game/Mobile/OrientationManager.cs | 84 +++++++++++++++++++++ osu.iOS/IOSOrientationHandler.cs | 76 ------------------- osu.iOS/IOSOrientationManager.cs | 41 ++++++++++ osu.iOS/OsuGameIOS.cs | 2 +- 9 files changed, 204 insertions(+), 114 deletions(-) create mode 100644 osu.Android/AndroidOrientationManager.cs delete mode 100644 osu.Android/GameplayScreenRotationLocker.cs create mode 100644 osu.Game/Mobile/GameOrientation.cs create mode 100644 osu.Game/Mobile/OrientationManager.cs delete mode 100644 osu.iOS/IOSOrientationHandler.cs create mode 100644 osu.iOS/IOSOrientationManager.cs diff --git a/osu.Android/AndroidOrientationManager.cs b/osu.Android/AndroidOrientationManager.cs new file mode 100644 index 0000000000..76d2fc24cb --- /dev/null +++ b/osu.Android/AndroidOrientationManager.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Android.Content.PM; +using Android.Content.Res; +using osu.Framework.Allocation; +using osu.Game.Mobile; + +namespace osu.Android +{ + public partial class AndroidOrientationManager : OrientationManager + { + [Resolved] + private OsuGameActivity gameActivity { get; set; } = null!; + + protected override bool IsCurrentOrientationPortrait => gameActivity.Resources!.Configuration!.Orientation == Orientation.Portrait; + protected override bool IsTablet => gameActivity.IsTablet; + + protected override void SetAllowedOrientations(GameOrientation? orientation) + => gameActivity.RequestedOrientation = orientation == null ? gameActivity.DefaultOrientation : toScreenOrientation(orientation.Value); + + private static ScreenOrientation toScreenOrientation(GameOrientation orientation) + { + if (orientation == GameOrientation.Locked) + return ScreenOrientation.Locked; + + if (orientation == GameOrientation.Portrait) + return ScreenOrientation.Portrait; + + if (orientation == GameOrientation.Landscape) + return ScreenOrientation.Landscape; + + if (orientation == GameOrientation.FullPortrait) + return ScreenOrientation.SensorPortrait; + + return ScreenOrientation.SensorLandscape; + } + } +} diff --git a/osu.Android/GameplayScreenRotationLocker.cs b/osu.Android/GameplayScreenRotationLocker.cs deleted file mode 100644 index 42583b5dc2..0000000000 --- a/osu.Android/GameplayScreenRotationLocker.cs +++ /dev/null @@ -1,34 +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 Android.Content.PM; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Game.Screens.Play; - -namespace osu.Android -{ - public partial class GameplayScreenRotationLocker : Component - { - private IBindable localUserPlaying = null!; - - [Resolved] - private OsuGameActivity gameActivity { get; set; } = null!; - - [BackgroundDependencyLoader] - private void load(ILocalUserPlayInfo localUserPlayInfo) - { - localUserPlaying = localUserPlayInfo.PlayingState.GetBoundCopy(); - localUserPlaying.BindValueChanged(updateLock, true); - } - - private void updateLock(ValueChangedEvent userPlaying) - { - gameActivity.RunOnUiThread(() => - { - gameActivity.RequestedOrientation = userPlaying.NewValue == LocalUserPlayingState.Playing ? ScreenOrientation.Locked : gameActivity.DefaultOrientation; - }); - } - } -} diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index bbee491d90..b3717791da 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -50,6 +50,8 @@ namespace osu.Android /// Adjusted on startup to match expected UX for the current device type (phone/tablet). public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified; + public bool IsTablet { get; private set; } + private OsuGameAndroid game = null!; protected override Framework.Game CreateGame() => game = new OsuGameAndroid(this); @@ -76,9 +78,9 @@ namespace osu.Android WindowManager.DefaultDisplay.GetSize(displaySize); #pragma warning restore CA1422 float smallestWidthDp = Math.Min(displaySize.X, displaySize.Y) / Resources.DisplayMetrics.Density; - bool isTablet = smallestWidthDp >= 600f; + IsTablet = smallestWidthDp >= 600f; - RequestedOrientation = DefaultOrientation = isTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape; + RequestedOrientation = DefaultOrientation = IsTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape; // Currently (SDK 6.0.200), BundleAssemblies is not runnable for net6-android. // The assembly files are not available as files either after native AOT. diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index ffab7dd86d..4143c8cae6 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -71,7 +71,7 @@ namespace osu.Android protected override void LoadComplete() { base.LoadComplete(); - LoadComponentAsync(new GameplayScreenRotationLocker(), Add); + LoadComponentAsync(new AndroidOrientationManager(), Add); } public override void SetHost(GameHost host) diff --git a/osu.Game/Mobile/GameOrientation.cs b/osu.Game/Mobile/GameOrientation.cs new file mode 100644 index 0000000000..0022c8fefb --- /dev/null +++ b/osu.Game/Mobile/GameOrientation.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Mobile +{ + public enum GameOrientation + { + /// + /// Lock the game orientation. + /// + Locked, + + /// + /// Display the game in regular portrait orientation. + /// + Portrait, + + /// + /// Display the game in landscape-right orientation. + /// + Landscape, + + /// + /// Display the game in landscape-right/landscape-left orientations. + /// + FullLandscape, + + /// + /// Display the game in portrait/portrait-upside-down orientations. + /// This is exclusive to tablet mobile devices. + /// + FullPortrait, + } +} diff --git a/osu.Game/Mobile/OrientationManager.cs b/osu.Game/Mobile/OrientationManager.cs new file mode 100644 index 0000000000..b78bf8e760 --- /dev/null +++ b/osu.Game/Mobile/OrientationManager.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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Screens.Play; + +namespace osu.Game.Mobile +{ + /// + /// A that manages the device orientations a game can display in. + /// + public abstract partial class OrientationManager : Component + { + /// + /// Whether the current orientation of the game is portrait. + /// + protected abstract bool IsCurrentOrientationPortrait { get; } + + /// + /// Whether the mobile device is considered a tablet. + /// + protected abstract bool IsTablet { get; } + + [Resolved] + private OsuGame game { get; set; } = null!; + + [Resolved] + private ILocalUserPlayInfo localUserPlayInfo { get; set; } = null!; + + private IBindable requiresPortraitOrientation = null!; + private IBindable localUserPlaying = null!; + + protected override void LoadComplete() + { + base.LoadComplete(); + + requiresPortraitOrientation = game.RequiresPortraitOrientation.GetBoundCopy(); + requiresPortraitOrientation.BindValueChanged(_ => updateOrientations()); + + localUserPlaying = localUserPlayInfo.PlayingState.GetBoundCopy(); + localUserPlaying.BindValueChanged(_ => updateOrientations()); + + updateOrientations(); + } + + private void updateOrientations() + { + bool lockCurrentOrientation = localUserPlaying.Value == LocalUserPlayingState.Playing; + bool lockToPortrait = requiresPortraitOrientation.Value; + + if (lockCurrentOrientation) + { + if (lockToPortrait && !IsCurrentOrientationPortrait) + SetAllowedOrientations(GameOrientation.Portrait); + else if (!lockToPortrait && IsCurrentOrientationPortrait && !IsTablet) + SetAllowedOrientations(GameOrientation.Landscape); + else + SetAllowedOrientations(GameOrientation.Locked); + + return; + } + + if (lockToPortrait) + { + if (IsTablet) + SetAllowedOrientations(GameOrientation.FullPortrait); + else + SetAllowedOrientations(GameOrientation.Portrait); + + return; + } + + SetAllowedOrientations(null); + } + + /// + /// Sets the allowed orientations the device can rotate to. + /// + /// The allowed orientations, or null to return back to default. + protected abstract void SetAllowedOrientations(GameOrientation? orientation); + } +} diff --git a/osu.iOS/IOSOrientationHandler.cs b/osu.iOS/IOSOrientationHandler.cs deleted file mode 100644 index 9b60497be8..0000000000 --- a/osu.iOS/IOSOrientationHandler.cs +++ /dev/null @@ -1,76 +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.Bindables; -using osu.Framework.Graphics; -using osu.Game; -using osu.Game.Screens.Play; -using UIKit; - -namespace osu.iOS -{ - public partial class IOSOrientationHandler : Component - { - private readonly AppDelegate appDelegate; - - [Resolved] - private OsuGame game { get; set; } = null!; - - [Resolved] - private ILocalUserPlayInfo localUserPlayInfo { get; set; } = null!; - - private IBindable requiresPortraitOrientation = null!; - private IBindable localUserPlaying = null!; - - public IOSOrientationHandler(AppDelegate appDelegate) - { - this.appDelegate = appDelegate; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - requiresPortraitOrientation = game.RequiresPortraitOrientation.GetBoundCopy(); - requiresPortraitOrientation.BindValueChanged(_ => updateOrientations()); - - localUserPlaying = localUserPlayInfo.PlayingState.GetBoundCopy(); - localUserPlaying.BindValueChanged(_ => updateOrientations()); - - updateOrientations(); - } - - private void updateOrientations() - { - UIInterfaceOrientation currentOrientation = appDelegate.CurrentOrientation; - bool lockCurrentOrientation = localUserPlaying.Value == LocalUserPlayingState.Playing; - bool lockToPortrait = requiresPortraitOrientation.Value; - bool isPhone = UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Phone; - - if (lockCurrentOrientation) - { - if (lockToPortrait && !currentOrientation.IsPortrait()) - currentOrientation = UIInterfaceOrientation.Portrait; - else if (!lockToPortrait && currentOrientation.IsPortrait() && isPhone) - currentOrientation = UIInterfaceOrientation.LandscapeRight; - - appDelegate.Orientations = (UIInterfaceOrientationMask)(1 << (int)currentOrientation); - return; - } - - if (lockToPortrait) - { - UIInterfaceOrientationMask portraitOrientations = UIInterfaceOrientationMask.Portrait; - - if (!isPhone) - portraitOrientations |= UIInterfaceOrientationMask.PortraitUpsideDown; - - appDelegate.Orientations = portraitOrientations; - return; - } - - appDelegate.Orientations = null; - } - } -} diff --git a/osu.iOS/IOSOrientationManager.cs b/osu.iOS/IOSOrientationManager.cs new file mode 100644 index 0000000000..6d5bb990c2 --- /dev/null +++ b/osu.iOS/IOSOrientationManager.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Mobile; +using UIKit; + +namespace osu.iOS +{ + public partial class IOSOrientationManager : OrientationManager + { + private readonly AppDelegate appDelegate; + + protected override bool IsCurrentOrientationPortrait => appDelegate.CurrentOrientation.IsPortrait(); + protected override bool IsTablet => UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad; + + public IOSOrientationManager(AppDelegate appDelegate) + { + this.appDelegate = appDelegate; + } + + protected override void SetAllowedOrientations(GameOrientation? orientation) + => appDelegate.Orientations = orientation == null ? null : toUIInterfaceOrientationMask(orientation.Value); + + private UIInterfaceOrientationMask toUIInterfaceOrientationMask(GameOrientation orientation) + { + if (orientation == GameOrientation.Locked) + return (UIInterfaceOrientationMask)(1 << (int)appDelegate.CurrentOrientation); + + if (orientation == GameOrientation.Portrait) + return UIInterfaceOrientationMask.Portrait; + + if (orientation == GameOrientation.Landscape) + return UIInterfaceOrientationMask.LandscapeRight; + + if (orientation == GameOrientation.FullPortrait) + return UIInterfaceOrientationMask.Portrait | UIInterfaceOrientationMask.PortraitUpsideDown; + + return UIInterfaceOrientationMask.Landscape; + } + } +} diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index 6a3d0d0ba4..ed47a1e8b8 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -28,7 +28,7 @@ namespace osu.iOS protected override void LoadComplete() { base.LoadComplete(); - Add(new IOSOrientationHandler(appDelegate)); + LoadComponentAsync(new IOSOrientationManager(appDelegate), Add); } protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier(); From 1e08b3dbdac1ed07fd56c0d55d83ce200053c336 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 29 Dec 2024 23:33:32 -0500 Subject: [PATCH 046/801] Make mania judgements relative to the hit target position This improves display in portrait screen, where the stage is scaled up. --- .../Mods/ManiaModWithPlayfieldCover.cs | 2 +- .../Skinning/Argon/ArgonJudgementPiece.cs | 2 +- .../Skinning/Legacy/LegacyManiaJudgementPiece.cs | 12 +++++------- .../UI/Components/ColumnHitObjectArea.cs | 2 +- ...bjectArea.cs => HitPositionPaddedContainer.cs} | 15 ++++----------- .../UI/DrawableManiaJudgement.cs | 3 +++ osu.Game.Rulesets.Mania/UI/Stage.cs | 12 ++++++------ 7 files changed, 21 insertions(+), 27 deletions(-) rename osu.Game.Rulesets.Mania/UI/Components/{HitObjectArea.cs => HitPositionPaddedContainer.cs} (74%) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs index 864ef6c3d6..1bc16112c5 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Mania.Mods foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns)) { - HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer; + HitObjectContainer hoc = column.HitObjectContainer; Container hocParent = (Container)hoc.Parent!; hocParent.Remove(hoc, false); diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs index 0052fd8b78..a1c81d3a6a 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon { public partial class ArgonJudgementPiece : TextJudgementPiece, IAnimatableJudgement { - private const float judgement_y_position = 160; + private const float judgement_y_position = -180f; private RingExplosion? ringExplosion; diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs index d21a8cd140..4b0cc482d9 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; @@ -23,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy this.result = result; this.animation = animation; - Anchor = Anchor.Centre; + Anchor = Anchor.BottomCentre; Origin = Anchor.Centre; AutoSizeAxes = Axes.Both; @@ -32,12 +31,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy [BackgroundDependencyLoader] private void load(ISkinSource skin) { - float? scorePosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value; + float hitPosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? 0; + float scorePosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value ?? 0; - if (scorePosition != null) - scorePosition -= Stage.HIT_TARGET_POSITION + 150; - - Y = scorePosition ?? 0; + float absoluteHitPosition = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition; + Y = scorePosition - absoluteHitPosition; InternalChild = animation.With(d => { diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs index 91e0f2c19b..2d719ef764 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs @@ -9,7 +9,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.UI.Components { - public partial class ColumnHitObjectArea : HitObjectArea + public partial class ColumnHitObjectArea : HitPositionPaddedContainer { public readonly Container Explosions; diff --git a/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs similarity index 74% rename from osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs rename to osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs index 2ad6e4f076..f591102f6c 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs @@ -1,29 +1,22 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Skinning; -using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.UI.Components { - public partial class HitObjectArea : SkinReloadableDrawable + public partial class HitPositionPaddedContainer : SkinReloadableDrawable { protected readonly IBindable Direction = new Bindable(); - public readonly HitObjectContainer HitObjectContainer; - public HitObjectArea(HitObjectContainer hitObjectContainer) + public HitPositionPaddedContainer(Drawable child) { - InternalChild = new Container - { - RelativeSizeAxes = Axes.Both, - Child = HitObjectContainer = hitObjectContainer - }; + InternalChild = child; } [BackgroundDependencyLoader] diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index 9f25a44e21..5b87c74bbe 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -15,9 +15,12 @@ namespace osu.Game.Rulesets.Mania.UI private partial class DefaultManiaJudgementPiece : DefaultJudgementPiece { + private const float judgement_y_position = -180f; + public DefaultManiaJudgementPiece(HitResult result) : base(result) { + Y = judgement_y_position; } protected override void LoadComplete() diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index 9fb77a4995..2d73e7bcbe 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Mania.UI Width = 1366, // Bar lines should only be masked on the vertical axis BypassAutoSizeAxes = Axes.Both, Masking = true, - Child = barLineContainer = new HitObjectArea(HitObjectContainer) + Child = barLineContainer = new HitPositionPaddedContainer(HitObjectContainer) { Name = "Bar lines", Anchor = Anchor.TopCentre, @@ -119,12 +119,12 @@ namespace osu.Game.Rulesets.Mania.UI { RelativeSizeAxes = Axes.Both }, - judgements = new JudgementContainer + new HitPositionPaddedContainer(judgements = new JudgementContainer + { + RelativeSizeAxes = Axes.Both, + }) { - Anchor = Anchor.TopCentre, - Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Y = HIT_TARGET_POSITION + 150 }, topLevelContainer = new Container { RelativeSizeAxes = Axes.Both } } @@ -218,7 +218,7 @@ namespace osu.Game.Rulesets.Mania.UI { j.Apply(result, judgedObject); - j.Anchor = Anchor.Centre; + j.Anchor = Anchor.BottomCentre; j.Origin = Anchor.Centre; })!); } From bea61d24835e31af9821bce4e96e1cfd33c9f988 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 31 Dec 2024 12:28:04 -0500 Subject: [PATCH 047/801] Replace `ManiaTouchInputArea` with touchable columns --- .../TestSceneManiaTouchInput.cs | 68 ++++++ .../TestSceneManiaTouchInputArea.cs | 49 ----- osu.Game.Rulesets.Mania/UI/Column.cs | 24 +++ .../UI/DrawableManiaRuleset.cs | 2 - .../UI/ManiaTouchInputArea.cs | 199 ------------------ 5 files changed, 92 insertions(+), 250 deletions(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs delete mode 100644 osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInputArea.cs delete mode 100644 osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs new file mode 100644 index 0000000000..dc95cd9ca0 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs @@ -0,0 +1,68 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Input; +using osu.Framework.Testing; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests +{ + public partial class TestSceneManiaTouchInput : PlayerTestScene + { + protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); + + [Test] + public void TestTouchInput() + { + for (int i = 0; i < 4; i++) + { + int index = i; + + AddStep($"touch column {index}", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getColumn(index).ScreenSpaceDrawQuad.Centre))); + + AddAssert("action sent", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Contain(getColumn(index).Action.Value)); + + AddStep($"release column {index}", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(index).ScreenSpaceDrawQuad.Centre))); + + AddAssert("action released", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Not.Contain(getColumn(index).Action.Value)); + } + } + + [Test] + public void TestOneColumnMultipleTouches() + { + AddStep("touch column 0", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre))); + + AddAssert("action sent", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Contain(getColumn(0).Action.Value)); + + AddStep("touch another finger", () => InputManager.BeginTouch(new Touch(TouchSource.Touch2, getColumn(0).ScreenSpaceDrawQuad.Centre))); + + AddAssert("action still pressed", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Contain(getColumn(0).Action.Value)); + + AddStep("release first finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre))); + + AddAssert("action still pressed", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Contain(getColumn(0).Action.Value)); + + AddStep("release second finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch2, getColumn(0).ScreenSpaceDrawQuad.Centre))); + + AddAssert("action released", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Not.Contain(getColumn(0).Action.Value)); + } + + private Column getColumn(int index) => this.ChildrenOfType().ElementAt(index); + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInputArea.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInputArea.cs deleted file mode 100644 index 30c0113bff..0000000000 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInputArea.cs +++ /dev/null @@ -1,49 +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 NUnit.Framework; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input; -using osu.Framework.Testing; -using osu.Game.Rulesets.Mania.UI; -using osu.Game.Tests.Visual; - -namespace osu.Game.Rulesets.Mania.Tests -{ - public partial class TestSceneManiaTouchInputArea : PlayerTestScene - { - protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); - - [Test] - public void TestTouchAreaNotInitiallyVisible() - { - AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden); - } - - [Test] - public void TestPressReceptors() - { - AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden); - - for (int i = 0; i < 4; i++) - { - int index = i; - - AddStep($"touch receptor {index}", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre))); - - AddAssert("action sent", - () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), - () => Does.Contain(getReceptor(index).Action.Value)); - - AddStep($"release receptor {index}", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre))); - - AddAssert("touch area visible", () => getTouchOverlay()?.State.Value == Visibility.Visible); - } - } - - private ManiaTouchInputArea? getTouchOverlay() => this.ChildrenOfType().SingleOrDefault(); - - private ManiaTouchInputArea.ColumnInputReceptor getReceptor(int index) => this.ChildrenOfType().ElementAt(index); - } -} diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index c05a8f2a29..99d952ef1f 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -180,5 +180,29 @@ namespace osu.Game.Rulesets.Mania.UI public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) // This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border => DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos)); + + #region Touch Input + + [Resolved(canBeNull: true)] + private ManiaInputManager maniaInputManager { get; set; } + + private int touchActivationCount; + + protected override bool OnTouchDown(TouchDownEvent e) + { + maniaInputManager.KeyBindingContainer.TriggerPressed(Action.Value); + touchActivationCount++; + return true; + } + + protected override void OnTouchUp(TouchUpEvent e) + { + touchActivationCount--; + + if (touchActivationCount == 0) + maniaInputManager.KeyBindingContainer.TriggerReleased(Action.Value); + } + + #endregion } } diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index 136b172a59..65841af5de 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -112,8 +112,6 @@ namespace osu.Game.Rulesets.Mania.UI configScrollSpeed.BindValueChanged(speed => TargetTimeRange = ComputeScrollTime(speed.NewValue)); TimeRange.Value = TargetTimeRange = currentTimeRange = ComputeScrollTime(configScrollSpeed.Value); - - KeyBindingInputManager.Add(new ManiaTouchInputArea()); } protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount; diff --git a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs deleted file mode 100644 index 8c4a71cf24..0000000000 --- a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs +++ /dev/null @@ -1,199 +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.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; -using osu.Game.Configuration; -using osuTK; - -namespace osu.Game.Rulesets.Mania.UI -{ - /// - /// An overlay that captures and displays osu!mania mouse and touch input. - /// - public partial class ManiaTouchInputArea : VisibilityContainer - { - // visibility state affects our child. we always want to handle input. - public override bool PropagatePositionalInputSubTree => true; - public override bool PropagateNonPositionalInputSubTree => true; - - [SettingSource("Spacing", "The spacing between receptors.")] - public BindableFloat Spacing { get; } = new BindableFloat(10) - { - Precision = 1, - MinValue = 0, - MaxValue = 100, - }; - - [SettingSource("Opacity", "The receptor opacity.")] - public BindableFloat Opacity { get; } = new BindableFloat(1) - { - Precision = 0.1f, - MinValue = 0, - MaxValue = 1 - }; - - [Resolved] - private DrawableManiaRuleset drawableRuleset { get; set; } = null!; - - private GridContainer gridContainer = null!; - - public ManiaTouchInputArea() - { - Anchor = Anchor.BottomCentre; - Origin = Anchor.BottomCentre; - - RelativeSizeAxes = Axes.Both; - Height = 0.5f; - } - - [BackgroundDependencyLoader] - private void load() - { - List receptorGridContent = new List(); - List receptorGridDimensions = new List(); - - bool first = true; - - foreach (var stage in drawableRuleset.Playfield.Stages) - { - foreach (var column in stage.Columns) - { - if (!first) - { - receptorGridContent.Add(new Gutter { Spacing = { BindTarget = Spacing } }); - receptorGridDimensions.Add(new Dimension(GridSizeMode.AutoSize)); - } - - receptorGridContent.Add(new ColumnInputReceptor { Action = { BindTarget = column.Action } }); - receptorGridDimensions.Add(new Dimension()); - - first = false; - } - } - - InternalChild = gridContainer = new GridContainer - { - RelativeSizeAxes = Axes.Both, - AlwaysPresent = true, - Content = new[] { receptorGridContent.ToArray() }, - ColumnDimensions = receptorGridDimensions.ToArray() - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - Opacity.BindValueChanged(o => Alpha = o.NewValue, true); - } - - protected override bool OnKeyDown(KeyDownEvent e) - { - // Hide whenever the keyboard is used. - Hide(); - return false; - } - - protected override bool OnTouchDown(TouchDownEvent e) - { - Show(); - return true; - } - - protected override void PopIn() - { - gridContainer.FadeIn(500, Easing.OutQuint); - } - - protected override void PopOut() - { - gridContainer.FadeOut(300); - } - - public partial class ColumnInputReceptor : CompositeDrawable - { - public readonly IBindable Action = new Bindable(); - - private readonly Box highlightOverlay; - - [Resolved] - private ManiaInputManager? inputManager { get; set; } - - private bool isPressed; - - public ColumnInputReceptor() - { - RelativeSizeAxes = Axes.Both; - - InternalChildren = new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = 10, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0.15f, - }, - highlightOverlay = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - Blending = BlendingParameters.Additive, - } - } - } - }; - } - - protected override bool OnTouchDown(TouchDownEvent e) - { - updateButton(true); - return false; // handled by parent container to show overlay. - } - - protected override void OnTouchUp(TouchUpEvent e) - { - updateButton(false); - } - - private void updateButton(bool press) - { - if (press == isPressed) - return; - - isPressed = press; - - if (press) - { - inputManager?.KeyBindingContainer.TriggerPressed(Action.Value); - highlightOverlay.FadeTo(0.1f, 80, Easing.OutQuint); - } - else - { - inputManager?.KeyBindingContainer.TriggerReleased(Action.Value); - highlightOverlay.FadeTo(0, 400, Easing.OutQuint); - } - } - } - - private partial class Gutter : Drawable - { - public readonly IBindable Spacing = new Bindable(); - - public Gutter() - { - Spacing.BindValueChanged(s => Size = new Vector2(s.NewValue)); - } - } - } -} From 64e557d00f98728e5a67d84c3158a8a11478c168 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 31 Dec 2024 20:01:21 -0500 Subject: [PATCH 048/801] Simplify portrait check --- osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs index d7cb211d4a..f7c4850a94 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.UI base.Update(); float aspectRatio = DrawWidth / DrawHeight; - bool isPortrait = aspectRatio < 4 / 3f; + bool isPortrait = aspectRatio < 1f; if (isPortrait && drawableManiaRuleset.Beatmap.Stages.Count == 1) { From e5713e52392066a1430ebce460d07d8af01ad29f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 31 Dec 2024 21:31:52 -0500 Subject: [PATCH 049/801] Fix triangles judgement mispositioned on a miss Similar to mania's `ArgonJudgementPiece`. --- .../UI/DrawableManiaJudgement.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index 5b87c74bbe..a1dabd66bc 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; +using osuTK; namespace osu.Game.Rulesets.Mania.UI { @@ -35,8 +36,20 @@ namespace osu.Game.Rulesets.Mania.UI switch (Result) { case HitResult.None: + this.FadeOutFromOne(800); + break; + case HitResult.Miss: - base.PlayAnimation(); + this.ScaleTo(1.6f); + this.ScaleTo(1, 100, Easing.In); + + this.MoveToY(judgement_y_position); + this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); + + this.RotateTo(0); + this.RotateTo(40, 800, Easing.InQuint); + + this.FadeOutFromOne(800); break; default: From c221a0c9f93c20949f26459405cfcc5047a39e0b Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 1 Jan 2025 01:43:43 -0500 Subject: [PATCH 050/801] Improve UI scale on iOS devices --- osu.Game/Graphics/Containers/ScalingContainer.cs | 6 ++++++ osu.Game/OsuGame.cs | 5 +++++ osu.iOS/OsuGameIOS.cs | 3 +++ 3 files changed, 14 insertions(+) diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index c47aba2f0c..ac76c0546b 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -3,6 +3,7 @@ #nullable disable +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -99,6 +100,10 @@ namespace osu.Game.Graphics.Containers this.applyUIScale = applyUIScale; } + [Resolved(canBeNull: true)] + [CanBeNull] + private OsuGame game { get; set; } + [BackgroundDependencyLoader] private void load(OsuConfigManager osuConfig) { @@ -111,6 +116,7 @@ namespace osu.Game.Graphics.Containers protected override void Update() { + TargetDrawSize = new Vector2(1024, 1024 / (game?.BaseAspectRatio ?? 1f)); Scale = new Vector2(CurrentScale); Size = new Vector2(1 / CurrentScale); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index c20536a1ec..5227400694 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -831,6 +831,11 @@ namespace osu.Game protected virtual UpdateManager CreateUpdateManager() => new UpdateManager(); + /// + /// The base aspect ratio to use in all s. + /// + protected internal virtual float BaseAspectRatio => 4f / 3f; + protected override Container CreateScalingContainer() => new ScalingContainer(ScalingMode.Everything); #region Beatmap progression diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index a9ca1778a0..b3d9be04a1 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -10,6 +10,7 @@ using osu.Framework.Platform; using osu.Game; using osu.Game.Updater; using osu.Game.Utils; +using UIKit; namespace osu.iOS { @@ -19,6 +20,8 @@ namespace osu.iOS public override bool HideUnlicensedContent => true; + protected override float BaseAspectRatio => (float)(UIScreen.MainScreen.Bounds.Width / UIScreen.MainScreen.Bounds.Height); + protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier(); protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo(); From 21389820c5f415dab2db00530a860f1eb93ee270 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 4 Jan 2025 02:35:48 -0500 Subject: [PATCH 051/801] Fix player no longer handling non-loaded beatmaps --- osu.Game/Screens/Play/Player.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index e50f97f912..02a8a6d2cc 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Play public override bool HideMenuCursorOnNonMouseInput => true; - public override bool RequiresPortraitOrientation => DrawableRuleset.RequiresPortraitOrientation; + public override bool RequiresPortraitOrientation => DrawableRuleset?.RequiresPortraitOrientation == true; protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered; From a241d1f5032f453d7a83e0b6fb0a8502bd42e431 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 4 Jan 2025 02:36:06 -0500 Subject: [PATCH 052/801] Fix `DrawableManiaRuleset` not cached as itself in subtypes i.e. editor mania ruleset --- osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index 65841af5de..d6794d0b4f 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -32,7 +32,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.UI { - [Cached] + [Cached(typeof(DrawableManiaRuleset))] public partial class DrawableManiaRuleset : DrawableScrollingRuleset { /// From 76ac11ff593bafc32a99a92368f79c94dac2f512 Mon Sep 17 00:00:00 2001 From: StanR Date: Mon, 6 Jan 2025 20:08:14 +0500 Subject: [PATCH 053/801] Fix angle bonuses calculating repetition incorrectly, apply distance scaling to wide bonus (#31320) * Fix angle bonuses calculating repetition incorrectly, apply distance scaling to wide bonus * Buff speed to compensate for streams losing pp * Adjust speed multiplier * Adjust wide scaling * Fix tests --- .../OsuDifficultyCalculatorTest.cs | 18 ++++++++--------- .../Difficulty/Evaluators/AimEvaluator.cs | 20 ++++++++++--------- .../Difficulty/Evaluators/SpeedEvaluator.cs | 2 +- .../Difficulty/Skills/Speed.cs | 2 +- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index c0a6d3a755..842a34aaa8 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,22 +15,22 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests"; - [TestCase(6.718709884850683d, 239, "diffcalc-test")] - [TestCase(1.4485749025771304d, 54, "zero-length-sliders")] - [TestCase(0.42630400627180914d, 4, "very-fast-slider")] + [TestCase(6.7153612142198682d, 239, "diffcalc-test")] + [TestCase(1.4484916289194889d, 54, "zero-length-sliders")] + [TestCase(0.42912495021837549d, 4, "very-fast-slider")] [TestCase(0.14143808967817237d, 2, "nan-slider")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(9.6343245007055653d, 239, "diffcalc-test")] - [TestCase(1.7550169162648608d, 54, "zero-length-sliders")] - [TestCase(0.55231632896800109d, 4, "very-fast-slider")] + [TestCase(9.6358837846598835d, 239, "diffcalc-test")] + [TestCase(1.754888327422514d, 54, "zero-length-sliders")] + [TestCase(0.55601568006454294d, 4, "very-fast-slider")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); - [TestCase(6.718709884850683d, 239, "diffcalc-test")] - [TestCase(1.4485749025771304d, 54, "zero-length-sliders")] - [TestCase(0.42630400627180914d, 4, "very-fast-slider")] + [TestCase(6.7153612142198682d, 239, "diffcalc-test")] + [TestCase(1.4484916289194889d, 54, "zero-length-sliders")] + [TestCase(0.42912495021837549d, 4, "very-fast-slider")] public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index fdf94719ed..cff2eae357 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -80,17 +80,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double angleBonus = Math.Min(currVelocity, prevVelocity); wideAngleBonus = calcWideAngleBonus(currAngle); + acuteAngleBonus = calcAcuteAngleBonus(currAngle); + + // Penalize angle repetition. + wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3)); + acuteAngleBonus *= 0.03 + 0.97 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); + + // Apply full wide angle bonus for distance more than one diameter + wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter); // Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter - acuteAngleBonus = calcAcuteAngleBonus(currAngle) * - angleBonus * - DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2), 300, 400) * - DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, diameter, diameter * 2); - - // Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute. - wideAngleBonus *= angleBonus * (1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3))); - // Penalize acute angles if they're repeated, reducing the penalty as the lastAngle gets more obtuse. - acuteAngleBonus *= 0.03 + 0.97 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); + acuteAngleBonus *= angleBonus * + DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2), 300, 400) * + DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, diameter, diameter * 2); // Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle // https://www.desmos.com/calculator/dp0v0nvowc diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs index e5e9769081..769220ece0 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators private const double single_spacing_threshold = OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.25; // 1.25 circles distance between centers private const double min_speed_bonus = 200; // 200 BPM 1/4th private const double speed_balancing_factor = 40; - private const double distance_multiplier = 0.94; + private const double distance_multiplier = 0.9; /// /// Evaluates the difficulty of tapping the current object, based on: diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 5dae9a9fc5..f2e2c2ec5f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// public class Speed : OsuStrainSkill { - private double skillMultiplier => 1.430; + private double skillMultiplier => 1.45; private double strainDecayBase => 0.3; private double currentStrain; From 4095b2662bc67da4e3eeb90da0d747b2cc135dcb Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Tue, 7 Jan 2025 21:36:56 +1000 Subject: [PATCH 054/801] Add `consistentRatioPenalty` to the `Colour` skill. (#31285) * fix colour * review fix Co-authored-by: StanR * remove cancelled out operand * increase nerf, adjust tests * fix automated spacing issues * up penalty * adjust tests * apply review changes * fix nullable hell --------- Co-authored-by: StanR --- .../TaikoDifficultyCalculatorTest.cs | 8 +-- .../Difficulty/Evaluators/ColourEvaluator.cs | 54 ++++++++++++++++++- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index ba247c68d4..de3bec5fcf 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(3.0950934814938953d, 200, "diffcalc-test")] - [TestCase(3.0950934814938953d, 200, "diffcalc-test-strong")] + [TestCase(2.837609165845338d, 200, "diffcalc-test")] + [TestCase(2.837609165845338d, 200, "diffcalc-test-strong")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(4.0839365008715403d, 200, "diffcalc-test")] - [TestCase(4.0839365008715403d, 200, "diffcalc-test-strong")] + [TestCase(3.8005218640444949, 200, "diffcalc-test")] + [TestCase(3.8005218640444949, 200, "diffcalc-test-strong")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime()); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index 25428c8b2f..3ff5b87fb6 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -36,18 +36,70 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators return 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E)); } + /// + /// Calculates a consistency penalty based on the number of consecutive consistent intervals, + /// considering the delta time between each colour sequence. + /// + /// The current hitObject to consider. + /// The allowable margin of error for determining whether ratios are consistent. + /// The maximum objects to check per count of consistent ratio. + private static double consistentRatioPenalty(TaikoDifficultyHitObject hitObject, double threshold = 0.01, int maxObjectsToCheck = 64) + { + int consistentRatioCount = 0; + double totalRatioCount = 0.0; + + TaikoDifficultyHitObject current = hitObject; + + for (int i = 0; i < maxObjectsToCheck; i++) + { + // Break if there is no valid previous object + if (current.Index <= 1) + break; + + var previousHitObject = (TaikoDifficultyHitObject)current.Previous(1); + + double currentRatio = current.Rhythm.Ratio; + double previousRatio = previousHitObject.Rhythm.Ratio; + + // A consistent interval is defined as the percentage difference between the two rhythmic ratios with the margin of error. + if (Math.Abs(1 - currentRatio / previousRatio) <= threshold) + { + consistentRatioCount++; + totalRatioCount += currentRatio; + break; + } + + // Move to the previous object + current = previousHitObject; + } + + // Ensure no division by zero + double ratioPenalty = 1 - totalRatioCount / (consistentRatioCount + 1) * 0.80; + + return ratioPenalty; + } + + /// + /// Evaluate the difficulty of the first hitobject within a colour streak. + /// public static double EvaluateDifficultyOf(DifficultyHitObject hitObject) { - TaikoDifficultyHitObjectColour colour = ((TaikoDifficultyHitObject)hitObject).Colour; + var taikoObject = (TaikoDifficultyHitObject)hitObject; + TaikoDifficultyHitObjectColour colour = taikoObject.Colour; double difficulty = 0.0d; if (colour.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak difficulty += EvaluateDifficultyOf(colour.MonoStreak); + if (colour.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern difficulty += EvaluateDifficultyOf(colour.AlternatingMonoPattern); + if (colour.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern difficulty += EvaluateDifficultyOf(colour.RepeatingHitPattern); + double consistencyPenalty = consistentRatioPenalty(taikoObject); + difficulty *= consistencyPenalty; + return difficulty; } } From 3b58d5e43565e9b16b94667972ba968dbea36ba1 Mon Sep 17 00:00:00 2001 From: StanR Date: Tue, 7 Jan 2025 17:49:55 +0500 Subject: [PATCH 055/801] Clamp OD in performance calculation to fix negative OD gaining pp (#31447) Co-authored-by: James Wilson --- .../Difficulty/OsuPerformanceCalculator.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index df418fb3f8..5cf7a56d8a 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -191,7 +191,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= accuracy; // It is important to consider accuracy difficulty when scaling with accuracy. - aimValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500; + aimValue *= 0.98 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 2500; return aimValue; } @@ -238,7 +238,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0); // Scale the speed value with accuracy and OD. - speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 2); + speedValue *= (0.95 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 2); // Scale the speed value with # of 50s to punish doubletapping. speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); @@ -305,7 +305,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Scale the flashlight value with accuracy _slightly_. flashlightValue *= 0.5 + accuracy / 2.0; // It is important to also consider accuracy difficulty when doing that. - flashlightValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500; + flashlightValue *= 0.98 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 2500; return flashlightValue; } From 9c05837b3a36e26b4cbe6cdb6b364b03d99b585c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 8 Jan 2025 18:45:35 +0900 Subject: [PATCH 056/801] Change to using a 'FreeStyle' boolean --- .../Online/Rooms/MultiplayerPlaylistItem.cs | 5 +-- osu.Game/Online/Rooms/PlaylistItem.cs | 18 ++++---- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 41 ++++++------------- .../Multiplayer/MultiplayerMatchSongSelect.cs | 4 +- .../Multiplayer/MultiplayerMatchSubScreen.cs | 3 ++ .../OnlinePlay/OnlinePlaySongSelect.cs | 5 +-- .../OnlinePlay/OnlinePlayStyleSelect.cs | 13 ++++-- .../Playlists/PlaylistsRoomSubScreen.cs | 8 ++++ .../Playlists/PlaylistsSongSelect.cs | 2 +- 9 files changed, 49 insertions(+), 50 deletions(-) diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index 4a15fd9690..4dfb3b389d 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -57,11 +57,10 @@ namespace osu.Game.Online.Rooms public double StarRating { get; set; } /// - /// A non-null value indicates "freestyle" mode where players are able to individually select - /// their own choice of beatmap (from the respective beatmap set) and ruleset to play in the room. + /// Indicates whether participants in the room are able to pick their own choice of beatmap difficulty and ruleset. /// [Key(11)] - public int? BeatmapSetID { get; set; } + public bool FreeStyle { get; set; } [SerializationConstructor] public MultiplayerPlaylistItem() diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 16c252befc..e8725b6792 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -68,11 +68,10 @@ namespace osu.Game.Online.Rooms } /// - /// A non-null value indicates "freestyle" mode where players are able to individually select - /// their own choice of beatmap (from the respective beatmap set) and ruleset to play in the room. + /// Indicates whether participants in the room are able to pick their own choice of beatmap difficulty and ruleset. /// - [JsonProperty("beatmapset_id")] - public int? BeatmapSetId { get; set; } + [JsonProperty("freestyle")] + public bool FreeStyle { get; set; } /// /// A beatmap representing this playlist item. @@ -108,7 +107,7 @@ namespace osu.Game.Online.Rooms PlayedAt = item.PlayedAt; RequiredMods = item.RequiredMods.ToArray(); AllowedMods = item.AllowedMods.ToArray(); - BeatmapSetId = item.BeatmapSetID; + FreeStyle = item.FreeStyle; } public void MarkInvalid() => valid.Value = false; @@ -128,8 +127,7 @@ namespace osu.Game.Online.Rooms #endregion - public PlaylistItem With(Optional id = default, Optional beatmap = default, Optional playlistOrder = default, - Optional ruleset = default) + public PlaylistItem With(Optional id = default, Optional beatmap = default, Optional playlistOrder = default, Optional ruleset = default) { return new PlaylistItem(beatmap.GetOr(Beatmap)) { @@ -141,19 +139,19 @@ namespace osu.Game.Online.Rooms PlayedAt = PlayedAt, AllowedMods = AllowedMods, RequiredMods = RequiredMods, + FreeStyle = FreeStyle, valid = { Value = Valid.Value }, - BeatmapSetId = BeatmapSetId }; } public bool Equals(PlaylistItem? other) => ID == other?.ID && Beatmap.OnlineID == other.Beatmap.OnlineID - && BeatmapSetId == other.BeatmapSetId && RulesetID == other.RulesetID && Expired == other.Expired && PlaylistOrder == other.PlaylistOrder && AllowedMods.SequenceEqual(other.AllowedMods) - && RequiredMods.SequenceEqual(other.RequiredMods); + && RequiredMods.SequenceEqual(other.RequiredMods) + && FreeStyle == other.FreeStyle; } } diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index b51679ded6..ec2ed90eca 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -272,21 +272,9 @@ namespace osu.Game.Screens.OnlinePlay.Match base.LoadComplete(); SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); - - UserMods.BindValueChanged(_ => Scheduler.AddOnce(updateMods)); - - UserBeatmap.BindValueChanged(_ => Scheduler.AddOnce(() => - { - updateBeatmap(); - updateUserStyle(); - })); - - UserRuleset.BindValueChanged(_ => Scheduler.AddOnce(() => - { - updateUserMods(); - updateRuleset(); - updateUserStyle(); - })); + UserMods.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); + UserBeatmap.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); + UserRuleset.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateBeatmap()); @@ -458,14 +446,6 @@ namespace osu.Game.Screens.OnlinePlay.Match if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) return; - // Reset user style if no longer valid. - // Todo: In the future this can be made more lenient, such as allowing a non-null ruleset as the set changes. - if (item.BeatmapSetId == null || item.BeatmapSetId != UserBeatmap.Value?.BeatmapSet!.OnlineID) - { - UserBeatmap.Value = null; - UserRuleset.Value = null; - } - updateUserMods(); updateBeatmap(); updateMods(); @@ -487,10 +467,10 @@ namespace osu.Game.Screens.OnlinePlay.Match UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); } - if (item.BeatmapSetId == null) - UserStyleSection?.Hide(); - else + if (item.FreeStyle) UserStyleSection?.Show(); + else + UserStyleSection?.Hide(); } private void updateUserMods() @@ -499,8 +479,13 @@ namespace osu.Game.Screens.OnlinePlay.Match return; // Remove any user mods that are no longer allowed. - var rulesetInstance = GetGameplayRuleset().CreateInstance(); - var allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance)); + Ruleset rulesetInstance = GetGameplayRuleset().CreateInstance(); + Mod[] allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); + Mod[] newUserMods = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); + + if (newUserMods.SequenceEqual(UserMods.Value)) + return; + UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList(); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 9f9e6349a6..5754bcb963 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -83,11 +83,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { ID = itemToEdit?.ID ?? 0, BeatmapID = item.Beatmap.OnlineID, - BeatmapSetID = item.BeatmapSetId, BeatmapChecksum = item.Beatmap.MD5Hash, RulesetID = item.RulesetID, RequiredMods = item.RequiredMods.ToArray(), - AllowedMods = item.AllowedMods.ToArray() + AllowedMods = item.AllowedMods.ToArray(), + FreeStyle = item.FreeStyle }; Task task = itemToEdit != null ? client.EditPlaylistItem(multiplayerItem) : client.AddPlaylistItem(multiplayerItem); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index edfb059c77..34a1eb70a9 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -403,7 +403,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void updateCurrentItem() { Debug.Assert(client.Room != null); + SelectedItem.Value = Room.Playlist.SingleOrDefault(i => i.ID == client.Room.Settings.PlaylistItemId); + UserBeatmap.Value = client.LocalUser?.BeatmapId == null ? null : UserBeatmap.Value; + UserRuleset.Value = client.LocalUser?.RulesetId == null ? null : UserRuleset.Value; } private void handleRoomLost() => Schedule(() => diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index a91f43635b..9df01ead42 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -111,8 +111,7 @@ namespace osu.Game.Screens.OnlinePlay FreeMods.Value = initialItem.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); } - if (initialItem.BeatmapSetId != null) - FreeStyle.Value = true; + FreeStyle.Value = initialItem.FreeStyle; } Mods.BindValueChanged(onModsChanged); @@ -162,7 +161,7 @@ namespace osu.Game.Screens.OnlinePlay RulesetID = Ruleset.Value.OnlineID, RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(), - BeatmapSetId = FreeStyle.Value ? Beatmap.Value.BeatmapSetInfo.OnlineID : null + FreeStyle = FreeStyle.Value }; return SelectItem(item); diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs index 029ca68e36..d1fcf94152 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs @@ -63,6 +63,7 @@ namespace osu.Game.Screens.OnlinePlay { private readonly PlaylistItem item; private double itemLength; + private int beatmapSetId; public DifficultySelectFilterControl(PlaylistItem item) { @@ -72,8 +73,14 @@ namespace osu.Game.Screens.OnlinePlay [BackgroundDependencyLoader] private void load(RealmAccess realm) { - int beatmapId = item.Beatmap.OnlineID; - itemLength = realm.Run(r => r.All().FirstOrDefault(b => b.OnlineID == beatmapId)?.Length ?? 0); + realm.Run(r => + { + int beatmapId = item.Beatmap.OnlineID; + BeatmapInfo? beatmap = r.All().FirstOrDefault(b => b.OnlineID == beatmapId); + + itemLength = beatmap?.Length ?? 0; + beatmapSetId = beatmap?.BeatmapSet?.OnlineID ?? 0; + }); } public override FilterCriteria CreateCriteria() @@ -81,7 +88,7 @@ namespace osu.Game.Screens.OnlinePlay var criteria = base.CreateCriteria(); // Must be from the same set as the playlist item. - criteria.BeatmapSetId = item.BeatmapSetId; + criteria.BeatmapSetId = beatmapSetId; // Must be within 30s of the playlist item. criteria.Length.Min = itemLength - 30000; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index b941bbd290..eaadfb6507 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -67,6 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { base.LoadComplete(); + SelectedItem.BindValueChanged(onSelectedItemChanged, true); isIdle.BindValueChanged(_ => updatePollingRate(), true); Room.PropertyChanged += onRoomPropertyChanged; @@ -75,6 +76,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists updateRoomPlaylist(); } + private void onSelectedItemChanged(ValueChangedEvent item) + { + // Simplest for now. + UserBeatmap.Value = null; + UserRuleset.Value = null; + } + private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e) { switch (e.PropertyName) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs index a3b8a1575e..abf80c0d44 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs @@ -37,10 +37,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private PlaylistItem createNewItem() => new PlaylistItem(Beatmap.Value.BeatmapInfo) { ID = room.Playlist.Count == 0 ? 0 : room.Playlist.Max(p => p.ID) + 1, - BeatmapSetId = FreeStyle.Value ? Beatmap.Value.BeatmapSetInfo.OnlineID : null, RulesetID = Ruleset.Value.OnlineID, RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(), + FreeStyle = FreeStyle.Value }; } } From be33addae16f589dda941d27d2e49a25ec61d0bd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 8 Jan 2025 18:57:22 +0900 Subject: [PATCH 057/801] Fix possible null reference --- .../Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 34a1eb70a9..b5fe8bf631 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -274,7 +274,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override APIMod[] GetGameplayMods() { // Using the room's reported status makes the server authoritative. - return client.LocalUser?.Mods.Concat(SelectedItem.Value!.RequiredMods).ToArray()!; + return client.LocalUser?.Mods != null ? client.LocalUser.Mods.Concat(SelectedItem.Value!.RequiredMods).ToArray() : base.GetGameplayMods(); } protected override RulesetInfo GetGameplayRuleset() From 392bb5718cbbab3a2b3738d460ea3cbbc4d46885 Mon Sep 17 00:00:00 2001 From: StanR Date: Wed, 8 Jan 2025 15:03:22 +0500 Subject: [PATCH 058/801] Simplify angle bonus formula (#31449) * Simplify angle bonus formula * Simplify further * Simplify acute too * Tests --- .../OsuDifficultyCalculatorTest.cs | 6 +++--- .../Difficulty/Evaluators/AimEvaluator.cs | 4 ++-- .../Difficulty/Utils/DifficultyCalculationUtils.cs | 13 +++++++++++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index 842a34aaa8..fbd865df47 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,20 +15,20 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests"; - [TestCase(6.7153612142198682d, 239, "diffcalc-test")] + [TestCase(6.7230435389286045d, 239, "diffcalc-test")] [TestCase(1.4484916289194889d, 54, "zero-length-sliders")] [TestCase(0.42912495021837549d, 4, "very-fast-slider")] [TestCase(0.14143808967817237d, 2, "nan-slider")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(9.6358837846598835d, 239, "diffcalc-test")] + [TestCase(9.6468019709446171d, 239, "diffcalc-test")] [TestCase(1.754888327422514d, 54, "zero-length-sliders")] [TestCase(0.55601568006454294d, 4, "very-fast-slider")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); - [TestCase(6.7153612142198682d, 239, "diffcalc-test")] + [TestCase(6.7230435389286045d, 239, "diffcalc-test")] [TestCase(1.4484916289194889d, 54, "zero-length-sliders")] [TestCase(0.42912495021837549d, 4, "very-fast-slider")] public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index cff2eae357..8c41240a24 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -142,8 +142,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators return aimStrain; } - private static double calcWideAngleBonus(double angle) => Math.Pow(Math.Sin(3.0 / 4 * (Math.Min(5.0 / 6 * Math.PI, Math.Max(Math.PI / 6, angle)) - Math.PI / 6)), 2); + private static double calcWideAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(30), double.DegreesToRadians(150)); - private static double calcAcuteAngleBonus(double angle) => 1 - calcWideAngleBonus(angle); + private static double calcAcuteAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(150), double.DegreesToRadians(30)); } } diff --git a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs index 497a1f8234..aeccf2fd55 100644 --- a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs @@ -66,6 +66,19 @@ namespace osu.Game.Rulesets.Difficulty.Utils /// The output of the bell curve function of public static double BellCurve(double x, double mean, double width, double multiplier = 1.0) => multiplier * Math.Exp(Math.E * -(Math.Pow(x - mean, 2) / Math.Pow(width, 2))); + /// + /// Smoothstep function (https://en.wikipedia.org/wiki/Smoothstep) + /// + /// Value to calculate the function for + /// Value at which function returns 0 + /// Value at which function returns 1 + public static double Smoothstep(double x, double start, double end) + { + x = Math.Clamp((x - start) / (end - start), 0.0, 1.0); + + return x * x * (3.0 - 2.0 * x); + } + /// /// Smootherstep function (https://en.wikipedia.org/wiki/Smoothstep#Variations) /// From db58ec864569889a17952149ff85a05d28a07133 Mon Sep 17 00:00:00 2001 From: StanR Date: Thu, 9 Jan 2025 14:57:48 +0500 Subject: [PATCH 059/801] Apply a bunch of balancing changes to aim (#31456) * Apply a bunch of balancing changes to aim * Update tests --------- Co-authored-by: James Wilson --- .../OsuDifficultyCalculatorTest.cs | 18 +++++++++--------- .../Difficulty/Evaluators/AimEvaluator.cs | 8 ++++---- .../Difficulty/Skills/Speed.cs | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index fbd865df47..9af5051f45 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,22 +15,22 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests"; - [TestCase(6.7230435389286045d, 239, "diffcalc-test")] - [TestCase(1.4484916289194889d, 54, "zero-length-sliders")] - [TestCase(0.42912495021837549d, 4, "very-fast-slider")] + [TestCase(6.6860329680488437d, 239, "diffcalc-test")] + [TestCase(1.4485740324170036d, 54, "zero-length-sliders")] + [TestCase(0.43052813047866129d, 4, "very-fast-slider")] [TestCase(0.14143808967817237d, 2, "nan-slider")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(9.6468019709446171d, 239, "diffcalc-test")] - [TestCase(1.754888327422514d, 54, "zero-length-sliders")] - [TestCase(0.55601568006454294d, 4, "very-fast-slider")] + [TestCase(9.6300773538770041d, 239, "diffcalc-test")] + [TestCase(1.7550155729445993d, 54, "zero-length-sliders")] + [TestCase(0.55785578988249407d, 4, "very-fast-slider")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); - [TestCase(6.7230435389286045d, 239, "diffcalc-test")] - [TestCase(1.4484916289194889d, 54, "zero-length-sliders")] - [TestCase(0.42912495021837549d, 4, "very-fast-slider")] + [TestCase(6.6860329680488437d, 239, "diffcalc-test")] + [TestCase(1.4485740324170036d, 54, "zero-length-sliders")] + [TestCase(0.43052813047866129d, 4, "very-fast-slider")] public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index 8c41240a24..e279ed889a 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators public static class AimEvaluator { private const double wide_angle_multiplier = 1.5; - private const double acute_angle_multiplier = 2.7; + private const double acute_angle_multiplier = 2.6; private const double slider_multiplier = 1.35; private const double velocity_change_multiplier = 0.75; private const double wiggle_multiplier = 1.02; @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // Penalize angle repetition. wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3)); - acuteAngleBonus *= 0.03 + 0.97 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); + acuteAngleBonus *= 0.1 + 0.9 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); // Apply full wide angle bonus for distance more than one diameter wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter); @@ -142,8 +142,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators return aimStrain; } - private static double calcWideAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(30), double.DegreesToRadians(150)); + private static double calcWideAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(40), double.DegreesToRadians(140)); - private static double calcAcuteAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(150), double.DegreesToRadians(30)); + private static double calcAcuteAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(140), double.DegreesToRadians(40)); } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index f2e2c2ec5f..bdeea0e918 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// public class Speed : OsuStrainSkill { - private double skillMultiplier => 1.45; + private double skillMultiplier => 1.46; private double strainDecayBase => 0.3; private double currentStrain; From b21c6457b1a1febd004d508e3597815b64a2a6d4 Mon Sep 17 00:00:00 2001 From: Givikap120 <89256026+Givikap120@users.noreply.github.com> Date: Thu, 9 Jan 2025 15:27:54 +0200 Subject: [PATCH 060/801] Punish speed PP for scores with high deviation (#30907) --- .../Difficulty/OsuDifficultyAttributes.cs | 31 ++++- .../Difficulty/OsuDifficultyCalculator.cs | 5 + .../Difficulty/OsuPerformanceAttributes.cs | 3 + .../Difficulty/OsuPerformanceCalculator.cs | 119 +++++++++++++++++- .../Difficulty/DifficultyAttributes.cs | 1 + 5 files changed, 149 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index 3b9a23df23..395f581b65 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -62,21 +62,33 @@ namespace osu.Game.Rulesets.Osu.Difficulty /// /// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc). /// - /// - /// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing. - /// [JsonProperty("approach_rate")] public double ApproachRate { get; set; } /// /// The perceived overall difficulty inclusive of rate-adjusting mods (DT/HT/etc). /// - /// - /// Rate-adjusting mods don't directly affect the overall difficulty value, but have a perceived effect as a result of adjusting audio timing. - /// [JsonProperty("overall_difficulty")] public double OverallDifficulty { get; set; } + /// + /// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc). + /// + [JsonProperty("great_hit_window")] + public double GreatHitWindow { get; set; } + + /// + /// The perceived hit window for an OK hit inclusive of rate-adjusting mods (DT/HT/etc). + /// + [JsonProperty("ok_hit_window")] + public double OkHitWindow { get; set; } + + /// + /// The perceived hit window for a MEH hit inclusive of rate-adjusting mods (DT/HT/etc). + /// + [JsonProperty("meh_hit_window")] + public double MehHitWindow { get; set; } + /// /// The beatmap's drain rate. This doesn't scale with rate-adjusting mods. /// @@ -107,6 +119,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty); yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate); yield return (ATTRIB_ID_DIFFICULTY, StarRating); + yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow); if (ShouldSerializeFlashlightDifficulty()) yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty); @@ -117,6 +130,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty yield return (ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount); yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount); yield return (ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT, AimDifficultSliderCount); + + yield return (ATTRIB_ID_OK_HIT_WINDOW, OkHitWindow); + yield return (ATTRIB_ID_MEH_HIT_WINDOW, MehHitWindow); } public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) @@ -128,12 +144,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY]; ApproachRate = values[ATTRIB_ID_APPROACH_RATE]; StarRating = values[ATTRIB_ID_DIFFICULTY]; + GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW]; FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT); SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR]; AimDifficultStrainCount = values[ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT]; SpeedDifficultStrainCount = values[ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT]; SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT]; AimDifficultSliderCount = values[ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT]; + OkHitWindow = values[ATTRIB_ID_OK_HIT_WINDOW]; + MehHitWindow = values[ATTRIB_ID_MEH_HIT_WINDOW]; DrainRate = onlineInfo.DrainRate; HitCircleCount = onlineInfo.CircleCount; SliderCount = onlineInfo.SliderCount; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index d0f23735c3..5a61ea586a 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -99,6 +99,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate; + double hitWindowOk = hitWindows.WindowFor(HitResult.Ok) / clockRate; + double hitWindowMeh = hitWindows.WindowFor(HitResult.Meh) / clockRate; OsuDifficultyAttributes attributes = new OsuDifficultyAttributes { @@ -114,6 +116,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty SpeedDifficultStrainCount = speedDifficultyStrainCount, ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5, OverallDifficulty = (80 - hitWindowGreat) / 6, + GreatHitWindow = hitWindowGreat, + OkHitWindow = hitWindowOk, + MehHitWindow = hitWindowMeh, DrainRate = drainRate, MaxCombo = beatmap.GetMaxCombo(), HitCircleCount = hitCirclesCount, diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs index 0aeaf7669f..de4491a31b 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs @@ -24,6 +24,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty [JsonProperty("effective_miss_count")] public double EffectiveMissCount { get; set; } + [JsonProperty("speed_deviation")] + public double? SpeedDeviation { get; set; } + public override IEnumerable GetAttributesForDisplay() { foreach (var attribute in base.GetAttributesForDisplay()) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 5cf7a56d8a..91cd270966 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -9,6 +9,7 @@ using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Utils; namespace osu.Game.Rulesets.Osu.Difficulty { @@ -40,6 +41,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty /// private double effectiveMissCount; + private double? speedDeviation; + public OsuPerformanceCalculator() : base(new OsuRuleset()) { @@ -110,10 +113,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits); } + speedDeviation = calculateSpeedDeviation(osuAttributes); + double aimValue = computeAimValue(score, osuAttributes); double speedValue = computeSpeedValue(score, osuAttributes); double accuracyValue = computeAccuracyValue(score, osuAttributes); double flashlightValue = computeFlashlightValue(score, osuAttributes); + double totalValue = Math.Pow( Math.Pow(aimValue, 1.1) + @@ -129,6 +135,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty Accuracy = accuracyValue, Flashlight = flashlightValue, EffectiveMissCount = effectiveMissCount, + SpeedDeviation = speedDeviation, Total = totalValue }; } @@ -198,7 +205,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double computeSpeedValue(ScoreInfo score, OsuDifficultyAttributes attributes) { - if (score.Mods.Any(h => h is OsuModRelax)) + if (score.Mods.Any(h => h is OsuModRelax) || speedDeviation == null) return 0.0; double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty); @@ -230,6 +237,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); } + double speedHighDeviationMultiplier = calculateSpeedHighDeviationNerf(attributes); + speedValue *= speedHighDeviationMultiplier; + // Calculate accuracy assuming the worst case scenario double relevantTotalDiff = totalHits - attributes.SpeedNoteCount; double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff); @@ -240,9 +250,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Scale the speed value with accuracy and OD. speedValue *= (0.95 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 2); - // Scale the speed value with # of 50s to punish doubletapping. - speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); - return speedValue; } @@ -310,12 +317,116 @@ namespace osu.Game.Rulesets.Osu.Difficulty return flashlightValue; } + /// + /// Estimates player's deviation on speed notes using , assuming worst-case. + /// Treats all speed notes as hit circles. + /// + private double? calculateSpeedDeviation(OsuDifficultyAttributes attributes) + { + if (totalSuccessfulHits == 0) + return null; + + // Calculate accuracy assuming the worst case scenario + double speedNoteCount = attributes.SpeedNoteCount; + speedNoteCount += (totalHits - attributes.SpeedNoteCount) * 0.1; + + // Assume worst case: all mistakes were on speed notes + double relevantCountMiss = Math.Min(countMiss, speedNoteCount); + double relevantCountMeh = Math.Min(countMeh, speedNoteCount - relevantCountMiss); + double relevantCountOk = Math.Min(countOk, speedNoteCount - relevantCountMiss - relevantCountMeh); + double relevantCountGreat = Math.Max(0, speedNoteCount - relevantCountMiss - relevantCountMeh - relevantCountOk); + + return calculateDeviation(attributes, relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss); + } + + /// + /// Estimates the player's tap deviation based on the OD, given number of greats, oks, mehs and misses, + /// assuming the player's mean hit error is 0. The estimation is consistent in that two SS scores on the same map with the same settings + /// will always return the same deviation. Misses are ignored because they are usually due to misaiming. + /// Greats and oks are assumed to follow a normal distribution, whereas mehs are assumed to follow a uniform distribution. + /// + private double? calculateDeviation(OsuDifficultyAttributes attributes, double relevantCountGreat, double relevantCountOk, double relevantCountMeh, double relevantCountMiss) + { + if (relevantCountGreat + relevantCountOk + relevantCountMeh <= 0) + return null; + + double objectCount = relevantCountGreat + relevantCountOk + relevantCountMeh + relevantCountMiss; + + double hitWindowGreat = attributes.GreatHitWindow; + double hitWindowOk = attributes.OkHitWindow; + double hitWindowMeh = attributes.MehHitWindow; + + // The probability that a player hits a circle is unknown, but we can estimate it to be + // the number of greats on circles divided by the number of circles, and then add one + // to the number of circles as a bias correction. + double n = Math.Max(1, objectCount - relevantCountMiss - relevantCountMeh); + const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed). + + // Proportion of greats hit on circles, ignoring misses and 50s. + double p = relevantCountGreat / n; + + // We can be 99% confident that p is at least this value. + double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4); + + // Compute the deviation assuming greats and oks are normally distributed, and mehs are uniformly distributed. + // Begin with greats and oks first. Ignoring mehs, we can be 99% confident that the deviation is not higher than: + double deviation = hitWindowGreat / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); + + double randomValue = Math.Sqrt(2 / Math.PI) * hitWindowOk * Math.Exp(-0.5 * Math.Pow(hitWindowOk / deviation, 2)) + / (deviation * SpecialFunctions.Erf(hitWindowOk / (Math.Sqrt(2) * deviation))); + + deviation *= Math.Sqrt(1 - randomValue); + + // Value deviation approach as greatCount approaches 0 + double limitValue = hitWindowOk / Math.Sqrt(3); + + // If precision is not enough to compute true deviation - use limit value + if (pLowerBound == 0 || randomValue >= 1 || deviation > limitValue) + deviation = limitValue; + + // Then compute the variance for mehs. + double mehVariance = (hitWindowMeh * hitWindowMeh + hitWindowOk * hitWindowMeh + hitWindowOk * hitWindowOk) / 3; + + // Find the total deviation. + deviation = Math.Sqrt(((relevantCountGreat + relevantCountOk) * Math.Pow(deviation, 2) + relevantCountMeh * mehVariance) / (relevantCountGreat + relevantCountOk + relevantCountMeh)); + + return deviation; + } + + // Calculates multiplier for speed to account for improper tapping based on the deviation and speed difficulty + // https://www.desmos.com/calculator/dmogdhzofn + private double calculateSpeedHighDeviationNerf(OsuDifficultyAttributes attributes) + { + if (speedDeviation == null) + return 0; + + double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty); + + // Decides a point where the PP value achieved compared to the speed deviation is assumed to be tapped improperly. Any PP above this point is considered "excess" speed difficulty. + // This is used to cause PP above the cutoff to scale logarithmically towards the original speed value thus nerfing the value. + double excessSpeedDifficultyCutoff = 100 + 220 * Math.Pow(22 / speedDeviation.Value, 6.5); + + if (speedValue <= excessSpeedDifficultyCutoff) + return 1.0; + + const double scale = 50; + double adjustedSpeedValue = scale * (Math.Log((speedValue - excessSpeedDifficultyCutoff) / scale + 1) + excessSpeedDifficultyCutoff / scale); + + // 200 UR and less are considered tapped correctly to ensure that normal scores will be punished as little as possible + double lerp = 1 - Math.Clamp((speedDeviation.Value - 20) / (24 - 20), 0, 1); + adjustedSpeedValue = double.Lerp(adjustedSpeedValue, speedValue, lerp); + + return adjustedSpeedValue / speedValue; + } + // Miss penalty assumes that a player will miss on the hardest parts of a map, // so we use the amount of relatively difficult sections to adjust miss penalty // to make it more punishing on maps with lower amount of hard sections. private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.96 / ((missCount / (4 * Math.Pow(Math.Log(difficultStrainCount), 0.94))) + 1); private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0); + private int totalHits => countGreat + countOk + countMeh + countMiss; + private int totalSuccessfulHits => countGreat + countOk + countMeh; private int totalImperfectHits => countOk + countMeh + countMiss; } } diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index f5ed5a180b..1d6cee043b 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -31,6 +31,7 @@ namespace osu.Game.Rulesets.Difficulty protected const int ATTRIB_ID_OK_HIT_WINDOW = 27; protected const int ATTRIB_ID_MONO_STAMINA_FACTOR = 29; protected const int ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT = 31; + protected const int ATTRIB_ID_MEH_HIT_WINDOW = 33; /// /// The mods which were applied to the beatmap. From f71869610292ff7be0025f149cb92664e7809aea Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 12 Jan 2025 02:34:36 -0500 Subject: [PATCH 061/801] Allow landscape orientation on tablet devices in osu!mania --- osu.Game/Mobile/OrientationManager.cs | 19 ++++++++++--------- osu.Game/OsuGame.cs | 3 ++- osu.Game/Screens/IOsuScreen.cs | 5 +++-- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/osu.Game/Mobile/OrientationManager.cs b/osu.Game/Mobile/OrientationManager.cs index b78bf8e760..0f9b56d434 100644 --- a/osu.Game/Mobile/OrientationManager.cs +++ b/osu.Game/Mobile/OrientationManager.cs @@ -48,27 +48,28 @@ namespace osu.Game.Mobile private void updateOrientations() { bool lockCurrentOrientation = localUserPlaying.Value == LocalUserPlayingState.Playing; - bool lockToPortrait = requiresPortraitOrientation.Value; + bool lockToPortraitOnPhone = requiresPortraitOrientation.Value; if (lockCurrentOrientation) { - if (lockToPortrait && !IsCurrentOrientationPortrait) + if (!IsTablet && lockToPortraitOnPhone && !IsCurrentOrientationPortrait) SetAllowedOrientations(GameOrientation.Portrait); - else if (!lockToPortrait && IsCurrentOrientationPortrait && !IsTablet) + else if (!IsTablet && !lockToPortraitOnPhone && IsCurrentOrientationPortrait) SetAllowedOrientations(GameOrientation.Landscape); else + { + // if the orientation is already portrait/landscape according to the game's specifications, + // then use Locked instead of Portrait/Landscape to handle the case where the device is + // in landscape-left or reverse-portrait. SetAllowedOrientations(GameOrientation.Locked); + } return; } - if (lockToPortrait) + if (!IsTablet && lockToPortraitOnPhone) { - if (IsTablet) - SetAllowedOrientations(GameOrientation.FullPortrait); - else - SetAllowedOrientations(GameOrientation.Portrait); - + SetAllowedOrientations(GameOrientation.Portrait); return; } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index e72d106928..0d725bf07c 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -174,7 +174,8 @@ namespace osu.Game public readonly IBindable OverlayActivationMode = new Bindable(); /// - /// On mobile devices, this specifies whether the device should be set and locked to portrait orientation. + /// On mobile phones, this specifies whether the device should be set and locked to portrait orientation. + /// Tablet devices are unaffected by this property. /// /// /// Implementations can be viewed in mobile projects. diff --git a/osu.Game/Screens/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs index 8b3ff4306f..0fd7299115 100644 --- a/osu.Game/Screens/IOsuScreen.cs +++ b/osu.Game/Screens/IOsuScreen.cs @@ -62,10 +62,11 @@ namespace osu.Game.Screens bool HideMenuCursorOnNonMouseInput { get; } /// - /// On mobile devices, this specifies whether this requires the device to be in portrait orientation. + /// On mobile phones, this specifies whether this requires the device to be in portrait orientation. + /// Tablet devices are unaffected by this property. /// /// - /// By default, all screens in the game display in landscape orientation. + /// By default, all screens in the game display in landscape orientation on phones. /// Setting this to true will display this screen in portrait orientation instead, /// and switch back to landscape when transitioning back to a regular non-portrait screen. /// From c1ac27d65894b4418c9d700ec87972728f0c26d9 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 12 Jan 2025 22:56:28 -0500 Subject: [PATCH 062/801] Fix failing tests - Caches `DrawableRuleset` in editor compose screen for mania playfield adjustment container (because it's used to wrap the blueprint container as well) - Fixes `ManiaModWithPlayfieldCover` performing a no-longer-correct direct cast with a naive-but-working approach. --- .../Mods/ManiaModWithPlayfieldCover.cs | 4 ++-- .../Components/HitPositionPaddedContainer.cs | 10 +++++++++ .../UI/DrawableManiaRuleset.cs | 1 - .../UI/ManiaPlayfieldAdjustmentContainer.cs | 4 +++- .../Edit/DrawableEditorRulesetWrapper.cs | 22 +++++++++---------- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 1 + 6 files changed, 27 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs index 1bc16112c5..b6e6ee7481 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs @@ -5,9 +5,9 @@ using System; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Mania.Mods foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns)) { HitObjectContainer hoc = column.HitObjectContainer; - Container hocParent = (Container)hoc.Parent!; + ColumnHitObjectArea hocParent = (ColumnHitObjectArea)hoc.Parent!; hocParent.Remove(hoc, false); hocParent.Add(CreateCover(hoc).With(c => diff --git a/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs b/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs index f591102f6c..f550e3b241 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs @@ -19,6 +19,16 @@ namespace osu.Game.Rulesets.Mania.UI.Components InternalChild = child; } + internal void Add(Drawable drawable) + { + base.AddInternal(drawable); + } + + internal void Remove(Drawable drawable, bool disposeImmediately = true) + { + base.RemoveInternal(drawable, disposeImmediately); + } + [BackgroundDependencyLoader] private void load(IScrollingInfo scrollingInfo) { diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index d6794d0b4f..a186d9aa7d 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -32,7 +32,6 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.UI { - [Cached(typeof(DrawableManiaRuleset))] public partial class DrawableManiaRuleset : DrawableScrollingRuleset { /// diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs index f7c4850a94..b0203643b0 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.UI } [Resolved] - private DrawableManiaRuleset drawableManiaRuleset { get; set; } = null!; + private DrawableRuleset drawableRuleset { get; set; } = null!; protected override void Update() { @@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Mania.UI float aspectRatio = DrawWidth / DrawHeight; bool isPortrait = aspectRatio < 1f; + var drawableManiaRuleset = (DrawableManiaRuleset)drawableRuleset; + if (isPortrait && drawableManiaRuleset.Beatmap.Stages.Count == 1) { // Scale playfield up by 25% to become playable on mobile devices, diff --git a/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs b/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs index 174b278d89..573eb8c42f 100644 --- a/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs +++ b/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs @@ -19,16 +19,16 @@ namespace osu.Game.Rulesets.Edit internal partial class DrawableEditorRulesetWrapper : CompositeDrawable where TObject : HitObject { - public Playfield Playfield => drawableRuleset.Playfield; + public Playfield Playfield => DrawableRuleset.Playfield; - private readonly DrawableRuleset drawableRuleset; + public readonly DrawableRuleset DrawableRuleset; [Resolved] private EditorBeatmap beatmap { get; set; } = null!; public DrawableEditorRulesetWrapper(DrawableRuleset drawableRuleset) { - this.drawableRuleset = drawableRuleset; + DrawableRuleset = drawableRuleset; RelativeSizeAxes = Axes.Both; @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Edit [BackgroundDependencyLoader] private void load() { - drawableRuleset.FrameStablePlayback = false; + DrawableRuleset.FrameStablePlayback = false; Playfield.DisplayJudgements.Value = false; } @@ -67,27 +67,27 @@ namespace osu.Game.Rulesets.Edit private void regenerateAutoplay() { - var autoplayMod = drawableRuleset.Mods.OfType().Single(); - drawableRuleset.SetReplayScore(autoplayMod.CreateScoreFromReplayData(drawableRuleset.Beatmap, drawableRuleset.Mods)); + var autoplayMod = DrawableRuleset.Mods.OfType().Single(); + DrawableRuleset.SetReplayScore(autoplayMod.CreateScoreFromReplayData(DrawableRuleset.Beatmap, DrawableRuleset.Mods)); } private void addHitObject(HitObject hitObject) { - drawableRuleset.AddHitObject((TObject)hitObject); - drawableRuleset.Playfield.PostProcess(); + DrawableRuleset.AddHitObject((TObject)hitObject); + DrawableRuleset.Playfield.PostProcess(); } private void removeHitObject(HitObject hitObject) { - drawableRuleset.RemoveHitObject((TObject)hitObject); - drawableRuleset.Playfield.PostProcess(); + DrawableRuleset.RemoveHitObject((TObject)hitObject); + DrawableRuleset.Playfield.PostProcess(); } public override bool PropagatePositionalInputSubTree => false; public override bool PropagateNonPositionalInputSubTree => false; - public PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => drawableRuleset.CreatePlayfieldAdjustmentContainer(); + public PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => DrawableRuleset.CreatePlayfieldAdjustmentContainer(); protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 9f277b6190..8cc7072582 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -132,6 +132,7 @@ namespace osu.Game.Rulesets.Edit if (DrawableRuleset is IDrawableScrollingRuleset scrollingRuleset) dependencies.CacheAs(scrollingRuleset.ScrollingInfo); + dependencies.CacheAs(drawableRulesetWrapper.DrawableRuleset); dependencies.CacheAs(Playfield); InternalChildren = new[] From 4774d9c9ae2652ceb002444dfcc37d176cdbfa45 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 12 Jan 2025 22:56:39 -0500 Subject: [PATCH 063/801] Fix mania fade in test not actually testing the mod --- .../Mods/TestSceneManiaModFadeIn.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFadeIn.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFadeIn.cs index f403d67377..7b8156c74f 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFadeIn.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFadeIn.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods { CreateModTest(new ModTestData { - Mod = new ManiaModHidden(), + Mod = new ManiaModFadeIn(), PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE) }); } @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods { CreateModTest(new ModTestData { - Mod = new ManiaModHidden(), + Mod = new ManiaModFadeIn(), PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE) }); @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods { CreateModTest(new ModTestData { - Mod = new ManiaModHidden(), + Mod = new ManiaModFadeIn(), PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE) }); @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods { CreateModTest(new ModTestData { - Mod = new ManiaModHidden(), + Mod = new ManiaModFadeIn(), PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE) }); @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods { CreateModTest(new ModTestData { - Mod = new ManiaModHidden(), + Mod = new ManiaModFadeIn(), CreateBeatmap = () => new Beatmap { HitObjects = Enumerable.Range(1, 100).Select(i => (HitObject)new Note { StartTime = 1000 + 200 * i }).ToList(), From b0c0c98c5dff7ac92e67d5f25a0c20749568adda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 Jan 2025 11:19:17 +0100 Subject: [PATCH 064/801] Refetch local metadata cache if corruption is detected Addresses one of the points in https://github.com/ppy/osu/issues/31496. Not going to lie, this is mostly best-effort stuff (while the refetch is happening, metadata lookups using the local source *will* fail), but I see this as a marginal scenario anyways. --- .../LocalCachedBeatmapMetadataSource.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs index 66fad6c8d8..7495805cff 100644 --- a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs +++ b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs @@ -114,6 +114,15 @@ namespace osu.Game.Beatmaps } } } + catch (SqliteException sqliteException) when (sqliteException.SqliteErrorCode == 11 || sqliteException.SqliteErrorCode == 26) // SQLITE_CORRUPT, SQLITE_NOTADB + { + // only attempt purge & refetch if there is no other refetch in progress + if (cacheDownloadRequest == null) + { + tryPurgeCache(); + prepareLocalCache(); + } + } catch (Exception ex) { logForModel(beatmapInfo.BeatmapSet, $@"Cached local retrieval for {beatmapInfo} failed with {ex}."); @@ -125,6 +134,22 @@ namespace osu.Game.Beatmaps return false; } + private void tryPurgeCache() + { + log(@"Local metadata cache is corrupted; attempting purge."); + + try + { + File.Delete(storage.GetFullPath(cache_database_name)); + } + catch (Exception ex) + { + log($@"Failed to purge local metadata cache: {ex}"); + } + + log(@"Local metadata cache purged due to corruption."); + } + private SqliteConnection getConnection() => new SqliteConnection(string.Concat(@"Data Source=", storage.GetFullPath(@"online.db", true))); From c53188cf450bf5eb9efb903e5e295b7435971386 Mon Sep 17 00:00:00 2001 From: StanR Date: Tue, 14 Jan 2025 18:18:02 +0500 Subject: [PATCH 065/801] Use total deviation to scale accuracy on aim, general aim buff (#31498) * Make aim accuracy scaling harsher * Use deviation-based scaling * Bring the balancing multiplier down * Adjust multipliers, fix incorrect deviation when using slider accuracy * Adjust multipliers * Update osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs Co-authored-by: James Wilson * Change high speed deviation threshold to 22-27 instead of 20-24 * Update tests --------- Co-authored-by: James Wilson --- .../OsuDifficultyCalculatorTest.cs | 12 ++-- .../Difficulty/Evaluators/AimEvaluator.cs | 2 +- .../Difficulty/OsuPerformanceAttributes.cs | 3 + .../Difficulty/OsuPerformanceCalculator.cs | 57 +++++++++++++++++-- .../Difficulty/Skills/Aim.cs | 2 +- 5 files changed, 63 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index 9af5051f45..a68d9dad39 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,21 +15,21 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests"; - [TestCase(6.6860329680488437d, 239, "diffcalc-test")] - [TestCase(1.4485740324170036d, 54, "zero-length-sliders")] + [TestCase(6.7443067697205539d, 239, "diffcalc-test")] + [TestCase(1.4630292101418947d, 54, "zero-length-sliders")] [TestCase(0.43052813047866129d, 4, "very-fast-slider")] [TestCase(0.14143808967817237d, 2, "nan-slider")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(9.6300773538770041d, 239, "diffcalc-test")] - [TestCase(1.7550155729445993d, 54, "zero-length-sliders")] + [TestCase(9.7058844423552308d, 239, "diffcalc-test")] + [TestCase(1.7724929629205366d, 54, "zero-length-sliders")] [TestCase(0.55785578988249407d, 4, "very-fast-slider")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); - [TestCase(6.6860329680488437d, 239, "diffcalc-test")] - [TestCase(1.4485740324170036d, 54, "zero-length-sliders")] + [TestCase(6.7443067697205539d, 239, "diffcalc-test")] + [TestCase(1.4630292101418947d, 54, "zero-length-sliders")] [TestCase(0.43052813047866129d, 4, "very-fast-slider")] public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index e279ed889a..858ce673ee 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // Penalize angle repetition. wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3)); - acuteAngleBonus *= 0.1 + 0.9 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); + acuteAngleBonus *= 0.09 + 0.91 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); // Apply full wide angle bonus for distance more than one diameter wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter); diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs index de4491a31b..9c30c0f7c7 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs @@ -24,6 +24,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty [JsonProperty("effective_miss_count")] public double EffectiveMissCount { get; set; } + [JsonProperty("total_deviation")] + public double? TotalDeviation { get; set; } + [JsonProperty("speed_deviation")] public double? SpeedDeviation { get; set; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 91cd270966..a03e3fd6ef 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; @@ -41,6 +42,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty /// private double effectiveMissCount; + private double? totalDeviation; private double? speedDeviation; public OsuPerformanceCalculator() @@ -113,6 +115,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits); } + totalDeviation = calculateTotalDeviation(osuAttributes); speedDeviation = calculateSpeedDeviation(osuAttributes); double aimValue = computeAimValue(score, osuAttributes); @@ -135,6 +138,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty Accuracy = accuracyValue, Flashlight = flashlightValue, EffectiveMissCount = effectiveMissCount, + TotalDeviation = totalDeviation, SpeedDeviation = speedDeviation, Total = totalValue }; @@ -145,6 +149,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(h => h is OsuModAutopilot)) return 0.0; + if (totalDeviation == null) + return 0; + double aimDifficulty = attributes.AimDifficulty; if (attributes.SliderCount > 0 && attributes.AimDifficultSliderCount > 0) @@ -196,9 +203,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); } - aimValue *= accuracy; - // It is important to consider accuracy difficulty when scaling with accuracy. - aimValue *= 0.98 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 2500; + aimValue *= SpecialFunctions.Erf(25.0 / (Math.Sqrt(2) * totalDeviation.Value)); return aimValue; } @@ -317,6 +322,48 @@ namespace osu.Game.Rulesets.Osu.Difficulty return flashlightValue; } + /// + /// Using estimates player's deviation on accuracy objects. + /// Returns deviation for circles and sliders if score was set with slideracc. + /// Returns the min between deviation of circles and deviation on circles and sliders (assuming slider hits are 50s), if score was set without slideracc. + /// + private double? calculateTotalDeviation(OsuDifficultyAttributes attributes) + { + if (totalSuccessfulHits == 0) + return null; + + int accuracyObjectCount = attributes.HitCircleCount; + + if (!usingClassicSliderAccuracy) + accuracyObjectCount += attributes.SliderCount; + + // Assume worst case: all mistakes was on accuracy objects + int relevantCountMiss = Math.Min(countMiss, accuracyObjectCount); + int relevantCountMeh = Math.Min(countMeh, accuracyObjectCount - relevantCountMiss); + int relevantCountOk = Math.Min(countOk, accuracyObjectCount - relevantCountMiss - relevantCountMeh); + int relevantCountGreat = Math.Max(0, accuracyObjectCount - relevantCountMiss - relevantCountMeh - relevantCountOk); + + // Calculate deviation on accuracy objects + double? deviation = calculateDeviation(attributes, relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss); + if (deviation == null) + return null; + + if (!usingClassicSliderAccuracy) + return deviation.Value; + + // If score was set without slider accuracy - also compute deviation with sliders + // Assume that all hits was 50s + int totalCountWithSliders = attributes.HitCircleCount + attributes.SliderCount; + int missCountWithSliders = Math.Min(totalCountWithSliders, countMiss); + int hitCountWithSliders = totalCountWithSliders - missCountWithSliders; + + double hitProbabilityWithSliders = hitCountWithSliders / (totalCountWithSliders + 1.0); + double deviationWithSliders = attributes.MehHitWindow / (Math.Sqrt(2) * SpecialFunctions.ErfInv(hitProbabilityWithSliders)); + + // Min is needed for edgecase maps with 1 circle and 999 sliders, as deviation on sliders can be lower in this case + return Math.Min(deviation.Value, deviationWithSliders); + } + /// /// Estimates player's deviation on speed notes using , assuming worst-case. /// Treats all speed notes as hit circles. @@ -412,8 +459,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty const double scale = 50; double adjustedSpeedValue = scale * (Math.Log((speedValue - excessSpeedDifficultyCutoff) / scale + 1) + excessSpeedDifficultyCutoff / scale); - // 200 UR and less are considered tapped correctly to ensure that normal scores will be punished as little as possible - double lerp = 1 - Math.Clamp((speedDeviation.Value - 20) / (24 - 20), 0, 1); + // 220 UR and less are considered tapped correctly to ensure that normal scores will be punished as little as possible + double lerp = 1 - DifficultyCalculationUtils.ReverseLerp(speedDeviation.Value, 22.0, 27.0); adjustedSpeedValue = double.Lerp(adjustedSpeedValue, speedValue, lerp); return adjustedSpeedValue / speedValue; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 400bc97fbc..69211b610f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double currentStrain; - private double skillMultiplier => 25.18; + private double skillMultiplier => 25.7; private double strainDecayBase => 0.15; private readonly List sliderStrains = new List(); From 6cf15e3e5a2f5aa0df42886a60367eb2f184fe30 Mon Sep 17 00:00:00 2001 From: James Wilson Date: Tue, 14 Jan 2025 18:27:25 +0000 Subject: [PATCH 066/801] Remove problematic total deviation scaling, rebalance aim (#31515) * Remove problematic total deviation scaling, rebalance aim * Fix tests --- .../OsuDifficultyCalculatorTest.cs | 12 ++--- .../Difficulty/Evaluators/AimEvaluator.cs | 2 +- .../Difficulty/OsuPerformanceAttributes.cs | 3 -- .../Difficulty/OsuPerformanceCalculator.cs | 52 ++----------------- .../Difficulty/Skills/Aim.cs | 2 +- 5 files changed, 11 insertions(+), 60 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index a68d9dad39..7cf5b0529f 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,21 +15,21 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests"; - [TestCase(6.7443067697205539d, 239, "diffcalc-test")] - [TestCase(1.4630292101418947d, 54, "zero-length-sliders")] + [TestCase(6.7331304290522747d, 239, "diffcalc-test")] + [TestCase(1.4602604078137214d, 54, "zero-length-sliders")] [TestCase(0.43052813047866129d, 4, "very-fast-slider")] [TestCase(0.14143808967817237d, 2, "nan-slider")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(9.7058844423552308d, 239, "diffcalc-test")] - [TestCase(1.7724929629205366d, 54, "zero-length-sliders")] + [TestCase(9.6779397290273756d, 239, "diffcalc-test")] + [TestCase(1.7691451263718989d, 54, "zero-length-sliders")] [TestCase(0.55785578988249407d, 4, "very-fast-slider")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); - [TestCase(6.7443067697205539d, 239, "diffcalc-test")] - [TestCase(1.4630292101418947d, 54, "zero-length-sliders")] + [TestCase(6.7331304290522747d, 239, "diffcalc-test")] + [TestCase(1.4602604078137214d, 54, "zero-length-sliders")] [TestCase(0.43052813047866129d, 4, "very-fast-slider")] public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index 858ce673ee..9a5533e536 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // Penalize angle repetition. wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3)); - acuteAngleBonus *= 0.09 + 0.91 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); + acuteAngleBonus *= 0.08 + 0.92 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); // Apply full wide angle bonus for distance more than one diameter wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter); diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs index 9c30c0f7c7..de4491a31b 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs @@ -24,9 +24,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty [JsonProperty("effective_miss_count")] public double EffectiveMissCount { get; set; } - [JsonProperty("total_deviation")] - public double? TotalDeviation { get; set; } - [JsonProperty("speed_deviation")] public double? SpeedDeviation { get; set; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index a03e3fd6ef..7013ee55c4 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -42,7 +42,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty /// private double effectiveMissCount; - private double? totalDeviation; private double? speedDeviation; public OsuPerformanceCalculator() @@ -115,7 +114,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits); } - totalDeviation = calculateTotalDeviation(osuAttributes); speedDeviation = calculateSpeedDeviation(osuAttributes); double aimValue = computeAimValue(score, osuAttributes); @@ -138,7 +136,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty Accuracy = accuracyValue, Flashlight = flashlightValue, EffectiveMissCount = effectiveMissCount, - TotalDeviation = totalDeviation, SpeedDeviation = speedDeviation, Total = totalValue }; @@ -149,9 +146,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(h => h is OsuModAutopilot)) return 0.0; - if (totalDeviation == null) - return 0; - double aimDifficulty = attributes.AimDifficulty; if (attributes.SliderCount > 0 && attributes.AimDifficultSliderCount > 0) @@ -203,7 +197,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); } - aimValue *= SpecialFunctions.Erf(25.0 / (Math.Sqrt(2) * totalDeviation.Value)); + aimValue *= accuracy; + // It is important to consider accuracy difficulty when scaling with accuracy. + aimValue *= 0.98 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 2500; return aimValue; } @@ -322,48 +318,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty return flashlightValue; } - /// - /// Using estimates player's deviation on accuracy objects. - /// Returns deviation for circles and sliders if score was set with slideracc. - /// Returns the min between deviation of circles and deviation on circles and sliders (assuming slider hits are 50s), if score was set without slideracc. - /// - private double? calculateTotalDeviation(OsuDifficultyAttributes attributes) - { - if (totalSuccessfulHits == 0) - return null; - - int accuracyObjectCount = attributes.HitCircleCount; - - if (!usingClassicSliderAccuracy) - accuracyObjectCount += attributes.SliderCount; - - // Assume worst case: all mistakes was on accuracy objects - int relevantCountMiss = Math.Min(countMiss, accuracyObjectCount); - int relevantCountMeh = Math.Min(countMeh, accuracyObjectCount - relevantCountMiss); - int relevantCountOk = Math.Min(countOk, accuracyObjectCount - relevantCountMiss - relevantCountMeh); - int relevantCountGreat = Math.Max(0, accuracyObjectCount - relevantCountMiss - relevantCountMeh - relevantCountOk); - - // Calculate deviation on accuracy objects - double? deviation = calculateDeviation(attributes, relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss); - if (deviation == null) - return null; - - if (!usingClassicSliderAccuracy) - return deviation.Value; - - // If score was set without slider accuracy - also compute deviation with sliders - // Assume that all hits was 50s - int totalCountWithSliders = attributes.HitCircleCount + attributes.SliderCount; - int missCountWithSliders = Math.Min(totalCountWithSliders, countMiss); - int hitCountWithSliders = totalCountWithSliders - missCountWithSliders; - - double hitProbabilityWithSliders = hitCountWithSliders / (totalCountWithSliders + 1.0); - double deviationWithSliders = attributes.MehHitWindow / (Math.Sqrt(2) * SpecialFunctions.ErfInv(hitProbabilityWithSliders)); - - // Min is needed for edgecase maps with 1 circle and 999 sliders, as deviation on sliders can be lower in this case - return Math.Min(deviation.Value, deviationWithSliders); - } - /// /// Estimates player's deviation on speed notes using , assuming worst-case. /// Treats all speed notes as hit circles. diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 69211b610f..f04b679b73 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double currentStrain; - private double skillMultiplier => 25.7; + private double skillMultiplier => 25.6; private double strainDecayBase => 0.15; private readonly List sliderStrains = new List(); From 5bed7c22e351a60a9ae22b2f736da4871646911d Mon Sep 17 00:00:00 2001 From: Natelytle <92956514+Natelytle@users.noreply.github.com> Date: Tue, 14 Jan 2025 14:12:08 -0500 Subject: [PATCH 067/801] Remove lower cap on deviation without misses (#31499) --- .../Difficulty/TaikoPerformanceCalculator.cs | 48 ++++--------------- 1 file changed, 8 insertions(+), 40 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 5da18e7963..4933c9dee6 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -123,53 +123,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// private double? computeDeviationUpperBound(TaikoDifficultyAttributes attributes) { - if (totalSuccessfulHits == 0 || attributes.GreatHitWindow <= 0) + if (countGreat == 0 || attributes.GreatHitWindow <= 0) return null; - double h300 = attributes.GreatHitWindow; - double h100 = attributes.OkHitWindow; - const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed). - double? deviationGreatWindow = calcDeviationGreatWindow(); - double? deviationGoodWindow = calcDeviationGoodWindow(); + double n = totalHits; - return deviationGreatWindow is null ? deviationGoodWindow : Math.Min(deviationGreatWindow.Value, deviationGoodWindow!.Value); + // Proportion of greats hit. + double p = countGreat / n; - // The upper bound on deviation, calculated with the ratio of 300s to objects, and the great hit window. - double? calcDeviationGreatWindow() - { - if (countGreat == 0) return null; + // We can be 99% confident that p is at least this value. + double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4); - double n = totalHits; - - // Proportion of greats hit. - double p = countGreat / n; - - // We can be 99% confident that p is at least this value. - double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4); - - // We can be 99% confident that the deviation is not higher than: - return h300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); - } - - // The upper bound on deviation, calculated with the ratio of 300s + 100s to objects, and the good hit window. - // This will return a lower value than the first method when the number of 100s is high, but the miss count is low. - double? calcDeviationGoodWindow() - { - if (totalSuccessfulHits == 0) return null; - - double n = totalHits; - - // Proportion of greats + goods hit. - double p = Math.Max(0, totalSuccessfulHits - 0.0005 * countOk) / n; - - // We can be 99% confident that p is at least this value. - double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4); - - // We can be 99% confident that the deviation is not higher than: - return h100 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); - } + // We can be 99% confident that the deviation is not higher than: + return attributes.GreatHitWindow / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); } private int totalHits => countGreat + countOk + countMeh + countMiss; From 8985a387344b91ce8ec48da0bfc183db67b14b4f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 15 Jan 2025 16:53:55 +0900 Subject: [PATCH 068/801] Display up-to-date online status in user panels --- .../Visual/Online/TestSceneUserPanel.cs | 221 +++++++++--------- osu.Game/Online/Metadata/MetadataClient.cs | 16 ++ .../Dashboard/CurrentlyOnlineDisplay.cs | 59 ++--- osu.Game/Users/ExtendedUserPanel.cs | 105 +++++---- 4 files changed, 202 insertions(+), 199 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index 4c2e47d336..b4dafd3107 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -4,17 +4,18 @@ using System; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Online; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Metadata; using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Visual.Metadata; using osu.Game.Users; using osuTK; @@ -23,144 +24,138 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public partial class TestSceneUserPanel : OsuTestScene { - private readonly Bindable activity = new Bindable(); - private readonly Bindable status = new Bindable(); - - private UserGridPanel boundPanel1 = null!; - private TestUserListPanel boundPanel2 = null!; - [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - [Cached(typeof(LocalUserStatisticsProvider))] - private readonly TestUserStatisticsProvider statisticsProvider = new TestUserStatisticsProvider(); - [Resolved] private IRulesetStore rulesetStore { get; set; } = null!; + private TestUserStatisticsProvider statisticsProvider = null!; + private TestMetadataClient metadataClient = null!; + private TestUserListPanel panel = null!; + [SetUp] public void SetUp() => Schedule(() => { - activity.Value = null; - status.Value = null; - - Remove(statisticsProvider, false); - Clear(); - Add(statisticsProvider); - - Add(new FillFlowContainer + Child = new DependencyProvidingContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Spacing = new Vector2(10f), + RelativeSizeAxes = Axes.Both, + CachedDependencies = + [ + (typeof(LocalUserStatisticsProvider), statisticsProvider = new TestUserStatisticsProvider()), + (typeof(MetadataClient), metadataClient = new TestMetadataClient()) + ], Children = new Drawable[] { - new UserBrickPanel(new APIUser + statisticsProvider, + metadataClient, + new FillFlowContainer { - Username = @"flyte", - Id = 3103765, - CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg", - }), - new UserBrickPanel(new APIUser - { - Username = @"peppy", - Id = 2, - Colour = "99EB47", - CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", - }), - new UserGridPanel(new APIUser - { - Username = @"flyte", - Id = 3103765, - CountryCode = CountryCode.JP, - CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg", - IsOnline = true - }) { Width = 300 }, - boundPanel1 = new UserGridPanel(new APIUser - { - Username = @"peppy", - Id = 2, - CountryCode = CountryCode.AU, - CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", - IsSupporter = true, - SupportLevel = 3, - }) { Width = 300 }, - boundPanel2 = new TestUserListPanel(new APIUser - { - Username = @"Evast", - Id = 8195163, - CountryCode = CountryCode.BY, - CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", - IsOnline = false, - LastVisit = DateTimeOffset.Now - }), - new UserRankPanel(new APIUser - { - Username = @"flyte", - Id = 3103765, - CountryCode = CountryCode.JP, - CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg", - Statistics = new UserStatistics { GlobalRank = 12345, CountryRank = 1234 } - }) { Width = 300 }, - new UserRankPanel(new APIUser - { - Username = @"peppy", - Id = 2, - Colour = "99EB47", - CountryCode = CountryCode.AU, - CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", - Statistics = new UserStatistics { GlobalRank = null, CountryRank = null } - }) { Width = 300 } + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Spacing = new Vector2(10f), + Children = new Drawable[] + { + new UserBrickPanel(new APIUser + { + Username = @"flyte", + Id = 3103765, + CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg", + }), + new UserBrickPanel(new APIUser + { + Username = @"peppy", + Id = 2, + Colour = "99EB47", + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", + }), + new UserGridPanel(new APIUser + { + Username = @"flyte", + Id = 3103765, + CountryCode = CountryCode.JP, + CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg", + IsOnline = true + }) { Width = 300 }, + new UserGridPanel(new APIUser + { + Username = @"peppy", + Id = 2, + CountryCode = CountryCode.AU, + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", + IsSupporter = true, + SupportLevel = 3, + }) { Width = 300 }, + panel = new TestUserListPanel(new APIUser + { + Username = @"peppy", + Id = 2, + CountryCode = CountryCode.AU, + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", + LastVisit = DateTimeOffset.Now + }), + new UserRankPanel(new APIUser + { + Username = @"flyte", + Id = 3103765, + CountryCode = CountryCode.JP, + CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg", + Statistics = new UserStatistics { GlobalRank = 12345, CountryRank = 1234 } + }) { Width = 300 }, + new UserRankPanel(new APIUser + { + Username = @"peppy", + Id = 2, + Colour = "99EB47", + CountryCode = CountryCode.AU, + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", + Statistics = new UserStatistics { GlobalRank = null, CountryRank = null } + }) { Width = 300 } + } + } } - }); + }; - boundPanel1.Status.BindTo(status); - boundPanel1.Activity.BindTo(activity); - - boundPanel2.Status.BindTo(status); - boundPanel2.Activity.BindTo(activity); + metadataClient.BeginWatchingUserPresence(); }); [Test] public void TestUserStatus() { - AddStep("online", () => status.Value = UserStatus.Online); - AddStep("do not disturb", () => status.Value = UserStatus.DoNotDisturb); - AddStep("offline", () => status.Value = UserStatus.Offline); - AddStep("null status", () => status.Value = null); + AddStep("online", () => setPresence(UserStatus.Online, null)); + AddStep("do not disturb", () => setPresence(UserStatus.DoNotDisturb, null)); + AddStep("offline", () => setPresence(UserStatus.Offline, null)); } [Test] public void TestUserActivity() { - AddStep("set online status", () => status.Value = UserStatus.Online); - - AddStep("idle", () => activity.Value = null); - AddStep("watching replay", () => activity.Value = new UserActivity.WatchingReplay(createScore(@"nats"))); - AddStep("spectating user", () => activity.Value = new UserActivity.SpectatingUser(createScore(@"mrekk"))); - AddStep("solo (osu!)", () => activity.Value = soloGameStatusForRuleset(0)); - AddStep("solo (osu!taiko)", () => activity.Value = soloGameStatusForRuleset(1)); - AddStep("solo (osu!catch)", () => activity.Value = soloGameStatusForRuleset(2)); - AddStep("solo (osu!mania)", () => activity.Value = soloGameStatusForRuleset(3)); - AddStep("choosing", () => activity.Value = new UserActivity.ChoosingBeatmap()); - AddStep("editing beatmap", () => activity.Value = new UserActivity.EditingBeatmap(new BeatmapInfo())); - AddStep("modding beatmap", () => activity.Value = new UserActivity.ModdingBeatmap(new BeatmapInfo())); - AddStep("testing beatmap", () => activity.Value = new UserActivity.TestingBeatmap(new BeatmapInfo())); + AddStep("idle", () => setPresence(UserStatus.Online, null)); + AddStep("watching replay", () => setPresence(UserStatus.Online, new UserActivity.WatchingReplay(createScore(@"nats")))); + AddStep("spectating user", () => setPresence(UserStatus.Online, new UserActivity.SpectatingUser(createScore(@"mrekk")))); + AddStep("solo (osu!)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(0))); + AddStep("solo (osu!taiko)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(1))); + AddStep("solo (osu!catch)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(2))); + AddStep("solo (osu!mania)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(3))); + AddStep("choosing", () => setPresence(UserStatus.Online, new UserActivity.ChoosingBeatmap())); + AddStep("editing beatmap", () => setPresence(UserStatus.Online, new UserActivity.EditingBeatmap(new BeatmapInfo()))); + AddStep("modding beatmap", () => setPresence(UserStatus.Online, new UserActivity.ModdingBeatmap(new BeatmapInfo()))); + AddStep("testing beatmap", () => setPresence(UserStatus.Online, new UserActivity.TestingBeatmap(new BeatmapInfo()))); } [Test] public void TestUserActivityChange() { - AddAssert("visit message is visible", () => boundPanel2.LastVisitMessage.IsPresent); - AddStep("set online status", () => status.Value = UserStatus.Online); - AddAssert("visit message is not visible", () => !boundPanel2.LastVisitMessage.IsPresent); - AddStep("set choosing activity", () => activity.Value = new UserActivity.ChoosingBeatmap()); - AddStep("set offline status", () => status.Value = UserStatus.Offline); - AddAssert("visit message is visible", () => boundPanel2.LastVisitMessage.IsPresent); - AddStep("set online status", () => status.Value = UserStatus.Online); - AddAssert("visit message is not visible", () => !boundPanel2.LastVisitMessage.IsPresent); + AddAssert("visit message is visible", () => panel.LastVisitMessage.IsPresent); + AddStep("set online status", () => setPresence(UserStatus.Online, null)); + AddAssert("visit message is not visible", () => !panel.LastVisitMessage.IsPresent); + AddStep("set choosing activity", () => setPresence(UserStatus.Online, new UserActivity.ChoosingBeatmap())); + AddStep("set offline status", () => setPresence(UserStatus.Offline, null)); + AddAssert("visit message is visible", () => panel.LastVisitMessage.IsPresent); + AddStep("set online status", () => setPresence(UserStatus.Online, null)); + AddAssert("visit message is not visible", () => !panel.LastVisitMessage.IsPresent); } [Test] @@ -185,6 +180,14 @@ namespace osu.Game.Tests.Visual.Online AddStep("set statistics to empty", () => statisticsProvider.UpdateStatistics(new UserStatistics(), Ruleset.Value)); } + private void setPresence(UserStatus status, UserActivity? activity) + { + if (status == UserStatus.Offline) + metadataClient.UserPresenceUpdated(panel.User.OnlineID, null); + else + metadataClient.UserPresenceUpdated(panel.User.OnlineID, new UserPresence { Status = status, Activity = activity }); + } + private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(new BeatmapInfo(), rulesetStore.GetRuleset(rulesetId)!); private ScoreInfo createScore(string name) => new ScoreInfo(new TestBeatmap(Ruleset.Value).BeatmapInfo) diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs index 6578f70f74..8f1fe0641f 100644 --- a/osu.Game/Online/Metadata/MetadataClient.cs +++ b/osu.Game/Online/Metadata/MetadataClient.cs @@ -47,6 +47,22 @@ namespace osu.Game.Online.Metadata /// public abstract IBindableDictionary FriendStates { get; } + /// + /// Attempts to retrieve the presence of a user. + /// + /// The user ID. + /// The user presence, or null if not available or the user's offline. + public UserPresence? GetPresence(int userId) + { + if (FriendStates.TryGetValue(userId, out UserPresence presence)) + return presence; + + if (UserStates.TryGetValue(userId, out presence)) + return presence; + + return null; + } + /// public abstract Task UpdateActivity(UserActivity? activity); diff --git a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs index 2ca548fdf5..ef07f4538c 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; @@ -40,17 +38,20 @@ namespace osu.Game.Overlays.Dashboard private readonly IBindableDictionary onlineUsers = new BindableDictionary(); private readonly Dictionary userPanels = new Dictionary(); - private SearchContainer userFlow; - private BasicSearchTextBox searchTextBox; + private SearchContainer userFlow = null!; + private BasicSearchTextBox searchTextBox = null!; [Resolved] - private IAPIProvider api { get; set; } + private IAPIProvider api { get; set; } = null!; [Resolved] - private SpectatorClient spectatorClient { get; set; } + private SpectatorClient spectatorClient { get; set; } = null!; [Resolved] - private MetadataClient metadataClient { get; set; } + private MetadataClient metadataClient { get; set; } = null!; + + [Resolved] + private UserLookupCache users { get; set; } = null!; [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) @@ -99,9 +100,6 @@ namespace osu.Game.Overlays.Dashboard searchTextBox.Current.ValueChanged += text => userFlow.SearchTerm = text.NewValue; } - [Resolved] - private UserLookupCache users { get; set; } - protected override void LoadComplete() { base.LoadComplete(); @@ -120,7 +118,7 @@ namespace osu.Game.Overlays.Dashboard searchTextBox.TakeFocus(); } - private void onUserUpdated(object sender, NotifyDictionaryChangedEventArgs e) => Schedule(() => + private void onUserUpdated(object? sender, NotifyDictionaryChangedEventArgs e) => Schedule(() => { switch (e.Action) { @@ -133,38 +131,13 @@ namespace osu.Game.Overlays.Dashboard users.GetUserAsync(userId).ContinueWith(task => { - APIUser user = task.GetResultSafely(); - - if (user == null) - return; - - Schedule(() => - { - userFlow.Add(userPanels[userId] = createUserPanel(user).With(p => - { - p.Status.Value = onlineUsers.GetValueOrDefault(userId).Status; - p.Activity.Value = onlineUsers.GetValueOrDefault(userId).Activity; - })); - }); + if (task.GetResultSafely() is APIUser user) + Schedule(() => userFlow.Add(userPanels[userId] = createUserPanel(user))); }); } break; - case NotifyDictionaryChangedAction.Replace: - Debug.Assert(e.NewItems != null); - - foreach (var kvp in e.NewItems) - { - if (userPanels.TryGetValue(kvp.Key, out var panel)) - { - panel.Activity.Value = kvp.Value.Activity; - panel.Status.Value = kvp.Value.Status; - } - } - - break; - case NotifyDictionaryChangedAction.Remove: Debug.Assert(e.OldItems != null); @@ -179,7 +152,7 @@ namespace osu.Game.Overlays.Dashboard } }); - private void onPlayingUsersChanged(object sender, NotifyCollectionChangedEventArgs e) + private void onPlayingUsersChanged(object? sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) { @@ -219,9 +192,6 @@ namespace osu.Game.Overlays.Dashboard { public readonly APIUser User; - public readonly Bindable Status = new Bindable(); - public readonly Bindable Activity = new Bindable(); - public BindableBool CanSpectate { get; } = new BindableBool(); public IEnumerable FilterTerms { get; } @@ -268,10 +238,7 @@ namespace osu.Game.Overlays.Dashboard { RelativeSizeAxes = Axes.X, Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - // this is SHOCKING - Activity = { BindTarget = Activity }, - Status = { BindTarget = Status }, + Origin = Anchor.TopCentre }, new PurpleRoundedButton { diff --git a/osu.Game/Users/ExtendedUserPanel.cs b/osu.Game/Users/ExtendedUserPanel.cs index e33fb7a44e..eb1115e296 100644 --- a/osu.Game/Users/ExtendedUserPanel.cs +++ b/osu.Game/Users/ExtendedUserPanel.cs @@ -1,10 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - +using System; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -14,44 +12,56 @@ using osu.Game.Graphics.Sprites; using osu.Game.Users.Drawables; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Metadata; namespace osu.Game.Users { public abstract partial class ExtendedUserPanel : UserPanel { - public readonly Bindable Status = new Bindable(); + protected TextFlowContainer LastVisitMessage { get; private set; } = null!; - public readonly IBindable Activity = new Bindable(); + private StatusIcon statusIcon = null!; + private StatusText statusMessage = null!; - protected TextFlowContainer LastVisitMessage { get; private set; } + [Resolved] + private MetadataClient? metadata { get; set; } - private StatusIcon statusIcon; - private StatusText statusMessage; + [Resolved] + private IAPIProvider? api { get; set; } + + private UserStatus? lastStatus; + private UserActivity? lastActivity; + private DateTimeOffset? lastVisit; protected ExtendedUserPanel(APIUser user) : base(user) { + lastVisit = user.LastVisit; } [BackgroundDependencyLoader] private void load() { BorderColour = ColourProvider?.Light1 ?? Colours.GreyVioletLighter; - - Status.ValueChanged += status => displayStatus(status.NewValue, Activity.Value); - Activity.ValueChanged += activity => displayStatus(Status.Value, activity.NewValue); } protected override void LoadComplete() { base.LoadComplete(); - Status.TriggerChange(); + updatePresence(); // Colour should be applied immediately on first load. statusIcon.FinishTransforms(); } + protected override void Update() + { + base.Update(); + updatePresence(); + } + protected Container CreateStatusIcon() => statusIcon = new StatusIcon(); protected FillFlowContainer CreateStatusMessage(bool rightAlignedChildren) @@ -70,15 +80,6 @@ namespace osu.Game.Users text.Origin = alignment; text.AutoSizeAxes = Axes.Both; text.Alpha = 0; - - if (User.LastVisit.HasValue) - { - text.AddText(@"Last seen "); - text.AddText(new DrawableDate(User.LastVisit.Value, italic: false) - { - Shadow = false - }); - } })); statusContainer.Add(statusMessage = new StatusText @@ -91,37 +92,53 @@ namespace osu.Game.Users return statusContainer; } - private void displayStatus(UserStatus? status, UserActivity activity = null) + private void updatePresence() { - if (status != null) + UserPresence? presence; + + if (User.Equals(api?.LocalUser.Value)) + presence = new UserPresence { Status = api.Status.Value, Activity = api.Activity.Value }; + else + presence = metadata?.GetPresence(User.OnlineID); + + UserStatus status = presence?.Status ?? UserStatus.Offline; + UserActivity? activity = presence?.Activity; + + if (status == lastStatus && activity == lastActivity) + return; + + if (status == UserStatus.Offline && lastVisit != null) { - LastVisitMessage.FadeTo(status == UserStatus.Offline && User.LastVisit.HasValue ? 1 : 0); - - // Set status message based on activity (if we have one) and status is not offline - if (activity != null && status != UserStatus.Offline) + LastVisitMessage.FadeTo(1); + LastVisitMessage.Clear(); + LastVisitMessage.AddText(@"Last seen "); + LastVisitMessage.AddText(new DrawableDate(lastVisit.Value, italic: false) { - statusMessage.Text = activity.GetStatus(); - statusMessage.TooltipText = activity.GetDetails(); - statusIcon.FadeColour(activity.GetAppropriateColour(Colours), 500, Easing.OutQuint); - return; - } + Shadow = false + }); + } + else + LastVisitMessage.FadeTo(0); - // Otherwise use only status + // Set status message based on activity (if we have one) and status is not offline + if (activity != null && status != UserStatus.Offline) + { + statusMessage.Text = activity.GetStatus(); + statusMessage.TooltipText = activity.GetDetails() ?? string.Empty; + statusIcon.FadeColour(activity.GetAppropriateColour(Colours), 500, Easing.OutQuint); + } + + // Otherwise use only status + else + { statusMessage.Text = status.GetLocalisableDescription(); statusMessage.TooltipText = string.Empty; - statusIcon.FadeColour(status.Value.GetAppropriateColour(Colours), 500, Easing.OutQuint); - - return; + statusIcon.FadeColour(status.GetAppropriateColour(Colours), 500, Easing.OutQuint); } - // Fallback to web status if local one is null - if (User.IsOnline) - { - Status.Value = UserStatus.Online; - return; - } - - Status.Value = UserStatus.Offline; + lastStatus = status; + lastActivity = activity; + lastVisit = status != UserStatus.Offline ? DateTimeOffset.Now : lastVisit; } protected override bool OnHover(HoverEvent e) From 2763cb0b4e9febbfc7f9d185c4acc737214e9a58 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 15 Jan 2025 17:14:16 +0900 Subject: [PATCH 069/801] Fix inspection --- osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs index ef07f4538c..e6e1850721 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs @@ -196,8 +196,8 @@ namespace osu.Game.Overlays.Dashboard public IEnumerable FilterTerms { get; } - [Resolved(canBeNull: true)] - private IPerformFromScreenRunner performer { get; set; } + [Resolved] + private IPerformFromScreenRunner? performer { get; set; } public bool FilteringActive { set; get; } From 0a21183e54648953b653e2e56b8150a11a93c69a Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Wed, 15 Jan 2025 20:34:21 +1000 Subject: [PATCH 070/801] reading mono nerf (#31510) --- osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs index 9de058f289..885131404a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs @@ -3,6 +3,7 @@ using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; @@ -34,6 +35,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills } var taikoObject = (TaikoDifficultyHitObject)current; + int index = taikoObject.Colour.MonoStreak?.HitObjects.IndexOf(taikoObject) ?? 0; + + currentStrain *= DifficultyCalculationUtils.Logistic(index, 4, -1 / 25.0, 0.5) + 0.5; currentStrain *= StrainDecayBase; currentStrain += ReadingEvaluator.EvaluateDifficultyOf(taikoObject) * SkillMultiplier; From 43fc48a3f300c13433f957ed99c65541e0c4f801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 Jan 2025 14:44:13 +0100 Subject: [PATCH 071/801] Add client methods allowing users to be notified of who is watching them --- .../Visual/Gameplay/TestSceneSpectatorList.cs | 9 ++++- osu.Game/Online/Spectator/ISpectatorClient.cs | 12 ++++++ .../Online/Spectator/OnlineSpectatorClient.cs | 2 + osu.Game/Online/Spectator/SpectatorClient.cs | 35 ++++++++++++++++- osu.Game/Online/Spectator/SpectatorUser.cs | 39 +++++++++++++++++++ osu.Game/Screens/Play/HUD/SpectatorList.cs | 14 ++----- 6 files changed, 97 insertions(+), 14 deletions(-) create mode 100644 osu.Game/Online/Spectator/SpectatorUser.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs index 3cd37baafd..5be1829b85 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Game.Graphics; +using osu.Game.Online.Spectator; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; @@ -15,7 +16,7 @@ namespace osu.Game.Tests.Visual.Gameplay [TestFixture] public partial class TestSceneSpectatorList : OsuTestScene { - private readonly BindableList spectators = new BindableList(); + private readonly BindableList spectators = new BindableList(); private readonly Bindable localUserPlayingState = new Bindable(); private int counter; @@ -36,7 +37,11 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("add a user", () => { int id = Interlocked.Increment(ref counter); - spectators.Add(new SpectatorList.Spectator(id, $"User {id}")); + spectators.Add(new SpectatorUser + { + OnlineID = id, + Username = $"User {id}" + }); }); AddStep("remove random user", () => spectators.RemoveAt(RNG.Next(0, spectators.Count))); AddStep("enter break", () => localUserPlayingState.Value = LocalUserPlayingState.Break); diff --git a/osu.Game/Online/Spectator/ISpectatorClient.cs b/osu.Game/Online/Spectator/ISpectatorClient.cs index 2dc2283c23..2b73037cb8 100644 --- a/osu.Game/Online/Spectator/ISpectatorClient.cs +++ b/osu.Game/Online/Spectator/ISpectatorClient.cs @@ -37,5 +37,17 @@ namespace osu.Game.Online.Spectator /// The ID of the user who achieved the score. /// The ID of the score. Task UserScoreProcessed(int userId, long scoreId); + + /// + /// Signals that another user has started watching this client. + /// + /// The information about the user who started watching. + Task UserStartedWatching(SpectatorUser[] user); + + /// + /// Signals that another user has ended watching this client + /// + /// The ID of the user who ended watching. + Task UserEndedWatching(int userId); } } diff --git a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs index 036cfa1d76..645d7054dc 100644 --- a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs +++ b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs @@ -42,6 +42,8 @@ namespace osu.Game.Online.Spectator connection.On(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames); connection.On(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying); connection.On(nameof(ISpectatorClient.UserScoreProcessed), ((ISpectatorClient)this).UserScoreProcessed); + connection.On(nameof(ISpectatorClient.UserStartedWatching), ((ISpectatorClient)this).UserStartedWatching); + connection.On(nameof(ISpectatorClient.UserEndedWatching), ((ISpectatorClient)this).UserEndedWatching); connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IStatefulUserHubClient)this).DisconnectRequested); }; diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index fb7a3d13ca..ac11dad0f0 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -36,9 +36,14 @@ namespace osu.Game.Online.Spectator public abstract IBindable IsConnected { get; } /// - /// The states of all users currently being watched. + /// The states of all users currently being watched by the local user. /// - public virtual IBindableDictionary WatchedUserStates => watchedUserStates; + public IBindableDictionary WatchedUserStates => watchedUserStates; + + /// + /// All users who are currently watching the local user. + /// + public IBindableList WatchingUsers => watchingUsers; /// /// A global list of all players currently playing. @@ -82,6 +87,7 @@ namespace osu.Game.Online.Spectator private readonly BindableDictionary watchedUserStates = new BindableDictionary(); + private readonly BindableList watchingUsers = new BindableList(); private readonly BindableList playingUsers = new BindableList(); private readonly SpectatorState currentState = new SpectatorState(); @@ -127,6 +133,7 @@ namespace osu.Game.Online.Spectator { playingUsers.Clear(); watchedUserStates.Clear(); + watchingUsers.Clear(); } }), true); } @@ -179,6 +186,30 @@ namespace osu.Game.Online.Spectator return Task.CompletedTask; } + Task ISpectatorClient.UserStartedWatching(SpectatorUser[] users) + { + Schedule(() => + { + foreach (var user in users) + { + if (!watchingUsers.Contains(user)) + watchingUsers.Add(user); + } + }); + + return Task.CompletedTask; + } + + Task ISpectatorClient.UserEndedWatching(int userId) + { + Schedule(() => + { + watchingUsers.RemoveAll(u => u.OnlineID == userId); + }); + + return Task.CompletedTask; + } + Task IStatefulUserHubClient.DisconnectRequested() { Schedule(() => DisconnectInternal()); diff --git a/osu.Game/Online/Spectator/SpectatorUser.cs b/osu.Game/Online/Spectator/SpectatorUser.cs new file mode 100644 index 0000000000..9c9563be70 --- /dev/null +++ b/osu.Game/Online/Spectator/SpectatorUser.cs @@ -0,0 +1,39 @@ +// 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 MessagePack; +using osu.Game.Users; + +namespace osu.Game.Online.Spectator +{ + [Serializable] + [MessagePackObject] + public class SpectatorUser : IUser, IEquatable + { + [Key(0)] + public int OnlineID { get; set; } + + [Key(1)] + public string Username { get; set; } = string.Empty; + + [IgnoreMember] + public CountryCode CountryCode => CountryCode.Unknown; + + [IgnoreMember] + public bool IsBot => false; + + public bool Equals(SpectatorUser? other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + + return OnlineID == other.OnlineID; + } + + public override bool Equals(object? obj) => Equals(obj as SpectatorUser); + + // ReSharper disable once NonReadonlyMemberInGetHashCode + public override int GetHashCode() => OnlineID; + } +} diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index ad94b23cd7..90b2ae0a3d 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -13,9 +13,9 @@ using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; -using osu.Game.Users; using osu.Game.Localisation.HUD; using osu.Game.Localisation.SkinComponents; +using osu.Game.Online.Spectator; namespace osu.Game.Screens.Play.HUD { @@ -23,7 +23,7 @@ namespace osu.Game.Screens.Play.HUD { private const int max_spectators_displayed = 10; - public BindableList Spectators { get; } = new BindableList(); + public BindableList Spectators { get; } = new BindableList(); public Bindable UserPlayingState { get; } = new Bindable(); [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))] @@ -91,7 +91,7 @@ namespace osu.Game.Screens.Play.HUD { for (int i = 0; i < e.NewItems!.Count; i++) { - var spectator = (Spectator)e.NewItems![i]!; + var spectator = (SpectatorUser)e.NewItems![i]!; int index = e.NewStartingIndex + i; if (index >= max_spectators_displayed) @@ -157,7 +157,7 @@ namespace osu.Game.Screens.Play.HUD private partial class SpectatorListEntry : PoolableDrawable { - public Bindable Current { get; } = new Bindable(); + public Bindable Current { get; } = new Bindable(); private readonly BindableWithCurrent current = new BindableWithCurrent(); @@ -209,11 +209,5 @@ namespace osu.Game.Screens.Play.HUD linkCompiler.Enabled.Value = UserPlayingState.Value != LocalUserPlayingState.Playing; } } - - public record Spectator(int OnlineID, string Username) : IUser - { - public CountryCode CountryCode => CountryCode.Unknown; - public bool IsBot => false; - } } } From 12b2631e5e2b85f621866e87579ef69b218e2ee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 Jan 2025 15:03:37 +0100 Subject: [PATCH 072/801] Add a skinnable variant of spectator list & hook it up to online data --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 90b2ae0a3d..733f2d2514 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -16,6 +16,8 @@ using osu.Game.Online.Chat; using osu.Game.Localisation.HUD; using osu.Game.Localisation.SkinComponents; using osu.Game.Online.Spectator; +using osu.Game.Skinning; +using osuTK; namespace osu.Game.Screens.Play.HUD { @@ -43,8 +45,9 @@ namespace osu.Game.Screens.Play.HUD { AutoSizeAxes = Axes.Both; - InternalChildren = new Drawable[] + InternalChildren = new[] { + Empty().With(t => t.Size = new Vector2(100, 50)), mainFlow = new FillFlowContainer { AutoSizeAxes = Axes.Both, @@ -210,4 +213,16 @@ namespace osu.Game.Screens.Play.HUD } } } + + public partial class SkinnableSpectatorList : SpectatorList, ISerialisableDrawable + { + public bool UsesFixedAnchor { get; set; } + + [BackgroundDependencyLoader] + private void load(SpectatorClient client, Player player) + { + ((IBindableList)Spectators).BindTo(client.WatchingUsers); + ((IBindable)UserPlayingState).BindTo(player.PlayingState); + } + } } From 99c7e164dc7465d2bd748b0c20d895e79087e429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 15 Jan 2025 13:08:32 +0100 Subject: [PATCH 073/801] Add skinnable spectator list to default skins --- .../Legacy/CatchLegacySkinTransformer.cs | 10 ++++++ .../Argon/ManiaArgonSkinTransformer.cs | 11 ++++++ .../Legacy/ManiaLegacySkinTransformer.cs | 11 ++++++ .../Legacy/OsuLegacySkinTransformer.cs | 14 ++++++++ osu.Game/Skinning/ArgonSkin.cs | 35 +++++++++++++++---- osu.Game/Skinning/LegacySkin.cs | 15 +++++++- osu.Game/Skinning/TrianglesSkin.cs | 24 +++++++++---- 7 files changed, 106 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs index 69efb7fbca..978a098990 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs @@ -4,6 +4,7 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -47,6 +48,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy return new DefaultSkinComponentsContainer(container => { var keyCounter = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); if (keyCounter != null) { @@ -55,11 +57,19 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy keyCounter.Origin = Anchor.TopRight; keyCounter.Position = new Vector2(0, -40) * 1.6f; } + + if (spectatorList != null) + { + spectatorList.Anchor = Anchor.BottomLeft; + spectatorList.Origin = Anchor.BottomLeft; + spectatorList.Position = new Vector2(10, -10); + } }) { Children = new Drawable[] { new LegacyKeyCounterDisplay(), + new SkinnableSpectatorList(), } }; } diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs index c37c18081a..48c487e70d 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs @@ -9,7 +9,9 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; +using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Skinning.Argon @@ -39,6 +41,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon return new DefaultSkinComponentsContainer(container => { var combo = container.ChildrenOfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); if (combo != null) { @@ -47,9 +50,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon combo.Origin = Anchor.Centre; combo.Y = 200; } + + if (spectatorList != null) + spectatorList.Position = new Vector2(36, -66); }) { new ArgonManiaComboCounter(), + new SkinnableSpectatorList + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + } }; } diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index 8f425edc44..359f21561f 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -15,7 +15,9 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Mania.Skinning.Legacy { @@ -95,6 +97,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy return new DefaultSkinComponentsContainer(container => { var combo = container.ChildrenOfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); if (combo != null) { @@ -102,9 +105,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy combo.Origin = Anchor.Centre; combo.Y = this.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ComboPosition)?.Value ?? 0; } + + if (spectatorList != null) + { + spectatorList.Anchor = Anchor.BottomLeft; + spectatorList.Origin = Anchor.BottomLeft; + spectatorList.Position = new Vector2(10, -10); + } }) { new LegacyManiaComboCounter(), + new SkinnableSpectatorList(), }; } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 636a9ecb21..03e4bb24f1 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; using osuTK; @@ -70,12 +71,24 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy } var combo = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); + + Vector2 pos = new Vector2(); if (combo != null) { combo.Anchor = Anchor.BottomLeft; combo.Origin = Anchor.BottomLeft; combo.Scale = new Vector2(1.28f); + + pos += new Vector2(10, -(combo.DrawHeight * 1.56f + 20) * combo.Scale.X); + } + + if (spectatorList != null) + { + spectatorList.Anchor = Anchor.BottomLeft; + spectatorList.Origin = Anchor.BottomLeft; + spectatorList.Position = pos; } }) { @@ -83,6 +96,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { new LegacyDefaultComboCounter(), new LegacyKeyCounterDisplay(), + new SkinnableSpectatorList(), } }; } diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 771d10d73b..c3319b738d 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -7,7 +7,6 @@ using JetBrains.Annotations; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; using osu.Game.Audio; using osu.Game.Beatmaps.Formats; @@ -110,15 +109,37 @@ namespace osu.Game.Skinning case GlobalSkinnableContainers.MainHUDComponents: if (containerLookup.Ruleset != null) { - return new Container + return new DefaultSkinComponentsContainer(container => + { + var comboCounter = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); + + Vector2 pos = new Vector2(36, -66); + + if (comboCounter != null) + { + comboCounter.Position = pos; + pos -= new Vector2(0, comboCounter.DrawHeight * 1.4f + 20); + } + + if (spectatorList != null) + spectatorList.Position = pos; + }) { RelativeSizeAxes = Axes.Both, - Child = new ArgonComboCounter + Children = new Drawable[] { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Position = new Vector2(36, -66), - Scale = new Vector2(1.3f), + new ArgonComboCounter + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Scale = new Vector2(1.3f), + }, + new SkinnableSpectatorList + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + } }, }; } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 6faadfba9b..c607c57fcc 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -367,16 +367,29 @@ namespace osu.Game.Skinning return new DefaultSkinComponentsContainer(container => { var combo = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); + + Vector2 pos = new Vector2(); if (combo != null) { combo.Anchor = Anchor.BottomLeft; combo.Origin = Anchor.BottomLeft; combo.Scale = new Vector2(1.28f); + + pos += new Vector2(10, -(combo.DrawHeight * 1.56f + 20) * combo.Scale.X); + } + + if (spectatorList != null) + { + spectatorList.Anchor = Anchor.BottomLeft; + spectatorList.Origin = Anchor.BottomLeft; + spectatorList.Position = pos; } }) { - new LegacyDefaultComboCounter() + new LegacyDefaultComboCounter(), + new SkinnableSpectatorList(), }; } diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs index d562fd3256..8853a5c4ac 100644 --- a/osu.Game/Skinning/TrianglesSkin.cs +++ b/osu.Game/Skinning/TrianglesSkin.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Textures; using osu.Game.Audio; using osu.Game.Beatmaps.Formats; using osu.Game.Extensions; +using osu.Game.Graphics; using osu.Game.IO; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.HitErrorMeters; @@ -90,6 +91,7 @@ namespace osu.Game.Skinning var ppCounter = container.OfType().FirstOrDefault(); var songProgress = container.OfType().FirstOrDefault(); var keyCounter = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); if (score != null) { @@ -142,17 +144,26 @@ namespace osu.Game.Skinning } } + const float padding = 10; + + // Hard to find this at runtime, so taken from the most expanded state during replay. + const float song_progress_offset_height = 73; + if (songProgress != null && keyCounter != null) { - const float padding = 10; - - // Hard to find this at runtime, so taken from the most expanded state during replay. - const float song_progress_offset_height = 73; - keyCounter.Anchor = Anchor.BottomRight; keyCounter.Origin = Anchor.BottomRight; keyCounter.Position = new Vector2(-padding, -(song_progress_offset_height + padding)); } + + if (spectatorList != null) + { + spectatorList.Font.Value = Typeface.Venera; + spectatorList.HeaderColour.Value = new OsuColour().BlueLighter; + spectatorList.Anchor = Anchor.BottomLeft; + spectatorList.Origin = Anchor.BottomLeft; + spectatorList.Position = new Vector2(padding, -(song_progress_offset_height + padding)); + } }) { Children = new Drawable[] @@ -165,7 +176,8 @@ namespace osu.Game.Skinning new DefaultKeyCounterDisplay(), new BarHitErrorMeter(), new BarHitErrorMeter(), - new TrianglesPerformancePointsCounter() + new TrianglesPerformancePointsCounter(), + new SkinnableSpectatorList(), } }; From 46e9da7960ef551d4127305d7ce66907bb47e774 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Jan 2025 15:34:20 +0900 Subject: [PATCH 074/801] Fix style display refreshing on all room updates --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index ec2ed90eca..edb44a7666 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -523,19 +523,21 @@ namespace osu.Game.Screens.OnlinePlay.Match if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) return; - if (UserStyleDisplayContainer != null) - { - PlaylistItem gameplayItem = SelectedItem.Value.With( - ruleset: GetGameplayRuleset().OnlineID, - beatmap: new Optional(GetGameplayBeatmap())); + if (UserStyleDisplayContainer == null) + return; - UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem) - { - AllowReordering = false, - AllowEditing = true, - RequestEdit = _ => OpenStyleSelection() - }; - } + PlaylistItem gameplayItem = SelectedItem.Value.With(ruleset: GetGameplayRuleset().OnlineID, beatmap: new Optional(GetGameplayBeatmap())); + PlaylistItem? currentItem = UserStyleDisplayContainer.SingleOrDefault()?.Item; + + if (gameplayItem.Equals(currentItem)) + return; + + UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem) + { + AllowReordering = false, + AllowEditing = true, + RequestEdit = _ => OpenStyleSelection() + }; } protected virtual APIMod[] GetGameplayMods() From 409ea53ad96441104494bb73e75f6155bcd0be76 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Jan 2025 15:51:53 +0900 Subject: [PATCH 075/801] Send `beatmap_id` when creating score --- osu.Game/Online/Rooms/CreateRoomScoreRequest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs b/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs index e0f91032fd..eb2879ba6c 100644 --- a/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs +++ b/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs @@ -31,6 +31,7 @@ namespace osu.Game.Online.Rooms var req = base.CreateWebRequest(); req.Method = HttpMethod.Post; req.AddParameter("version_hash", versionHash); + req.AddParameter("beatmap_id", beatmapInfo.OnlineID.ToString(CultureInfo.InvariantCulture)); req.AddParameter("beatmap_hash", beatmapInfo.MD5Hash); req.AddParameter("ruleset_id", rulesetId.ToString(CultureInfo.InvariantCulture)); return req; From aa3ae8324e19769df585bb45fe8af3935f830113 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Jan 2025 17:29:43 +0900 Subject: [PATCH 076/801] Add test for local user presence --- .../Visual/Online/TestSceneUserPanel.cs | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index b4dafd3107..684e8b7b86 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Online; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Metadata; using osu.Game.Overlays; @@ -112,7 +113,11 @@ namespace osu.Game.Tests.Visual.Online CountryCode = CountryCode.AU, CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", Statistics = new UserStatistics { GlobalRank = null, CountryRank = null } - }) { Width = 300 } + }) { Width = 300 }, + new UserGridPanel(API.LocalUser.Value) + { + Width = 300 + } } } } @@ -180,6 +185,23 @@ namespace osu.Game.Tests.Visual.Online AddStep("set statistics to empty", () => statisticsProvider.UpdateStatistics(new UserStatistics(), Ruleset.Value)); } + [Test] + public void TestLocalUserActivity() + { + AddStep("idle", () => setLocalUserPresence(UserStatus.Online, null)); + AddStep("watching replay", () => setLocalUserPresence(UserStatus.Online, new UserActivity.WatchingReplay(createScore(@"nats")))); + AddStep("spectating user", () => setLocalUserPresence(UserStatus.Online, new UserActivity.SpectatingUser(createScore(@"mrekk")))); + AddStep("solo (osu!)", () => setLocalUserPresence(UserStatus.Online, soloGameStatusForRuleset(0))); + AddStep("solo (osu!taiko)", () => setLocalUserPresence(UserStatus.Online, soloGameStatusForRuleset(1))); + AddStep("solo (osu!catch)", () => setLocalUserPresence(UserStatus.Online, soloGameStatusForRuleset(2))); + AddStep("solo (osu!mania)", () => setLocalUserPresence(UserStatus.Online, soloGameStatusForRuleset(3))); + AddStep("choosing", () => setLocalUserPresence(UserStatus.Online, new UserActivity.ChoosingBeatmap())); + AddStep("editing beatmap", () => setLocalUserPresence(UserStatus.Online, new UserActivity.EditingBeatmap(new BeatmapInfo()))); + AddStep("modding beatmap", () => setLocalUserPresence(UserStatus.Online, new UserActivity.ModdingBeatmap(new BeatmapInfo()))); + AddStep("testing beatmap", () => setLocalUserPresence(UserStatus.Online, new UserActivity.TestingBeatmap(new BeatmapInfo()))); + AddStep("set offline status", () => setLocalUserPresence(UserStatus.Offline, null)); + } + private void setPresence(UserStatus status, UserActivity? activity) { if (status == UserStatus.Offline) @@ -188,6 +210,13 @@ namespace osu.Game.Tests.Visual.Online metadataClient.UserPresenceUpdated(panel.User.OnlineID, new UserPresence { Status = status, Activity = activity }); } + private void setLocalUserPresence(UserStatus status, UserActivity? activity) + { + DummyAPIAccess dummyAPI = (DummyAPIAccess)API; + dummyAPI.Status.Value = status; + dummyAPI.Activity.Value = activity; + } + private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(new BeatmapInfo(), rulesetStore.GetRuleset(rulesetId)!); private ScoreInfo createScore(string name) => new ScoreInfo(new TestBeatmap(Ruleset.Value).BeatmapInfo) From 974fa76987a445f0d0d18f823e11e0bb4ffec842 Mon Sep 17 00:00:00 2001 From: molneya <62799417+molneya@users.noreply.github.com> Date: Thu, 16 Jan 2025 17:08:47 +0800 Subject: [PATCH 077/801] fix spinners not increasing cumulative strain time (#31525) Co-authored-by: StanR --- .../Difficulty/Evaluators/FlashlightEvaluator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs index 5cb5a8f934..9d05f0b074 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs @@ -52,12 +52,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators var currentObj = (OsuDifficultyHitObject)current.Previous(i); var currentHitObject = (OsuHitObject)(currentObj.BaseObject); + cumulativeStrainTime += lastObj.StrainTime; + if (!(currentObj.BaseObject is Spinner)) { double jumpDistance = (osuHitObject.StackedPosition - currentHitObject.StackedEndPosition).Length; - cumulativeStrainTime += lastObj.StrainTime; - // We want to nerf objects that can be easily seen within the Flashlight circle radius. if (i == 0) smallDistNerf = Math.Min(1.0, jumpDistance / 75.0); From 9da8dcd8151009a2252c9b3f45d258f92a501895 Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Thu, 16 Jan 2025 20:30:02 +1000 Subject: [PATCH 078/801] osu!taiko stamina balancing (#31337) * stamina considerations * remove consecutive note count * adjust multiplier * add back comment * adjust tests * adjusts tests post merge * use diffcalcutils --------- Co-authored-by: StanR --- .../TaikoDifficultyCalculatorTest.cs | 8 ++++---- .../Difficulty/Evaluators/StaminaEvaluator.cs | 17 ++++++++--------- .../Difficulty/Skills/Stamina.cs | 9 ++++++--- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index de3bec5fcf..517f62b6f5 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(2.837609165845338d, 200, "diffcalc-test")] - [TestCase(2.837609165845338d, 200, "diffcalc-test-strong")] + [TestCase(2.912326627861987d, 200, "diffcalc-test")] + [TestCase(2.912326627861987d, 200, "diffcalc-test-strong")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(3.8005218640444949, 200, "diffcalc-test")] - [TestCase(3.8005218640444949, 200, "diffcalc-test-strong")] + [TestCase(3.9339069955362014d, 200, "diffcalc-test")] + [TestCase(3.9339069955362014d, 200, "diffcalc-test-strong")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime()); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs index 84d5de4c63..a273d91a38 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators // Interval is capped at a very small value to prevent infinite values. interval = Math.Max(interval, 1); - return 30 / interval; + return 20 / interval; } /// @@ -59,16 +59,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators // Find the previous hit object hit by the current finger, which is n notes prior, n being the number of // available fingers. TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current; - TaikoDifficultyHitObject? keyPrevious = taikoCurrent.PreviousMono(availableFingersFor(taikoCurrent) - 1); - - if (keyPrevious == null) - { - // There is no previous hit object hit by the current finger - return 0.0; - } + TaikoDifficultyHitObject? taikoPrevious = current.Previous(1) as TaikoDifficultyHitObject; + TaikoDifficultyHitObject? previousMono = taikoCurrent.PreviousMono(availableFingersFor(taikoCurrent) - 1); double objectStrain = 0.5; // Add a base strain to all objects - objectStrain += speedBonus(taikoCurrent.StartTime - keyPrevious.StartTime); + if (taikoPrevious == null) return objectStrain; + + if (previousMono != null) + objectStrain += speedBonus(taikoCurrent.StartTime - previousMono.StartTime) + 0.5 * speedBonus(taikoCurrent.StartTime - taikoPrevious.StartTime); + return objectStrain; } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index f6914039f0..29f9f16033 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -4,6 +4,7 @@ using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; @@ -44,10 +45,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills var currentObject = current as TaikoDifficultyHitObject; int index = currentObject?.Colour.MonoStreak?.HitObjects.IndexOf(currentObject) ?? 0; - if (singleColourStamina) - return currentStrain / (1 + Math.Exp(-(index - 10) / 2.0)); + double monolengthBonus = 1 + Math.Min(Math.Max((index - 5) / 50.0, 0), 0.30); - return currentStrain; + if (singleColourStamina) + return DifficultyCalculationUtils.Logistic(-(index - 10) / 2.0, currentStrain); + + return currentStrain * monolengthBonus; } protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => singleColourStamina ? 0 : currentStrain * strainDecay(time - current.Previous(0).StartTime); From 65b88ab365df223e07a4b7c56794b75b42d4b338 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Jan 2025 19:38:14 +0900 Subject: [PATCH 079/801] Use MetadataClient for local user status --- .../Visual/Online/TestSceneUserPanel.cs | 38 ++++++++----------- osu.Game/Users/ExtendedUserPanel.cs | 8 +--- 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index 684e8b7b86..f4fc15da20 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Online; -using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Metadata; using osu.Game.Overlays; @@ -188,33 +187,26 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestLocalUserActivity() { - AddStep("idle", () => setLocalUserPresence(UserStatus.Online, null)); - AddStep("watching replay", () => setLocalUserPresence(UserStatus.Online, new UserActivity.WatchingReplay(createScore(@"nats")))); - AddStep("spectating user", () => setLocalUserPresence(UserStatus.Online, new UserActivity.SpectatingUser(createScore(@"mrekk")))); - AddStep("solo (osu!)", () => setLocalUserPresence(UserStatus.Online, soloGameStatusForRuleset(0))); - AddStep("solo (osu!taiko)", () => setLocalUserPresence(UserStatus.Online, soloGameStatusForRuleset(1))); - AddStep("solo (osu!catch)", () => setLocalUserPresence(UserStatus.Online, soloGameStatusForRuleset(2))); - AddStep("solo (osu!mania)", () => setLocalUserPresence(UserStatus.Online, soloGameStatusForRuleset(3))); - AddStep("choosing", () => setLocalUserPresence(UserStatus.Online, new UserActivity.ChoosingBeatmap())); - AddStep("editing beatmap", () => setLocalUserPresence(UserStatus.Online, new UserActivity.EditingBeatmap(new BeatmapInfo()))); - AddStep("modding beatmap", () => setLocalUserPresence(UserStatus.Online, new UserActivity.ModdingBeatmap(new BeatmapInfo()))); - AddStep("testing beatmap", () => setLocalUserPresence(UserStatus.Online, new UserActivity.TestingBeatmap(new BeatmapInfo()))); - AddStep("set offline status", () => setLocalUserPresence(UserStatus.Offline, null)); + AddStep("idle", () => setPresence(UserStatus.Online, null, API.LocalUser.Value.OnlineID)); + AddStep("watching replay", () => setPresence(UserStatus.Online, new UserActivity.WatchingReplay(createScore(@"nats")), API.LocalUser.Value.OnlineID)); + AddStep("spectating user", () => setPresence(UserStatus.Online, new UserActivity.SpectatingUser(createScore(@"mrekk")), API.LocalUser.Value.OnlineID)); + AddStep("solo (osu!)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(0), API.LocalUser.Value.OnlineID)); + AddStep("solo (osu!taiko)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(1), API.LocalUser.Value.OnlineID)); + AddStep("solo (osu!catch)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(2), API.LocalUser.Value.OnlineID)); + AddStep("solo (osu!mania)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(3), API.LocalUser.Value.OnlineID)); + AddStep("choosing", () => setPresence(UserStatus.Online, new UserActivity.ChoosingBeatmap(), API.LocalUser.Value.OnlineID)); + AddStep("editing beatmap", () => setPresence(UserStatus.Online, new UserActivity.EditingBeatmap(new BeatmapInfo()), API.LocalUser.Value.OnlineID)); + AddStep("modding beatmap", () => setPresence(UserStatus.Online, new UserActivity.ModdingBeatmap(new BeatmapInfo()), API.LocalUser.Value.OnlineID)); + AddStep("testing beatmap", () => setPresence(UserStatus.Online, new UserActivity.TestingBeatmap(new BeatmapInfo()), API.LocalUser.Value.OnlineID)); + AddStep("set offline status", () => setPresence(UserStatus.Offline, null, API.LocalUser.Value.OnlineID)); } - private void setPresence(UserStatus status, UserActivity? activity) + private void setPresence(UserStatus status, UserActivity? activity, int? userId = null) { if (status == UserStatus.Offline) - metadataClient.UserPresenceUpdated(panel.User.OnlineID, null); + metadataClient.UserPresenceUpdated(userId ?? panel.User.OnlineID, null); else - metadataClient.UserPresenceUpdated(panel.User.OnlineID, new UserPresence { Status = status, Activity = activity }); - } - - private void setLocalUserPresence(UserStatus status, UserActivity? activity) - { - DummyAPIAccess dummyAPI = (DummyAPIAccess)API; - dummyAPI.Status.Value = status; - dummyAPI.Activity.Value = activity; + metadataClient.UserPresenceUpdated(userId ?? panel.User.OnlineID, new UserPresence { Status = status, Activity = activity }); } private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(new BeatmapInfo(), rulesetStore.GetRuleset(rulesetId)!); diff --git a/osu.Game/Users/ExtendedUserPanel.cs b/osu.Game/Users/ExtendedUserPanel.cs index eb1115e296..2fc2a97b47 100644 --- a/osu.Game/Users/ExtendedUserPanel.cs +++ b/osu.Game/Users/ExtendedUserPanel.cs @@ -94,13 +94,7 @@ namespace osu.Game.Users private void updatePresence() { - UserPresence? presence; - - if (User.Equals(api?.LocalUser.Value)) - presence = new UserPresence { Status = api.Status.Value, Activity = api.Activity.Value }; - else - presence = metadata?.GetPresence(User.OnlineID); - + UserPresence? presence = metadata?.GetPresence(User.OnlineID); UserStatus status = presence?.Status ?? UserStatus.Offline; UserActivity? activity = presence?.Activity; From 94db39317b626a90c6dd60c2c9dee530bdc58fe2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Jan 2025 20:43:22 +0900 Subject: [PATCH 080/801] Add xmldoc --- osu.Game/Configuration/SessionStatics.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index bdfb0217ad..d2069e4027 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -95,6 +95,9 @@ namespace osu.Game.Configuration /// DailyChallengeIntroPlayed, + /// + /// The activity for the current user to broadcast to other players. + /// UserOnlineActivity, } } From 1f1e940adaa1d943707cd3191d876d054659c66b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 16 Jan 2025 15:13:16 +0100 Subject: [PATCH 081/801] Restore virtual modifier to fix tests (and mark for posterity) --- osu.Game/Online/Spectator/SpectatorClient.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index ac11dad0f0..91f009b76f 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Development; @@ -38,7 +39,8 @@ namespace osu.Game.Online.Spectator /// /// The states of all users currently being watched by the local user. /// - public IBindableDictionary WatchedUserStates => watchedUserStates; + [UsedImplicitly] // Marked virtual due to mock use in testing + public virtual IBindableDictionary WatchedUserStates => watchedUserStates; /// /// All users who are currently watching the local user. @@ -58,6 +60,7 @@ namespace osu.Game.Online.Spectator /// /// Called whenever new frames arrive from the server. /// + [UsedImplicitly] // Marked virtual due to mock use in testing public virtual event Action? OnNewFrames; /// From 5c799d733f2543cbb35295cea68333ab4bd4f31d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 16 Jan 2025 15:25:56 +0100 Subject: [PATCH 082/801] Bind to playing state via `GameplayState` instead to fix more tests --- osu.Game/Screens/Play/GameplayState.cs | 11 ++++++++++- osu.Game/Screens/Play/HUD/SpectatorList.cs | 4 ++-- osu.Game/Screens/Play/Player.cs | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index 478acd7229..bfeabcc82e 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -69,6 +69,11 @@ namespace osu.Game.Screens.Play private readonly Bindable lastJudgementResult = new Bindable(); + /// + /// The local user's playing state (whether actively playing, paused, or not playing due to watching a replay or similar). + /// + public IBindable Playing { get; } = new Bindable(); + public GameplayState( IBeatmap beatmap, Ruleset ruleset, @@ -76,7 +81,8 @@ namespace osu.Game.Screens.Play Score? score = null, ScoreProcessor? scoreProcessor = null, HealthProcessor? healthProcessor = null, - Storyboard? storyboard = null) + Storyboard? storyboard = null, + IBindable? localUserPlaying = null) { Beatmap = beatmap; Ruleset = ruleset; @@ -92,6 +98,9 @@ namespace osu.Game.Screens.Play ScoreProcessor = scoreProcessor ?? ruleset.CreateScoreProcessor(); HealthProcessor = healthProcessor ?? ruleset.CreateHealthProcessor(beatmap.HitObjects[0].StartTime); Storyboard = storyboard ?? new Storyboard(); + + if (localUserPlaying != null) + Playing.BindTo(localUserPlaying); } /// diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index ab4958f0c1..35a2d1eefb 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -242,10 +242,10 @@ namespace osu.Game.Screens.Play.HUD public bool UsesFixedAnchor { get; set; } [BackgroundDependencyLoader] - private void load(SpectatorClient client, Player player) + private void load(SpectatorClient client, GameplayState gameplayState) { ((IBindableList)Spectators).BindTo(client.WatchingUsers); - ((IBindable)UserPlayingState).BindTo(player.PlayingState); + ((IBindable)UserPlayingState).BindTo(gameplayState.Playing); } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 228b77b780..a797603e17 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -261,7 +261,7 @@ namespace osu.Game.Screens.Play Score.ScoreInfo.Ruleset = ruleset.RulesetInfo; Score.ScoreInfo.Mods = gameplayMods; - dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score, ScoreProcessor, HealthProcessor, Beatmap.Value.Storyboard)); + dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score, ScoreProcessor, HealthProcessor, Beatmap.Value.Storyboard, PlayingState)); var rulesetSkinProvider = new RulesetSkinProvidingContainer(ruleset, playableBeatmap, Beatmap.Value.Skin); GameplayClockContainer.Add(new GameplayScrollWheelHandling()); From 1949c01103c4dec239761c4591222044554e5045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 16 Jan 2025 15:29:07 +0100 Subject: [PATCH 083/801] Fix skin deserialisation test --- .../Archives/modified-argon-20250116.osk | Bin 0 -> 1675 bytes osu.Game.Tests/Skins/SkinDeserialisationTest.cs | 2 ++ 2 files changed, 2 insertions(+) create mode 100644 osu.Game.Tests/Resources/Archives/modified-argon-20250116.osk diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-20250116.osk b/osu.Game.Tests/Resources/Archives/modified-argon-20250116.osk new file mode 100644 index 0000000000000000000000000000000000000000..811e91b74916988a54a9ff2b73cafe0e0ff0a490 GIT binary patch literal 1675 zcmZ{kdpHw%7{`Au>w-j9CyA7%NLq|6k|()sWSeVcGHlFcE*qLu?u>?RYA&UyL}IHY zw}iukIc8W+qm!d@B$-}iam4+gEE2n7JF0(J@TWABHC zhspo|t=~NWP(#TSV={#dJI7XPwNZmerCE186rJYOQfZ#Co*{Z0v?59~;u@ms)E3N@ z4|bh^`|pFdCHQ`*XkF|GHdEAY^ohcD8EGoSlvszBP3?iUYP{ZTiEs@bavhzYN)2vRc0<$f-j=rNLdmlSS^{G) z(+6+GE^f?}5xbRp9gY-x-6in25_!xIn?!B>m=Fs9$N>PX`F9gCg%n{NLXDtQj^i+x zS#*z!2M34g-ec_Ho{2>nq4@l4EUpi30ypV*@9X@{wac03>>_8vRB=IErrE?7W#cr^ zv)MMHLt>72mM7L7yGCz^G3YQB1ICijuhGd8Ov=ZRr2Gj;P|)}r;`pt1$mA(7LxcKV zK>2XNj|1*H?-iG>0Dzhf02=@RE(9{g(c`dfM0jKbg-D@MzfU^N!bgbU(DV<|UE5-n zvDS{d7vlvj8wbqSFk_p_D>_qc#|UA8mf=T*OV&I3@)H{vyVuY{>UPqne@D)giggNy z_1el$uQZ02a%|DNQBYAIWJ>SH1ekqg6UVRD3!E6v$jxq!Fo!aVO22xG zV25`R$Dl8uU>&aVV6U~GJdX%+`DJrjFhpZmXH3lb_P4BODY3QYgZuOaA?2;7xrEeJ z6Z$nVZ3p+xBXxL(xee1}qoWw-luT~;rJkX|}<#Bb}GI_!*eBBh?Z zI>P@zd2-L)rZcHLE*f&<>A^o)i7bDVaDb(4UT9izp#c7pUHDm%G;#*aq7f4_<{cbJI-Lc#)cST=F9`=s#G>pDQ0Z#2`^SFH&I!O6J z)>|cItJXk<$n!}zwvfHc>_q0Ej%s_!yZq;{jL7sa?vjeU?1lBiY1YV^-0oa!M+sIs5)b7)i*5og$;VB4BQi_q>xZ$JwMI)1!Qph>(JpwEGItxb7 zu^yV1D<1DH$(Eklm-N-Kzn1n~Y2pa0 zzVcj+w9U2H6`vLEzyGyyBrVQN`9uEkUgWVL8p;W|vTl86ZY^iad^$CA@B(~MGkY?J zTafeiA-=lKC1-k=G%UX7mrE~w!>YPXJo-lq96@lW2obAG*oG&cHR`Xbe^J~>Vx7(5 zM8q}Ad~SE`t3*=(01^P83IM=_5$Gf`F)Z+ZwkV-@8}|0_-8eY-pxg{TYaLJHVl?i6 zxPLY&S!lDjB=}5}L7s-T@@rwVP#$2=a`I5{a|$mj>6`T*lVz)9iMOnzZdv}xmddhd mSxKRCKO>WD5810MD+vaXy%7ctS@A8d2o%8#03a3eE&Ct9*SzTf literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs index 962a9b2a7a..55836302e6 100644 --- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs +++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs @@ -71,6 +71,8 @@ namespace osu.Game.Tests.Skins "Archives/modified-classic-20240724.osk", // Covers skinnable mod display "Archives/modified-default-20241207.osk", + // Covers skinnable spectator list + "Archives/modified-argon-20250116.osk", }; /// From b9894f67ceac3ba42995cd81b0692c414620053f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 16 Jan 2025 12:30:27 +0100 Subject: [PATCH 084/801] Bump NVika tool to 4.0.0 Code quality CI runs have suddenly started failing out of nowhere: - Passing run: https://github.com/ppy/osu/actions/runs/12806242929/job/35704267944#step:10:1 - Failing run: https://github.com/ppy/osu/actions/runs/12807108792/job/35707131634#step:10:1 In classic github fashion, they began rolling out another runner change wherein `ubuntu-latest` has started meaning `ubuntu-24.04` rather than `ubuntu-22.04`. `ubuntu-24.04` no longer has .NET 6 bundled. Therefore, upgrade NVika to 4.0.0 because that version is compatible with .NET 8. --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index c4ba6e5143..6ec071be2f 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -9,7 +9,7 @@ ] }, "nvika": { - "version": "3.0.0", + "version": "4.0.0", "commands": [ "nvika" ] From a83f917d87c51f95d2778afce0048c08f8af125f Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Fri, 17 Jan 2025 07:14:05 +1000 Subject: [PATCH 085/801] osu!taiko star rating and performance points rebalance (#31338) * rebalance * revert pp scaling change * further rebalancing * comment * adjust tests --- .../TaikoDifficultyCalculatorTest.cs | 8 +++--- .../Difficulty/TaikoDifficultyAttributes.cs | 4 +-- .../Difficulty/TaikoDifficultyCalculator.cs | 27 +++++++++++++------ .../Difficulty/TaikoPerformanceCalculator.cs | 8 +++--- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index 517f62b6f5..b4cbe03511 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(2.912326627861987d, 200, "diffcalc-test")] - [TestCase(2.912326627861987d, 200, "diffcalc-test-strong")] + [TestCase(3.3172381854905493d, 200, "diffcalc-test")] + [TestCase(3.3172381854905493d, 200, "diffcalc-test-strong")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(3.9339069955362014d, 200, "diffcalc-test")] - [TestCase(3.9339069955362014d, 200, "diffcalc-test-strong")] + [TestCase(4.4640702427013101d, 200, "diffcalc-test")] + [TestCase(4.4640702427013101d, 200, "diffcalc-test-strong")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime()); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index ef729e1f07..37e6996e5a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -40,8 +40,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("mono_stamina_factor")] public double MonoStaminaFactor { get; set; } - [JsonProperty("reading_difficult_strains")] - public double ReadingTopStrains { get; set; } + [JsonProperty("rhythm_difficult_strains")] + public double RhythmTopStrains { get; set; } [JsonProperty("colour_difficult_strains")] public double ColourTopStrains { get; set; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index f8ff6f6065..3ad9d17526 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -24,10 +24,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty public class TaikoDifficultyCalculator : DifficultyCalculator { private const double difficulty_multiplier = 0.084375; - private const double rhythm_skill_multiplier = 1.24 * difficulty_multiplier; + private const double rhythm_skill_multiplier = 0.65 * difficulty_multiplier; private const double reading_skill_multiplier = 0.100 * difficulty_multiplier; private const double colour_skill_multiplier = 0.375 * difficulty_multiplier; - private const double stamina_skill_multiplier = 0.375 * difficulty_multiplier; + private const double stamina_skill_multiplier = 0.445 * difficulty_multiplier; + + private double strainLengthBonus; + private double patternMultiplier; public override int Version => 20241007; @@ -116,8 +119,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double monoStaminaFactor = staminaRating == 0 ? 1 : Math.Pow(monoStaminaRating / staminaRating, 5); double colourDifficultStrains = colour.CountTopWeightedStrains(); - double readingDifficultStrains = reading.CountTopWeightedStrains(); - double staminaDifficultStrains = stamina.CountTopWeightedStrains(); + double rhythmDifficultStrains = rhythm.CountTopWeightedStrains(); + // Due to constraints of strain in cases where difficult strain values don't shift with range changes, we manually apply clockrate. + double staminaDifficultStrains = stamina.CountTopWeightedStrains() * clockRate; + + // As we don't have pattern integration in osu!taiko, we apply the other two skills relative to rhythm. + patternMultiplier = Math.Pow(staminaRating * colourRating, 0.10); + + strainLengthBonus = 1 + + Math.Min(Math.Max((staminaDifficultStrains - 1350) / 5000, 0), 0.15) + + Math.Min(Math.Max((staminaRating - 7.0) / 1.0, 0), 0.05); double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax); double starRating = rescale(combinedRating * 1.4); @@ -125,7 +136,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty // Converts are penalised outside the scope of difficulty calculation, as our assumptions surrounding standard play-styles becomes out-of-scope. if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0) { - starRating *= 0.825; + starRating *= 0.7; // For maps with relax, multiple inputs are more likely to be abused. if (isRelax) @@ -144,7 +155,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty ColourDifficulty = colourRating, StaminaDifficulty = staminaRating, MonoStaminaFactor = monoStaminaFactor, - ReadingTopStrains = readingDifficultStrains, + RhythmTopStrains = rhythmDifficultStrains, ColourTopStrains = colourDifficultStrains, StaminaTopStrains = staminaDifficultStrains, GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate, @@ -173,10 +184,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty for (int i = 0; i < colourPeaks.Count; i++) { - double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier; + double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier * patternMultiplier; double readingPeak = readingPeaks[i] * reading_skill_multiplier; double colourPeak = colourPeaks[i] * colour_skill_multiplier; - double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier; + double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier * strainLengthBonus; if (isRelax) { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 4933c9dee6..c29ea3ba73 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -73,8 +73,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes) { - double baseDifficulty = 5 * Math.Max(1.0, attributes.StarRating / 0.115) - 4.0; - double difficultyValue = Math.Min(Math.Pow(baseDifficulty, 3) / 69052.51, Math.Pow(baseDifficulty, 2.25) / 1150.0); + double baseDifficulty = 5 * Math.Max(1.0, attributes.StarRating / 0.110) - 4.0; + double difficultyValue = Math.Min(Math.Pow(baseDifficulty, 3) / 69052.51, Math.Pow(baseDifficulty, 2.25) / 1250.0); + + difficultyValue *= 1 + 0.10 * Math.Max(0, attributes.StarRating - 10); double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0); difficultyValue *= lengthBonus; @@ -95,7 +97,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty // Scale accuracy more harshly on nearly-completely mono (single coloured) speed maps. double accScalingExponent = 2 + attributes.MonoStaminaFactor; - double accScalingShift = 400 - 100 * attributes.MonoStaminaFactor; + double accScalingShift = 500 - 100 * attributes.MonoStaminaFactor; return difficultyValue * Math.Pow(SpecialFunctions.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent); } From daa7921c2d1510db65cd638a29662aef2b0aca91 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jan 2025 12:55:11 +0900 Subject: [PATCH 086/801] Mark `IsTablet` with `new` to avoid inspection Co-authored-by: Susko3 --- osu.Android/OsuGameActivity.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index 0b5deef6fb..66c697801b 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -49,7 +49,7 @@ namespace osu.Android /// Adjusted on startup to match expected UX for the current device type (phone/tablet). public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified; - public bool IsTablet { get; private set; } + public new bool IsTablet { get; private set; } private readonly OsuGameAndroid game; From 626be9d7806b44434bb773cb9afe002c0639356b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 17 Jan 2025 16:01:11 +0900 Subject: [PATCH 087/801] Return local user state where appropriate --- osu.Game/Online/Metadata/MetadataClient.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs index a72377721a..1b6f96d91b 100644 --- a/osu.Game/Online/Metadata/MetadataClient.cs +++ b/osu.Game/Online/Metadata/MetadataClient.cs @@ -4,8 +4,10 @@ using System; using System.Linq; using System.Threading.Tasks; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Online.API; using osu.Game.Users; namespace osu.Game.Online.Metadata @@ -14,6 +16,9 @@ namespace osu.Game.Online.Metadata { public abstract IBindable IsConnected { get; } + [Resolved] + private IAPIProvider api { get; set; } = null!; + #region Beatmap metadata updates public abstract Task GetChangesSince(int queueId); @@ -59,6 +64,9 @@ namespace osu.Game.Online.Metadata /// The user presence, or null if not available or the user's offline. public UserPresence? GetPresence(int userId) { + if (userId == api.LocalUser.Value.OnlineID) + return LocalUserState; + if (FriendStates.TryGetValue(userId, out UserPresence presence)) return presence; From f59762f0cb4f199e4e00c034807e1084a3237edc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jan 2025 17:11:40 +0900 Subject: [PATCH 088/801] `Playing` -> `PlayingState` --- osu.Game/Screens/Play/GameplayState.cs | 8 ++++---- osu.Game/Screens/Play/HUD/SpectatorList.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index bfeabcc82e..851e95495f 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -72,7 +72,7 @@ namespace osu.Game.Screens.Play /// /// The local user's playing state (whether actively playing, paused, or not playing due to watching a replay or similar). /// - public IBindable Playing { get; } = new Bindable(); + public IBindable PlayingState { get; } = new Bindable(); public GameplayState( IBeatmap beatmap, @@ -82,7 +82,7 @@ namespace osu.Game.Screens.Play ScoreProcessor? scoreProcessor = null, HealthProcessor? healthProcessor = null, Storyboard? storyboard = null, - IBindable? localUserPlaying = null) + IBindable? localUserPlayingState = null) { Beatmap = beatmap; Ruleset = ruleset; @@ -99,8 +99,8 @@ namespace osu.Game.Screens.Play HealthProcessor = healthProcessor ?? ruleset.CreateHealthProcessor(beatmap.HitObjects[0].StartTime); Storyboard = storyboard ?? new Storyboard(); - if (localUserPlaying != null) - Playing.BindTo(localUserPlaying); + if (localUserPlayingState != null) + PlayingState.BindTo(localUserPlayingState); } /// diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 35a2d1eefb..ffe6bbf571 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -245,7 +245,7 @@ namespace osu.Game.Screens.Play.HUD private void load(SpectatorClient client, GameplayState gameplayState) { ((IBindableList)Spectators).BindTo(client.WatchingUsers); - ((IBindable)UserPlayingState).BindTo(gameplayState.Playing); + ((IBindable)UserPlayingState).BindTo(gameplayState.PlayingState); } } } From c8b38f05d5990c7a97740f6d6523737297d965b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jan 2025 17:14:06 +0900 Subject: [PATCH 089/801] Add note about the visibility logic because it tripped me up --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index ffe6bbf571..7158f69a7a 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -159,6 +159,7 @@ namespace osu.Game.Screens.Play.HUD private void updateVisibility() { + // We don't want to show spectators when we are watching a replay. mainFlow.FadeTo(Spectators.Count > 0 && UserPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); } From ad422295c85d257044edd33dba7284b7c8d9b631 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 10 Jan 2025 21:38:37 +0900 Subject: [PATCH 090/801] Add ctor to create Rooms from MultiplayerRooms --- osu.Game/Online/Rooms/Room.cs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index f8660a656e..7647134646 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -342,6 +342,29 @@ namespace osu.Game.Online.Rooms // Not yet serialised (not implemented). private RoomAvailability availability; + public Room() + { + } + + /// + /// Creates a from a . + /// + public Room(MultiplayerRoom room) + { + RoomID = room.RoomID; + Host = room.Host?.User; + + Name = room.Settings.Name; + Password = room.Settings.Password; + Type = room.Settings.MatchType; + QueueMode = room.Settings.QueueMode; + AutoStartDuration = room.Settings.AutoStartDuration; + AutoSkip = room.Settings.AutoSkip; + + Playlist = room.Playlist.Select(item => new PlaylistItem(item)).ToArray(); + CurrentPlaylistItem = Playlist.FirstOrDefault(item => item.ID == room.Settings.PlaylistItemId); + } + /// /// Copies values from another into this one. /// From 3d2d4ee89f06a88feabcfdda1b73ac1cbeaf1c49 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 10 Jan 2025 22:07:13 +0900 Subject: [PATCH 091/801] Add ctor to create MultiplayerPlaylistItem from PlaylistItem --- osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index 8be703e620..6e467c1d26 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -60,5 +60,20 @@ namespace osu.Game.Online.Rooms public MultiplayerPlaylistItem() { } + + public MultiplayerPlaylistItem(PlaylistItem item) + { + ID = item.ID; + OwnerID = item.OwnerID; + BeatmapID = item.Beatmap.OnlineID; + BeatmapChecksum = item.Beatmap.MD5Hash; + RulesetID = item.RulesetID; + RequiredMods = item.RequiredMods.ToArray(); + AllowedMods = item.AllowedMods.ToArray(); + Expired = item.Expired; + PlaylistOrder = item.PlaylistOrder ?? 0; + PlayedAt = item.PlayedAt; + StarRating = item.Beatmap.StarRating; + } } } From 3c4bfc0a01f8a1474de23078e935ce64f58f2ccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 Jan 2025 11:16:35 +0100 Subject: [PATCH 092/801] Merge spectator list classes into one skinnable --- .../Legacy/CatchLegacySkinTransformer.cs | 4 +- .../Argon/ManiaArgonSkinTransformer.cs | 4 +- .../Legacy/ManiaLegacySkinTransformer.cs | 4 +- .../Legacy/OsuLegacySkinTransformer.cs | 4 +- .../Archives/modified-argon-20250116.osk | Bin 1675 -> 1670 bytes .../Visual/Gameplay/TestSceneSpectatorList.cs | 55 ++++++++++++------ osu.Game/Screens/Play/HUD/SpectatorList.cs | 17 ++---- osu.Game/Skinning/ArgonSkin.cs | 4 +- osu.Game/Skinning/LegacySkin.cs | 4 +- osu.Game/Skinning/TrianglesSkin.cs | 4 +- 10 files changed, 57 insertions(+), 43 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs index 978a098990..11649da2f1 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy return new DefaultSkinComponentsContainer(container => { var keyCounter = container.OfType().FirstOrDefault(); - var spectatorList = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); if (keyCounter != null) { @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy Children = new Drawable[] { new LegacyKeyCounterDisplay(), - new SkinnableSpectatorList(), + new SpectatorList(), } }; } diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs index 48c487e70d..6f010ffe48 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon return new DefaultSkinComponentsContainer(container => { var combo = container.ChildrenOfType().FirstOrDefault(); - var spectatorList = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); if (combo != null) { @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon }) { new ArgonManiaComboCounter(), - new SkinnableSpectatorList + new SpectatorList { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index 359f21561f..76af569b95 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy return new DefaultSkinComponentsContainer(container => { var combo = container.ChildrenOfType().FirstOrDefault(); - var spectatorList = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); if (combo != null) { @@ -115,7 +115,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy }) { new LegacyManiaComboCounter(), - new SkinnableSpectatorList(), + new SpectatorList(), }; } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 03e4bb24f1..d39e05b262 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy } var combo = container.OfType().FirstOrDefault(); - var spectatorList = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); Vector2 pos = new Vector2(); @@ -96,7 +96,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { new LegacyDefaultComboCounter(), new LegacyKeyCounterDisplay(), - new SkinnableSpectatorList(), + new SpectatorList(), } }; } diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-20250116.osk b/osu.Game.Tests/Resources/Archives/modified-argon-20250116.osk index 811e91b74916988a54a9ff2b73cafe0e0ff0a490..23322e7373514b80b5a36897959ac92d5b074187 100644 GIT binary patch delta 1068 zcmV+{1k?MA4TcR8P)h>@6aWAK2mr@fFL*ij< zWIMF1>VKaTXiLD|6fF&OD!nLq?btc-qw^k{yCcJ>{Qxd7qqX;jT~DvO9NnN1flqY8 zlz68!rACB}5K-4x*|o9Ov$pf)98^nz)Un=(S?h;Q%rIme zIxL|QcxB8u+AQPkmzgvG9Wq<`pOJChQkc2H941^XM8M`K#B!O1?s&PNM9gjif<{1p z9!9PLm_8sP<1Q9+C09m_7MOh}8S9xOQ;3*ylFSJ4AbyzN-F{h#fPe#0?_>diz`x*y zfSF=S)zg_BHk&i5lA16-u-h{NGxfbPR&=Pi`$eVc=}B=5xRGs@LW;`w6nMbV7$+i& zI-gZ`-Kdv+DsiLJFibcKmsJ~5!#Tiz{8H4Aa^AClN0L0*sdKW$4aO_;NJppwssZo` z<1<7<7%;4o(qYimvdy;o_$3i$na4X*DD8hI?1cg9V|m6o<6l7mqudPfoU&I>dtKQn zXJ2KT+RsGod($E#AecfRV;oK?h_qjGE4G!f1!=*wzygmjS)sO*t}@hSY@z0V3y|8v zp$K%{Qn%y~%n+X{TNQGdy<$7pCj<7W?Ty);=!-MksKip6Z_ri)?dmXs(P+R~#MN8a zE2a&*JWwsOU6@n5*v8V)=2oX1Of#e+j^^>Rz)3vQVy4@7>dL~(Hyjylik5XHSoLW} z!}V6RpTA3<@4qTrcYPgtIkgyndVgK)?Em$uSGv5lfBA`}!)7B^^?tzBotT%q@UCXz z-E!+WM{A8vDLLmL_X5K?glZ)i|AIwzr&+dZdfu_;YgW{1nXcV(_10WXx@dW3t7%!5 zR|K6GJO`aHDt9WG7|>XiIU5esZg;zd9A=etj?!=UzX4E70|XQR000OC0LNJ|lQIQf z2*+75T13(TCX<*2E(ph2F@6aWAK2mnQVFk0@{+t-m2BmoeSH=PJYd@x#ZA@zKb+)4p7 zlP3XA5=DG4S}T!PcZLH101zGk02Tm~90nMZp8*knAd`C0!5Gsv4UKlasH(_>L*ij< zWIJtH)&D*x(3XI^i7gFuYPl$R?btc-qw^k{+atrMeFrWuqqVn&UC*w&99^Rkfsb^; zlz6K&rN)Gf5K-3``L(gq^S0B)98^nu6kHT|(==AXLJNr_B398*XoN2G2H(h@aDbQn@5>l%_&OTrQ%VhwM z&!qGlB{aNj$aQs*#p#*WgLbzz)Hf#4YN*0}wPAz^=q9jr!#^?lIUdVIwY%-&;C;Yx zB2`1nDd;CON*GsquqJ-xIL35CJQE;#_y)#*54TJZ5wIQrNQr4IHeVTpMs0<@USeo^yz484{r`M}#L+m7&j5%b_cX4^N(f%@sY5JNab*xunR{CKRGmIFA z4oj&7UKulvHp_VXWhM(ihs=ildt_X<6lSh5hY42;5wbaouw3M!J6<6uk+AEKpb?ON z`;n^vrVoh7q>IH|$(0eK1!mt##(E;j>_kjCNoIt2nB33rZoaHkK*)ipcX9=xz(3)c zfSF=S)zg_BHk&i5lA18zv77eS9*ecB5YYSBV>)L{Z97w5ZyU8qNWK=9gU!so*{PcO=QvtvV-P+;F@!k8Ff0pc(+5 zFg`;xf&s%SDIEq~E!zx&pxyRMLd-JTTXQJwe@E<@3F>3{o^!^(fFefO8R7+Hucr30 z(o?~|%67FMsoMBvRYpiKg-|9qnoYw; z)E*ASpzD^|C5Lc^_*D6-kc<2k+l4+ku#au8%x*=$JF|^SJhk}>Z8gv?Pa_zO2b{%R zy@kDE+OW$5)iQ&fIkk7&SvuO>>Xd_Nj#SLiJb4m0iDz2ObQ@1yU0C^!Bd1N#vML3u zKF(;kURL}Ct!?mp|3$gF>uW22LjSMH`{P>Y|1X!lvem8q%TFvFHXE_3_XDo(#Jt>v zcQp&|mRrs_T4!`hNjV3(708dbvLE_}bBNTEF#Z{f>W;OVj_I^ruenW*?b~MC^Q>Tx zd86f-t)^vJ-oY?5VN`C_Gzp-wDtOksCJ4ISoxEn5e~z+mcfSBoO9KQ66aWAK2mnQV zFq16>UI;~eFk0@{+t-tm1uh6hd@x#ZA@zKd$pteFMSL(?E0I@sh64Zq5R(Z8Hy=fO rFj~sD spectators = new BindableList(); - private readonly Bindable localUserPlayingState = new Bindable(); - private int counter; [Test] public void TestBasics() { SpectatorList list = null!; - AddStep("create spectator list", () => Child = list = new SpectatorList + Bindable playingState = new Bindable(); + GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), healthProcessor: new OsuHealthProcessor(0), localUserPlayingState: playingState); + TestSpectatorClient client = new TestSpectatorClient(); + + AddStep("create spectator list", () => { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Spectators = { BindTarget = spectators }, - UserPlayingState = { BindTarget = localUserPlayingState } + Children = new Drawable[] + { + client, + new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = + [ + (typeof(GameplayState), gameplayState), + (typeof(SpectatorClient), client) + ], + Child = list = new SpectatorList + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + } + }; }); - AddStep("start playing", () => localUserPlayingState.Value = LocalUserPlayingState.Playing); + AddStep("start playing", () => playingState.Value = LocalUserPlayingState.Playing); AddRepeatStep("add a user", () => { int id = Interlocked.Increment(ref counter); - spectators.Add(new SpectatorUser - { - OnlineID = id, - Username = $"User {id}" - }); + ((ISpectatorClient)client).UserStartedWatching([ + new SpectatorUser + { + OnlineID = id, + Username = $"User {id}" + } + ]); }, 10); - AddRepeatStep("remove random user", () => spectators.RemoveAt(RNG.Next(0, spectators.Count)), 5); + AddRepeatStep("remove random user", () => ((ISpectatorClient)client).UserEndedWatching(client.WatchingUsers[RNG.Next(client.WatchingUsers.Count)].OnlineID), 5); AddStep("change font to venera", () => list.Font.Value = Typeface.Venera); AddStep("change font to torus", () => list.Font.Value = Typeface.Torus); AddStep("change header colour", () => list.HeaderColour.Value = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1)); - AddStep("enter break", () => localUserPlayingState.Value = LocalUserPlayingState.Break); - AddStep("stop playing", () => localUserPlayingState.Value = LocalUserPlayingState.NotPlaying); + AddStep("enter break", () => playingState.Value = LocalUserPlayingState.Break); + AddStep("stop playing", () => playingState.Value = LocalUserPlayingState.NotPlaying); } } } diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 7158f69a7a..7b6bf6f55e 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -24,7 +24,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { - public partial class SpectatorList : CompositeDrawable + public partial class SpectatorList : CompositeDrawable, ISerialisableDrawable { private const int max_spectators_displayed = 10; @@ -44,7 +44,7 @@ namespace osu.Game.Screens.Play.HUD private DrawablePool pool = null!; [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, SpectatorClient client, GameplayState gameplayState) { AutoSizeAxes = Axes.Y; @@ -73,6 +73,9 @@ namespace osu.Game.Screens.Play.HUD }; HeaderColour.Value = Header.Colour; + + ((IBindableList)Spectators).BindTo(client.WatchingUsers); + ((IBindable)UserPlayingState).BindTo(gameplayState.PlayingState); } protected override void LoadComplete() @@ -236,17 +239,7 @@ namespace osu.Game.Screens.Play.HUD linkCompiler.Enabled.Value = UserPlayingState.Value != LocalUserPlayingState.Playing; } } - } - public partial class SkinnableSpectatorList : SpectatorList, ISerialisableDrawable - { public bool UsesFixedAnchor { get; set; } - - [BackgroundDependencyLoader] - private void load(SpectatorClient client, GameplayState gameplayState) - { - ((IBindableList)Spectators).BindTo(client.WatchingUsers); - ((IBindable)UserPlayingState).BindTo(gameplayState.PlayingState); - } } } diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index c3319b738d..bd31ccd5c9 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -112,7 +112,7 @@ namespace osu.Game.Skinning return new DefaultSkinComponentsContainer(container => { var comboCounter = container.OfType().FirstOrDefault(); - var spectatorList = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); Vector2 pos = new Vector2(36, -66); @@ -135,7 +135,7 @@ namespace osu.Game.Skinning Origin = Anchor.BottomLeft, Scale = new Vector2(1.3f), }, - new SkinnableSpectatorList + new SpectatorList { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index c607c57fcc..08fa068830 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -367,7 +367,7 @@ namespace osu.Game.Skinning return new DefaultSkinComponentsContainer(container => { var combo = container.OfType().FirstOrDefault(); - var spectatorList = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); Vector2 pos = new Vector2(); @@ -389,7 +389,7 @@ namespace osu.Game.Skinning }) { new LegacyDefaultComboCounter(), - new SkinnableSpectatorList(), + new SpectatorList(), }; } diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs index 8853a5c4ac..06fe1c80ee 100644 --- a/osu.Game/Skinning/TrianglesSkin.cs +++ b/osu.Game/Skinning/TrianglesSkin.cs @@ -91,7 +91,7 @@ namespace osu.Game.Skinning var ppCounter = container.OfType().FirstOrDefault(); var songProgress = container.OfType().FirstOrDefault(); var keyCounter = container.OfType().FirstOrDefault(); - var spectatorList = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); if (score != null) { @@ -177,7 +177,7 @@ namespace osu.Game.Skinning new BarHitErrorMeter(), new BarHitErrorMeter(), new TrianglesPerformancePointsCounter(), - new SkinnableSpectatorList(), + new SpectatorList(), } }; From a42c03cea457b9e6786983d77d966a461d1a10ed Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Fri, 17 Jan 2025 21:15:22 +1000 Subject: [PATCH 093/801] osu!taiko further considerations for rhythm (#31339) * further considerations for rhythm * new rhythm balancing * fix license header * use isNormal to validate ratio * adjust tests --------- Co-authored-by: StanR --- .../TaikoDifficultyCalculatorTest.cs | 8 ++-- .../Difficulty/Evaluators/RhythmEvaluator.cs | 48 +++++++++++++------ 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index b4cbe03511..d760b9aef6 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(3.3172381854905493d, 200, "diffcalc-test")] - [TestCase(3.3172381854905493d, 200, "diffcalc-test-strong")] + [TestCase(3.3167800835687551d, 200, "diffcalc-test")] + [TestCase(3.3167800835687551d, 200, "diffcalc-test-strong")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(4.4640702427013101d, 200, "diffcalc-test")] - [TestCase(4.4640702427013101d, 200, "diffcalc-test-strong")] + [TestCase(4.4631326105105122d, 200, "diffcalc-test")] + [TestCase(4.4631326105105122d, 200, "diffcalc-test-strong")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime()); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs index 3a294f7123..7d58eada5e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs @@ -21,27 +21,39 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators return -multiplier * Math.Pow(Math.Cos(denominator * Math.PI * ratio), power); } + /// + /// Validates the ratio by ensuring it is a normal number in cases where maps breach regular mapping conditions. + /// + private static double validateRatio(double ratio) + { + return double.IsNormal(ratio) ? ratio : 0; + } + /// /// Calculates the difficulty of a given ratio using a combination of periodic penalties and bonuses. /// private static double ratioDifficulty(double ratio, int terms = 8) { double difficulty = 0; + ratio = validateRatio(ratio); for (int i = 1; i <= terms; ++i) { - difficulty += termPenalty(ratio, i, 2, 1); + difficulty += termPenalty(ratio, i, 4, 1); } - difficulty += terms; + difficulty += terms / (1 + ratio); // Give bonus to near-1 ratios - difficulty += DifficultyCalculationUtils.BellCurve(ratio, 1, 0.7); + difficulty += DifficultyCalculationUtils.BellCurve(ratio, 1, 0.5); // Penalize ratios that are VERY near 1 - difficulty -= DifficultyCalculationUtils.BellCurve(ratio, 1, 0.5); + difficulty -= DifficultyCalculationUtils.BellCurve(ratio, 1, 0.3); - return difficulty / Math.Sqrt(8); + difficulty = Math.Max(difficulty, 0); + difficulty /= Math.Sqrt(8); + + return difficulty; } /// @@ -55,10 +67,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators ? sameInterval(sameRhythmHitObjects, 4) : 1.0; // Returns a non-penalty if there are 6 or more notes within an interval. - // Scale penalties dynamically based on hit object duration relative to hitWindow. - double penaltyScaling = Math.Max(1 - sameRhythmHitObjects.Duration / (hitWindow * 2), 0.5); + // The duration penalty is based on hit object duration relative to hitWindow. + double durationPenalty = Math.Max(1 - sameRhythmHitObjects.Duration * 2 / hitWindow, 0.5); - return Math.Min(longIntervalPenalty, shortIntervalPenalty) * penaltyScaling; + return Math.Min(longIntervalPenalty, shortIntervalPenalty) * durationPenalty; double sameInterval(SameRhythmHitObjects startObject, int intervalCount) { @@ -82,7 +94,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators { double ratio = intervals[i]!.Value / intervals[j]!.Value; if (Math.Abs(1 - ratio) <= threshold) // If any two intervals are similar, apply a penalty. - return 0.3; + return 0.80; } } @@ -95,6 +107,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators double intervalDifficulty = ratioDifficulty(sameRhythmHitObjects.HitObjectIntervalRatio); double? previousInterval = sameRhythmHitObjects.Previous?.HitObjectInterval; + intervalDifficulty *= repeatedIntervalPenalty(sameRhythmHitObjects, hitWindow); + // If a previous interval exists and there are multiple hit objects in the sequence: if (previousInterval != null && sameRhythmHitObjects.Children.Count > 1) { @@ -111,9 +125,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators } } - // Apply consistency penalty. - intervalDifficulty *= repeatedIntervalPenalty(sameRhythmHitObjects, hitWindow); - // Penalise patterns that can be hit within a single hit window. intervalDifficulty *= DifficultyCalculationUtils.Logistic( sameRhythmHitObjects.Duration / hitWindow, @@ -137,11 +148,20 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators TaikoDifficultyHitObjectRhythm rhythm = ((TaikoDifficultyHitObject)hitObject).Rhythm; double difficulty = 0.0d; + double sameRhythm = 0; + double samePattern = 0; + double intervalPenalty = 0; + if (rhythm.SameRhythmHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmHitObjects - difficulty += evaluateDifficultyOf(rhythm.SameRhythmHitObjects, hitWindow); + { + sameRhythm += 10.0 * evaluateDifficultyOf(rhythm.SameRhythmHitObjects, hitWindow); + intervalPenalty = repeatedIntervalPenalty(rhythm.SameRhythmHitObjects, hitWindow); + } if (rhythm.SamePatterns?.FirstHitObject == hitObject) // Difficulty for SamePatterns - difficulty += 0.5 * evaluateDifficultyOf(rhythm.SamePatterns); + samePattern += 1.15 * evaluateDifficultyOf(rhythm.SamePatterns); + + difficulty += Math.Max(sameRhythm, samePattern) * intervalPenalty; return difficulty; } From ad28de8ae3aa1d2817fc8511929d52c2c3ab0b20 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 17 Jan 2025 21:44:40 +0900 Subject: [PATCH 094/801] Create multiplayer rooms via multiplayer server --- .../Multiplayer/IMultiplayerLoungeServer.cs | 2 + .../Online/Multiplayer/MultiplayerClient.cs | 42 ++++++++++++------ .../Online/Multiplayer/MultiplayerRoom.cs | 9 ++++ .../Multiplayer/MultiplayerRoomSettings.cs | 14 ++++++ .../Multiplayer/OnlineMultiplayerClient.cs | 25 +++++++++++ osu.Game/Online/Rooms/Room.cs | 23 ---------- .../Match/MultiplayerMatchSettingsOverlay.cs | 44 +++++++++---------- .../Multiplayer/MultiplayerMatchSubScreen.cs | 5 +-- .../Multiplayer/TestMultiplayerClient.cs | 5 +++ 9 files changed, 105 insertions(+), 64 deletions(-) diff --git a/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs index f266c38b8b..c5eb6f9b36 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs @@ -10,6 +10,8 @@ namespace osu.Game.Online.Multiplayer /// public interface IMultiplayerLoungeServer { + Task CreateRoom(MultiplayerRoom room); + /// /// Request to join a multiplayer room. /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 4a28124583..d0c3a1fa06 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -165,6 +165,15 @@ namespace osu.Game.Online.Multiplayer private readonly TaskChain joinOrLeaveTaskChain = new TaskChain(); private CancellationTokenSource? joinCancellationSource; + public async Task CreateRoom(Room room) + { + if (Room != null) + throw new InvalidOperationException("Cannot join a multiplayer room while already in one."); + + var cancellationSource = joinCancellationSource = new CancellationTokenSource(); + await initRoom(room, r => CreateRoom(new MultiplayerRoom(room)), cancellationSource.Token); + } + /// /// Joins the for a given API . /// @@ -175,34 +184,34 @@ namespace osu.Game.Online.Multiplayer if (Room != null) throw new InvalidOperationException("Cannot join a multiplayer room while already in one."); - var cancellationSource = joinCancellationSource = new CancellationTokenSource(); + Debug.Assert(room.RoomID != null); + var cancellationSource = joinCancellationSource = new CancellationTokenSource(); + await initRoom(room, r => JoinRoom(room.RoomID.Value, password ?? room.Password), cancellationSource.Token); + } + + private async Task initRoom(Room room, Func> initFunc, CancellationToken cancellationToken) + { await joinOrLeaveTaskChain.Add(async () => { - Debug.Assert(room.RoomID != null); - - // Join the server-side room. - var joinedRoom = await JoinRoom(room.RoomID.Value, password ?? room.Password).ConfigureAwait(false); - Debug.Assert(joinedRoom != null); + // Initialise the server-side room. + MultiplayerRoom joinedRoom = await initFunc(room).ConfigureAwait(false); // Populate users. - Debug.Assert(joinedRoom.Users != null); await PopulateUsers(joinedRoom.Users).ConfigureAwait(false); // Update the stored room (must be done on update thread for thread-safety). await runOnUpdateThreadAsync(() => { Debug.Assert(Room == null); + Debug.Assert(APIRoom == null); Room = joinedRoom; APIRoom = room; - Debug.Assert(joinedRoom.Playlist.Count > 0); - + APIRoom.RoomID = joinedRoom.RoomID; APIRoom.Playlist = joinedRoom.Playlist.Select(item => new PlaylistItem(item)).ToArray(); APIRoom.CurrentPlaylistItem = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId); - - // The server will null out the end date upon the host joining the room, but the null value is never communicated to the client. APIRoom.EndDate = null; Debug.Assert(LocalUser != null); @@ -216,8 +225,8 @@ namespace osu.Game.Online.Multiplayer postServerShuttingDownNotification(); OnRoomJoined(); - }, cancellationSource.Token).ConfigureAwait(false); - }, cancellationSource.Token).ConfigureAwait(false); + }, cancellationToken).ConfigureAwait(false); + }, cancellationToken).ConfigureAwait(false); } /// @@ -227,6 +236,13 @@ namespace osu.Game.Online.Multiplayer { } + /// + /// Creates the with the given settings. + /// + /// The room. + /// The joined + protected abstract Task CreateRoom(MultiplayerRoom room); + /// /// Joins the with a given ID. /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs index 00048fa931..f7bd4490ff 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using MessagePack; using Newtonsoft.Json; using osu.Game.Online.Rooms; @@ -65,6 +66,14 @@ namespace osu.Game.Online.Multiplayer RoomID = roomId; } + public MultiplayerRoom(Room room) + { + RoomID = room.RoomID ?? 0; + Settings = new MultiplayerRoomSettings(room); + Host = room.Host != null ? new MultiplayerRoomUser(room.Host.OnlineID) : null; + Playlist = room.Playlist.Select(p => new MultiplayerPlaylistItem(p)).ToArray(); + } + public override string ToString() => $"RoomID:{RoomID} Host:{Host?.UserID} Users:{Users.Count} State:{State} Settings: [{Settings}]"; } } diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs index c73b02874e..c264ec1eef 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs @@ -35,6 +35,20 @@ namespace osu.Game.Online.Multiplayer [IgnoreMember] public bool AutoStartEnabled => AutoStartDuration != TimeSpan.Zero; + public MultiplayerRoomSettings() + { + } + + public MultiplayerRoomSettings(Room room) + { + Name = room.Name; + Password = room.Password ?? string.Empty; + MatchType = room.Type; + QueueMode = room.QueueMode; + AutoStartDuration = room.AutoStartDuration; + AutoSkip = room.AutoSkip; + } + public bool Equals(MultiplayerRoomSettings? other) { if (ReferenceEquals(this, other)) return true; diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 40436d730e..524873ef66 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -266,6 +266,31 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.RemovePlaylistItem), playlistItemId); } + protected override async Task CreateRoom(MultiplayerRoom room) + { + if (!IsConnected.Value) + throw new OperationCanceledException(); + + Debug.Assert(connection != null); + + try + { + return await connection.InvokeAsync(nameof(IMultiplayerServer.CreateRoom), room); + } + catch (HubException exception) + { + if (exception.GetHubExceptionMessage() == HubClientConnector.SERVER_SHUTDOWN_MESSAGE) + { + Debug.Assert(connector != null); + + await connector.Reconnect().ConfigureAwait(false); + return await CreateRoom(room).ConfigureAwait(false); + } + + throw; + } + } + public override Task DisconnectInternal() { if (connector == null) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 7647134646..f8660a656e 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -342,29 +342,6 @@ namespace osu.Game.Online.Rooms // Not yet serialised (not implemented). private RoomAvailability availability; - public Room() - { - } - - /// - /// Creates a from a . - /// - public Room(MultiplayerRoom room) - { - RoomID = room.RoomID; - Host = room.Host?.User; - - Name = room.Settings.Name; - Password = room.Settings.Password; - Type = room.Settings.MatchType; - QueueMode = room.Settings.QueueMode; - AutoStartDuration = room.Settings.AutoStartDuration; - AutoSkip = room.Settings.AutoSkip; - - Playlist = room.Playlist.Select(item => new PlaylistItem(item)).ToArray(); - CurrentPlaylistItem = Playlist.FirstOrDefault(item => item.ID == room.Settings.PlaylistItemId); - } - /// /// Copies values from another into this one. /// diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 1372054149..279b140d36 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -29,12 +29,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { public partial class MultiplayerMatchSettingsOverlay : RoomSettingsOverlay { - public required Bindable SelectedItem - { - get => selectedItem; - set => selectedItem.Current = value; - } - protected override OsuButton SubmitButton => settings.ApplyButton; protected override bool IsLoading => ongoingOperationTracker.InProgress.Value; @@ -56,7 +50,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match RelativeSizeAxes = Axes.Both, RelativePositionAxes = Axes.Y, SettingsApplied = Hide, - SelectedItem = { BindTarget = SelectedItem } }; protected partial class MatchSettings : CompositeDrawable @@ -65,7 +58,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; - public readonly Bindable SelectedItem = new Bindable(); public Action? SettingsApplied; public OsuTextBox NameField = null!; @@ -86,9 +78,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match [Resolved] private MultiplayerMatchSubScreen matchSubScreen { get; set; } = null!; - [Resolved] - private IRoomManager manager { get; set; } = null!; - [Resolved] private MultiplayerClient client { get; set; } = null!; @@ -279,7 +268,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { RelativeSizeAxes = Axes.X, Height = DrawableRoomPlaylistItem.HEIGHT, - SelectedItem = { BindTarget = SelectedItem } }, selectBeatmapButton = new RoundedButton { @@ -482,19 +470,27 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match } else { - room.Name = NameField.Text; - room.Type = TypePicker.Current.Value; - room.Password = PasswordTextBox.Current.Value; - room.QueueMode = QueueModeDropdown.Current.Value; - room.AutoStartDuration = TimeSpan.FromSeconds((int)startModeDropdown.Current.Value); - room.AutoSkip = AutoSkipCheckbox.Current.Value; + client.CreateRoom(room).ContinueWith(t => Schedule(() => + { + if (t.IsCompleted) + onSuccess(room); + else if (t.IsFaulted) + { + Exception? exception = t.Exception; - if (int.TryParse(MaxParticipantsField.Text, out int max)) - room.MaxParticipants = max; - else - room.MaxParticipants = null; + if (exception is AggregateException ae) + exception = ae.InnerException; - manager.CreateRoom(room, onSuccess, onError); + Debug.Assert(exception != null); + + if (exception.GetHubExceptionMessage() is string message) + onError(message); + else + onError($"Error creating room: {exception}"); + } + else + onError("Error creating room."); + })); } } @@ -520,7 +516,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match if (text.StartsWith(not_found_prefix, StringComparison.Ordinal)) { ErrorText.Text = "The selected beatmap is not available online."; - SelectedItem.Value?.MarkInvalid(); + room.Playlist.SingleOrDefault()?.MarkInvalid(); } else { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index edc45dbf7c..06ea5ee033 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -233,10 +233,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer SelectedItem = SelectedItem }; - protected override RoomSettingsOverlay CreateRoomSettingsOverlay(Room room) => new MultiplayerMatchSettingsOverlay(room) - { - SelectedItem = SelectedItem - }; + protected override RoomSettingsOverlay CreateRoomSettingsOverlay(Room room) => new MultiplayerMatchSettingsOverlay(room); protected override void UpdateMods() { diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 4d812abf11..70e298f3e0 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -483,6 +483,11 @@ namespace osu.Game.Tests.Visual.Multiplayer public override Task RemovePlaylistItem(long playlistItemId) => RemoveUserPlaylistItem(api.LocalUser.Value.OnlineID, clone(playlistItemId)); + protected override Task CreateRoom(MultiplayerRoom room) + { + throw new NotImplementedException(); + } + private async Task changeMatchType(MatchType type) { Debug.Assert(ServerRoom != null); From 5b4ba9225d7810c21a2456c9824e2a3fe621306a Mon Sep 17 00:00:00 2001 From: Natelytle <92956514+Natelytle@users.noreply.github.com> Date: Fri, 17 Jan 2025 14:37:34 -0500 Subject: [PATCH 095/801] Move error function from osu.Game.Utils to osu.Game.Rulesets.Difficulty.Utils (#31520) * Move error function implementation to osu.Game.Rulesets.Difficulty.Utils * Rename ErrorFunction.cs to DifficultyCalculationUtils_ErrorFunction.cs --- .../Difficulty/OsuPerformanceCalculator.cs | 5 ++--- .../Difficulty/TaikoPerformanceCalculator.cs | 6 +++--- .../Difficulty/Utils/DifficultyCalculationUtils.cs | 2 +- .../Utils/DifficultyCalculationUtils_ErrorFunction.cs} | 7 ++----- 4 files changed, 8 insertions(+), 12 deletions(-) rename osu.Game/{Utils/SpecialFunctions.cs => Rulesets/Difficulty/Utils/DifficultyCalculationUtils_ErrorFunction.cs} (99%) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 7013ee55c4..f191180630 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -10,7 +10,6 @@ using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; -using osu.Game.Utils; namespace osu.Game.Rulesets.Osu.Difficulty { @@ -371,10 +370,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Compute the deviation assuming greats and oks are normally distributed, and mehs are uniformly distributed. // Begin with greats and oks first. Ignoring mehs, we can be 99% confident that the deviation is not higher than: - double deviation = hitWindowGreat / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); + double deviation = hitWindowGreat / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound)); double randomValue = Math.Sqrt(2 / Math.PI) * hitWindowOk * Math.Exp(-0.5 * Math.Pow(hitWindowOk / deviation, 2)) - / (deviation * SpecialFunctions.Erf(hitWindowOk / (Math.Sqrt(2) * deviation))); + / (deviation * DifficultyCalculationUtils.Erf(hitWindowOk / (Math.Sqrt(2) * deviation))); deviation *= Math.Sqrt(1 - randomValue); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index c29ea3ba73..9e7bf7cb7a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -5,11 +5,11 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Scoring; -using osu.Game.Utils; namespace osu.Game.Rulesets.Taiko.Difficulty { @@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double accScalingExponent = 2 + attributes.MonoStaminaFactor; double accScalingShift = 500 - 100 * attributes.MonoStaminaFactor; - return difficultyValue * Math.Pow(SpecialFunctions.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent); + return difficultyValue * Math.Pow(DifficultyCalculationUtils.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent); } private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool isConvert) @@ -139,7 +139,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4); // We can be 99% confident that the deviation is not higher than: - return attributes.GreatHitWindow / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); + return attributes.GreatHitWindow / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound)); } private int totalHits => countGreat + countOk + countMeh + countMiss; diff --git a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs index aeccf2fd55..78df8a139b 100644 --- a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs @@ -6,7 +6,7 @@ using System.Linq; namespace osu.Game.Rulesets.Difficulty.Utils { - public static class DifficultyCalculationUtils + public static partial class DifficultyCalculationUtils { /// /// Converts BPM value into milliseconds diff --git a/osu.Game/Utils/SpecialFunctions.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils_ErrorFunction.cs similarity index 99% rename from osu.Game/Utils/SpecialFunctions.cs rename to osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils_ErrorFunction.cs index 795a84a973..4b89cbe7cc 100644 --- a/osu.Game/Utils/SpecialFunctions.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils_ErrorFunction.cs @@ -3,7 +3,6 @@ // All code is referenced from the following: // https://github.com/mathnet/mathnet-numerics/blob/master/src/Numerics/SpecialFunctions/Erf.cs -// https://github.com/mathnet/mathnet-numerics/blob/master/src/Numerics/Optimization/NelderMeadSimplex.cs /* Copyright (c) 2002-2022 Math.NET @@ -14,12 +13,10 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI using System; -namespace osu.Game.Utils +namespace osu.Game.Rulesets.Difficulty.Utils { - public class SpecialFunctions + public partial class DifficultyCalculationUtils { - private const double sqrt2_pi = 2.5066282746310005024157652848110452530069867406099d; - /// /// ************************************** /// COEFFICIENTS FOR METHOD ErfImp * From 8354cd5f93a7c1989dbee48fb0c1403b96a7b420 Mon Sep 17 00:00:00 2001 From: Eloise Date: Sat, 18 Jan 2025 13:52:47 +0000 Subject: [PATCH 096/801] Penalise the reading difficulty of high velocity notes using "note density" (#31512) * Penalise reading difficulty of high velocity notes at high densities * Use System for math functions * Lawtrohux changes * Clean up density penalty comment * Swap midVelocity and highVelocity back around * code quality pass --------- Co-authored-by: Jay Lawton Co-authored-by: StanR --- .../Difficulty/Evaluators/ReadingEvaluator.cs | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs index a6a1513842..2a08f65c7b 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; @@ -31,13 +32,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// The reading difficulty value for the given hit object. public static double EvaluateDifficultyOf(TaikoDifficultyHitObject noteObject) { - double effectiveBPM = noteObject.EffectiveBPM; - var highVelocity = new VelocityRange(480, 640); var midVelocity = new VelocityRange(360, 480); - return 1.0 * DifficultyCalculationUtils.Logistic(effectiveBPM, highVelocity.Center, 1.0 / (highVelocity.Range / 10)) - + 0.5 * DifficultyCalculationUtils.Logistic(effectiveBPM, midVelocity.Center, 1.0 / (midVelocity.Range / 10)); + // Apply a cap to prevent outlier values on maps that exceed the editor's parameters. + double effectiveBPM = Math.Max(1.0, noteObject.EffectiveBPM); + + double midVelocityDifficulty = 0.5 * DifficultyCalculationUtils.Logistic(effectiveBPM, midVelocity.Center, 1.0 / (midVelocity.Range / 10)); + + // Expected DeltaTime is the DeltaTime this note would need to be spaced equally to a base slider velocity 1/4 note. + double expectedDeltaTime = 21000.0 / effectiveBPM; + double objectDensity = expectedDeltaTime / Math.Max(1.0, noteObject.DeltaTime); + + // High density is penalised at high velocity as it is generally considered easier to read. See https://www.desmos.com/calculator/u63f3ntdsi + double densityPenalty = DifficultyCalculationUtils.Logistic(objectDensity, 0.925, 15); + + double highVelocityDifficulty = (1.0 - 0.33 * densityPenalty) * DifficultyCalculationUtils.Logistic + (effectiveBPM, highVelocity.Center + 8 * densityPenalty, (1.0 + 0.5 * densityPenalty) / (highVelocity.Range / 10)); + + return midVelocityDifficulty + highVelocityDifficulty; } } } From 67723b3e5201f8b10e2aaac8831c4f4960e934ba Mon Sep 17 00:00:00 2001 From: "Bastien D." <37190278+bastoo0@users.noreply.github.com> Date: Sat, 18 Jan 2025 20:26:23 +0100 Subject: [PATCH 097/801] Fix osu!catch "buzz slider" SR abuse (#31126) * Implement fix for catch buzz sliders SR abuse * Run formatting --------- Co-authored-by: StanR --- .../Difficulty/Skills/Movement.cs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index 54b85f1745..2d1adbd056 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -26,7 +26,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills private float? lastPlayerPosition; private float lastDistanceMoved; + private float lastExactDistanceMoved; private double lastStrainTime; + private bool isBuzzSliderTriggered; /// /// The speed multiplier applied to the player's catcher. @@ -59,6 +61,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills float distanceMoved = playerPosition - lastPlayerPosition.Value; + // For the exact position we consider that the catcher is in the correct position for both objects + float exactDistanceMoved = catchCurrent.NormalizedPosition - lastPlayerPosition.Value; + double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catcherSpeedMultiplier); double distanceAddition = (Math.Pow(Math.Abs(distanceMoved), 1.3) / 510); @@ -92,12 +97,30 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills playerPosition = catchCurrent.NormalizedPosition; } - distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values + distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20) + * Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values + } + + // There is an edge case where horizontal back and forth sliders create "buzz" patterns which are repeated "movements" with a distance lower than + // the platter's width but high enough to be considered a movement due to the absolute_player_positioning_error and normalized_hitobject_radius offsets + // We are detecting this exact scenario. The first back and forth is counted but all subsequent ones are nullified. + // To achieve that, we need to store the exact distances (distance ignoring absolute_player_positioning_error and normalized_hitobject_radius) + if (Math.Abs(exactDistanceMoved) <= HalfCatcherWidth * 2 && exactDistanceMoved == -lastExactDistanceMoved && catchCurrent.StrainTime == lastStrainTime) + { + if (isBuzzSliderTriggered) + distanceAddition = 0; + else + isBuzzSliderTriggered = true; + } + else + { + isBuzzSliderTriggered = false; } lastPlayerPosition = playerPosition; lastDistanceMoved = distanceMoved; lastStrainTime = catchCurrent.StrainTime; + lastExactDistanceMoved = exactDistanceMoved; return distanceAddition / weightedStrainTime; } From e320f17fafa8d904bb7a436971feabaeb3f64e3b Mon Sep 17 00:00:00 2001 From: James Wilson Date: Sun, 19 Jan 2025 15:47:39 +0000 Subject: [PATCH 098/801] Remove redundant angle check (#31566) --- osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs | 2 +- osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index 7cf5b0529f..defd02b830 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Tests public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(9.6779397290273756d, 239, "diffcalc-test")] + [TestCase(9.6779746353001634d, 239, "diffcalc-test")] [TestCase(1.7691451263718989d, 54, "zero-length-sliders")] [TestCase(0.55785578988249407d, 4, "very-fast-slider")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index 9a5533e536..d1c92ed6a7 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators if (Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime) < 1.25 * Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime)) // If rhythms are the same. { - if (osuCurrObj.Angle != null && osuLastObj.Angle != null && osuLastLastObj.Angle != null) + if (osuCurrObj.Angle != null && osuLastObj.Angle != null) { double currAngle = osuCurrObj.Angle.Value; double lastAngle = osuLastObj.Angle.Value; From e04727afb13d5478608987e1080270a54bee66ed Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Mon, 20 Jan 2025 07:55:34 +1000 Subject: [PATCH 099/801] Improve convert considerations in osu!taiko (#31546) * return a higher finger count * implement isConvert * diffcalc cleanup * harshen monostaminafactor accuracy curve * readd comment * adjusts tests --- .../TaikoDifficultyCalculatorTest.cs | 8 ++--- .../Difficulty/Evaluators/StaminaEvaluator.cs | 2 +- .../Difficulty/Skills/Stamina.cs | 7 +++-- .../Difficulty/TaikoDifficultyCalculator.cs | 31 ++++++------------- .../Difficulty/TaikoPerformanceCalculator.cs | 2 +- 5 files changed, 21 insertions(+), 29 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index d760b9aef6..6f5c26816f 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(3.3167800835687551d, 200, "diffcalc-test")] - [TestCase(3.3167800835687551d, 200, "diffcalc-test-strong")] + [TestCase(3.3056113401782845d, 200, "diffcalc-test")] + [TestCase(3.3056113401782845d, 200, "diffcalc-test-strong")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(4.4631326105105122d, 200, "diffcalc-test")] - [TestCase(4.4631326105105122d, 200, "diffcalc-test-strong")] + [TestCase(4.4473902679506896d, 200, "diffcalc-test")] + [TestCase(4.4473902679506896d, 200, "diffcalc-test-strong")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime()); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs index a273d91a38..b39ad953a4 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators return 2; } - return 4; + return 8; } /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 29f9f16033..aea491aca3 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills private double strainDecayBase => 0.4; private readonly bool singleColourStamina; + private readonly bool isConvert; private double currentStrain; @@ -28,10 +29,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// /// Mods for use in skill calculations. /// Reads when Stamina is from a single coloured pattern. - public Stamina(Mod[] mods, bool singleColourStamina) + /// Determines if the currently evaluated beatmap is converted. + public Stamina(Mod[] mods, bool singleColourStamina, bool isConvert) : base(mods) { this.singleColourStamina = singleColourStamina; + this.isConvert = isConvert; } private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); @@ -45,7 +48,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills var currentObject = current as TaikoDifficultyHitObject; int index = currentObject?.Colour.MonoStreak?.HitObjects.IndexOf(currentObject) ?? 0; - double monolengthBonus = 1 + Math.Min(Math.Max((index - 5) / 50.0, 0), 0.30); + double monolengthBonus = isConvert ? 1 : 1 + Math.Min(Math.Max((index - 5) / 50.0, 0), 0.30); if (singleColourStamina) return DifficultyCalculationUtils.Logistic(-(index - 10) / 2.0, currentStrain); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 3ad9d17526..efd3001764 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -32,6 +32,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double strainLengthBonus; private double patternMultiplier; + private bool isConvert; + public override int Version => 20241007; public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) @@ -44,13 +46,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty HitWindows hitWindows = new HitWindows(); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); + isConvert = beatmap.BeatmapInfo.Ruleset.OnlineID == 0; + return new Skill[] { new Rhythm(mods, hitWindows.WindowFor(HitResult.Great) / clockRate), new Reading(mods), new Colour(mods), - new Stamina(mods, false), - new Stamina(mods, true) + new Stamina(mods, false, isConvert), + new Stamina(mods, true, isConvert) }; } @@ -130,19 +134,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty + Math.Min(Math.Max((staminaDifficultStrains - 1350) / 5000, 0), 0.15) + Math.Min(Math.Max((staminaRating - 7.0) / 1.0, 0), 0.05); - double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax); + double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax, isConvert); double starRating = rescale(combinedRating * 1.4); - // Converts are penalised outside the scope of difficulty calculation, as our assumptions surrounding standard play-styles becomes out-of-scope. - if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0) - { - starRating *= 0.7; - - // For maps with relax, multiple inputs are more likely to be abused. - if (isRelax) - starRating *= 0.60; - } - HitWindows hitWindows = new TaikoHitWindows(); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); @@ -173,7 +167,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// For each section, the peak strains of all separate skills are combined into a single peak strain for the section. /// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more). /// - private double combinedDifficultyValue(Rhythm rhythm, Reading reading, Colour colour, Stamina stamina, bool isRelax) + private double combinedDifficultyValue(Rhythm rhythm, Reading reading, Colour colour, Stamina stamina, bool isRelax, bool isConvert) { List peaks = new List(); @@ -186,14 +180,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier * patternMultiplier; double readingPeak = readingPeaks[i] * reading_skill_multiplier; - double colourPeak = colourPeaks[i] * colour_skill_multiplier; + double colourPeak = isRelax ? 0 : colourPeaks[i] * colour_skill_multiplier; // There is no colour difficulty in relax. double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier * strainLengthBonus; - - if (isRelax) - { - colourPeak = 0; // There is no colour difficulty in relax. - staminaPeak /= 1.5; // Stamina difficulty is decreased with an increased available finger count. - } + staminaPeak /= isConvert || isRelax ? 1.5 : 1.0; // Available finger count is increased by 150%, thus we adjust accordingly. double peak = DifficultyCalculationUtils.Norm(2, DifficultyCalculationUtils.Norm(1.5, colourPeak, staminaPeak), rhythmPeak, readingPeak); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 9e7bf7cb7a..bcd3693119 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty // Scale accuracy more harshly on nearly-completely mono (single coloured) speed maps. double accScalingExponent = 2 + attributes.MonoStaminaFactor; - double accScalingShift = 500 - 100 * attributes.MonoStaminaFactor; + double accScalingShift = 500 - 100 * (attributes.MonoStaminaFactor * 3); return difficultyValue * Math.Pow(DifficultyCalculationUtils.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent); } From 2d0bc6cb62bd9fe84b7fffb8019ff2e503a6ffc1 Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Mon, 20 Jan 2025 08:40:09 +1000 Subject: [PATCH 100/801] Rebalance stamina length bonus in osu!taiko (#31556) * adjust straincount to assume 1300 * remove comment --------- Co-authored-by: StanR --- .../Difficulty/TaikoDifficultyCalculator.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index efd3001764..b1dcf2d7a0 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -124,14 +124,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double colourDifficultStrains = colour.CountTopWeightedStrains(); double rhythmDifficultStrains = rhythm.CountTopWeightedStrains(); - // Due to constraints of strain in cases where difficult strain values don't shift with range changes, we manually apply clockrate. - double staminaDifficultStrains = stamina.CountTopWeightedStrains() * clockRate; + double staminaDifficultStrains = stamina.CountTopWeightedStrains(); // As we don't have pattern integration in osu!taiko, we apply the other two skills relative to rhythm. patternMultiplier = Math.Pow(staminaRating * colourRating, 0.10); strainLengthBonus = 1 - + Math.Min(Math.Max((staminaDifficultStrains - 1350) / 5000, 0), 0.15) + + Math.Min(Math.Max((staminaDifficultStrains - 1000) / 3700, 0), 0.15) + Math.Min(Math.Max((staminaRating - 7.0) / 1.0, 0), 0.05); double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax, isConvert); From a6ca9ba9fb0630562425fe37d0445da5f75e9635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 20 Jan 2025 00:51:43 +0100 Subject: [PATCH 101/801] Display up to 2 decimal places in `MetronomeDisplay` --- .../Screens/Edit/Timing/MetronomeDisplay.cs | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs index 5e5b740b62..5325c8640b 100644 --- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs @@ -228,11 +228,13 @@ namespace osu.Game.Screens.Edit.Timing private double effectiveBeatLength; + private double effectiveBpm => 60_000 / effectiveBeatLength; + private TimingControlPoint timingPoint = null!; private bool isSwinging; - private readonly BindableInt interpolatedBpm = new BindableInt(); + private readonly BindableDouble interpolatedBpm = new BindableDouble(); private ScheduledDelegate? latchDelegate; @@ -255,7 +257,17 @@ namespace osu.Game.Screens.Edit.Timing { base.LoadComplete(); - interpolatedBpm.BindValueChanged(_ => bpmText.Text = interpolatedBpm.Value.ToLocalisableString()); + interpolatedBpm.BindValueChanged(_ => updateBpmText()); + } + + private void updateBpmText() + { + double bpm = Math.Round(interpolatedBpm.Value); + + if (Precision.AlmostEquals(bpm, effectiveBpm, 1.0)) + bpm = effectiveBpm; + + bpmText.Text = bpm.ToLocalisableString("0.##"); } protected override void Update() @@ -277,12 +289,11 @@ namespace osu.Game.Screens.Edit.Timing EarlyActivationMilliseconds = timingPoint.BeatLength / 2; - double effectiveBpm = 60000 / effectiveBeatLength; - float bpmRatio = (float)Interpolation.ApplyEasing(Easing.OutQuad, Math.Clamp((effectiveBpm - 30) / 480, 0, 1)); weight.MoveToY((float)Interpolation.Lerp(0.1f, 0.83f, bpmRatio), 600, Easing.OutQuint); - this.TransformBindableTo(interpolatedBpm, (int)Math.Round(effectiveBpm), 600, Easing.OutQuint); + + this.TransformBindableTo(interpolatedBpm, effectiveBpm, 600, Easing.OutQuint); } if (!BeatSyncSource.Clock.IsRunning && isSwinging) From 3532ce1636460d0988fa7d0c3832b25065600cca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 20 Jan 2025 01:07:13 +0100 Subject: [PATCH 102/801] Olibomby insisted on it being like this so i concede --- osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs index 5325c8640b..f8236f922a 100644 --- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs @@ -262,10 +262,9 @@ namespace osu.Game.Screens.Edit.Timing private void updateBpmText() { - double bpm = Math.Round(interpolatedBpm.Value); - - if (Precision.AlmostEquals(bpm, effectiveBpm, 1.0)) - bpm = effectiveBpm; + double bpm = Precision.AlmostEquals(interpolatedBpm.Value, effectiveBpm, 1.0) + ? effectiveBpm + : Math.Round(interpolatedBpm.Value); bpmText.Text = bpm.ToLocalisableString("0.##"); } From 8f33b4cc6159b4b65fc7df4b757c1d5418eb15ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 20 Jan 2025 01:14:21 +0100 Subject: [PATCH 103/801] Add comment --- osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs index f8236f922a..8a4f1c01b1 100644 --- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs @@ -262,6 +262,8 @@ namespace osu.Game.Screens.Edit.Timing private void updateBpmText() { + // While interpolating between two integer values, showing the decimal places would look a bit odd + // so rounding is applied until we're close to the final value. double bpm = Precision.AlmostEquals(interpolatedBpm.Value, effectiveBpm, 1.0) ? effectiveBpm : Math.Round(interpolatedBpm.Value); From e386c9e373618fea4acb371447db8d2bee637701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 20 Jan 2025 01:25:22 +0100 Subject: [PATCH 104/801] Apply snapping when pasting hitobjects --- osu.Game/Screens/Edit/Compose/ComposeScreen.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index f7e523db25..195625dcde 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -31,6 +31,9 @@ namespace osu.Game.Screens.Edit.Compose [Resolved] private IGameplaySettings globalGameplaySettings { get; set; } + [Resolved] + private IBeatSnapProvider beatSnapProvider { get; set; } + private Bindable clipboard { get; set; } private HitObjectComposer composer; @@ -150,7 +153,7 @@ namespace osu.Game.Screens.Edit.Compose Debug.Assert(objects.Any()); - double timeOffset = clock.CurrentTime - objects.Min(o => o.StartTime); + double timeOffset = beatSnapProvider.SnapTime(clock.CurrentTime) - objects.Min(o => o.StartTime); foreach (var h in objects) h.StartTime += timeOffset; From 45e0d9154e410e0db5aab353c5d67b3e539db015 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 20 Jan 2025 01:38:18 +0100 Subject: [PATCH 105/801] Adjust tests to worked with snapped start time --- osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs index a766b253aa..ce9dbd5fb1 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1); - AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == newTime); + AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == EditorBeatmap.SnapTime(newTime, null)); } [Test] @@ -122,6 +122,8 @@ namespace osu.Game.Tests.Visual.Editing [TestCase(true)] public void TestCopyPaste(bool deselectAfterCopy) { + const int paste_time = 2000; + var addedObject = new HitCircle { StartTime = 1000 }; AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); @@ -130,7 +132,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("copy hitobject", () => Editor.Copy()); - AddStep("move forward in time", () => EditorClock.Seek(2000)); + AddStep("move forward in time", () => EditorClock.Seek(paste_time)); if (deselectAfterCopy) { @@ -144,7 +146,7 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("are two objects", () => EditorBeatmap.HitObjects.Count == 2); - AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == 2000); + AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == EditorBeatmap.SnapTime(paste_time, null)); AddUntilStep("timeline selection box is visible", () => Editor.ChildrenOfType().First().ChildrenOfType().First().Alpha > 0); AddUntilStep("composer selection box is visible", () => Editor.ChildrenOfType().First().ChildrenOfType().First().Alpha > 0); From 2b5ea4e6e0e859599affbcb5cf9151060679450b Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 20 Jan 2025 03:17:01 -0500 Subject: [PATCH 106/801] Fix recent editor textbox regressions --- .../UserInterface/TestSceneFormControls.cs | 2 +- .../Graphics/UserInterfaceV2/FormNumberBox.cs | 20 +++++++++---------- .../Graphics/UserInterfaceV2/FormSliderBar.cs | 3 +-- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs index b9ff78b49f..118fbca97b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.UserInterface Current = { Disabled = true }, TabbableContentContainer = this, }, - new FormNumberBox + new FormNumberBox(allowDecimals: true) { Caption = "Number", HintText = "Insert your favourite number", diff --git a/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs index 61d3b3fc31..b739155a36 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs @@ -1,32 +1,30 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Globalization; using osu.Framework.Input; namespace osu.Game.Graphics.UserInterfaceV2 { public partial class FormNumberBox : FormTextBox { - public bool AllowDecimals { get; init; } + private readonly bool allowDecimals; - internal override InnerTextBox CreateTextBox() => new InnerNumberBox + public FormNumberBox(bool allowDecimals = false) + { + this.allowDecimals = allowDecimals; + } + + internal override InnerTextBox CreateTextBox() => new InnerNumberBox(allowDecimals) { - AllowDecimals = AllowDecimals, SelectAllOnFocus = true, }; internal partial class InnerNumberBox : InnerTextBox { - public bool AllowDecimals { get; init; } - - public InnerNumberBox() + public InnerNumberBox(bool allowDecimals) { - InputProperties = new TextInputProperties(TextInputType.Number, false); + InputProperties = new TextInputProperties(allowDecimals ? TextInputType.Decimal : TextInputType.Number, false); } - - protected override bool CanAddCharacter(char character) - => char.IsAsciiDigit(character) || (AllowDecimals && CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator.Contains(character)); } } } diff --git a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs index 532423876e..4e43b133c7 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs @@ -119,7 +119,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 Caption = Caption, TooltipText = HintText, }, - textBox = new FormNumberBox.InnerNumberBox + textBox = new FormNumberBox.InnerNumberBox(allowDecimals: true) { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, @@ -127,7 +127,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 Width = 0.5f, CommitOnFocusLost = true, SelectAllOnFocus = true, - AllowDecimals = true, OnInputError = () => { flashLayer.Colour = ColourInfo.GradientVertical(colours.Red3.Opacity(0), colours.Red3); From e57565435ed58fc4e549559350886df1fa4d4189 Mon Sep 17 00:00:00 2001 From: Eloise Date: Mon, 20 Jan 2025 08:40:52 +0000 Subject: [PATCH 107/801] osu!taiko new rhythm penalty for long intervals using stamina difficulty (#31573) * Replace long interval nerf with a new one that uses stamina difficulty * Turn tabs into spaces * Update unit tests --------- Co-authored-by: StanR --- .../TaikoDifficultyCalculatorTest.cs | 8 ++++---- osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index 6f5c26816f..76b86eb4d6 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(3.3056113401782845d, 200, "diffcalc-test")] - [TestCase(3.3056113401782845d, 200, "diffcalc-test-strong")] + [TestCase(3.305554470092722d, 200, "diffcalc-test")] + [TestCase(3.305554470092722d, 200, "diffcalc-test-strong")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(4.4473902679506896d, 200, "diffcalc-test")] - [TestCase(4.4473902679506896d, 200, "diffcalc-test-strong")] + [TestCase(4.4472572672057815d, 200, "diffcalc-test")] + [TestCase(4.4472572672057815d, 200, "diffcalc-test-strong")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime()); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index 4fe1ea693e..45d0d0a548 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -30,7 +30,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills double difficulty = RhythmEvaluator.EvaluateDifficultyOf(current, greatHitWindow); // To prevent abuse of exceedingly long intervals between awkward rhythms, we penalise its difficulty. - difficulty *= DifficultyCalculationUtils.Logistic(current.DeltaTime, 350, -1 / 25.0, 0.5) + 0.5; + double staminaDifficulty = StaminaEvaluator.EvaluateDifficultyOf(current) - 0.5; // Remove base strain + difficulty *= DifficultyCalculationUtils.Logistic(staminaDifficulty, 1 / 15.0, 50.0); return difficulty; } From 22e839d62b646f6f42b129df83336694547bef8e Mon Sep 17 00:00:00 2001 From: StanR Date: Mon, 20 Jan 2025 14:39:35 +0500 Subject: [PATCH 108/801] Replace indexed skill access with `skills.OfType<...>().Single()` (#30034) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Replace indexed skill access with `skills.First(s is ...)` * Fix comment * Further refactoring to remove casts --------- Co-authored-by: Dan Balasescu Co-authored-by: Bartłomiej Dach --- .../Difficulty/CatchDifficultyCalculator.cs | 3 ++- .../Difficulty/ManiaDifficultyCalculator.cs | 2 +- .../Difficulty/OsuDifficultyCalculator.cs | 24 ++++++++++--------- .../Difficulty/Skills/Aim.cs | 10 ++++---- .../Difficulty/Skills/Stamina.cs | 8 +++---- .../Difficulty/TaikoDifficultyCalculator.cs | 10 ++++---- 6 files changed, 30 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 7d21409ee8..99df2731ff 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Difficulty.Preprocessing; @@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty CatchDifficultyAttributes attributes = new CatchDifficultyAttributes { - StarRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier, + StarRating = Math.Sqrt(skills.OfType().Single().DifficultyValue()) * difficulty_multiplier, Mods = mods, ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0, MaxCombo = beatmap.GetMaxCombo(), diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index ff9aa4aa7b..1efa7cb42f 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty ManiaDifficultyAttributes attributes = new ManiaDifficultyAttributes { - StarRating = skills[0].DifficultyValue() * difficulty_multiplier, + StarRating = skills.OfType().Single().DifficultyValue() * difficulty_multiplier, Mods = mods, // In osu-stable mania, rate-adjustment mods don't affect the hit window. // This is done the way it is to introduce fractional differences in order to match osu-stable for the time being. diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 5a61ea586a..1505c51592 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -36,20 +36,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (beatmap.HitObjects.Count == 0) return new OsuDifficultyAttributes { Mods = mods }; - double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier; - double aimRatingNoSliders = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier; - double speedRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier; - double speedNotes = ((Speed)skills[2]).RelevantNoteCount(); - double difficultSliders = ((Aim)skills[0]).GetDifficultSliders(); - double flashlightRating = 0.0; - - if (mods.Any(h => h is OsuModFlashlight)) - flashlightRating = Math.Sqrt(skills[3].DifficultyValue()) * difficulty_multiplier; + var aim = skills.OfType().Single(a => a.IncludeSliders); + double aimRating = Math.Sqrt(aim.DifficultyValue()) * difficulty_multiplier; + double aimDifficultyStrainCount = aim.CountTopWeightedStrains(); + double difficultSliders = aim.GetDifficultSliders(); + var aimWithoutSliders = skills.OfType().Single(a => !a.IncludeSliders); + double aimRatingNoSliders = Math.Sqrt(aimWithoutSliders.DifficultyValue()) * difficulty_multiplier; double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1; - double aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).CountTopWeightedStrains(); - double speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).CountTopWeightedStrains(); + var speed = skills.OfType().Single(); + double speedRating = Math.Sqrt(speed.DifficultyValue()) * difficulty_multiplier; + double speedNotes = speed.RelevantNoteCount(); + double speedDifficultyStrainCount = speed.CountTopWeightedStrains(); + + var flashlight = skills.OfType().SingleOrDefault(); + double flashlightRating = flashlight == null ? 0.0 : Math.Sqrt(flashlight.DifficultyValue()) * difficulty_multiplier; if (mods.Any(m => m is OsuModTouchDevice)) { diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index f04b679b73..89adda302c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -16,14 +16,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// public class Aim : OsuStrainSkill { - public Aim(Mod[] mods, bool withSliders) + public readonly bool IncludeSliders; + + public Aim(Mod[] mods, bool includeSliders) : base(mods) { - this.withSliders = withSliders; + IncludeSliders = includeSliders; } - private readonly bool withSliders; - private double currentStrain; private double skillMultiplier => 25.6; @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills protected override double StrainValueAt(DifficultyHitObject current) { currentStrain *= strainDecay(current.DeltaTime); - currentStrain += AimEvaluator.EvaluateDifficultyOf(current, withSliders) * skillMultiplier; + currentStrain += AimEvaluator.EvaluateDifficultyOf(current, IncludeSliders) * skillMultiplier; if (current.BaseObject is Slider) { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index aea491aca3..12e1396dd7 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills private double skillMultiplier => 1.1; private double strainDecayBase => 0.4; - private readonly bool singleColourStamina; + public readonly bool SingleColourStamina; private readonly bool isConvert; private double currentStrain; @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills public Stamina(Mod[] mods, bool singleColourStamina, bool isConvert) : base(mods) { - this.singleColourStamina = singleColourStamina; + SingleColourStamina = singleColourStamina; this.isConvert = isConvert; } @@ -50,12 +50,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills double monolengthBonus = isConvert ? 1 : 1 + Math.Min(Math.Max((index - 5) / 50.0, 0), 0.30); - if (singleColourStamina) + if (SingleColourStamina) return DifficultyCalculationUtils.Logistic(-(index - 10) / 2.0, currentStrain); return currentStrain * monolengthBonus; } - protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => singleColourStamina ? 0 : currentStrain * strainDecay(time - current.Previous(0).StartTime); + protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => SingleColourStamina ? 0 : currentStrain * strainDecay(time - current.Previous(0).StartTime); } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index b1dcf2d7a0..bcd26a06bc 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -109,11 +109,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty bool isRelax = mods.Any(h => h is TaikoModRelax); - Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm); - Reading reading = (Reading)skills.First(x => x is Reading); - Colour colour = (Colour)skills.First(x => x is Colour); - Stamina stamina = (Stamina)skills.First(x => x is Stamina); - Stamina singleColourStamina = (Stamina)skills.Last(x => x is Stamina); + var rhythm = skills.OfType().Single(); + var reading = skills.OfType().Single(); + var colour = skills.OfType().Single(); + var stamina = skills.OfType().Single(s => !s.SingleColourStamina); + var singleColourStamina = skills.OfType().Single(s => s.SingleColourStamina); double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier; double readingRating = reading.DifficultyValue() * reading_skill_multiplier; From a77dfb106834e8818574e81b1d7880d38c0e929b Mon Sep 17 00:00:00 2001 From: James Wilson Date: Mon, 20 Jan 2025 12:04:31 +0000 Subject: [PATCH 109/801] Use correct `HitWindows` class for osu!taiko hit windows in difficulty calculator (#31579) * Use correct `HitWindows` class for osu!taiko hit windows in difficulty calculator * Remove redundant (and incorrect) hit window creation * Balance rhythm against hit window changes --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 2 +- .../Difficulty/TaikoDifficultyCalculator.cs | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs index 7d58eada5e..e7d82453eb 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators intervalDifficulty *= DifficultyCalculationUtils.Logistic( durationDifference / hitWindow, midpointOffset: 0.7, - multiplier: 1.5, + multiplier: 1.0, maxValue: 1); } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index bcd26a06bc..f3b976f970 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) { - HitWindows hitWindows = new HitWindows(); + HitWindows hitWindows = new TaikoHitWindows(); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); isConvert = beatmap.BeatmapInfo.Ruleset.OnlineID == 0; @@ -68,9 +68,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { - var hitWindows = new HitWindows(); - hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); - var difficultyHitObjects = new List(); var centreObjects = new List(); var rimObjects = new List(); From 89586d5ab25cb7108ed71d7c516debf9950f60cf Mon Sep 17 00:00:00 2001 From: Rudi Herouard Date: Mon, 20 Jan 2025 13:43:45 +0100 Subject: [PATCH 110/801] Fix settings in replay hiding when dragging a slider --- osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index 668c74e0c2..b285b1b799 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -122,7 +122,10 @@ namespace osu.Game.Screens.Play.HUD { float screenMouseX = inputManager.CurrentState.Mouse.Position.X; - Expanded.Value = screenMouseX >= button.ScreenSpaceDrawQuad.TopLeft.X && screenMouseX <= ToScreenSpace(new Vector2(DrawWidth + EXPANDED_WIDTH, 0)).X; + Expanded.Value = + (screenMouseX >= button.ScreenSpaceDrawQuad.TopLeft.X && screenMouseX <= ToScreenSpace(new Vector2(DrawWidth + EXPANDED_WIDTH, 0)).X) + // Stay expanded if the user is dragging a slider. + || inputManager.DraggedDrawable != null; } protected override void OnHoverLost(HoverLostEvent e) From 6b524aba60e2474b9faa281f299fb4ee365fd974 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 4 Jan 2025 14:27:48 +0900 Subject: [PATCH 111/801] Enable sentry caching to avoid sentry writing outside of game directory See https://github.com/ppy/osu/discussions/31412. Probably safe enough. --- osu.Game/OsuGame.cs | 8 ++++++-- osu.Game/Utils/SentryLogger.cs | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 40d13ae0b7..47e301c4e4 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -233,8 +233,6 @@ namespace osu.Game forwardGeneralLogsToNotifications(); forwardTabletLogsToNotifications(); - - SentryLogger = new SentryLogger(this); } #region IOverlayManager @@ -320,6 +318,12 @@ namespace osu.Game private readonly List dragDropFiles = new List(); private ScheduledDelegate dragDropImportSchedule; + public override void SetupLogging(Storage gameStorage, Storage cacheStorage) + { + base.SetupLogging(gameStorage, cacheStorage); + SentryLogger = new SentryLogger(this, cacheStorage); + } + public override void SetHost(GameHost host) { base.SetHost(host); diff --git a/osu.Game/Utils/SentryLogger.cs b/osu.Game/Utils/SentryLogger.cs index 8d3e5fb834..ed644bf5cb 100644 --- a/osu.Game/Utils/SentryLogger.cs +++ b/osu.Game/Utils/SentryLogger.cs @@ -11,6 +11,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Configuration; using osu.Framework.Logging; +using osu.Framework.Platform; using osu.Framework.Statistics; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -36,7 +37,7 @@ namespace osu.Game.Utils private readonly OsuGame game; - public SentryLogger(OsuGame game) + public SentryLogger(OsuGame game, Storage? storage = null) { this.game = game; @@ -49,6 +50,7 @@ namespace osu.Game.Utils options.AutoSessionTracking = true; options.IsEnvironmentUser = false; options.IsGlobalModeEnabled = true; + options.CacheDirectoryPath = storage?.GetFullPath(string.Empty); // The reported release needs to match version as reported to Sentry in .github/workflows/sentry-release.yml options.Release = $"osu@{game.Version.Replace($@"-{OsuGameBase.BUILD_SUFFIX}", string.Empty)}"; }); From c8b05ce114a00e9123ba5b3ac8930f1fafde88a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jan 2025 13:40:55 +0900 Subject: [PATCH 112/801] Tidy up code quality of `RhythmEvaluator` --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 149 ++++++++---------- 1 file changed, 68 insertions(+), 81 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs index e7d82453eb..22321a8f6e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs @@ -14,48 +14,64 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators public class RhythmEvaluator { /// - /// Multiplier for a given denominator term. + /// Evaluate the difficulty of a hitobject considering its interval change. /// - private static double termPenalty(double ratio, int denominator, double power, double multiplier) + public static double EvaluateDifficultyOf(DifficultyHitObject hitObject, double hitWindow) { - return -multiplier * Math.Pow(Math.Cos(denominator * Math.PI * ratio), power); - } + TaikoDifficultyHitObjectRhythm rhythm = ((TaikoDifficultyHitObject)hitObject).Rhythm; + double difficulty = 0.0d; - /// - /// Validates the ratio by ensuring it is a normal number in cases where maps breach regular mapping conditions. - /// - private static double validateRatio(double ratio) - { - return double.IsNormal(ratio) ? ratio : 0; - } + double sameRhythm = 0; + double samePattern = 0; + double intervalPenalty = 0; - /// - /// Calculates the difficulty of a given ratio using a combination of periodic penalties and bonuses. - /// - private static double ratioDifficulty(double ratio, int terms = 8) - { - double difficulty = 0; - ratio = validateRatio(ratio); - - for (int i = 1; i <= terms; ++i) + if (rhythm.SameRhythmHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmHitObjects { - difficulty += termPenalty(ratio, i, 4, 1); + sameRhythm += 10.0 * evaluateDifficultyOf(rhythm.SameRhythmHitObjects, hitWindow); + intervalPenalty = repeatedIntervalPenalty(rhythm.SameRhythmHitObjects, hitWindow); } - difficulty += terms / (1 + ratio); + if (rhythm.SamePatterns?.FirstHitObject == hitObject) // Difficulty for SamePatterns + samePattern += 1.15 * ratioDifficulty(rhythm.SamePatterns.IntervalRatio); - // Give bonus to near-1 ratios - difficulty += DifficultyCalculationUtils.BellCurve(ratio, 1, 0.5); - - // Penalize ratios that are VERY near 1 - difficulty -= DifficultyCalculationUtils.BellCurve(ratio, 1, 0.3); - - difficulty = Math.Max(difficulty, 0); - difficulty /= Math.Sqrt(8); + difficulty += Math.Max(sameRhythm, samePattern) * intervalPenalty; return difficulty; } + private static double evaluateDifficultyOf(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow) + { + double intervalDifficulty = ratioDifficulty(sameRhythmHitObjects.HitObjectIntervalRatio); + double? previousInterval = sameRhythmHitObjects.Previous?.HitObjectInterval; + + intervalDifficulty *= repeatedIntervalPenalty(sameRhythmHitObjects, hitWindow); + + // If a previous interval exists and there are multiple hit objects in the sequence: + if (previousInterval != null && sameRhythmHitObjects.Children.Count > 1) + { + double expectedDurationFromPrevious = (double)previousInterval * sameRhythmHitObjects.Children.Count; + double durationDifference = sameRhythmHitObjects.Duration - expectedDurationFromPrevious; + + if (durationDifference > 0) + { + intervalDifficulty *= DifficultyCalculationUtils.Logistic( + durationDifference / hitWindow, + midpointOffset: 0.7, + multiplier: 1.0, + maxValue: 1); + } + } + + // Penalise patterns that can be hit within a single hit window. + intervalDifficulty *= DifficultyCalculationUtils.Logistic( + sameRhythmHitObjects.Duration / hitWindow, + midpointOffset: 0.6, + multiplier: 1, + maxValue: 1); + + return Math.Pow(intervalDifficulty, 0.75); + } + /// /// Determines if the changes in hit object intervals is consistent based on a given threshold. /// @@ -102,68 +118,39 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators } } - private static double evaluateDifficultyOf(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow) - { - double intervalDifficulty = ratioDifficulty(sameRhythmHitObjects.HitObjectIntervalRatio); - double? previousInterval = sameRhythmHitObjects.Previous?.HitObjectInterval; - - intervalDifficulty *= repeatedIntervalPenalty(sameRhythmHitObjects, hitWindow); - - // If a previous interval exists and there are multiple hit objects in the sequence: - if (previousInterval != null && sameRhythmHitObjects.Children.Count > 1) - { - double expectedDurationFromPrevious = (double)previousInterval * sameRhythmHitObjects.Children.Count; - double durationDifference = sameRhythmHitObjects.Duration - expectedDurationFromPrevious; - - if (durationDifference > 0) - { - intervalDifficulty *= DifficultyCalculationUtils.Logistic( - durationDifference / hitWindow, - midpointOffset: 0.7, - multiplier: 1.0, - maxValue: 1); - } - } - - // Penalise patterns that can be hit within a single hit window. - intervalDifficulty *= DifficultyCalculationUtils.Logistic( - sameRhythmHitObjects.Duration / hitWindow, - midpointOffset: 0.6, - multiplier: 1, - maxValue: 1); - - return Math.Pow(intervalDifficulty, 0.75); - } - - private static double evaluateDifficultyOf(SamePatterns samePatterns) - { - return ratioDifficulty(samePatterns.IntervalRatio); - } - /// - /// Evaluate the difficulty of a hitobject considering its interval change. + /// Calculates the difficulty of a given ratio using a combination of periodic penalties and bonuses. /// - public static double EvaluateDifficultyOf(DifficultyHitObject hitObject, double hitWindow) + private static double ratioDifficulty(double ratio, int terms = 8) { - TaikoDifficultyHitObjectRhythm rhythm = ((TaikoDifficultyHitObject)hitObject).Rhythm; - double difficulty = 0.0d; + double difficulty = 0; - double sameRhythm = 0; - double samePattern = 0; - double intervalPenalty = 0; + // Validate the ratio by ensuring it is a normal number in cases where maps breach regular mapping conditions. + ratio = double.IsNormal(ratio) ? ratio : 0; - if (rhythm.SameRhythmHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmHitObjects + for (int i = 1; i <= terms; ++i) { - sameRhythm += 10.0 * evaluateDifficultyOf(rhythm.SameRhythmHitObjects, hitWindow); - intervalPenalty = repeatedIntervalPenalty(rhythm.SameRhythmHitObjects, hitWindow); + difficulty += termPenalty(ratio, i, 4, 1); } - if (rhythm.SamePatterns?.FirstHitObject == hitObject) // Difficulty for SamePatterns - samePattern += 1.15 * evaluateDifficultyOf(rhythm.SamePatterns); + difficulty += terms / (1 + ratio); - difficulty += Math.Max(sameRhythm, samePattern) * intervalPenalty; + // Give bonus to near-1 ratios + difficulty += DifficultyCalculationUtils.BellCurve(ratio, 1, 0.5); + + // Penalize ratios that are VERY near 1 + difficulty -= DifficultyCalculationUtils.BellCurve(ratio, 1, 0.3); + + difficulty = Math.Max(difficulty, 0); + difficulty /= Math.Sqrt(8); return difficulty; } + + /// + /// Multiplier for a given denominator term. + /// + private static double termPenalty(double ratio, int denominator, double power, double multiplier) => + -multiplier * Math.Pow(Math.Cos(denominator * Math.PI * ratio), power); } } From 46ff9d1aad2d70616114a6b6075b1bdbe6a8f0f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jan 2025 14:13:42 +0900 Subject: [PATCH 113/801] Fix beat snap grid being lines not being corectly centered to time This was pointed out as an issue in the osu!taiko editor, but actually affects all rulesets. Has now been fixed everywhere. --- Closes https://github.com/ppy/osu/issues/31548. osu!mania could arguable be consdiered "more correct" with the old display, but I don't think it's a huge deal either way (subjective at best). --- .../Edit/Compose/Components/BeatSnapGrid.cs | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/BeatSnapGrid.cs index 766d5b5601..f1b7951999 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatSnapGrid.cs @@ -185,9 +185,28 @@ namespace osu.Game.Screens.Edit.Compose.Components private void onDirectionChanged(ValueChangedEvent direction) { - Origin = Anchor = direction.NewValue == ScrollingDirection.Up - ? Anchor.TopLeft - : Anchor.BottomLeft; + switch (direction.NewValue) + { + case ScrollingDirection.Up: + Anchor = Anchor.TopLeft; + Origin = Anchor.CentreLeft; + break; + + case ScrollingDirection.Down: + Anchor = Anchor.BottomLeft; + Origin = Anchor.CentreLeft; + break; + + case ScrollingDirection.Left: + Anchor = Anchor.TopLeft; + Origin = Anchor.TopCentre; + break; + + case ScrollingDirection.Right: + Anchor = Anchor.TopRight; + Origin = Anchor.TopCentre; + break; + } bool isHorizontal = direction.NewValue == ScrollingDirection.Left || direction.NewValue == ScrollingDirection.Right; From f13304293603b49b304d6acf66f2941310943064 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 21 Jan 2025 01:14:18 -0500 Subject: [PATCH 114/801] Fix silly mistake --- .../Overlays/Settings/Sections/Maintenance/GeneralSettings.cs | 2 +- .../Sections/Maintenance/SystemFileImportComponent.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index ed3e72adbe..99b25808a1 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance private SystemFileImportComponent systemFileImport = null!; [BackgroundDependencyLoader] - private void load(OsuGame game, GameHost host, IPerformFromScreenRunner? performer) + private void load(OsuGameBase game, GameHost host, IPerformFromScreenRunner? performer) { Add(systemFileImport = new SystemFileImportComponent(game, host)); diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs index 9827872702..ded8c81891 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs @@ -10,12 +10,12 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { public partial class SystemFileImportComponent : Component { - private readonly OsuGame game; + private readonly OsuGameBase game; private readonly GameHost host; private ISystemFileSelector? selector; - public SystemFileImportComponent(OsuGame game, GameHost host) + public SystemFileImportComponent(OsuGameBase game, GameHost host) { this.game = game; this.host = host; From a7c9f84a93fd285c58a914615f40380a454a6884 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jan 2025 15:14:39 +0900 Subject: [PATCH 115/801] Adjust visuals slightly --- .../Screens/Edit/Timing/MetronomeDisplay.cs | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs index 8a4f1c01b1..f3bd9ff257 100644 --- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs @@ -17,9 +17,10 @@ using osu.Framework.Threading; using osu.Framework.Timing; using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; using osu.Game.Overlays; +using osu.Game.Utils; using osuTK; namespace osu.Game.Screens.Edit.Timing @@ -28,7 +29,7 @@ namespace osu.Game.Screens.Edit.Timing { private Container swing = null!; - private OsuSpriteText bpmText = null!; + private OsuTextFlowContainer bpmText = null!; private Drawable weight = null!; private Drawable stick = null!; @@ -213,10 +214,15 @@ namespace osu.Game.Screens.Edit.Timing }, } }, - bpmText = new OsuSpriteText + bpmText = new OsuTextFlowContainer(st => + { + st.Font = OsuFont.Default.With(fixedWidth: true); + st.Spacing = new Vector2(-2.2f, 0); + }) { Name = @"BPM display", Colour = overlayColourProvider.Content1, + AutoSizeAxes = Axes.Both, Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, Y = -3, @@ -262,13 +268,20 @@ namespace osu.Game.Screens.Edit.Timing private void updateBpmText() { + int intPart = (int)interpolatedBpm.Value; + + bpmText.Text = intPart.ToLocalisableString(); + // While interpolating between two integer values, showing the decimal places would look a bit odd // so rounding is applied until we're close to the final value. - double bpm = Precision.AlmostEquals(interpolatedBpm.Value, effectiveBpm, 1.0) - ? effectiveBpm - : Math.Round(interpolatedBpm.Value); + int decimalPlaces = FormatUtils.FindPrecision((decimal)effectiveBpm); - bpmText.Text = bpm.ToLocalisableString("0.##"); + if (decimalPlaces > 0) + { + bool reachedFinalNumber = intPart == (int)effectiveBpm; + + bpmText.AddText((effectiveBpm % 1).ToLocalisableString("." + new string('0', decimalPlaces)), cp => cp.Alpha = reachedFinalNumber ? 0.5f : 0.1f); + } } protected override void Update() @@ -294,7 +307,7 @@ namespace osu.Game.Screens.Edit.Timing weight.MoveToY((float)Interpolation.Lerp(0.1f, 0.83f, bpmRatio), 600, Easing.OutQuint); - this.TransformBindableTo(interpolatedBpm, effectiveBpm, 600, Easing.OutQuint); + this.TransformBindableTo(interpolatedBpm, effectiveBpm, 300, Easing.OutExpo); } if (!BeatSyncSource.Clock.IsRunning && isSwinging) From 3f51626f07cd76c332518e277000f9931cd4ccee Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 21 Jan 2025 02:20:48 -0500 Subject: [PATCH 116/801] Simplify code immensely Co-authored-by: Dean Herbert --- .../Sections/Maintenance/GeneralSettings.cs | 15 +++--- .../Maintenance/SystemFileImportComponent.cs | 51 ------------------- 2 files changed, 9 insertions(+), 57 deletions(-) delete mode 100644 osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 99b25808a1..47314dcafe 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; @@ -17,12 +19,13 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { protected override LocalisableString Header => CommonStrings.General; - private SystemFileImportComponent systemFileImport = null!; + private ISystemFileSelector? selector; [BackgroundDependencyLoader] private void load(OsuGameBase game, GameHost host, IPerformFromScreenRunner? performer) { - Add(systemFileImport = new SystemFileImportComponent(game, host)); + if ((selector = host.CreateSystemFileSelector(game.HandledExtensions.ToArray())) != null) + selector.Selected += f => Task.Run(() => game.Import(f.FullName)); AddRange(new Drawable[] { @@ -31,10 +34,10 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Text = DebugSettingsStrings.ImportFiles, Action = () => { - if (systemFileImport.PresentIfAvailable()) - return; - - performer?.PerformFromScreen(menu => menu.Push(new FileImportScreen())); + if (selector != null) + selector.Present(); + else + performer?.PerformFromScreen(menu => menu.Push(new FileImportScreen())); }, }, new SettingsButton diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs deleted file mode 100644 index ded8c81891..0000000000 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs +++ /dev/null @@ -1,51 +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 System.Threading.Tasks; -using osu.Framework.Graphics; -using osu.Framework.Platform; - -namespace osu.Game.Overlays.Settings.Sections.Maintenance -{ - public partial class SystemFileImportComponent : Component - { - private readonly OsuGameBase game; - private readonly GameHost host; - - private ISystemFileSelector? selector; - - public SystemFileImportComponent(OsuGameBase game, GameHost host) - { - this.game = game; - this.host = host; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - selector = host.CreateSystemFileSelector(game.HandledExtensions.ToArray()); - - if (selector != null) - selector.Selected += f => Schedule(() => startImport(f.FullName)); - } - - public bool PresentIfAvailable() - { - if (selector == null) - return false; - - selector.Present(); - return true; - } - - private void startImport(string path) - { - Task.Factory.StartNew(async () => - { - await game.Import(path).ConfigureAwait(false); - }, TaskCreationOptions.LongRunning); - } - } -} From 3a37817ab20bc1add6534b4a077f56619ead6dcc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jan 2025 19:01:33 +0900 Subject: [PATCH 117/801] Don't block `Popover` escape handling (just let it work in addition to `GlobalAction.Back`) --- osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs b/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs index 9b4689958c..7abaca4092 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs @@ -14,7 +14,6 @@ using osu.Framework.Input.Events; using osu.Game.Input.Bindings; using osu.Game.Overlays; using osuTK; -using osuTK.Input; namespace osu.Game.Graphics.UserInterfaceV2 { @@ -75,14 +74,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 samplePopOut?.Play(); } - protected override bool OnKeyDown(KeyDownEvent e) - { - if (e.Key == Key.Escape) - return false; // disable the framework-level handling of escape key for conformity (we use GlobalAction.Back). - - return base.OnKeyDown(e); - } - public virtual bool OnPressed(KeyBindingPressEvent e) { if (e.Repeat) From 9a12f48dcc2ccae6889f35a4add888c4112babd9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jan 2025 18:55:42 +0900 Subject: [PATCH 118/801] Fix `ComposeBlueprintContainer` handling nudge keys when it can't nudge --- .../Components/ComposeBlueprintContainer.cs | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 5d93c4ea9d..15bbddd97e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -111,25 +111,26 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override bool OnKeyDown(KeyDownEvent e) { + // Until the keys below are global actions, this will prevent conflicts with "seek between sample points" + // which has a default of ctrl+shift+arrows. + if (e.ShiftPressed) + return false; + if (e.ControlPressed) { switch (e.Key) { case Key.Left: - nudgeSelection(new Vector2(-1, 0)); - return true; + return nudgeSelection(new Vector2(-1, 0)); case Key.Right: - nudgeSelection(new Vector2(1, 0)); - return true; + return nudgeSelection(new Vector2(1, 0)); case Key.Up: - nudgeSelection(new Vector2(0, -1)); - return true; + return nudgeSelection(new Vector2(0, -1)); case Key.Down: - nudgeSelection(new Vector2(0, 1)); - return true; + return nudgeSelection(new Vector2(0, 1)); } } @@ -151,7 +152,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Move the current selection spatially by the specified delta, in gamefield coordinates (ie. the same coordinates as the blueprints). /// /// - private void nudgeSelection(Vector2 delta) + private bool nudgeSelection(Vector2 delta) { if (!nudgeMovementActive) { @@ -162,12 +163,13 @@ namespace osu.Game.Screens.Edit.Compose.Components var firstBlueprint = SelectionHandler.SelectedBlueprints.FirstOrDefault(); if (firstBlueprint == null) - return; + return false; // convert to game space coordinates delta = firstBlueprint.ToScreenSpace(delta) - firstBlueprint.ToScreenSpace(Vector2.Zero); SelectionHandler.HandleMovement(new MoveSelectionEvent(firstBlueprint, delta)); + return true; } private void updatePlacementNewCombo() From aeca91cde28d29a82a4d159f7f93f9e2251b4b47 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jan 2025 18:55:28 +0900 Subject: [PATCH 119/801] Fix main menu osu logo being activated by function keys and escape --- osu.Game/Screens/Menu/ButtonSystem.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 41920605b0..25fa689d4c 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -245,6 +245,15 @@ namespace osu.Game.Screens.Menu if (e.Repeat || e.ControlPressed || e.ShiftPressed || e.AltPressed || e.SuperPressed) return false; + if (e.Key >= Key.F1 && e.Key <= Key.F35) + return false; + + switch (e.Key) + { + case Key.Escape: + return false; + } + if (triggerInitialOsuLogo()) return true; From b6e7b43b11859046b71c0023e4bfca090cd2f961 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jan 2025 18:52:18 +0900 Subject: [PATCH 120/801] Remove unnecessary input blocking This was already done by `OverlayContainer`. --- osu.Game/Screens/Play/GameplayMenuOverlay.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayMenuOverlay.cs b/osu.Game/Screens/Play/GameplayMenuOverlay.cs index 2b961278d5..ffd7845356 100644 --- a/osu.Game/Screens/Play/GameplayMenuOverlay.cs +++ b/osu.Game/Screens/Play/GameplayMenuOverlay.cs @@ -166,11 +166,6 @@ namespace osu.Game.Screens.Play protected override void PopOut() => this.FadeOut(TRANSITION_DURATION, Easing.In); - // Don't let mouse down events through the overlay or people can click circles while paused. - protected override bool OnMouseDown(MouseDownEvent e) => true; - - protected override bool OnMouseMove(MouseMoveEvent e) => true; - protected void AddButton(LocalisableString text, Color4 colour, Action? action) { var button = new Button From c8cc36e9af69c551a6149b12ed376fa84f1ac32d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jan 2025 17:24:38 +0900 Subject: [PATCH 121/801] Add failing test coverage of random rewind button not working --- .../Navigation/TestSceneScreenNavigation.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 521d097fb9..88b482ab4c 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -48,6 +48,7 @@ using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.Select.Options; using osu.Game.Tests.Beatmaps.IO; +using osu.Game.Tests.Resources; using osu.Game.Utils; using osuTK; using osuTK.Input; @@ -202,6 +203,38 @@ namespace osu.Game.Tests.Visual.Navigation TextBox filterControlTextBox() => songSelect.ChildrenOfType().Single(); } + [Test] + public void TestSongSelectRandomRewindButton() + { + Guid? originalSelection = null; + TestPlaySongSelect songSelect = null; + + PushAndConfirm(() => songSelect = new TestPlaySongSelect()); + AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + + AddStep("Add two beatmaps", () => + { + Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo(8)); + Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo(8)); + }); + + AddUntilStep("wait for selected", () => + { + originalSelection = Game.Beatmap.Value.BeatmapInfo.ID; + return !Game.Beatmap.IsDefault; + }); + + AddStep("hit random", () => + { + InputManager.MoveMouseTo(Game.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("wait for selection changed", () => Game.Beatmap.Value.BeatmapInfo.ID, () => Is.Not.EqualTo(originalSelection)); + + AddStep("hit random rewind", () => InputManager.Click(MouseButton.Right)); + AddUntilStep("wait for selection reverted", () => Game.Beatmap.Value.BeatmapInfo.ID, () => Is.EqualTo(originalSelection)); + } + [Test] public void TestSongSelectScrollHandling() { From 66be9f2d1b9908001baacf24329bdba585a8ac3e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jan 2025 17:05:39 +0900 Subject: [PATCH 122/801] Remove right click default for absolute scroll --- osu.Game/Database/RealmAccess.cs | 14 +++++++++++++- osu.Game/Input/Bindings/GlobalActionContainer.cs | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index e1b8de89fa..f0f5864e32 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -97,8 +97,9 @@ namespace osu.Game.Database /// 44 2024-11-22 Removed several properties from BeatmapInfo which did not need to be persisted to realm. /// 45 2024-12-23 Change beat snap divisor adjust defaults to be Ctrl+Scroll instead of Ctrl+Shift+Scroll, if not already changed by user. /// 46 2024-12-26 Change beat snap divisor bindings to match stable directionality ¯\_(ツ)_/¯. + /// 47 2025-01-21 Remove right mouse button binding for absolute scroll. Never use mouse buttons (or scroll) for global actions. /// - private const int schema_version = 46; + private const int schema_version = 47; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. @@ -1239,6 +1240,17 @@ namespace osu.Game.Database break; } + + case 47: + { + var keyBindings = migration.NewRealm.All(); + + var existingBinding = keyBindings.FirstOrDefault(k => k.ActionInt == (int)GlobalAction.AbsoluteScrollSongList); + if (existingBinding != null && existingBinding.KeyCombination.Keys.SequenceEqual(new[] { InputKey.MouseRight })) + migration.NewRealm.Remove(existingBinding); + + break; + } } Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms"); diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 6c130ff309..599ca6d6c1 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -205,7 +205,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.BackSpace, GlobalAction.DeselectAllMods), new KeyBinding(new[] { InputKey.Control, InputKey.Up }, GlobalAction.IncreaseModSpeed), new KeyBinding(new[] { InputKey.Control, InputKey.Down }, GlobalAction.DecreaseModSpeed), - new KeyBinding(new[] { InputKey.MouseRight }, GlobalAction.AbsoluteScrollSongList), + new KeyBinding(InputKey.None, GlobalAction.AbsoluteScrollSongList), }; private static IEnumerable audioControlKeyBindings => new[] From 6c27e87714ec959d017a2c198b095ea5bfdbb08e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jan 2025 17:12:45 +0900 Subject: [PATCH 123/801] Add back explicit right click handling of carousel absolute scrolling --- osu.Game/Screens/Select/BeatmapCarousel.cs | 40 ++++++++++++++++----- osu.Game/Screens/SelectV2/Carousel.cs | 41 ++++++++++++++++------ 2 files changed, 62 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 7e3c26a1ba..a807fc6a34 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -1181,14 +1181,7 @@ namespace osu.Game.Screens.Select switch (e.Action) { case GlobalAction.AbsoluteScrollSongList: - // The default binding for absolute scroll is right mouse button. - // To avoid conflicts with context menus, disallow absolute scroll completely if it looks like things will fall over. - if (e.CurrentState.Mouse.Buttons.Contains(MouseButton.Right) - && GetContainingInputManager()!.HoveredDrawables.OfType().Any()) - return false; - - ScrollToAbsolutePosition(e.CurrentState.Mouse.Position); - absoluteScrolling = true; + beginAbsoluteScrolling(e); return true; } @@ -1200,11 +1193,32 @@ namespace osu.Game.Screens.Select switch (e.Action) { case GlobalAction.AbsoluteScrollSongList: - absoluteScrolling = false; + endAbsoluteScrolling(); break; } } + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button == MouseButton.Right) + { + // To avoid conflicts with context menus, disallow absolute scroll if it looks like things will fall over. + if (GetContainingInputManager()!.HoveredDrawables.OfType().Any()) + return false; + + beginAbsoluteScrolling(e); + } + + return base.OnMouseDown(e); + } + + protected override void OnMouseUp(MouseUpEvent e) + { + if (e.Button == MouseButton.Right) + endAbsoluteScrolling(); + base.OnMouseUp(e); + } + protected override bool OnMouseMove(MouseMoveEvent e) { if (absoluteScrolling) @@ -1216,6 +1230,14 @@ namespace osu.Game.Screens.Select return base.OnMouseMove(e); } + private void beginAbsoluteScrolling(UIEvent e) + { + ScrollToAbsolutePosition(e.CurrentState.Mouse.Position); + absoluteScrolling = true; + } + + private void endAbsoluteScrolling() => absoluteScrolling = false; + #endregion protected override ScrollbarContainer CreateScrollbar(Direction direction) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index a07022b32f..ec1bf6b7c0 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -493,15 +493,7 @@ namespace osu.Game.Screens.SelectV2 switch (e.Action) { case GlobalAction.AbsoluteScrollSongList: - - // The default binding for absolute scroll is right mouse button. - // To avoid conflicts with context menus, disallow absolute scroll completely if it looks like things will fall over. - if (e.CurrentState.Mouse.Buttons.Contains(MouseButton.Right) - && GetContainingInputManager()!.HoveredDrawables.OfType().Any()) - return false; - - ScrollToAbsolutePosition(e.CurrentState.Mouse.Position); - absoluteScrolling = true; + beginAbsoluteScrolling(e); return true; } @@ -513,11 +505,32 @@ namespace osu.Game.Screens.SelectV2 switch (e.Action) { case GlobalAction.AbsoluteScrollSongList: - absoluteScrolling = false; + endAbsoluteScrolling(); break; } } + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button == MouseButton.Right) + { + // To avoid conflicts with context menus, disallow absolute scroll if it looks like things will fall over. + if (GetContainingInputManager()!.HoveredDrawables.OfType().Any()) + return false; + + beginAbsoluteScrolling(e); + } + + return base.OnMouseDown(e); + } + + protected override void OnMouseUp(MouseUpEvent e) + { + if (e.Button == MouseButton.Right) + endAbsoluteScrolling(); + base.OnMouseUp(e); + } + protected override bool OnMouseMove(MouseMoveEvent e) { if (absoluteScrolling) @@ -529,6 +542,14 @@ namespace osu.Game.Screens.SelectV2 return base.OnMouseMove(e); } + private void beginAbsoluteScrolling(UIEvent e) + { + ScrollToAbsolutePosition(e.CurrentState.Mouse.Position); + absoluteScrolling = true; + } + + private void endAbsoluteScrolling() => absoluteScrolling = false; + #endregion } From 0265a2900050d0c11df6d09a38b970ab4f80b923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 21 Jan 2025 10:02:16 +0100 Subject: [PATCH 124/801] Move bindings to `LoadComplete()` --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 7b6bf6f55e..c784fc298a 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -43,8 +43,14 @@ namespace osu.Game.Screens.Play.HUD private FillFlowContainer spectatorsFlow = null!; private DrawablePool pool = null!; + [Resolved] + private SpectatorClient client { get; set; } = null!; + + [Resolved] + private GameplayState gameplayState { get; set; } = null!; + [BackgroundDependencyLoader] - private void load(OsuColour colours, SpectatorClient client, GameplayState gameplayState) + private void load(OsuColour colours) { AutoSizeAxes = Axes.Y; @@ -73,15 +79,15 @@ namespace osu.Game.Screens.Play.HUD }; HeaderColour.Value = Header.Colour; - - ((IBindableList)Spectators).BindTo(client.WatchingUsers); - ((IBindable)UserPlayingState).BindTo(gameplayState.PlayingState); } protected override void LoadComplete() { base.LoadComplete(); + ((IBindableList)Spectators).BindTo(client.WatchingUsers); + ((IBindable)UserPlayingState).BindTo(gameplayState.PlayingState); + Spectators.BindCollectionChanged(onSpectatorsChanged, true); UserPlayingState.BindValueChanged(_ => updateVisibility()); From f88102610d5272fccc32b5d0a73782b5d0c2d127 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jan 2025 18:35:56 +0900 Subject: [PATCH 125/801] Add tooltips explaining multiplayer mod selection buttons --- osu.Game/Localisation/MultiplayerMatchStrings.cs | 15 +++++++++++++++ .../Screens/OnlinePlay/FooterButtonFreeMods.cs | 3 +++ .../Screens/OnlinePlay/FooterButtonFreeStyle.cs | 3 +++ .../Screens/OnlinePlay/OnlinePlaySongSelect.cs | 7 +++++-- .../Screens/OnlinePlay/OnlinePlayStyleSelect.cs | 2 +- osu.Game/Screens/Select/SongSelect.cs | 2 +- 6 files changed, 28 insertions(+), 4 deletions(-) diff --git a/osu.Game/Localisation/MultiplayerMatchStrings.cs b/osu.Game/Localisation/MultiplayerMatchStrings.cs index 95c7168a09..8c9e76d722 100644 --- a/osu.Game/Localisation/MultiplayerMatchStrings.cs +++ b/osu.Game/Localisation/MultiplayerMatchStrings.cs @@ -24,6 +24,21 @@ namespace osu.Game.Localisation /// public static LocalisableString StartMatchWithCountdown(string humanReadableTime) => new TranslatableString(getKey(@"start_match_width_countdown"), @"Start match in {0}", humanReadableTime); + /// + /// "Choose the mods which all players should play with." + /// + public static LocalisableString RequiredModsButtonTooltip => new TranslatableString(getKey(@"required_mods_button_tooltip"), @"Choose the mods which all players should play with."); + + /// + /// "Each player can choose their preferred mods from a selected list." + /// + public static LocalisableString FreeModsButtonTooltip => new TranslatableString(getKey(@"free_mods_button_tooltip"), @"Each player can choose their preferred mods from a selected list."); + + /// + /// "Each player can choose their preferred difficulty, ruleset and mods." + /// + public static LocalisableString FreestyleButtonTooltip => new TranslatableString(getKey(@"freestyle_button_tooltip"), @"Each player can choose their preferred difficulty, ruleset and mods."); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs index 952b15a873..402f538716 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs @@ -18,6 +18,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select; using osuTK; +using osu.Game.Localisation; namespace osu.Game.Screens.OnlinePlay { @@ -95,6 +96,8 @@ namespace osu.Game.Screens.OnlinePlay SelectedColour = colours.Yellow; DeselectedColour = SelectedColour.Opacity(0.5f); Text = @"freemods"; + + TooltipText = MultiplayerMatchStrings.FreeModsButtonTooltip; } protected override void LoadComplete() diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs index cdfb73cee1..0e22b3d3fb 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Screens.Select; +using osu.Game.Localisation; namespace osu.Game.Screens.OnlinePlay { @@ -72,6 +73,8 @@ namespace osu.Game.Screens.OnlinePlay SelectedColour = colours.Yellow; DeselectedColour = SelectedColour.Opacity(0.5f); Text = @"freestyle"; + + TooltipText = MultiplayerMatchStrings.FreestyleButtonTooltip; } protected override void LoadComplete() diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 9df01ead42..f6403c010e 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -21,6 +21,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select; using osu.Game.Users; using osu.Game.Utils; +using osu.Game.Localisation; namespace osu.Game.Screens.OnlinePlay { @@ -196,14 +197,16 @@ namespace osu.Game.Screens.OnlinePlay IsValidMod = IsValidMod }; - protected override IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons() + protected override IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons() { var baseButtons = base.CreateSongSelectFooterButtons().ToList(); + baseButtons.Single(i => i.button is FooterButtonMods).button.TooltipText = MultiplayerMatchStrings.RequiredModsButtonTooltip; + freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) { Current = FreeMods }; var freeStyleButton = new FooterButtonFreeStyle { Current = FreeStyle }; - baseButtons.InsertRange(baseButtons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[] + baseButtons.InsertRange(baseButtons.FindIndex(b => b.button is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[] { (freeModsFooterButton, null), (freeStyleButton, null) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs index d1fcf94152..22290f8fed 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.OnlinePlay protected override FilterControl CreateFilterControl() => new DifficultySelectFilterControl(item); - protected override IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons() + protected override IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons() { // Required to create the drawable components. base.CreateSongSelectFooterButtons(); diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index dda7b568d2..c20dcb8593 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -415,7 +415,7 @@ namespace osu.Game.Screens.Select /// Creates the buttons to be displayed in the footer. /// /// A set of and an optional which the button opens when pressed. - protected virtual IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons() => new (FooterButton, OverlayContainer?)[] + protected virtual IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons() => new (FooterButton, OverlayContainer?)[] { (ModsFooterButton = new FooterButtonMods { Current = Mods }, ModSelect), (new FooterButtonRandom From 6ec718304e4df307d8ae3598de96585ff836a99e Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 21 Jan 2025 04:58:27 -0500 Subject: [PATCH 126/801] Revert "Fix triangles judgement mispositioned on a miss" This reverts commit e5713e52392066a1430ebce460d07d8af01ad29f. --- .../UI/DrawableManiaJudgement.cs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index a1dabd66bc..5b87c74bbe 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -6,7 +6,6 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; -using osuTK; namespace osu.Game.Rulesets.Mania.UI { @@ -36,20 +35,8 @@ namespace osu.Game.Rulesets.Mania.UI switch (Result) { case HitResult.None: - this.FadeOutFromOne(800); - break; - case HitResult.Miss: - this.ScaleTo(1.6f); - this.ScaleTo(1, 100, Easing.In); - - this.MoveToY(judgement_y_position); - this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); - - this.RotateTo(0); - this.RotateTo(40, 800, Easing.InQuint); - - this.FadeOutFromOne(800); + base.PlayAnimation(); break; default: From cc7c549468591c0414ad8425f8e0118b1b91dd33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Tue, 21 Jan 2025 11:02:28 +0100 Subject: [PATCH 127/801] Add test scene for clipboard snapping --- .../TestSceneEditorClipboardSnapping.cs | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 osu.Game.Tests/Visual/Editing/TestSceneEditorClipboardSnapping.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboardSnapping.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboardSnapping.cs new file mode 100644 index 0000000000..e32cad12d2 --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboardSnapping.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. + +#nullable disable + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Tests.Visual.Editing +{ + public partial class TestSceneEditorClipboardSnapping : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); + + private const double beat_length = 60_000 / 180.0; // 180 bpm + private const double timing_point_time = 1500; + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) + { + var controlPointInfo = new ControlPointInfo(); + controlPointInfo.Add(timing_point_time, new TimingControlPoint { BeatLength = beat_length }); + return new TestBeatmap(ruleset, false) + { + ControlPointInfo = controlPointInfo + }; + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + [TestCase(4)] + [TestCase(6)] + [TestCase(8)] + [TestCase(12)] + [TestCase(16)] + public void TestPasteSnapping(int divisor) + { + const double paste_time = timing_point_time + 1271; // arbitrary timestamp that doesn't snap to the timing point at any divisor + + var addedObjects = new HitObject[] + { + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 1200 }, + }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects)); + AddStep("select added objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects)); + AddStep("copy hitobjects", () => Editor.Copy()); + + AddStep($"set beat divisor to 1/{divisor}", () => + { + var beatDivisor = (BindableBeatDivisor)Editor.Dependencies.Get(typeof(BindableBeatDivisor)); + beatDivisor.SetArbitraryDivisor(divisor); + }); + + AddStep("move forward in time", () => EditorClock.Seek(paste_time)); + AddAssert("not at snapped time", () => EditorClock.CurrentTime != EditorBeatmap.SnapTime(EditorClock.CurrentTime, null)); + + AddStep("paste hitobjects", () => Editor.Paste()); + + AddAssert("first object is snapped", () => Precision.AlmostEquals( + EditorBeatmap.SelectedHitObjects.MinBy(h => h.StartTime).StartTime, + EditorBeatmap.ControlPointInfo.GetClosestSnappedTime(paste_time, divisor) + )); + + AddAssert("duration between pasted objects is same", () => + { + var firstObject = EditorBeatmap.SelectedHitObjects.MinBy(h => h.StartTime); + var secondObject = EditorBeatmap.SelectedHitObjects.MaxBy(h => h.StartTime); + + return Precision.AlmostEquals(secondObject.StartTime - firstObject.StartTime, addedObjects[1].StartTime - addedObjects[0].StartTime); + }); + } + } +} From 001d9cacf21cbe9dee9330b01b9e496e7be1f4f5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 21 Jan 2025 19:31:49 +0900 Subject: [PATCH 128/801] Configure awaiters --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 4 ++-- osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index d0c3a1fa06..e5eade8c1d 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -171,7 +171,7 @@ namespace osu.Game.Online.Multiplayer throw new InvalidOperationException("Cannot join a multiplayer room while already in one."); var cancellationSource = joinCancellationSource = new CancellationTokenSource(); - await initRoom(room, r => CreateRoom(new MultiplayerRoom(room)), cancellationSource.Token); + await initRoom(room, r => CreateRoom(new MultiplayerRoom(room)), cancellationSource.Token).ConfigureAwait(false); } /// @@ -187,7 +187,7 @@ namespace osu.Game.Online.Multiplayer Debug.Assert(room.RoomID != null); var cancellationSource = joinCancellationSource = new CancellationTokenSource(); - await initRoom(room, r => JoinRoom(room.RoomID.Value, password ?? room.Password), cancellationSource.Token); + await initRoom(room, r => JoinRoom(room.RoomID.Value, password ?? room.Password), cancellationSource.Token).ConfigureAwait(false); } private async Task initRoom(Room room, Func> initFunc, CancellationToken cancellationToken) diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 524873ef66..05f3e44405 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -275,7 +275,7 @@ namespace osu.Game.Online.Multiplayer try { - return await connection.InvokeAsync(nameof(IMultiplayerServer.CreateRoom), room); + return await connection.InvokeAsync(nameof(IMultiplayerServer.CreateRoom), room).ConfigureAwait(false); } catch (HubException exception) { From b63d94101c1ecc69b68d0e4002b208b5492ab4cf Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 21 Jan 2025 05:45:37 -0500 Subject: [PATCH 129/801] Reapply "Fix triangles judgement mispositioned on a miss" This reverts commit 6ec718304e4df307d8ae3598de96585ff836a99e. --- .../UI/DrawableManiaJudgement.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index 5b87c74bbe..a1dabd66bc 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; +using osuTK; namespace osu.Game.Rulesets.Mania.UI { @@ -35,8 +36,20 @@ namespace osu.Game.Rulesets.Mania.UI switch (Result) { case HitResult.None: + this.FadeOutFromOne(800); + break; + case HitResult.Miss: - base.PlayAnimation(); + this.ScaleTo(1.6f); + this.ScaleTo(1, 100, Easing.In); + + this.MoveToY(judgement_y_position); + this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); + + this.RotateTo(0); + this.RotateTo(40, 800, Easing.InQuint); + + this.FadeOutFromOne(800); break; default: From 459847cb80b3e34ca4d4bf35dabd7d1d081b94d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jan 2025 19:51:13 +0900 Subject: [PATCH 130/801] Perform client side validation that the selected beatmap and ruleset have valid online IDs This is local to playlists, since in multiplayer the validation is already provided by `osu-server-spectator`. --- osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs | 1 + .../OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs | 7 +++++++ osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs | 3 +++ osu.Game/Screens/Select/FilterCriteria.cs | 2 ++ 4 files changed, 13 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs index 22290f8fed..4d34000d3c 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs @@ -89,6 +89,7 @@ namespace osu.Game.Screens.OnlinePlay // Must be from the same set as the playlist item. criteria.BeatmapSetId = beatmapSetId; + criteria.HasOnlineID = true; // Must be within 30s of the playlist item. criteria.Length.Min = itemLength - 30000; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs index f3d868b0de..912496ba34 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs @@ -21,6 +21,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override bool OnStart() { + // Beatmaps without a valid online ID are filtered away; this is just a final safety. + if (base.Beatmap.Value.BeatmapInfo.OnlineID < 0) + return false; + + if (base.Ruleset.Value.OnlineID < 0) + return false; + Beatmap.Value = base.Beatmap.Value.BeatmapInfo; Ruleset.Value = base.Ruleset.Value; this.Exit(); diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 95186e98d8..dc77b0101e 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.HasOnlineID == true) + match &= BeatmapInfo.OnlineID >= 0; + if (match && criteria.BeatmapSetId != null) match &= criteria.BeatmapSetId == BeatmapInfo.BeatmapSet?.OnlineID; diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 63dbdfbed3..15cb3c5104 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -58,6 +58,8 @@ namespace osu.Game.Screens.Select public bool AllowConvertedBeatmaps; public int? BeatmapSetId; + public bool? HasOnlineID; + private string searchText = string.Empty; /// From fa20bc6631b084b4fbd3b97c3cd257a005379b0e Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 14:24:04 +0000 Subject: [PATCH 131/801] Remove `EffectiveBPMPreprocessor` --- .../Preprocessing/Reading/EffectiveBPM.cs | 50 ------------------- .../Preprocessing/TaikoDifficultyHitObject.cs | 27 +++++++++- .../Difficulty/TaikoDifficultyCalculator.cs | 13 +++-- 3 files changed, 32 insertions(+), 58 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs deleted file mode 100644 index 17e05d5fbf..0000000000 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs +++ /dev/null @@ -1,50 +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.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; - -namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Reading -{ - public class EffectiveBPMPreprocessor - { - private readonly IList noteObjects; - private readonly double globalSliderVelocity; - - public EffectiveBPMPreprocessor(IBeatmap beatmap, List noteObjects) - { - this.noteObjects = noteObjects; - globalSliderVelocity = beatmap.Difficulty.SliderMultiplier; - } - - /// - /// Calculates and sets the effective BPM and slider velocity for each note object, considering clock rate and scroll speed. - /// - public void ProcessEffectiveBPM(ControlPointInfo controlPointInfo, double clockRate) - { - foreach (var currentNoteObject in noteObjects) - { - double startTime = currentNoteObject.StartTime * clockRate; - - // Retrieve the timing point at the note's start time - TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(startTime); - - // Calculate the slider velocity at the note's start time. - double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, startTime, clockRate); - currentNoteObject.CurrentSliderVelocity = currentSliderVelocity; - - currentNoteObject.EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity; - } - } - - /// - /// Calculates the slider velocity based on control point info and clock rate. - /// - private double calculateSliderVelocity(ControlPointInfo controlPointInfo, double startTime, double clockRate) - { - var activeEffectControlPoint = controlPointInfo.EffectPointAt(startTime); - return globalSliderVelocity * (activeEffectControlPoint.ScrollSpeed) * clockRate; - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index dfcd08ed94..34c4871a42 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Objects; @@ -76,11 +77,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// The list of rim (kat) s in the current beatmap. /// The list of s that is a hit (i.e. not a drumroll or swell) in the current beatmap. /// The position of this in the list. + /// The control point info of the beatmap. + /// The global slider velocity of the beatmap. public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, List objects, List centreHitObjects, List rimHitObjects, - List noteObjects, int index) + List noteObjects, int index, + ControlPointInfo controlPointInfo, + double globalSliderVelocity) : base(hitObject, lastObject, clockRate, objects, index) { noteDifficultyHitObjects = noteObjects; @@ -111,6 +116,26 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing NoteIndex = noteObjects.Count; noteObjects.Add(this); } + + double startTime = hitObject.StartTime * clockRate; + + // Retrieve the timing point at the note's start time + TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(startTime); + + // Calculate the slider velocity at the note's start time. + double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, startTime, clockRate); + CurrentSliderVelocity = currentSliderVelocity; + + EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity; + } + + /// + /// Calculates the slider velocity based on control point info and clock rate. + /// + private static double calculateSliderVelocity(ControlPointInfo controlPointInfo, double globalSliderVelocity, double startTime, double clockRate) + { + var activeEffectControlPoint = controlPointInfo.EffectPointAt(startTime); + return globalSliderVelocity * (activeEffectControlPoint.ScrollSpeed) * clockRate; } public TaikoDifficultyHitObject? PreviousMono(int backwardsIndex) => monoDifficultyHitObjects?.ElementAtOrDefault(MonoIndex - (backwardsIndex + 1)); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index f3b976f970..1d3075e4ac 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -13,7 +13,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; -using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Reading; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data; using osu.Game.Rulesets.Taiko.Difficulty.Skills; using osu.Game.Rulesets.Taiko.Mods; @@ -72,7 +71,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty var centreObjects = new List(); var rimObjects = new List(); var noteObjects = new List(); - EffectiveBPMPreprocessor bpmLoader = new EffectiveBPMPreprocessor(beatmap, noteObjects); // Generate TaikoDifficultyHitObjects from the beatmap's hit objects. for (int i = 2; i < beatmap.HitObjects.Count; i++) @@ -86,15 +84,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty centreObjects, rimObjects, noteObjects, - difficultyHitObjects.Count + difficultyHitObjects.Count, + beatmap.ControlPointInfo, + beatmap.Difficulty.SliderMultiplier )); } - var groupedHitObjects = SameRhythmHitObjects.GroupHitObjects(noteObjects); - TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects); - SamePatterns.GroupPatterns(groupedHitObjects); - bpmLoader.ProcessEffectiveBPM(beatmap.ControlPointInfo, clockRate); + + var groupedHitObjects = SameRhythmGroupedHitObjects.GroupHitObjects(noteObjects); + SamePatternsGroupedHitObjects.GroupPatterns(groupedHitObjects); return difficultyHitObjects; } From dbe36887f6da2649e9c55e265d6e4eb15429929a Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 14:24:27 +0000 Subject: [PATCH 132/801] Refactor `ColourEvaluator` --- .../Difficulty/Evaluators/ColourEvaluator.cs | 41 ++++++------------- 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index 3ff5b87fb6..c0e90e83c1 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -10,32 +10,8 @@ using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data; namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators { - public class ColourEvaluator + public static class ColourEvaluator { - /// - /// Evaluate the difficulty of the first note of a . - /// - public static double EvaluateDifficultyOf(MonoStreak monoStreak) - { - return DifficultyCalculationUtils.Logistic(exponent: Math.E * monoStreak.Index - 2 * Math.E) * EvaluateDifficultyOf(monoStreak.Parent) * 0.5; - } - - /// - /// Evaluate the difficulty of the first note of a . - /// - public static double EvaluateDifficultyOf(AlternatingMonoPattern alternatingMonoPattern) - { - return DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * EvaluateDifficultyOf(alternatingMonoPattern.Parent); - } - - /// - /// Evaluate the difficulty of the first note of a . - /// - public static double EvaluateDifficultyOf(RepeatingHitPatterns repeatingHitPattern) - { - return 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E)); - } - /// /// Calculates a consistency penalty based on the number of consecutive consistent intervals, /// considering the delta time between each colour sequence. @@ -89,18 +65,27 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators double difficulty = 0.0d; if (colour.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak - difficulty += EvaluateDifficultyOf(colour.MonoStreak); + difficulty += evaluateMonoStreakDifficulty(colour.MonoStreak); if (colour.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern - difficulty += EvaluateDifficultyOf(colour.AlternatingMonoPattern); + difficulty += evaluateAlternatingMonoPatternDifficulty(colour.AlternatingMonoPattern); if (colour.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern - difficulty += EvaluateDifficultyOf(colour.RepeatingHitPattern); + difficulty += evaluateReadingHitPatternDifficulty(colour.RepeatingHitPattern); double consistencyPenalty = consistentRatioPenalty(taikoObject); difficulty *= consistencyPenalty; return difficulty; } + + private static double evaluateMonoStreakDifficulty(MonoStreak monoStreak) => + DifficultyCalculationUtils.Logistic(exponent: Math.E * monoStreak.Index - 2 * Math.E) * evaluateAlternatingMonoPatternDifficulty(monoStreak.Parent) * 0.5; + + private static double evaluateAlternatingMonoPatternDifficulty(AlternatingMonoPattern alternatingMonoPattern) => + DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * evaluateReadingHitPatternDifficulty(alternatingMonoPattern.Parent); + + private static double evaluateReadingHitPatternDifficulty(RepeatingHitPatterns repeatingHitPattern) => + 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E)); } } From 9919179b0b914aba42499467cba38ee2d311034b Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 14:24:46 +0000 Subject: [PATCH 133/801] Format `ReadingEvaluator` --- .../Difficulty/Evaluators/ReadingEvaluator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs index 2a08f65c7b..5871979613 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs @@ -47,8 +47,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators // High density is penalised at high velocity as it is generally considered easier to read. See https://www.desmos.com/calculator/u63f3ntdsi double densityPenalty = DifficultyCalculationUtils.Logistic(objectDensity, 0.925, 15); - double highVelocityDifficulty = (1.0 - 0.33 * densityPenalty) * DifficultyCalculationUtils.Logistic - (effectiveBPM, highVelocity.Center + 8 * densityPenalty, (1.0 + 0.5 * densityPenalty) / (highVelocity.Range / 10)); + double highVelocityDifficulty = (1.0 - 0.33 * densityPenalty) + * DifficultyCalculationUtils.Logistic(effectiveBPM, highVelocity.Center + 8 * densityPenalty, (1.0 + 0.5 * densityPenalty) / (highVelocity.Range / 10)); return midVelocityDifficulty + highVelocityDifficulty; } From b8c79d58a731943f46433298db8eb0523ec850b7 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 14:25:28 +0000 Subject: [PATCH 134/801] Refactor `StaminaEvaluator` --- .../Difficulty/Evaluators/StaminaEvaluator.cs | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs index b39ad953a4..a9884b2328 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs @@ -8,8 +8,34 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators { - public class StaminaEvaluator + public static class StaminaEvaluator { + /// + /// Evaluates the minimum mechanical stamina required to play the current object. This is calculated using the + /// maximum possible interval between two hits using the same key, by alternating available fingers for each colour. + /// + public static double EvaluateDifficultyOf(DifficultyHitObject current) + { + if (current.BaseObject is not Hit) + { + return 0.0; + } + + // Find the previous hit object hit by the current finger, which is n notes prior, n being the number of + // available fingers. + TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current; + TaikoDifficultyHitObject? taikoPrevious = current.Previous(1) as TaikoDifficultyHitObject; + TaikoDifficultyHitObject? previousMono = taikoCurrent.PreviousMono(availableFingersFor(taikoCurrent) - 1); + + double objectStrain = 0.5; // Add a base strain to all objects + if (taikoPrevious == null) return objectStrain; + + if (previousMono != null) + objectStrain += speedBonus(taikoCurrent.StartTime - previousMono.StartTime) + 0.5 * speedBonus(taikoCurrent.StartTime - taikoPrevious.StartTime); + + return objectStrain; + } + /// /// Applies a speed bonus dependent on the time since the last hit performed using this finger. /// @@ -44,31 +70,5 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators return 8; } - - /// - /// Evaluates the minimum mechanical stamina required to play the current object. This is calculated using the - /// maximum possible interval between two hits using the same key, by alternating available fingers for each colour. - /// - public static double EvaluateDifficultyOf(DifficultyHitObject current) - { - if (current.BaseObject is not Hit) - { - return 0.0; - } - - // Find the previous hit object hit by the current finger, which is n notes prior, n being the number of - // available fingers. - TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current; - TaikoDifficultyHitObject? taikoPrevious = current.Previous(1) as TaikoDifficultyHitObject; - TaikoDifficultyHitObject? previousMono = taikoCurrent.PreviousMono(availableFingersFor(taikoCurrent) - 1); - - double objectStrain = 0.5; // Add a base strain to all objects - if (taikoPrevious == null) return objectStrain; - - if (previousMono != null) - objectStrain += speedBonus(taikoCurrent.StartTime - previousMono.StartTime) + 0.5 * speedBonus(taikoCurrent.StartTime - taikoPrevious.StartTime); - - return objectStrain; - } } } From ef8867704adaeb813bce65fe1e44844aea86ddce Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 14:28:15 +0000 Subject: [PATCH 135/801] Add xmldoc to explain `IHasInterval.Interval` --- .../Difficulty/Preprocessing/Rhythm/IHasInterval.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs index 8f3917cbde..32b148da2e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs @@ -8,6 +8,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm /// public interface IHasInterval { + /// + /// The interval between 2 objects start times. + /// double Interval { get; } } } From 20a76d832df7986c623f9e7fecd468fc012782eb Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 14:29:07 +0000 Subject: [PATCH 136/801] Rename rhythm preprocessing objects to be clearer with intent --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 38 +++++++++---------- ...Rhythm.cs => IntervalGroupedHitObjects.cs} | 31 ++++++--------- ...ns.cs => SamePatternsGroupedHitObjects.cs} | 28 +++++++------- ...ects.cs => SameRhythmGroupedHitObjects.cs} | 30 +++++++-------- .../Rhythm/TaikoDifficultyHitObjectRhythm.cs | 4 +- 5 files changed, 60 insertions(+), 71 deletions(-) rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/{SameRhythm.cs => IntervalGroupedHitObjects.cs} (62%) rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/{SamePatterns.cs => SamePatternsGroupedHitObjects.cs} (50%) rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/{SameRhythmHitObjects.cs => SameRhythmGroupedHitObjects.cs} (70%) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs index 22321a8f6e..8accc6124c 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs @@ -25,32 +25,32 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators double samePattern = 0; double intervalPenalty = 0; - if (rhythm.SameRhythmHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmHitObjects + if (rhythm.SameRhythmGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmGroupedHitObjects { - sameRhythm += 10.0 * evaluateDifficultyOf(rhythm.SameRhythmHitObjects, hitWindow); - intervalPenalty = repeatedIntervalPenalty(rhythm.SameRhythmHitObjects, hitWindow); + sameRhythm += 10.0 * evaluateDifficultyOf(rhythm.SameRhythmGroupedHitObjects, hitWindow); + intervalPenalty = repeatedIntervalPenalty(rhythm.SameRhythmGroupedHitObjects, hitWindow); } - if (rhythm.SamePatterns?.FirstHitObject == hitObject) // Difficulty for SamePatterns - samePattern += 1.15 * ratioDifficulty(rhythm.SamePatterns.IntervalRatio); + if (rhythm.SamePatternsGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SamePatternsGroupedHitObjects + samePattern += 1.15 * ratioDifficulty(rhythm.SamePatternsGroupedHitObjects.IntervalRatio); difficulty += Math.Max(sameRhythm, samePattern) * intervalPenalty; return difficulty; } - private static double evaluateDifficultyOf(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow) + private static double evaluateDifficultyOf(SameRhythmGroupedHitObjects sameRhythmGroupedHitObjects, double hitWindow) { - double intervalDifficulty = ratioDifficulty(sameRhythmHitObjects.HitObjectIntervalRatio); - double? previousInterval = sameRhythmHitObjects.Previous?.HitObjectInterval; + double intervalDifficulty = ratioDifficulty(sameRhythmGroupedHitObjects.HitObjectIntervalRatio); + double? previousInterval = sameRhythmGroupedHitObjects.Previous?.HitObjectInterval; - intervalDifficulty *= repeatedIntervalPenalty(sameRhythmHitObjects, hitWindow); + intervalDifficulty *= repeatedIntervalPenalty(sameRhythmGroupedHitObjects, hitWindow); // If a previous interval exists and there are multiple hit objects in the sequence: - if (previousInterval != null && sameRhythmHitObjects.Children.Count > 1) + if (previousInterval != null && sameRhythmGroupedHitObjects.Children.Count > 1) { - double expectedDurationFromPrevious = (double)previousInterval * sameRhythmHitObjects.Children.Count; - double durationDifference = sameRhythmHitObjects.Duration - expectedDurationFromPrevious; + double expectedDurationFromPrevious = (double)previousInterval * sameRhythmGroupedHitObjects.Children.Count; + double durationDifference = sameRhythmGroupedHitObjects.Duration - expectedDurationFromPrevious; if (durationDifference > 0) { @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators // Penalise patterns that can be hit within a single hit window. intervalDifficulty *= DifficultyCalculationUtils.Logistic( - sameRhythmHitObjects.Duration / hitWindow, + sameRhythmGroupedHitObjects.Duration / hitWindow, midpointOffset: 0.6, multiplier: 1, maxValue: 1); @@ -75,20 +75,20 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// /// Determines if the changes in hit object intervals is consistent based on a given threshold. /// - private static double repeatedIntervalPenalty(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow, double threshold = 0.1) + private static double repeatedIntervalPenalty(SameRhythmGroupedHitObjects sameRhythmGroupedHitObjects, double hitWindow, double threshold = 0.1) { - double longIntervalPenalty = sameInterval(sameRhythmHitObjects, 3); + double longIntervalPenalty = sameInterval(sameRhythmGroupedHitObjects, 3); - double shortIntervalPenalty = sameRhythmHitObjects.Children.Count < 6 - ? sameInterval(sameRhythmHitObjects, 4) + double shortIntervalPenalty = sameRhythmGroupedHitObjects.Children.Count < 6 + ? sameInterval(sameRhythmGroupedHitObjects, 4) : 1.0; // Returns a non-penalty if there are 6 or more notes within an interval. // The duration penalty is based on hit object duration relative to hitWindow. - double durationPenalty = Math.Max(1 - sameRhythmHitObjects.Duration * 2 / hitWindow, 0.5); + double durationPenalty = Math.Max(1 - sameRhythmGroupedHitObjects.Duration * 2 / hitWindow, 0.5); return Math.Min(longIntervalPenalty, shortIntervalPenalty) * durationPenalty; - double sameInterval(SameRhythmHitObjects startObject, int intervalCount) + double sameInterval(SameRhythmGroupedHitObjects startObject, int intervalCount) { List intervals = new List(); var currentObject = startObject; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs similarity index 62% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs index b1ca22595b..930b3fc0e4 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs @@ -1,8 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; +using osu.Framework.Utils; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data { @@ -10,35 +10,26 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data /// A base class for grouping s by their interval. In edges where an interval change /// occurs, the is added to the group with the smaller interval. /// - public abstract class SameRhythm - where ChildType : IHasInterval + public abstract class IntervalGroupedHitObjects + where TChildType : IHasInterval { - public IReadOnlyList Children { get; private set; } + public IReadOnlyList Children { get; private set; } /// - /// Determines if the intervals between two child objects are within a specified margin of error, - /// indicating that the intervals are effectively "flat" or consistent. - /// - private bool isFlat(ChildType current, ChildType previous, double marginOfError) - { - return Math.Abs(current.Interval - previous.Interval) <= marginOfError; - } - - /// - /// Create a new from a list of s, and add + /// Create a new from a list of s, and add /// them to the list until the end of the group. /// /// The list of s. /// /// Index in to start adding children. This will be modified and should be passed into - /// the next 's constructor. + /// the next 's constructor. /// /// /// The margin of error for the interval, within of which no interval change is considered to have occured. /// - protected SameRhythm(List data, ref int i, double marginOfError) + protected IntervalGroupedHitObjects(List data, ref int i, double marginOfError) { - List children = new List(); + List children = new List(); Children = children; children.Add(data[i]); i++; @@ -46,9 +37,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data for (; i < data.Count - 1; i++) { // An interval change occured, add the current data if the next interval is larger. - if (!isFlat(data[i], data[i + 1], marginOfError)) + if (!Precision.AlmostEquals(data[i].Interval, data[i + 1].Interval, marginOfError)) { - if (data[i + 1].Interval > data[i].Interval + marginOfError) + if (Precision.DefinitelyBigger(data[i].Interval, data[i + 1].Interval, marginOfError)) { children.Add(data[i]); i++; @@ -63,7 +54,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data // Check if the last two objects in the data form a "flat" rhythm pattern within the specified margin of error. // If true, add the current object to the group and increment the index to process the next object. - if (data.Count > 2 && isFlat(data[^1], data[^2], marginOfError)) + if (data.Count > 2 && Precision.AlmostEquals(data[^1].Interval, data[^2].Interval, marginOfError)) { children.Add(data[i]); i++; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs similarity index 50% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs index 50839c4561..d4cbc9c1f9 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs @@ -7,21 +7,21 @@ using System.Linq; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data { /// - /// Represents grouped by their 's interval. + /// Represents grouped by their 's interval. /// - public class SamePatterns : SameRhythm + public class SamePatternsGroupedHitObjects : IntervalGroupedHitObjects { - public SamePatterns? Previous { get; private set; } + public SamePatternsGroupedHitObjects? Previous { get; private set; } /// - /// The between children within this group. - /// If there is only one child, this will have the value of the first child's . + /// The between children within this group. + /// If there is only one child, this will have the value of the first child's . /// public double ChildrenInterval => Children.Count > 1 ? Children[1].Interval : Children[0].Interval; /// - /// The ratio of between this and the previous . In the - /// case where there is no previous , this will have a value of 1. + /// The ratio of between this and the previous . In the + /// case where there is no previous , this will have a value of 1. /// public double IntervalRatio => ChildrenInterval / Previous?.ChildrenInterval ?? 1.0d; @@ -29,26 +29,26 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data public IEnumerable AllHitObjects => Children.SelectMany(child => child.Children); - private SamePatterns(SamePatterns? previous, List data, ref int i) + private SamePatternsGroupedHitObjects(SamePatternsGroupedHitObjects? previous, List data, ref int i) : base(data, ref i, 5) { Previous = previous; foreach (TaikoDifficultyHitObject hitObject in AllHitObjects) { - hitObject.Rhythm.SamePatterns = this; + hitObject.Rhythm.SamePatternsGroupedHitObjects = this; } } - public static void GroupPatterns(List data) + public static void GroupPatterns(List data) { - List samePatterns = new List(); + List samePatterns = new List(); - // Index does not need to be incremented, as it is handled within the SameRhythm constructor. + // Index does not need to be incremented, as it is handled within the IntervalGroupedHitObjects constructor. for (int i = 0; i < data.Count;) { - SamePatterns? previous = samePatterns.Count > 0 ? samePatterns[^1] : null; - samePatterns.Add(new SamePatterns(previous, data, ref i)); + SamePatternsGroupedHitObjects? previous = samePatterns.Count > 0 ? samePatterns[^1] : null; + samePatterns.Add(new SamePatternsGroupedHitObjects(previous, data, ref i)); } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs similarity index 70% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs index 0ccc6da026..0b59433a2e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs @@ -9,11 +9,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data /// /// Represents a group of s with no rhythm variation. /// - public class SameRhythmHitObjects : SameRhythm, IHasInterval + public class SameRhythmGroupedHitObjects : IntervalGroupedHitObjects, IHasInterval { public TaikoDifficultyHitObject FirstHitObject => Children[0]; - public SameRhythmHitObjects? Previous; + public SameRhythmGroupedHitObjects? Previous; /// /// of the first hit object. @@ -26,30 +26,28 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data public double Duration => Children[^1].StartTime - Children[0].StartTime; /// - /// The interval in ms of each hit object in this . This is only defined if there is - /// more than two hit objects in this . + /// The interval in ms of each hit object in this . This is only defined if there is + /// more than two hit objects in this . /// public double? HitObjectInterval; /// - /// The ratio of between this and the previous . In the + /// The ratio of between this and the previous . In the /// case where one or both of the is undefined, this will have a value of 1. /// public double HitObjectIntervalRatio = 1; - /// - /// The interval between the of this and the previous . - /// - public double Interval { get; private set; } = double.PositiveInfinity; + /// + public double Interval { get; private set; } - public SameRhythmHitObjects(SameRhythmHitObjects? previous, List data, ref int i) + public SameRhythmGroupedHitObjects(SameRhythmGroupedHitObjects? previous, List data, ref int i) : base(data, ref i, 5) { Previous = previous; foreach (var hitObject in Children) { - hitObject.Rhythm.SameRhythmHitObjects = this; + hitObject.Rhythm.SameRhythmGroupedHitObjects = this; // Pass the HitObjectInterval to each child. hitObject.HitObjectInterval = HitObjectInterval; @@ -58,15 +56,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data calculateIntervals(); } - public static List GroupHitObjects(List data) + public static List GroupHitObjects(List data) { - List flatPatterns = new List(); + List flatPatterns = new List(); - // Index does not need to be incremented, as it is handled within SameRhythm's constructor. + // Index does not need to be incremented, as it is handled within IntervalGroupedHitObjects's constructor. for (int i = 0; i < data.Count;) { - SameRhythmHitObjects? previous = flatPatterns.Count > 0 ? flatPatterns[^1] : null; - flatPatterns.Add(new SameRhythmHitObjects(previous, data, ref i)); + SameRhythmGroupedHitObjects? previous = flatPatterns.Count > 0 ? flatPatterns[^1] : null; + flatPatterns.Add(new SameRhythmGroupedHitObjects(previous, data, ref i)); } return flatPatterns; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs index beb7bfe5f6..351015ae08 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs @@ -15,12 +15,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm /// /// The group of hit objects with consistent rhythm that this object belongs to. /// - public SameRhythmHitObjects? SameRhythmHitObjects; + public SameRhythmGroupedHitObjects? SameRhythmGroupedHitObjects; /// /// The larger pattern of rhythm groups that this object is part of. /// - public SamePatterns? SamePatterns; + public SamePatternsGroupedHitObjects? SamePatternsGroupedHitObjects; /// /// The ratio of current From e0882d2a53d5452bb539bb9b16a0019b3f4094d2 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 14:33:40 +0000 Subject: [PATCH 137/801] Make `rescale` a static method --- .../Difficulty/TaikoDifficultyCalculator.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 1d3075e4ac..e07a965ab0 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -203,9 +203,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// Applies a final re-scaling of the star rating. /// /// The raw star rating value before re-scaling. - private double rescale(double sr) + private static double rescale(double sr) { - if (sr < 0) return sr; + if (sr < 0) + return sr; return 10.43 * Math.Log(sr / 8 + 1); } From 764b0001efc8ec7bc9aff48c525ee78f47b468aa Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 14:56:51 +0000 Subject: [PATCH 138/801] Fix typo in `ColourEvaluator` --- .../Difficulty/Evaluators/ColourEvaluator.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index c0e90e83c1..166c01f507 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators difficulty += evaluateAlternatingMonoPatternDifficulty(colour.AlternatingMonoPattern); if (colour.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern - difficulty += evaluateReadingHitPatternDifficulty(colour.RepeatingHitPattern); + difficulty += evaluateRepeatingHitPatternsDifficulty(colour.RepeatingHitPattern); double consistencyPenalty = consistentRatioPenalty(taikoObject); difficulty *= consistencyPenalty; @@ -83,9 +83,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators DifficultyCalculationUtils.Logistic(exponent: Math.E * monoStreak.Index - 2 * Math.E) * evaluateAlternatingMonoPatternDifficulty(monoStreak.Parent) * 0.5; private static double evaluateAlternatingMonoPatternDifficulty(AlternatingMonoPattern alternatingMonoPattern) => - DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * evaluateReadingHitPatternDifficulty(alternatingMonoPattern.Parent); + DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * evaluateRepeatingHitPatternsDifficulty(alternatingMonoPattern.Parent); - private static double evaluateReadingHitPatternDifficulty(RepeatingHitPatterns repeatingHitPattern) => + private static double evaluateRepeatingHitPatternsDifficulty(RepeatingHitPatterns repeatingHitPattern) => 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E)); } } From 1c4bc6dffd64126ab1b380ab0e6d11ff17c16a32 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 15:00:23 +0000 Subject: [PATCH 139/801] Revert `Precision.DefinitelyBigger` usage --- .../Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs index 930b3fc0e4..cc389d4091 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data // An interval change occured, add the current data if the next interval is larger. if (!Precision.AlmostEquals(data[i].Interval, data[i + 1].Interval, marginOfError)) { - if (Precision.DefinitelyBigger(data[i].Interval, data[i + 1].Interval, marginOfError)) + if (data[i + 1].Interval > data[i].Interval + marginOfError) { children.Add(data[i]); i++; From 14c68bcc583d1e980225da3f022176412ede3cb8 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 15:58:33 +0000 Subject: [PATCH 140/801] Replace weird `IntervalGroupedHitObjects` inheritance layer --- .../Rhythm/Data/IntervalGroupedHitObjects.cs | 64 ------------------- .../Data/SamePatternsGroupedHitObjects.cs | 27 ++------ .../Data/SameRhythmGroupedHitObjects.cs | 57 ++++------------- .../TaikoRhythmDifficultyPreprocessor.cs | 63 ++++++++++++++++++ .../Preprocessing/TaikoDifficultyHitObject.cs | 1 + .../Difficulty/TaikoDifficultyCalculator.cs | 6 +- .../Rhythm => Utils}/IHasInterval.cs | 4 +- .../Difficulty/Utils/IntervalGroupingUtils.cs | 64 +++++++++++++++++++ 8 files changed, 152 insertions(+), 134 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs rename osu.Game.Rulesets.Taiko/Difficulty/{Preprocessing/Rhythm => Utils}/IHasInterval.cs (73%) create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs deleted file mode 100644 index cc389d4091..0000000000 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs +++ /dev/null @@ -1,64 +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.Utils; - -namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data -{ - /// - /// A base class for grouping s by their interval. In edges where an interval change - /// occurs, the is added to the group with the smaller interval. - /// - public abstract class IntervalGroupedHitObjects - where TChildType : IHasInterval - { - public IReadOnlyList Children { get; private set; } - - /// - /// Create a new from a list of s, and add - /// them to the list until the end of the group. - /// - /// The list of s. - /// - /// Index in to start adding children. This will be modified and should be passed into - /// the next 's constructor. - /// - /// - /// The margin of error for the interval, within of which no interval change is considered to have occured. - /// - protected IntervalGroupedHitObjects(List data, ref int i, double marginOfError) - { - List children = new List(); - Children = children; - children.Add(data[i]); - i++; - - for (; i < data.Count - 1; i++) - { - // An interval change occured, add the current data if the next interval is larger. - if (!Precision.AlmostEquals(data[i].Interval, data[i + 1].Interval, marginOfError)) - { - if (data[i + 1].Interval > data[i].Interval + marginOfError) - { - children.Add(data[i]); - i++; - } - - return; - } - - // No interval change occured - children.Add(data[i]); - } - - // Check if the last two objects in the data form a "flat" rhythm pattern within the specified margin of error. - // If true, add the current object to the group and increment the index to process the next object. - if (data.Count > 2 && Precision.AlmostEquals(data[^1].Interval, data[^2].Interval, marginOfError)) - { - children.Add(data[i]); - i++; - } - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs index d4cbc9c1f9..cb22b2ef82 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs @@ -9,9 +9,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data /// /// Represents grouped by their 's interval. /// - public class SamePatternsGroupedHitObjects : IntervalGroupedHitObjects + public class SamePatternsGroupedHitObjects { - public SamePatternsGroupedHitObjects? Previous { get; private set; } + public IReadOnlyList Children { get; } + + public SamePatternsGroupedHitObjects? Previous { get; } /// /// The between children within this group. @@ -29,27 +31,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data public IEnumerable AllHitObjects => Children.SelectMany(child => child.Children); - private SamePatternsGroupedHitObjects(SamePatternsGroupedHitObjects? previous, List data, ref int i) - : base(data, ref i, 5) + public SamePatternsGroupedHitObjects(SamePatternsGroupedHitObjects? previous, List children) { Previous = previous; - - foreach (TaikoDifficultyHitObject hitObject in AllHitObjects) - { - hitObject.Rhythm.SamePatternsGroupedHitObjects = this; - } - } - - public static void GroupPatterns(List data) - { - List samePatterns = new List(); - - // Index does not need to be incremented, as it is handled within the IntervalGroupedHitObjects constructor. - for (int i = 0; i < data.Count;) - { - SamePatternsGroupedHitObjects? previous = samePatterns.Count > 0 ? samePatterns[^1] : null; - samePatterns.Add(new SamePatternsGroupedHitObjects(previous, data, ref i)); - } + Children = children; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs index 0b59433a2e..dc6cf45d23 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs @@ -3,14 +3,17 @@ using System.Collections.Generic; using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Difficulty.Utils; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data { /// /// Represents a group of s with no rhythm variation. /// - public class SameRhythmGroupedHitObjects : IntervalGroupedHitObjects, IHasInterval + public class SameRhythmGroupedHitObjects : IHasInterval { + public List Children { get; private set; } + public TaikoDifficultyHitObject FirstHitObject => Children[0]; public SameRhythmGroupedHitObjects? Previous; @@ -40,53 +43,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data /// public double Interval { get; private set; } - public SameRhythmGroupedHitObjects(SameRhythmGroupedHitObjects? previous, List data, ref int i) - : base(data, ref i, 5) + public SameRhythmGroupedHitObjects(SameRhythmGroupedHitObjects? previous, List children) { Previous = previous; + Children = children; - foreach (var hitObject in Children) - { - hitObject.Rhythm.SameRhythmGroupedHitObjects = this; + // Calculate the average interval between hitobjects, or null if there are fewer than two + HitObjectInterval = Children.Count < 2 ? null : Duration / (Children.Count - 1); - // Pass the HitObjectInterval to each child. - hitObject.HitObjectInterval = HitObjectInterval; - } + // Calculate the ratio between this group's interval and the previous group's interval + HitObjectIntervalRatio = Previous?.HitObjectInterval != null && HitObjectInterval != null + ? HitObjectInterval.Value / Previous.HitObjectInterval.Value + : 1; - calculateIntervals(); - } - - public static List GroupHitObjects(List data) - { - List flatPatterns = new List(); - - // Index does not need to be incremented, as it is handled within IntervalGroupedHitObjects's constructor. - for (int i = 0; i < data.Count;) - { - SameRhythmGroupedHitObjects? previous = flatPatterns.Count > 0 ? flatPatterns[^1] : null; - flatPatterns.Add(new SameRhythmGroupedHitObjects(previous, data, ref i)); - } - - return flatPatterns; - } - - private void calculateIntervals() - { - // Calculate the average interval between hitobjects, or null if there are fewer than two. - HitObjectInterval = Children.Count < 2 ? null : (Children[^1].StartTime - Children[0].StartTime) / (Children.Count - 1); - - // If both the current and previous intervals are available, calculate the ratio. - if (Previous?.HitObjectInterval != null && HitObjectInterval != null) - { - HitObjectIntervalRatio = HitObjectInterval.Value / Previous.HitObjectInterval.Value; - } - - if (Previous == null) - { - return; - } - - Interval = StartTime - Previous.StartTime; + // Calculate the interval from the previous group's start time + Interval = Previous != null ? StartTime - Previous.StartTime : 0; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs new file mode 100644 index 0000000000..fa2135caf3 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs @@ -0,0 +1,63 @@ +// 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.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm +{ + public static class TaikoRhythmDifficultyPreprocessor + { + public static void ProcessAndAssign(List hitObjects) + { + var rhythmGroups = createSameRhythmGroupedHitObjects(hitObjects); + + foreach (var rhythmGroup in rhythmGroups) + { + foreach (var hitObject in rhythmGroup.Children) + { + hitObject.Rhythm.SameRhythmGroupedHitObjects = rhythmGroup; + hitObject.HitObjectInterval = rhythmGroup.HitObjectInterval; + } + } + + var patternGroups = createSamePatternGroupedHitObjects(rhythmGroups); + + foreach (var patternGroup in patternGroups) + { + foreach (var hitObject in patternGroup.AllHitObjects) + { + hitObject.Rhythm.SamePatternsGroupedHitObjects = patternGroup; + } + } + } + + private static List createSameRhythmGroupedHitObjects(List hitObjects) + { + var rhythmGroups = new List(); + var groups = IntervalGroupingUtils.GroupByInterval(hitObjects); + + foreach (var group in groups) + { + var previous = rhythmGroups.Count > 0 ? rhythmGroups[^1] : null; + rhythmGroups.Add(new SameRhythmGroupedHitObjects(previous, group)); + } + + return rhythmGroups; + } + + private static List createSamePatternGroupedHitObjects(List rhythmGroups) + { + var patternGroups = new List(); + var groups = IntervalGroupingUtils.GroupByInterval(rhythmGroups); + + foreach (var group in groups) + { + var previous = patternGroups.Count > 0 ? patternGroups[^1] : null; + patternGroups.Add(new SamePatternsGroupedHitObjects(previous, group)); + } + + return patternGroups; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 34c4871a42..0c668797cd 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -9,6 +9,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm; +using osu.Game.Rulesets.Taiko.Difficulty.Utils; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index e07a965ab0..acd654f9b8 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -13,7 +13,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; -using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm; using osu.Game.Rulesets.Taiko.Difficulty.Skills; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Scoring; @@ -91,9 +91,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty } TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects); - - var groupedHitObjects = SameRhythmGroupedHitObjects.GroupHitObjects(noteObjects); - SamePatternsGroupedHitObjects.GroupPatterns(groupedHitObjects); + TaikoRhythmDifficultyPreprocessor.ProcessAndAssign(noteObjects); return difficultyHitObjects; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs similarity index 73% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs index 32b148da2e..8f80bb6079 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs @@ -1,10 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm +namespace osu.Game.Rulesets.Taiko.Difficulty.Utils { /// - /// The interface for hitobjects that provide an interval value. + /// The interface for objects that provide an interval value. /// public interface IHasInterval { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs new file mode 100644 index 0000000000..22ded8a966 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs @@ -0,0 +1,64 @@ +// 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.Utils; +using osu.Game.Rulesets.Taiko.Difficulty.Utils; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data +{ + public static class IntervalGroupingUtils + { + public static List> GroupByInterval(IReadOnlyList data, double marginOfError = 5) where T : IHasInterval + { + var groups = new List>(); + if (data.Count == 0) + return groups; + + int i = 0; + + while (i < data.Count) + { + var group = createGroup(data, ref i, marginOfError); + groups.Add(group); + } + + return groups; + } + + private static List createGroup(IReadOnlyList data, ref int i, double marginOfError) where T : IHasInterval + { + var children = new List { data[i] }; + i++; + + for (; i < data.Count - 1; i++) + { + // An interval change occured, add the current data if the next interval is larger. + if (!Precision.AlmostEquals(data[i].Interval, data[i + 1].Interval, marginOfError)) + { + if (data[i + 1].Interval > data[i].Interval + marginOfError) + { + children.Add(data[i]); + i++; + } + + return children; + } + + // No interval change occurred + children.Add(data[i]); + } + + // Check if the last two objects in the data form a "flat" rhythm pattern within the specified margin of error. + // If true, add the current object to the group and increment the index to process the next object. + if (data.Count > 2 && i < data.Count && + Precision.AlmostEquals(data[^1].Interval, data[^2].Interval, marginOfError)) + { + children.Add(data[i]); + i++; + } + + return children; + } + } +} From 2a5a2738e152e4d23835e0c618873792ed57f148 Mon Sep 17 00:00:00 2001 From: Layendan Date: Tue, 21 Jan 2025 12:45:23 -0700 Subject: [PATCH 141/801] Add context menu to open in browser to rooms --- .../Lounge/Components/DrawableRoom.cs | 35 ++++++++++++++++++- .../OnlinePlay/Lounge/DrawableLoungeRoom.cs | 16 +++++++++ .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 12 +++++-- 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index c39ca347c7..321a1131de 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -12,15 +12,19 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Online.Chat; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -31,11 +35,17 @@ using Container = osu.Framework.Graphics.Containers.Container; namespace osu.Game.Screens.OnlinePlay.Lounge.Components { - public abstract partial class DrawableRoom : CompositeDrawable + public abstract partial class DrawableRoom : CompositeDrawable, IHasContextMenu { protected const float CORNER_RADIUS = 10; private const float height = 100; + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private OsuGame? game { get; set; } = null!; + public readonly Room Room; protected readonly Bindable SelectedItem = new Bindable(); @@ -330,6 +340,29 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } + public MenuItem[] ContextMenuItems + { + get + { + var items = new List(); + + if (Room.RoomID.HasValue) + { + items.AddRange([new OsuMenuItem("View in browser", MenuItemType.Standard, () => + { + game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value)); + }), new OsuMenuItem("Copy link", MenuItemType.Standard, () => + { + game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value)); + })]); + } + + return items.ToArray(); + } + } + + private string formatRoomUrl(long id) => $@"{api.WebsiteRootUrl}/multiplayer/rooms/{id}"; + protected virtual UpdateableBeatmapBackgroundSprite CreateBackground() => new UpdateableBeatmapBackgroundSprite(); protected virtual IEnumerable CreateBottomDetails() diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 0a55472c2d..2c15e5107a 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -59,6 +59,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge [Resolved] private IAPIProvider api { get; set; } = null!; + [Resolved] + private OsuGame? game { get; set; } = null!; + private readonly BindableWithCurrent selectedRoom = new BindableWithCurrent(); private Sample? sampleSelect; private Sample? sampleJoin; @@ -167,6 +170,17 @@ namespace osu.Game.Screens.OnlinePlay.Lounge }) }; + if (Room.RoomID.HasValue) + { + items.AddRange([new OsuMenuItem("View in browser", MenuItemType.Standard, () => + { + game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value)); + }), new OsuMenuItem("Copy link", MenuItemType.Standard, () => + { + game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value)); + })]); + } + if (Room.Type == MatchType.Playlists && Room.Host?.Id == api.LocalUser.Value.Id && Room.StartDate?.AddMinutes(5) >= DateTimeOffset.Now && !Room.HasEnded) { items.Add(new OsuMenuItem("Close playlist", MenuItemType.Destructive, () => @@ -234,6 +248,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge Room.PropertyChanged -= onRoomPropertyChanged; } + private string formatRoomUrl(long id) => $@"{api.WebsiteRootUrl}/multiplayer/rooms/{id}"; + public partial class PasswordEntryPopover : OsuPopover { private readonly Room room; diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 4ef31c02c3..3ba056b18d 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -18,6 +18,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; using osu.Game.Audio; using osu.Game.Beatmaps; +using osu.Game.Graphics.Cursor; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -156,10 +157,15 @@ namespace osu.Game.Screens.OnlinePlay.Match { new Drawable[] { - new DrawableMatchRoom(Room, allowEdit) + new OsuContextMenuContainer { - OnEdit = () => settingsOverlay.Show(), - SelectedItem = SelectedItem + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = new DrawableMatchRoom(Room, allowEdit) + { + OnEdit = () => settingsOverlay.Show(), + SelectedItem = SelectedItem + } } }, null, From fde2b22bbcd86ed44c83a3c018abd15b57bfddf0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jan 2025 16:29:50 +0900 Subject: [PATCH 142/801] Add transient flag for notifications which shouldn't linger in history --- .../TestSceneNotificationOverlay.cs | 34 +++++++++++++++++++ osu.Game/Online/FriendPresenceNotifier.cs | 2 ++ .../Overlays/NotificationOverlayToastTray.cs | 11 ++++-- .../Overlays/Notifications/Notification.cs | 8 ++++- 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs index c584c7dba0..caee5e634e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs @@ -83,6 +83,40 @@ namespace osu.Game.Tests.Visual.UserInterface waitForCompletion(); } + [Test] + public void TestNormalDoesForwardToOverlay() + { + SimpleNotification notification = null!; + + AddStep(@"simple #1", () => notificationOverlay.Post(notification = new SimpleNotification + { + Text = @"This shouldn't annoy you too much", + Transient = false, + })); + + AddAssert("notification in toast tray", () => notification.IsInToastTray, () => Is.True); + AddUntilStep("wait for dismissed", () => notification.IsInToastTray, () => Is.False); + + checkDisplayedCount(1); + } + + [Test] + public void TestTransientDoesNotForwardToOverlay() + { + SimpleNotification notification = null!; + + AddStep(@"simple #1", () => notificationOverlay.Post(notification = new SimpleNotification + { + Text = @"This shouldn't annoy you too much", + Transient = true, + })); + + AddAssert("notification in toast tray", () => notification.IsInToastTray, () => Is.True); + AddUntilStep("wait for dismissed", () => notification.IsInToastTray, () => Is.False); + + checkDisplayedCount(0); + } + [Test] public void TestForwardWithFlingRight() { diff --git a/osu.Game/Online/FriendPresenceNotifier.cs b/osu.Game/Online/FriendPresenceNotifier.cs index dd141b756b..e39e3cf94d 100644 --- a/osu.Game/Online/FriendPresenceNotifier.cs +++ b/osu.Game/Online/FriendPresenceNotifier.cs @@ -169,6 +169,7 @@ namespace osu.Game.Online notifications.Post(new SimpleNotification { + Transient = true, Icon = FontAwesome.Solid.UserPlus, Text = $"Online: {string.Join(@", ", onlineAlertQueue.Select(u => u.Username))}", IconColour = colours.Green, @@ -204,6 +205,7 @@ namespace osu.Game.Online notifications.Post(new SimpleNotification { + Transient = true, Icon = FontAwesome.Solid.UserMinus, Text = $"Offline: {string.Join(@", ", offlineAlertQueue.Select(u => u.Username))}", IconColour = colours.Red diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index df07b4f138..ddb2e02fb8 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - public Action? ForwardNotificationToPermanentStore { get; set; } + public required Action ForwardNotificationToPermanentStore { get; init; } public int UnreadCount => Notifications.Count(n => !n.WasClosed && !n.Read); @@ -142,8 +142,15 @@ namespace osu.Game.Overlays notification.MoveToOffset(new Vector2(400, 0), NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint); notification.FadeOut(NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint).OnComplete(_ => { + if (notification.Transient) + { + notification.IsInToastTray = false; + notification.Close(false); + return; + } + RemoveInternal(notification, false); - ForwardNotificationToPermanentStore?.Invoke(notification); + ForwardNotificationToPermanentStore(notification); notification.FadeIn(300, Easing.OutQuint); }); diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index d48524d8b0..e41aa8b625 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -34,10 +34,16 @@ namespace osu.Game.Overlays.Notifications public abstract LocalisableString Text { get; set; } /// - /// Whether this notification should forcefully display itself. + /// Important notifications display for longer, and announce themselves at an OS level (ie flashing the taskbar). + /// This defaults to true. /// public virtual bool IsImportant => true; + /// + /// Transient notifications only show as a toast, and do not linger in notification history. + /// + public bool Transient { get; init; } + /// /// Run on user activating the notification. Return true to close. /// From 4cf4b8c73de124d99995a1a1ea4d1dab5f0e3e28 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jan 2025 16:36:12 +0900 Subject: [PATCH 143/801] Switch `IsImportant` to `init` property isntead of `virtual` --- osu.Desktop/Security/ElevatedPrivilegesChecker.cs | 2 -- .../UserInterface/TestSceneNotificationOverlay.cs | 10 ++++++++-- osu.Game/Database/ModelDownloader.cs | 7 ++++--- osu.Game/Overlays/Notifications/Notification.cs | 2 +- .../Overlays/Notifications/ProgressNotification.cs | 4 ++-- osu.Game/Screens/Play/PlayerLoader.cs | 4 ---- 6 files changed, 15 insertions(+), 14 deletions(-) diff --git a/osu.Desktop/Security/ElevatedPrivilegesChecker.cs b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs index 0bed9830df..4b6ebc9b56 100644 --- a/osu.Desktop/Security/ElevatedPrivilegesChecker.cs +++ b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs @@ -30,8 +30,6 @@ namespace osu.Desktop.Security private partial class ElevatedPrivilegesNotification : SimpleNotification { - public override bool IsImportant => true; - public ElevatedPrivilegesNotification() { Text = $"Running osu! as {(RuntimeInfo.IsUnix ? "root" : "administrator")} does not improve performance, may break integrations and poses a security risk. Please run the game as a normal user."; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs index caee5e634e..65c8b913d3 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs @@ -668,12 +668,18 @@ namespace osu.Game.Tests.Visual.UserInterface private partial class BackgroundNotification : SimpleNotification { - public override bool IsImportant => false; + public BackgroundNotification() + { + IsImportant = false; + } } private partial class BackgroundProgressNotification : ProgressNotification { - public override bool IsImportant => false; + public BackgroundProgressNotification() + { + IsImportant = false; + } } } } diff --git a/osu.Game/Database/ModelDownloader.cs b/osu.Game/Database/ModelDownloader.cs index dfeec259fe..8e89db4d06 100644 --- a/osu.Game/Database/ModelDownloader.cs +++ b/osu.Game/Database/ModelDownloader.cs @@ -131,8 +131,6 @@ namespace osu.Game.Database private partial class DownloadNotification : ProgressNotification { - public override bool IsImportant => false; - protected override Notification CreateCompletionNotification() => new SilencedProgressCompletionNotification { Activated = CompletionClickAction, @@ -141,7 +139,10 @@ namespace osu.Game.Database private partial class SilencedProgressCompletionNotification : ProgressCompletionNotification { - public override bool IsImportant => false; + public SilencedProgressCompletionNotification() + { + IsImportant = false; + } } } } diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index e41aa8b625..ccfd1adb39 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -37,7 +37,7 @@ namespace osu.Game.Overlays.Notifications /// Important notifications display for longer, and announce themselves at an OS level (ie flashing the taskbar). /// This defaults to true. /// - public virtual bool IsImportant => true; + public bool IsImportant { get; init; } = true; /// /// Transient notifications only show as a toast, and do not linger in notification history. diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 2362cb11f6..0b42188252 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -191,8 +191,6 @@ namespace osu.Game.Overlays.Notifications public override bool DisplayOnTop => false; - public override bool IsImportant => false; - private readonly ProgressBar progressBar; private Color4 colourQueued; private Color4 colourActive; @@ -206,6 +204,8 @@ namespace osu.Game.Overlays.Notifications public ProgressNotification() { + IsImportant = false; + Content.Add(textDrawable = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 14, weight: FontWeight.Medium)) { AutoSizeAxes = Axes.Y, diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 06086c1004..fc956e15fd 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -663,8 +663,6 @@ namespace osu.Game.Screens.Play private partial class MutedNotification : SimpleNotification { - public override bool IsImportant => true; - public MutedNotification() { Text = NotificationsStrings.GameVolumeTooLow; @@ -716,8 +714,6 @@ namespace osu.Game.Screens.Play private partial class BatteryWarningNotification : SimpleNotification { - public override bool IsImportant => true; - public BatteryWarningNotification() { Text = NotificationsStrings.BatteryLow; From 9e023340b011ae376d8e90823e0c730e33d2920c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jan 2025 16:36:48 +0900 Subject: [PATCH 144/801] Mark friend notifications as non-important --- osu.Game/Online/FriendPresenceNotifier.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Online/FriendPresenceNotifier.cs b/osu.Game/Online/FriendPresenceNotifier.cs index e39e3cf94d..75b487384a 100644 --- a/osu.Game/Online/FriendPresenceNotifier.cs +++ b/osu.Game/Online/FriendPresenceNotifier.cs @@ -170,6 +170,7 @@ namespace osu.Game.Online notifications.Post(new SimpleNotification { Transient = true, + IsImportant = false, Icon = FontAwesome.Solid.UserPlus, Text = $"Online: {string.Join(@", ", onlineAlertQueue.Select(u => u.Username))}", IconColour = colours.Green, @@ -206,6 +207,7 @@ namespace osu.Game.Online notifications.Post(new SimpleNotification { Transient = true, + IsImportant = false, Icon = FontAwesome.Solid.UserMinus, Text = $"Offline: {string.Join(@", ", offlineAlertQueue.Select(u => u.Username))}", IconColour = colours.Red From 910c0022e3638e204ba3a0fc201139fb0a55fd73 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jan 2025 17:03:01 +0900 Subject: [PATCH 145/801] Adjust code style slightly --- .../LocalCachedBeatmapMetadataSource.cs | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs index 7495805cff..113b16b0db 100644 --- a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs +++ b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs @@ -114,14 +114,25 @@ namespace osu.Game.Beatmaps } } } - catch (SqliteException sqliteException) when (sqliteException.SqliteErrorCode == 11 || sqliteException.SqliteErrorCode == 26) // SQLITE_CORRUPT, SQLITE_NOTADB + catch (SqliteException sqliteException) { - // only attempt purge & refetch if there is no other refetch in progress - if (cacheDownloadRequest == null) + // There have been cases where the user's local database is corrupt. + // Let's attempt to identify these cases and re-initialise the local cache. + switch (sqliteException.SqliteErrorCode) { - tryPurgeCache(); - prepareLocalCache(); + case 26: // SQLITE_NOTADB + case 11: // SQLITE_CORRUPT + // only attempt purge & re-download if there is no other refetch in progress + if (cacheDownloadRequest != null) + throw; + + tryPurgeCache(); + prepareLocalCache(); + onlineMetadata = null; + return false; } + + throw; } catch (Exception ex) { From 26ef23c9a9c84e582796b6bbc35d38e1493d42da Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jan 2025 17:04:24 +0900 Subject: [PATCH 146/801] Remove outdated ef related catch-when usage --- osu.Game/Database/RealmAccess.cs | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index e1b8de89fa..28033883d1 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -11,7 +11,6 @@ using System.Linq.Expressions; using System.Reflection; using System.Threading; using System.Threading.Tasks; -using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Development; using osu.Framework.Extensions; @@ -413,18 +412,7 @@ namespace osu.Game.Database /// Compact this realm. /// /// - public bool Compact() - { - try - { - return Realm.Compact(getConfiguration()); - } - // Catch can be removed along with entity framework. Is specifically to allow a failure message to arrive to the user (see similar catches in EFToRealmMigrator). - catch (AggregateException ae) when (RuntimeInfo.OS == RuntimeInfo.Platform.macOS && ae.Flatten().InnerException is TypeInitializationException) - { - return true; - } - } + public bool Compact() => Realm.Compact(getConfiguration()); /// /// Run work on realm with a return value. @@ -720,11 +708,6 @@ namespace osu.Game.Database return Realm.GetInstance(getConfiguration()); } - // Catch can be removed along with entity framework. Is specifically to allow a failure message to arrive to the user (see similar catches in EFToRealmMigrator). - catch (AggregateException ae) when (RuntimeInfo.OS == RuntimeInfo.Platform.macOS && ae.Flatten().InnerException is TypeInitializationException) - { - return Realm.GetInstance(); - } finally { if (tookSemaphoreLock) From 6ceb348cf6109c4b5acc653ff227b35dbaa198ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jan 2025 18:24:01 +0900 Subject: [PATCH 147/801] Adjust code again to avoid weird `throw` mishandling --- .../Beatmaps/LocalCachedBeatmapMetadataSource.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs index 113b16b0db..a1744f74b3 100644 --- a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs +++ b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs @@ -113,9 +113,14 @@ namespace osu.Game.Beatmaps return queryCacheVersion2(db, beatmapInfo, out onlineMetadata); } } + + onlineMetadata = null; + return false; } catch (SqliteException sqliteException) { + onlineMetadata = null; + // There have been cases where the user's local database is corrupt. // Let's attempt to identify these cases and re-initialise the local cache. switch (sqliteException.SqliteErrorCode) @@ -124,15 +129,15 @@ namespace osu.Game.Beatmaps case 11: // SQLITE_CORRUPT // only attempt purge & re-download if there is no other refetch in progress if (cacheDownloadRequest != null) - throw; + return false; tryPurgeCache(); prepareLocalCache(); - onlineMetadata = null; return false; } - throw; + logForModel(beatmapInfo.BeatmapSet, $@"Cached local retrieval for {beatmapInfo} failed with unhandled sqlite error {sqliteException}."); + return false; } catch (Exception ex) { @@ -140,9 +145,6 @@ namespace osu.Game.Beatmaps onlineMetadata = null; return false; } - - onlineMetadata = null; - return false; } private void tryPurgeCache() From c94b8bf051871e7e6495a20eacabbb0f26622bc2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jan 2025 18:36:13 +0900 Subject: [PATCH 148/801] Apply NRT to new class --- .../Visual/Editing/TestSceneEditorClipboardSnapping.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboardSnapping.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboardSnapping.cs index e32cad12d2..edaba67591 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboardSnapping.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboardSnapping.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Utils; @@ -68,14 +66,14 @@ namespace osu.Game.Tests.Visual.Editing AddStep("paste hitobjects", () => Editor.Paste()); AddAssert("first object is snapped", () => Precision.AlmostEquals( - EditorBeatmap.SelectedHitObjects.MinBy(h => h.StartTime).StartTime, + EditorBeatmap.SelectedHitObjects.MinBy(h => h.StartTime)!.StartTime, EditorBeatmap.ControlPointInfo.GetClosestSnappedTime(paste_time, divisor) )); AddAssert("duration between pasted objects is same", () => { - var firstObject = EditorBeatmap.SelectedHitObjects.MinBy(h => h.StartTime); - var secondObject = EditorBeatmap.SelectedHitObjects.MaxBy(h => h.StartTime); + var firstObject = EditorBeatmap.SelectedHitObjects.MinBy(h => h.StartTime)!; + var secondObject = EditorBeatmap.SelectedHitObjects.MaxBy(h => h.StartTime)!; return Precision.AlmostEquals(secondObject.StartTime - firstObject.StartTime, addedObjects[1].StartTime - addedObjects[0].StartTime); }); From 3da220b8f68829b691e4230a957c3ed2fcd77595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 22 Jan 2025 11:39:32 +0100 Subject: [PATCH 149/801] Fix crash from new combo colour selector when there are no combo colours present Closes https://github.com/ppy/osu/issues/31615. --- .../Edit/Components/TernaryButtons/NewComboTernaryButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs b/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs index 1f95d5f239..c6ecee5f45 100644 --- a/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs +++ b/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs @@ -149,7 +149,7 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons { Enabled.Value = SelectedHitObject.Value != null; - if (SelectedHitObject.Value == null || SelectedHitObject.Value.ComboOffset == 0) + if (SelectedHitObject.Value == null || SelectedHitObject.Value.ComboOffset == 0 || ComboColours.Count <= 1) { BackgroundColour = colourProvider.Background3; icon.Colour = BackgroundColour.Darken(0.5f); From 02369baec43f0a68a26a960bef20980289b1f6ab Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 22 Jan 2025 21:44:45 +0900 Subject: [PATCH 150/801] Join/Leave rooms via multiplayer server Relevant functionality has been removed from `RoomManager` in the process. --- .../TestSceneMultiplayerLoungeSubScreen.cs | 26 ------ .../Online/Multiplayer/MultiplayerClient.cs | 3 + osu.Game/Online/Rooms/CreateRoomRequest.cs | 2 +- osu.Game/Online/Rooms/JoinRoomRequest.cs | 1 + .../OnlinePlay/Components/RoomManager.cs | 80 ------------------- .../DailyChallenge/DailyChallenge.cs | 10 +-- osu.Game/Screens/OnlinePlay/IRoomManager.cs | 22 ----- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 9 ++- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 6 +- .../OnlinePlay/Multiplayer/Multiplayer.cs | 3 - .../Multiplayer/MultiplayerLoungeSubScreen.cs | 34 ++++---- .../Multiplayer/MultiplayerMatchSubScreen.cs | 2 + .../Multiplayer/MultiplayerRoomManager.cs | 72 ----------------- .../Screens/OnlinePlay/OnlinePlayScreen.cs | 12 +-- .../Screens/OnlinePlay/OnlinePlaySubScreen.cs | 4 - .../Playlists/PlaylistsLoungeSubScreen.cs | 15 ++++ .../Playlists/PlaylistsRoomSettingsOverlay.cs | 9 ++- .../Playlists/PlaylistsRoomSubScreen.cs | 2 + .../Multiplayer/MultiplayerTestScene.cs | 2 +- .../Multiplayer/TestMultiplayerRoomManager.cs | 10 +-- .../Visual/OnlinePlay/TestRoomManager.cs | 13 ++- 21 files changed, 74 insertions(+), 263 deletions(-) delete mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs index 9951f62c77..d06a91433d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Tests.Visual.OnlinePlay; @@ -21,23 +20,13 @@ namespace osu.Game.Tests.Visual.Multiplayer protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; private LoungeSubScreen loungeScreen = null!; - private Room? lastJoinedRoom; - private string? lastJoinedPassword; public override void SetUpSteps() { base.SetUpSteps(); AddStep("push screen", () => LoadScreen(loungeScreen = new MultiplayerLoungeSubScreen())); - AddUntilStep("wait for present", () => loungeScreen.IsCurrentScreen()); - - AddStep("bind to event", () => - { - lastJoinedRoom = null; - lastJoinedPassword = null; - RoomManager.JoinRoomRequested = onRoomJoined; - }); } [Test] @@ -46,9 +35,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("add room", () => RoomManager.AddRooms(1, withPassword: false)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("join room", () => InputManager.Key(Key.Enter)); - - AddAssert("room join requested", () => lastJoinedRoom == RoomManager.Rooms.First()); - AddAssert("room join password correct", () => lastJoinedPassword == null); } [Test] @@ -126,9 +112,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password"); AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().TriggerClick()); - - AddAssert("room join requested", () => lastJoinedRoom == RoomManager.Rooms.First()); - AddAssert("room join password correct", () => lastJoinedPassword == "password"); } [Test] @@ -142,15 +125,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password"); AddStep("press enter", () => InputManager.Key(Key.Enter)); - - AddAssert("room join requested", () => lastJoinedRoom == RoomManager.Rooms.First()); - AddAssert("room join password correct", () => lastJoinedPassword == "password"); - } - - private void onRoomJoined(Room room, string? password) - { - lastJoinedRoom = room; - lastJoinedPassword = password; } } } diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index e5eade8c1d..7dfe974651 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -253,6 +253,9 @@ namespace osu.Game.Online.Multiplayer public Task LeaveRoom() { + if (Room == null) + return Task.CompletedTask; + // The join may have not completed yet, so certain tasks that either update the room or reference the room should be cancelled. // This includes the setting of Room itself along with the initial update of the room settings on join. joinCancellationSource?.Cancel(); diff --git a/osu.Game/Online/Rooms/CreateRoomRequest.cs b/osu.Game/Online/Rooms/CreateRoomRequest.cs index 63a3b7bfa8..9773bb5e7d 100644 --- a/osu.Game/Online/Rooms/CreateRoomRequest.cs +++ b/osu.Game/Online/Rooms/CreateRoomRequest.cs @@ -15,6 +15,7 @@ namespace osu.Game.Online.Rooms public CreateRoomRequest(Room room) { Room = room; + Success += r => Room.CopyFrom(r); } protected override WebRequest CreateWebRequest() @@ -23,7 +24,6 @@ namespace osu.Game.Online.Rooms req.ContentType = "application/json"; req.Method = HttpMethod.Post; - req.AddRaw(JsonConvert.SerializeObject(Room)); return req; diff --git a/osu.Game/Online/Rooms/JoinRoomRequest.cs b/osu.Game/Online/Rooms/JoinRoomRequest.cs index dfc7a53fb2..13e7ac8c84 100644 --- a/osu.Game/Online/Rooms/JoinRoomRequest.cs +++ b/osu.Game/Online/Rooms/JoinRoomRequest.cs @@ -16,6 +16,7 @@ namespace osu.Game.Online.Rooms { Room = room; Password = password; + Success += r => Room.CopyFrom(r); } protected override WebRequest CreateWebRequest() diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index 73f980f0a3..3abb4098fb 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -5,12 +5,10 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Logging; -using osu.Game.Online.API; using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Components @@ -23,89 +21,11 @@ namespace osu.Game.Screens.OnlinePlay.Components public IBindableList Rooms => rooms; - protected IBindable JoinedRoom => joinedRoom; - private readonly Bindable joinedRoom = new Bindable(); - - [Resolved] - private IAPIProvider api { get; set; } = null!; - public RoomManager() { RelativeSizeAxes = Axes.Both; } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - PartRoom(); - } - - public virtual void CreateRoom(Room room, Action? onSuccess = null, Action? onError = null) - { - room.Host = api.LocalUser.Value; - - var req = new CreateRoomRequest(room); - - req.Success += result => - { - joinedRoom.Value = room; - - AddOrUpdateRoom(result); - room.CopyFrom(result); // Also copy back to the source model, since this is likely to have been stored elsewhere. - - // The server may not contain all properties (such as password), so invoke success with the given room. - onSuccess?.Invoke(room); - }; - - req.Failure += exception => - { - onError?.Invoke(req.Response?.Error ?? exception.Message); - }; - - api.Queue(req); - } - - private JoinRoomRequest? currentJoinRoomRequest; - - public virtual void JoinRoom(Room room, string? password = null, Action? onSuccess = null, Action? onError = null) - { - currentJoinRoomRequest?.Cancel(); - currentJoinRoomRequest = new JoinRoomRequest(room, password); - - currentJoinRoomRequest.Success += result => - { - joinedRoom.Value = room; - - AddOrUpdateRoom(result); - room.CopyFrom(result); // Also copy back to the source model, since this is likely to have been stored elsewhere. - - onSuccess?.Invoke(room); - }; - - currentJoinRoomRequest.Failure += exception => - { - if (exception is OperationCanceledException) - return; - - onError?.Invoke(exception.Message); - }; - - api.Queue(currentJoinRoomRequest); - } - - public virtual void PartRoom() - { - currentJoinRoomRequest?.Cancel(); - - if (joinedRoom.Value == null) - return; - - if (api.State.Value == APIState.Online) - api.Queue(new PartRoomRequest(joinedRoom.Value)); - - joinedRoom.Value = null; - } - private readonly HashSet ignoredRooms = new HashSet(); public void AddOrUpdateRoom(Room room) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 13a282dd52..e3d6d42c05 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -34,7 +34,6 @@ using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.DailyChallenge.Events; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match.Components; @@ -71,9 +70,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); - [Cached(Type = typeof(IRoomManager))] - private RoomManager roomManager { get; set; } - [Cached] private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); @@ -115,7 +111,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { this.room = room; playlistItem = room.Playlist.Single(); - roomManager = new RoomManager(); Padding = new MarginPadding { Horizontal = -HORIZONTAL_OVERFLOW_PADDING }; } @@ -131,7 +126,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - roomManager, beatmapAvailabilityTracker, new ScreenStack(new RoomBackgroundScreen(playlistItem)) { @@ -426,7 +420,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge base.OnEntering(e); waves.Show(); - roomManager.JoinRoom(room); + API.Queue(new JoinRoomRequest(room, null)); startLoopingTrack(this, musicController); metadataClient.BeginWatchingMultiplayerRoom(room.RoomID!.Value).ContinueWith(t => @@ -480,7 +474,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge previewTrackManager.StopAnyPlaying(this); this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut(); - roomManager.PartRoom(); + API.Queue(new PartRoomRequest(room)); metadataClient.EndWatchingMultiplayerRoom(room.RoomID!.Value).FireAndForget(); return base.OnExiting(e); diff --git a/osu.Game/Screens/OnlinePlay/IRoomManager.cs b/osu.Game/Screens/OnlinePlay/IRoomManager.cs index ed4fb7b15e..8ecb1dd7e0 100644 --- a/osu.Game/Screens/OnlinePlay/IRoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/IRoomManager.cs @@ -38,27 +38,5 @@ namespace osu.Game.Screens.OnlinePlay /// Removes all s from this . /// void ClearRooms(); - - /// - /// Creates a new . - /// - /// The to create. - /// An action to be invoked if the creation succeeds. - /// An action to be invoked if an error occurred. - void CreateRoom(Room room, Action? onSuccess = null, Action? onError = null); - - /// - /// Joins a . - /// - /// The to join. must be populated. - /// An optional password to use for the join operation. - /// - /// - void JoinRoom(Room room, string? password = null, Action? onSuccess = null, Action? onError = null); - - /// - /// Parts the currently-joined . - /// - void PartRoom(); } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index f00cf7427c..f3f4df166a 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -263,6 +263,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge music.EnsurePlayingSomething(); onReturning(); + + // Poll for any newly-created rooms (including potentially the user's own). + ListingPollingComponent.PollImmediately(); } public override bool OnExiting(ScreenExitEvent e) @@ -297,14 +300,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge popoverContainer.HidePopover(); } - public virtual void Join(Room room, string? password, Action? onSuccess = null, Action? onFailure = null) => Schedule(() => + public void Join(Room room, string? password, Action? onSuccess = null, Action? onFailure = null) => Schedule(() => { if (joiningRoomOperation != null) return; joiningRoomOperation = ongoingOperationTracker?.BeginOperation(); - RoomManager?.JoinRoom(room, password, _ => + TryJoin(room, password, r => { Open(room); joiningRoomOperation?.Dispose(); @@ -318,6 +321,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge }); }); + protected abstract void TryJoin(Room room, string? password, Action onSuccess, Action onFailure); + /// /// Copies a room and opens it as a fresh (not-yet-created) one. /// diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 4ef31c02c3..d37f3b877c 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -343,7 +343,9 @@ namespace osu.Game.Screens.OnlinePlay.Match if (!ensureExitConfirmed()) return true; - RoomManager?.PartRoom(); + if (Room.RoomID != null) + PartRoom(); + Mods.Value = Array.Empty(); onLeaving(); @@ -351,6 +353,8 @@ namespace osu.Game.Screens.OnlinePlay.Match return base.OnExiting(e); } + protected abstract void PartRoom(); + private bool ensureExitConfirmed() { if (ExitConfirmed) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index bf316bb3da..dfed32aebc 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -8,7 +8,6 @@ using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; namespace osu.Game.Screens.OnlinePlay.Multiplayer @@ -97,8 +96,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override string ScreenTitle => "Multiplayer"; - protected override RoomManager CreateRoomManager() => new MultiplayerRoomManager(); - protected override LoungeSubScreen CreateLounge() => new MultiplayerLoungeSubScreen(); public void Join(Room room, string? password) => Schedule(() => Lounge.Join(room, password)); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index dd61caa3db..e901ecbdce 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -1,12 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Logging; -using osu.Framework.Screens; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; using osu.Game.Configuration; @@ -32,19 +33,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private Dropdown roomAccessTypeDropdown = null!; private OsuCheckbox showInProgress = null!; - public override void OnResuming(ScreenTransitionEvent e) - { - base.OnResuming(e); - - // Upon having left a room, we don't know whether we were the only participant, and whether the room is now closed as a result of leaving it. - // To work around this, temporarily remove the room and trigger an immediate listing poll. - if (e.Last is MultiplayerMatchSubScreen match) - { - RoomManager?.RemoveRoom(match.Room); - ListingPollingComponent.PollImmediately(); - } - } - protected override IEnumerable CreateFilterControls() { foreach (var control in base.CreateFilterControls()) @@ -93,6 +81,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override ListingPollingComponent CreatePollingComponent() => new MultiplayerListingPollingComponent(); + protected override void TryJoin(Room room, string? password, Action onSuccess, Action onFailure) + { + client.JoinRoom(room, password).ContinueWith(result => + { + if (result.IsCompletedSuccessfully) + onSuccess(room); + else + { + const string message = "Failed to join multiplayer room."; + + if (result.Exception != null) + Logger.Error(result.Exception, message); + + onFailure.Invoke(result.Exception?.AsSingular().Message ?? message); + } + }); + } + protected override void OpenNewRoom(Room room) { if (!client.IsConnected.Value) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 06ea5ee033..553c0c9182 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -278,6 +278,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return base.OnExiting(e); } + protected override void PartRoom() => client.LeaveRoom(); + private ModSettingChangeTracker? modSettingChangeTracker; private ScheduledDelegate? debouncedModSettingsUpdate; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs deleted file mode 100644 index 7f09c9cbe9..0000000000 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs +++ /dev/null @@ -1,72 +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; -using System.Diagnostics; -using osu.Framework.Allocation; -using osu.Framework.Extensions.ExceptionExtensions; -using osu.Framework.Logging; -using osu.Game.Online.Multiplayer; -using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay.Components; - -namespace osu.Game.Screens.OnlinePlay.Multiplayer -{ - public partial class MultiplayerRoomManager : RoomManager - { - [Resolved] - private MultiplayerClient multiplayerClient { get; set; } = null!; - - public override void CreateRoom(Room room, Action? onSuccess = null, Action? onError = null) - => base.CreateRoom(room, r => joinMultiplayerRoom(r, r.Password, onSuccess, onError), onError); - - public override void JoinRoom(Room room, string? password = null, Action? onSuccess = null, Action? onError = null) - { - if (!multiplayerClient.IsConnected.Value) - { - onError?.Invoke("Not currently connected to the multiplayer server."); - return; - } - - // this is done here as a pre-check to avoid clicking on already closed rooms in the lounge from triggering a server join. - // should probably be done at a higher level, but due to the current structure of things this is the easiest place for now. - if (room.HasEnded) - { - onError?.Invoke("Cannot join an ended room."); - return; - } - - base.JoinRoom(room, password, r => joinMultiplayerRoom(r, password, onSuccess, onError), onError); - } - - public override void PartRoom() - { - if (JoinedRoom.Value == null) - return; - - base.PartRoom(); - multiplayerClient.LeaveRoom(); - } - - private void joinMultiplayerRoom(Room room, string? password, Action? onSuccess = null, Action? onError = null) - { - Debug.Assert(room.RoomID != null); - - multiplayerClient.JoinRoom(room, password).ContinueWith(t => - { - if (t.IsCompletedSuccessfully) - Schedule(() => onSuccess?.Invoke(room)); - else if (t.IsFaulted) - { - const string message = "Failed to join multiplayer room."; - - if (t.Exception != null) - Logger.Error(t.Exception, message); - - PartRoom(); - Schedule(() => onError?.Invoke(t.Exception?.AsSingular().Message ?? message)); - } - }); - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 17fb667e14..16462b90c1 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -36,12 +36,12 @@ namespace osu.Game.Screens.OnlinePlay private readonly ScreenStack screenStack = new OnlinePlaySubScreenStack { RelativeSizeAxes = Axes.Both }; private OnlinePlayScreenWaveContainer waves = null!; - [Cached(Type = typeof(IRoomManager))] - protected RoomManager RoomManager { get; private set; } - [Cached] private readonly OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker(); + [Cached(Type = typeof(IRoomManager))] + private readonly RoomManager roomManager = new RoomManager(); + [Resolved] protected IAPIProvider API { get; private set; } = null!; @@ -51,8 +51,6 @@ namespace osu.Game.Screens.OnlinePlay Origin = Anchor.Centre; RelativeSizeAxes = Axes.Both; Padding = new MarginPadding { Horizontal = -HORIZONTAL_OVERFLOW_PADDING }; - - RoomManager = CreateRoomManager(); } private readonly IBindable apiState = new Bindable(); @@ -67,7 +65,7 @@ namespace osu.Game.Screens.OnlinePlay { screenStack, new Header(ScreenTitle, screenStack), - RoomManager, + roomManager, ongoingOperationTracker, } }; @@ -165,8 +163,6 @@ namespace osu.Game.Screens.OnlinePlay subScreen.Exit(); } - RoomManager.PartRoom(); - waves.Hide(); this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut(); diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs index fa1ee004c9..9b35a794a3 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Screens; @@ -15,9 +14,6 @@ namespace osu.Game.Screens.OnlinePlay protected sealed override bool PlayExitSound => false; - [Resolved] - protected IRoomManager? RoomManager { get; private set; } - protected OnlinePlaySubScreen() { Anchor = Anchor.Centre; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs index d66b4f844c..92415e0eb1 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; @@ -59,6 +60,20 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return criteria; } + protected override void TryJoin(Room room, string? password, Action onSuccess, Action onFailure) + { + var joinRoomRequest = new JoinRoomRequest(room, password); + + joinRoomRequest.Success += r => onSuccess(r); + joinRoomRequest.Failure += exception => + { + if (exception is not OperationCanceledException) + onFailure(exception.Message); + }; + + api.Queue(joinRoomRequest); + } + protected override OsuButton CreateNewRoomButton() => new CreatePlaylistsRoomButton(); protected override Room CreateNewRoom() diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs index 88af161cc8..b3d1d577ed 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs @@ -75,9 +75,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private PurpleRoundedButton editPlaylistButton = null!; - [Resolved] - private IRoomManager? manager { get; set; } - [Resolved] private IAPIProvider api { get; set; } = null!; @@ -449,7 +446,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists room.Duration = DurationField.Current.Value; loadingLayer.Show(); - manager?.CreateRoom(room, onSuccess, onError); + + var req = new CreateRoomRequest(room); + req.Success += onSuccess; + req.Failure += e => onError(req.Response?.Error ?? e.Message); + api.Queue(req); } private void hideError() => ErrorText.FadeOut(50); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 9b4630ac0b..064c355a69 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -290,6 +290,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists })); } + protected override void PartRoom() => api.Queue(new PartRoomRequest(Room)); + protected override Screen CreateGameplayScreen(PlaylistItem selectedItem) { return new PlayerLoader(() => new PlaylistsPlayer(Room, selectedItem) diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index 42cf317829..dca1fc8f3c 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -56,7 +56,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("join room", () => { SelectedRoom.Value = CreateRoom(); - RoomManager.CreateRoom(SelectedRoom.Value); + API.Queue(new CreateRoomRequest(SelectedRoom.Value)); }); AddUntilStep("wait for room join", () => RoomJoined); diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs index b998a638e5..59ac9a9749 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs @@ -1,12 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; -using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Tests.Visual.OnlinePlay; namespace osu.Game.Tests.Visual.Multiplayer @@ -15,7 +13,7 @@ namespace osu.Game.Tests.Visual.Multiplayer /// A for use in multiplayer test scenes. /// Should generally not be used by itself outside of a . /// - public partial class TestMultiplayerRoomManager : MultiplayerRoomManager + public partial class TestMultiplayerRoomManager : RoomManager { private readonly TestRoomRequestsHandler requestsHandler; @@ -26,12 +24,6 @@ namespace osu.Game.Tests.Visual.Multiplayer public IReadOnlyList ServerSideRooms => requestsHandler.ServerSideRooms; - public override void CreateRoom(Room room, Action? onSuccess = null, Action? onError = null) - => base.CreateRoom(room, r => onSuccess?.Invoke(r), onError); - - public override void JoinRoom(Room room, string? password = null, Action? onSuccess = null, Action? onError = null) - => base.JoinRoom(room, password, r => onSuccess?.Invoke(r), onError); - /// /// Adds a room to a local "server-side" list that's returned when a is fired. /// diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs index b1e3eafacc..60d169a46f 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Allocation; using osu.Game.Beatmaps; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Rulesets; @@ -15,15 +17,10 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// public partial class TestRoomManager : RoomManager { - public Action? JoinRoomRequested; - private int currentRoomId; - public override void JoinRoom(Room room, string? password = null, Action? onSuccess = null, Action? onError = null) - { - JoinRoomRequested?.Invoke(room, password); - base.JoinRoom(room, password, onSuccess, onError); - } + [Resolved] + private IAPIProvider api { get; set; } = null!; public void AddRooms(int count, RulesetInfo? ruleset = null, bool withPassword = false, bool withSpotlightRooms = false) { @@ -49,7 +46,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay public void AddRoom(Room room) { room.RoomID = -currentRoomId; - CreateRoom(room); + api.Queue(new CreateRoomRequest(room)); currentRoomId++; } } From 2c0d6b14c82969a850b292f785a678016e06ed26 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Wed, 22 Jan 2025 13:24:30 +0000 Subject: [PATCH 151/801] Fix incorrect namespace --- .../Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs | 1 + .../Difficulty/Utils/IntervalGroupingUtils.cs | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs index fa2135caf3..cd56d835dc 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data; +using osu.Game.Rulesets.Taiko.Difficulty.Utils; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs index 22ded8a966..3b6f5406b4 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs @@ -3,9 +3,8 @@ using System.Collections.Generic; using osu.Framework.Utils; -using osu.Game.Rulesets.Taiko.Difficulty.Utils; -namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data +namespace osu.Game.Rulesets.Taiko.Difficulty.Utils { public static class IntervalGroupingUtils { From 753e9ef7c79f85d027557295c0c60fb4fa09210c Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Wed, 22 Jan 2025 13:26:12 +0000 Subject: [PATCH 152/801] Keep old behaviour of `double.PositiveInfinity` being the default for `Interval` --- .../Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs index dc6cf45d23..4f7023059f 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data : 1; // Calculate the interval from the previous group's start time - Interval = Previous != null ? StartTime - Previous.StartTime : 0; + Interval = Previous != null ? StartTime - Previous.StartTime : double.PositiveInfinity; } } } From f673d16a1f97d153f74cfbd6e8549886552910cb Mon Sep 17 00:00:00 2001 From: Layendan Date: Wed, 22 Jan 2025 11:42:11 -0700 Subject: [PATCH 153/801] Fix formatting --- .../Lounge/Components/DrawableRoom.cs | 19 ++++++++++------- .../OnlinePlay/Lounge/DrawableLoungeRoom.cs | 21 +++++++++++-------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 321a1131de..7fefa0a1a8 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -44,7 +44,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private IAPIProvider api { get; set; } = null!; [Resolved] - private OsuGame? game { get; set; } = null!; + private OsuGame? game { get; set; } public readonly Room Room; @@ -348,13 +348,16 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components if (Room.RoomID.HasValue) { - items.AddRange([new OsuMenuItem("View in browser", MenuItemType.Standard, () => - { - game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value)); - }), new OsuMenuItem("Copy link", MenuItemType.Standard, () => - { - game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value)); - })]); + items.AddRange([ + new OsuMenuItem("View in browser", MenuItemType.Standard, () => + { + game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value)); + }), + new OsuMenuItem("Copy link", MenuItemType.Standard, () => + { + game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value)); + }) + ]); } return items.ToArray(); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 2c15e5107a..da04152bd3 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -60,7 +60,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private IAPIProvider api { get; set; } = null!; [Resolved] - private OsuGame? game { get; set; } = null!; + private OsuGame? game { get; set; } private readonly BindableWithCurrent selectedRoom = new BindableWithCurrent(); private Sample? sampleSelect; @@ -158,7 +158,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge public Popover GetPopover() => new PasswordEntryPopover(Room); - public MenuItem[] ContextMenuItems + public new MenuItem[] ContextMenuItems { get { @@ -172,13 +172,16 @@ namespace osu.Game.Screens.OnlinePlay.Lounge if (Room.RoomID.HasValue) { - items.AddRange([new OsuMenuItem("View in browser", MenuItemType.Standard, () => - { - game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value)); - }), new OsuMenuItem("Copy link", MenuItemType.Standard, () => - { - game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value)); - })]); + items.AddRange([ + new OsuMenuItem("View in browser", MenuItemType.Standard, () => + { + game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value)); + }), + new OsuMenuItem("Copy link", MenuItemType.Standard, () => + { + game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value)); + }) + ]); } if (Room.Type == MatchType.Playlists && Room.Host?.Id == api.LocalUser.Value.Id && Room.StartDate?.AddMinutes(5) >= DateTimeOffset.Now && !Room.HasEnded) From 865757621082cb3e2cba36a7f6a5dbd5d71d74a4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jan 2025 18:25:31 +0900 Subject: [PATCH 154/801] Show selection defaults in test scene (and make prettier) --- .../SongSelect/TestSceneBeatmapCarouselV2.cs | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs index b13d450c32..984352b2f5 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs @@ -16,6 +16,7 @@ using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; @@ -117,12 +118,11 @@ namespace osu.Game.Tests.Visual.SongSelect } } }, - stats = new OsuTextFlowContainer(cp => cp.Font = FrameworkFont.Regular.With()) + stats = new OsuTextFlowContainer { + AutoSizeAxes = Axes.Both, Padding = new MarginPadding(10), TextAnchor = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, }, }; }); @@ -258,16 +258,29 @@ namespace osu.Game.Tests.Visual.SongSelect if (carousel.IsNull()) return; - stats.Text = $""" - store - sets: {beatmapSets.Count} - beatmaps: {beatmapCount} - carousel: - sorting: {carousel.IsFiltering} - tracked: {carousel.ItemsTracked} - displayable: {carousel.DisplayableItems} - displayed: {carousel.VisibleItems} - """; + stats.Clear(); + createHeader("beatmap store"); + stats.AddParagraph($""" + sets: {beatmapSets.Count} + beatmaps: {beatmapCount} + """); + createHeader("carousel"); + stats.AddParagraph($""" + sorting: {carousel.IsFiltering} + tracked: {carousel.ItemsTracked} + displayable: {carousel.DisplayableItems} + displayed: {carousel.VisibleItems} + selected: {carousel.CurrentSelection} + """); + + void createHeader(string text) + { + stats.AddParagraph(string.Empty); + stats.AddParagraph(text, cp => + { + cp.Font = cp.Font.With(size: 18, weight: FontWeight.Bold); + }); + } } } } From 6ac2dbc818ff5d5de1249280095e0804284ce327 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jan 2025 18:49:12 +0900 Subject: [PATCH 155/801] Reorder carousel methods into logical regions --- osu.Game/Screens/SelectV2/Carousel.cs | 68 +++++++++++++++++---------- 1 file changed, 44 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index ec1bf6b7c0..190792b19e 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -30,10 +30,7 @@ namespace osu.Game.Screens.SelectV2 /// public abstract partial class Carousel : CompositeDrawable { - /// - /// A collection of filters which should be run each time a is executed. - /// - protected IEnumerable Filters { get; init; } = Enumerable.Empty(); + #region Properties and methods for external usage /// /// Height of the area above the carousel that should be treated as visible due to transparency of elements in front of it. @@ -82,15 +79,6 @@ namespace osu.Game.Screens.SelectV2 /// public int VisibleItems => scroll.Panels.Count; - /// - /// All items which are to be considered for display in this carousel. - /// Mutating this list will automatically queue a . - /// - /// - /// Note that an may add new items which are displayed but not tracked in this list. - /// - protected readonly BindableList Items = new BindableList(); - /// /// The currently selected model. /// @@ -114,20 +102,31 @@ namespace osu.Game.Screens.SelectV2 } } - private List? displayedCarouselItems; + #endregion - private readonly CarouselScrollContainer scroll; + #region Properties and methods concerning implementations - protected Carousel() - { - InternalChild = scroll = new CarouselScrollContainer - { - RelativeSizeAxes = Axes.Both, - Masking = false, - }; + /// + /// A collection of filters which should be run each time a is executed. + /// + /// + /// Implementations should add all required filters as part of their initialisation. + /// + /// Importantly, each filter is sequentially run in the order provided. + /// Each filter receives the output of the previous filter. + /// + /// A filter may add, mutate or remove items. + /// + protected IEnumerable Filters { get; init; } = Enumerable.Empty(); - Items.BindCollectionChanged((_, _) => FilterAsync()); - } + /// + /// All items which are to be considered for display in this carousel. + /// Mutating this list will automatically queue a . + /// + /// + /// Note that an may add new items which are displayed but not tracked in this list. + /// + protected readonly BindableList Items = new BindableList(); /// /// Queue an asynchronous filter operation. @@ -151,8 +150,29 @@ namespace osu.Game.Screens.SelectV2 /// A representing the model. protected abstract CarouselItem CreateCarouselItemForModel(T model); + #endregion + + #region Initialisation + + private readonly CarouselScrollContainer scroll; + + protected Carousel() + { + InternalChild = scroll = new CarouselScrollContainer + { + RelativeSizeAxes = Axes.Both, + Masking = false, + }; + + Items.BindCollectionChanged((_, _) => FilterAsync()); + } + + #endregion + #region Filtering and display preparation + private List? displayedCarouselItems; + private Task filterTask = Task.CompletedTask; private CancellationTokenSource cancellationSource = new CancellationTokenSource(); From d5268356277030b4ef36b6fe2623d58193da256c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 04:03:43 +0900 Subject: [PATCH 156/801] Only show loading when doing a user triggered filter --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 93d4c90be0..d9c049bbae 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Threading; -using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -14,7 +13,6 @@ using osu.Framework.Graphics.Pooling; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Multiplayer; using osu.Game.Screens.Select; namespace osu.Game.Screens.SelectV2 @@ -93,14 +91,8 @@ namespace osu.Game.Screens.SelectV2 public void Filter(FilterCriteria criteria) { Criteria = criteria; - FilterAsync().FireAndForget(); - } - - protected override async Task FilterAsync() - { loading.Show(); - await base.FilterAsync().ConfigureAwait(true); - loading.Hide(); + FilterAsync().ContinueWith(_ => Schedule(() => loading.Hide())); } } } From ded1d9f01994e5e54e52f4ee02fd9f02ecad4847 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 15:58:35 +0900 Subject: [PATCH 157/801] `displayedCarouselItems` -> `carouselItems` --- osu.Game/Screens/SelectV2/Carousel.cs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 190792b19e..c042da167e 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -72,7 +72,7 @@ namespace osu.Game.Screens.SelectV2 /// /// The number of carousel items currently in rotation for display. /// - public int DisplayableItems => displayedCarouselItems?.Count ?? 0; + public int DisplayableItems => carouselItems?.Count ?? 0; /// /// The number of items currently actualised into drawables. @@ -171,7 +171,7 @@ namespace osu.Game.Screens.SelectV2 #region Filtering and display preparation - private List? displayedCarouselItems; + private List? carouselItems; private Task filterTask = Task.CompletedTask; private CancellationTokenSource cancellationSource = new CancellationTokenSource(); @@ -222,7 +222,7 @@ namespace osu.Game.Screens.SelectV2 return; log("Items ready for display"); - displayedCarouselItems = items.ToList(); + carouselItems = items.ToList(); displayedRange = null; updateSelection(); @@ -253,9 +253,9 @@ namespace osu.Game.Screens.SelectV2 { currentSelectionCarouselItem = null; - if (displayedCarouselItems == null) return; + if (carouselItems == null) return; - foreach (var item in displayedCarouselItems) + foreach (var item in carouselItems) { bool isSelected = item.Model == currentSelection; @@ -306,7 +306,7 @@ namespace osu.Game.Screens.SelectV2 { base.Update(); - if (displayedCarouselItems == null) + if (carouselItems == null) return; var range = getDisplayRange(); @@ -356,15 +356,15 @@ namespace osu.Game.Screens.SelectV2 private DisplayRange getDisplayRange() { - Debug.Assert(displayedCarouselItems != null); + Debug.Assert(carouselItems != null); // Find index range of all items that should be on-screen carouselBoundsItem.CarouselYPosition = visibleUpperBound - DistanceOffscreenToPreload; - int firstIndex = displayedCarouselItems.BinarySearch(carouselBoundsItem); + int firstIndex = carouselItems.BinarySearch(carouselBoundsItem); if (firstIndex < 0) firstIndex = ~firstIndex; carouselBoundsItem.CarouselYPosition = visibleBottomBound + DistanceOffscreenToPreload; - int lastIndex = displayedCarouselItems.BinarySearch(carouselBoundsItem); + int lastIndex = carouselItems.BinarySearch(carouselBoundsItem); if (lastIndex < 0) lastIndex = ~lastIndex; firstIndex = Math.Max(0, firstIndex - 1); @@ -375,11 +375,11 @@ namespace osu.Game.Screens.SelectV2 private void updateDisplayedRange(DisplayRange range) { - Debug.Assert(displayedCarouselItems != null); + Debug.Assert(carouselItems != null); List toDisplay = range.Last - range.First == 0 ? new List() - : displayedCarouselItems.GetRange(range.First, range.Last - range.First + 1); + : carouselItems.GetRange(range.First, range.Last - range.First + 1); // Iterate over all panels which are already displayed and figure which need to be displayed / removed. foreach (var panel in scroll.Panels) @@ -415,9 +415,9 @@ namespace osu.Game.Screens.SelectV2 // Update the total height of all items (to make the scroll container scrollable through the full height even though // most items are not displayed / loaded). - if (displayedCarouselItems.Count > 0) + if (carouselItems.Count > 0) { - var lastItem = displayedCarouselItems[^1]; + var lastItem = carouselItems[^1]; scroll.SetLayoutHeight((float)(lastItem.CarouselYPosition + lastItem.DrawHeight + visibleHalfHeight)); } else From 9a623257f5bd8cfed7f2d691fbb1c2959483c111 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 23 Jan 2025 16:19:09 +0900 Subject: [PATCH 158/801] Adjust + fix tests --- .../StatefulMultiplayerClientTest.cs | 7 +- .../TestSceneDrawableLoungeRoom.cs | 2 +- .../Multiplayer/TestSceneMultiplayer.cs | 15 ++-- .../TestSceneMultiplayerLoungeSubScreen.cs | 58 +++++++++++--- .../TestSceneMultiplayerPlaylist.cs | 10 +-- .../TestScenePlaylistsLoungeSubScreen.cs | 30 ++++++- .../TestScenePlaylistsMatchSettingsOverlay.cs | 78 +++++++------------ .../Visual/TestMultiplayerComponents.cs | 24 ++---- osu.Game/Online/Rooms/Room.cs | 17 ++++ .../OnlinePlay/Lounge/DrawableLoungeRoom.cs | 14 +--- .../OnlinePlay/Lounge/IOnlinePlayLounge.cs | 32 ++++++++ .../OnlinePlay/Lounge/LoungeSubScreen.cs | 19 +++-- .../Screens/OnlinePlay/OnlinePlayScreen.cs | 2 - .../IMultiplayerTestSceneDependencies.cs | 6 -- .../Multiplayer/MultiplayerTestScene.cs | 3 +- .../MultiplayerTestSceneDependencies.cs | 6 +- .../Multiplayer/TestMultiplayerClient.cs | 35 +++++++-- .../Multiplayer/TestMultiplayerRoomManager.cs | 34 -------- .../OnlinePlayTestSceneDependencies.cs | 4 +- .../Visual/OnlinePlay/TestRoomManager.cs | 20 +++-- 20 files changed, 232 insertions(+), 184 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs delete mode 100644 osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs index 559db16751..be30e06ed4 100644 --- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs +++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs @@ -8,7 +8,6 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Testing; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; -using osu.Game.Online.Rooms; using osu.Game.Tests.Visual.Multiplayer; namespace osu.Game.Tests.NonVisual.Multiplayer @@ -72,10 +71,6 @@ namespace osu.Game.Tests.NonVisual.Multiplayer AddStep("create room initially in gameplay", () => { - var newRoom = new Room(); - newRoom.CopyFrom(SelectedRoom.Value!); - - newRoom.RoomID = null; MultiplayerClient.RoomSetupAction = room => { room.State = MultiplayerRoomState.Playing; @@ -86,7 +81,7 @@ namespace osu.Game.Tests.NonVisual.Multiplayer }); }; - RoomManager.CreateRoom(newRoom); + MultiplayerClient.JoinRoom(MultiplayerClient.ServerSideRooms.Single()).ConfigureAwait(false); }); AddUntilStep("wait for room join", () => RoomJoined); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs index c5fb52461a..459a90d096 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load() { - var mockLounge = new Mock(); + var mockLounge = new Mock(); mockLounge .Setup(l => l.Join(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny>())) .Callback, Action>((_, _, _, d) => diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index fb653cea8b..0966c61a3a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -58,7 +58,6 @@ namespace osu.Game.Tests.Visual.Multiplayer private TestMultiplayerComponents multiplayerComponents = null!; private TestMultiplayerClient multiplayerClient => multiplayerComponents.MultiplayerClient; - private TestMultiplayerRoomManager roomManager => multiplayerComponents.RoomManager; [Resolved] private OsuConfigManager config { get; set; } = null!; @@ -257,7 +256,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create room", () => { - roomManager.AddServerSideRoom(new Room + multiplayerClient.AddServerSideRoom(new Room { Name = "Test Room", Playlist = @@ -286,7 +285,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create room", () => { - roomManager.AddServerSideRoom(new Room + multiplayerClient.AddServerSideRoom(new Room { Name = "Test Room", Playlist = @@ -336,7 +335,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create room", () => { - roomManager.AddServerSideRoom(new Room + multiplayerClient.AddServerSideRoom(new Room { Name = "Test Room", Password = "password", @@ -789,7 +788,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create room", () => { - roomManager.AddServerSideRoom(new Room + multiplayerClient.AddServerSideRoom(new Room { Name = "Test Room", QueueMode = QueueMode.AllPlayers, @@ -810,8 +809,8 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("disable polling", () => this.ChildrenOfType().Single().TimeBetweenPolls.Value = 0); AddStep("change server-side settings", () => { - roomManager.ServerSideRooms[0].Name = "New name"; - roomManager.ServerSideRooms[0].Playlist = + multiplayerClient.ServerSideRooms[0].Name = "New name"; + multiplayerClient.ServerSideRooms[0].Playlist = [ new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { @@ -828,7 +827,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("local room has correct settings", () => { var localRoom = this.ChildrenOfType().Single().Room; - return localRoom.Name == roomManager.ServerSideRooms[0].Name && localRoom.Playlist.Single().ID == 2; + return localRoom.Name == multiplayerClient.ServerSideRooms[0].Name && localRoom.Playlist.Single().ID == 2; }); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs index d06a91433d..4a259149e2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs @@ -9,18 +9,26 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.OnlinePlay.Lounge; +using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Tests.Visual.OnlinePlay; using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { - public partial class TestSceneMultiplayerLoungeSubScreen : OnlinePlayTestScene + public partial class TestSceneMultiplayerLoungeSubScreen : MultiplayerTestScene { protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; private LoungeSubScreen loungeScreen = null!; + private RoomsContainer roomsContainer => loungeScreen.ChildrenOfType().First(); + + public TestSceneMultiplayerLoungeSubScreen() + : base(false) + { + } + public override void SetUpSteps() { base.SetUpSteps(); @@ -32,15 +40,17 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestJoinRoomWithoutPassword() { - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: false)); + addRoom(false); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("join room", () => InputManager.Key(Key.Enter)); + + AddAssert("room joined", () => MultiplayerClient.RoomJoined); } [Test] public void TestPopoverHidesOnBackButton() { - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + addRoom(true); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); @@ -53,18 +63,22 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("hit escape", () => InputManager.Key(Key.Escape)); AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType().Any()); + + AddAssert("room not joined", () => !MultiplayerClient.RoomJoined); } [Test] public void TestPopoverHidesOnLeavingScreen() { - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + addRoom(true); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => InputManager.ChildrenOfType().Any()); AddStep("exit screen", () => Stack.Exit()); AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType().Any()); + + AddAssert("room not joined", () => !MultiplayerClient.RoomJoined); } [Test] @@ -72,16 +86,18 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + addRoom(true); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "wrong"); AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().TriggerClick()); - AddAssert("room not joined", () => loungeScreen.IsCurrentScreen()); + AddAssert("still at lounge", () => loungeScreen.IsCurrentScreen()); AddUntilStep("password prompt still visible", () => passwordEntryPopover!.State.Value == Visibility.Visible); AddAssert("textbox still focused", () => InputManager.FocusedDrawable is OsuPasswordTextBox); + + AddAssert("room not joined", () => !MultiplayerClient.RoomJoined); } [Test] @@ -89,16 +105,18 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + addRoom(true); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "wrong"); AddStep("press enter", () => InputManager.Key(Key.Enter)); - AddAssert("room not joined", () => loungeScreen.IsCurrentScreen()); + AddAssert("still at lounge", () => loungeScreen.IsCurrentScreen()); AddUntilStep("password prompt still visible", () => passwordEntryPopover!.State.Value == Visibility.Visible); AddAssert("textbox still focused", () => InputManager.FocusedDrawable is OsuPasswordTextBox); + + AddAssert("room not joined", () => !MultiplayerClient.RoomJoined); } [Test] @@ -106,12 +124,14 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + addRoom(true); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password"); AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().TriggerClick()); + + AddUntilStep("room joined", () => MultiplayerClient.RoomJoined); } [Test] @@ -119,12 +139,30 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + addRoom(true); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password"); AddStep("press enter", () => InputManager.Key(Key.Enter)); + + AddAssert("room joined", () => MultiplayerClient.RoomJoined); } + + private void addRoom(bool withPassword) + { + int initialRoomCount = 0; + + AddStep("add room", () => + { + initialRoomCount = roomsContainer.Rooms.Count; + RoomManager.AddRooms(1, withPassword: withPassword); + loungeScreen.RefreshRooms(); + }); + + AddUntilStep("wait for room to appear", () => roomsContainer.Rooms.Count == initialRoomCount + 1); + } + + protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies(); } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 36f5bba384..77b75f407b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -127,7 +127,7 @@ namespace osu.Game.Tests.Visual.Multiplayer addItemStep(); AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely()); - AddStep("leave room", () => RoomManager.PartRoom()); + AddStep("leave room", () => MultiplayerClient.LeaveRoom()); AddUntilStep("wait for room part", () => !RoomJoined); AddUntilStep("item 0 not in lists", () => !inHistoryList(0) && !inQueueList(0)); @@ -148,7 +148,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely()); assertQueueTabCount(2); - AddStep("leave room", () => RoomManager.PartRoom()); + AddStep("leave room", () => MultiplayerClient.LeaveRoom()); AddUntilStep("wait for room part", () => !RoomJoined); assertQueueTabCount(0); } @@ -157,12 +157,12 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestJoinRoomWithMixedItemsAddedInCorrectLists() { - AddStep("leave room", () => RoomManager.PartRoom()); + AddStep("leave room", () => MultiplayerClient.LeaveRoom()); AddUntilStep("wait for room part", () => !RoomJoined); AddStep("join room with items", () => { - RoomManager.CreateRoom(new Room + API.Queue(new CreateRoomRequest(new Room { Name = "test name", Playlist = @@ -177,7 +177,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Expired = true } ] - }); + })); }); AddUntilStep("wait for room join", () => RoomJoined); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs index 8c8dc8d69a..0897a3b2f5 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs @@ -35,7 +35,13 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestManyRooms() { - AddStep("add rooms", () => RoomManager.AddRooms(500)); + AddStep("add rooms", () => + { + RoomManager.AddRooms(500); + loungeScreen.RefreshRooms(); + }); + + AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 500); } [Test] @@ -43,7 +49,12 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("reset mouse", () => InputManager.ReleaseButton(MouseButton.Left)); - AddStep("add rooms", () => RoomManager.AddRooms(30)); + AddStep("add rooms", () => + { + RoomManager.AddRooms(30); + loungeScreen.RefreshRooms(); + }); + AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30); AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0])); @@ -60,7 +71,12 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestScrollSelectedIntoView() { - AddStep("add rooms", () => RoomManager.AddRooms(30)); + AddStep("add rooms", () => + { + RoomManager.AddRooms(30); + loungeScreen.RefreshRooms(); + }); + AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30); AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0])); @@ -74,7 +90,13 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestEnteringRoomTakesLeaseOnSelection() { - AddStep("add rooms", () => RoomManager.AddRooms(1)); + AddStep("add rooms", () => + { + RoomManager.AddRooms(1); + loungeScreen.RefreshRooms(); + }); + + AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 1); AddAssert("selected room is not disabled", () => !loungeScreen.SelectedRoom.Disabled); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs index 5868331451..51e39e1b7f 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs @@ -3,14 +3,13 @@ using System; using NUnit.Framework; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Online.API; using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Tests.Visual.OnlinePlay; @@ -21,13 +20,33 @@ namespace osu.Game.Tests.Visual.Playlists protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; private TestRoomSettings settings = null!; - - protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies(); + private Func? handleRequest; public override void SetUpSteps() { base.SetUpSteps(); + AddStep("setup api", () => + { + handleRequest = null; + ((DummyAPIAccess)API).HandleRequest = req => + { + if (req is not CreateRoomRequest createReq || handleRequest == null) + return false; + + if (handleRequest(createReq.Room) is string errorText) + createReq.TriggerFailure(new APIException(errorText, null)); + else + { + var createdRoom = new APICreatedRoom(); + createdRoom.CopyFrom(createReq.Room); + createReq.TriggerSuccess(createdRoom); + } + + return true; + }; + }); + AddStep("create overlay", () => { SelectedRoom.Value = new Room(); @@ -75,10 +94,10 @@ namespace osu.Game.Tests.Visual.Playlists settings.DurationField.Current.Value = expectedDuration; SelectedRoom.Value!.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)]; - RoomManager.CreateRequested = r => + handleRequest = r => { createdRoom = r; - return string.Empty; + return null; }; }); @@ -103,7 +122,7 @@ namespace osu.Game.Tests.Visual.Playlists errorMessage = $"{not_found_prefix} {beatmap.OnlineID}"; - RoomManager.CreateRequested = _ => errorMessage; + handleRequest = _ => errorMessage; }); AddAssert("error not displayed", () => !settings.ErrorText.IsPresent); @@ -128,7 +147,7 @@ namespace osu.Game.Tests.Visual.Playlists SelectedRoom.Value!.Name = "Test Room"; SelectedRoom.Value!.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)]; - RoomManager.CreateRequested = _ => failText; + handleRequest = _ => failText; }); AddAssert("error not displayed", () => !settings.ErrorText.IsPresent); @@ -159,48 +178,5 @@ namespace osu.Game.Tests.Visual.Playlists { } } - - private class TestDependencies : OnlinePlayTestSceneDependencies - { - protected override IRoomManager CreateRoomManager() => new TestRoomManager(); - } - - protected class TestRoomManager : IRoomManager - { - public Func? CreateRequested; - - public event Action RoomsUpdated - { - add { } - remove { } - } - - public IBindable InitialRoomsReceived { get; } = new Bindable(true); - - public IBindableList Rooms => null!; - - public void AddOrUpdateRoom(Room room) => throw new NotImplementedException(); - - public void RemoveRoom(Room room) => throw new NotImplementedException(); - - public void ClearRooms() => throw new NotImplementedException(); - - public void CreateRoom(Room room, Action? onSuccess = null, Action? onError = null) - { - if (CreateRequested == null) - return; - - string error = CreateRequested.Invoke(room); - - if (!string.IsNullOrEmpty(error)) - onError?.Invoke(error); - else - onSuccess?.Invoke(room); - } - - public void JoinRoom(Room room, string? password, Action? onSuccess = null, Action? onError = null) => throw new NotImplementedException(); - - public void PartRoom() => throw new NotImplementedException(); - } } } diff --git a/osu.Game.Tests/Visual/TestMultiplayerComponents.cs b/osu.Game.Tests/Visual/TestMultiplayerComponents.cs index 1814fb70c8..e385ff3a03 100644 --- a/osu.Game.Tests/Visual/TestMultiplayerComponents.cs +++ b/osu.Game.Tests/Visual/TestMultiplayerComponents.cs @@ -11,7 +11,6 @@ using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Screens; -using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Tests.Visual.Multiplayer; using osu.Game.Tests.Visual.OnlinePlay; @@ -26,15 +25,12 @@ namespace osu.Game.Tests.Visual /// Provides a to be resolved as a dependency in the screen, /// which is typically a part of . /// Rebinds the to handle requests via a . - /// Provides a for the screen. /// ///

///
public partial class TestMultiplayerComponents : OsuScreen { - public Screens.OnlinePlay.Multiplayer.Multiplayer MultiplayerScreen => multiplayerScreen; - - public TestMultiplayerRoomManager RoomManager => multiplayerScreen.RoomManager; + public Screens.OnlinePlay.Multiplayer.Multiplayer MultiplayerScreen { get; } public IScreen CurrentScreen => screenStack.CurrentScreen; @@ -53,17 +49,17 @@ namespace osu.Game.Tests.Visual private BeatmapManager beatmapManager { get; set; } private readonly OsuScreenStack screenStack; - private readonly TestMultiplayer multiplayerScreen; + private readonly TestRoomRequestsHandler requestsHandler = new TestRoomRequestsHandler(); public TestMultiplayerComponents() { - multiplayerScreen = new TestMultiplayer(); + MultiplayerScreen = new Screens.OnlinePlay.Multiplayer.Multiplayer(); InternalChildren = new Drawable[] { userLookupCache, beatmapLookupCache, - MultiplayerClient = new TestMultiplayerClient(RoomManager), + MultiplayerClient = new TestMultiplayerClient(requestsHandler), screenStack = new OsuScreenStack { Name = nameof(TestMultiplayerComponents), @@ -71,13 +67,13 @@ namespace osu.Game.Tests.Visual } }; - screenStack.Push(multiplayerScreen); + screenStack.Push(MultiplayerScreen); } [BackgroundDependencyLoader] private void load(IAPIProvider api) { - ((DummyAPIAccess)api).HandleRequest = request => multiplayerScreen.RequestsHandler.HandleRequest(request, api.LocalUser.Value, beatmapManager); + ((DummyAPIAccess)api).HandleRequest = request => requestsHandler.HandleRequest(request, api.LocalUser.Value, beatmapManager); } public override bool OnBackButton() => (screenStack.CurrentScreen as OsuScreen)?.OnBackButton() ?? base.OnBackButton(); @@ -90,13 +86,5 @@ namespace osu.Game.Tests.Visual screenStack.Exit(); return true; } - - private partial class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer - { - public new TestMultiplayerRoomManager RoomManager { get; private set; } - public TestRoomRequestsHandler RequestsHandler { get; private set; } - - protected override RoomManager CreateRoomManager() => RoomManager = new TestMultiplayerRoomManager(RequestsHandler = new TestRoomRequestsHandler()); - } } } diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index f8660a656e..c5e292a19d 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -342,6 +342,23 @@ namespace osu.Game.Online.Rooms // Not yet serialised (not implemented). private RoomAvailability availability; + public Room() + { + } + + public Room(MultiplayerRoom room) + { + RoomID = room.RoomID; + Name = room.Settings.Name; + Password = room.Settings.Password; + Type = room.Settings.MatchType; + QueueMode = room.Settings.QueueMode; + AutoStartDuration = room.Settings.AutoStartDuration; + AutoSkip = room.Settings.AutoSkip; + Host = room.Host != null ? new APIUser { Id = room.Host.UserID } : null; + Playlist = room.Playlist.Select(p => new PlaylistItem(p)).ToArray(); + } + /// /// Copies values from another into this one. /// diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 0a55472c2d..032a231ad3 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -24,7 +24,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Input.Bindings; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Components; @@ -51,7 +50,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge } [Resolved(canBeNull: true)] - private LoungeSubScreen? lounge { get; set; } + private IOnlinePlayLounge? lounge { get; set; } [Resolved] private IDialogOverlay? dialogOverlay { get; set; } @@ -163,7 +162,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { new OsuMenuItem("Create copy", MenuItemType.Standard, () => { - lounge?.OpenCopy(Room); + lounge?.Clone(Room); }) }; @@ -171,12 +170,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { items.Add(new OsuMenuItem("Close playlist", MenuItemType.Destructive, () => { - dialogOverlay?.Push(new ClosePlaylistDialog(Room, () => - { - var request = new ClosePlaylistRequest(Room.RoomID!.Value); - request.Success += () => lounge?.RefreshRooms(); - api.Queue(request); - })); + dialogOverlay?.Push(new ClosePlaylistDialog(Room, () => lounge?.Close(Room))); })); } @@ -239,7 +233,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private readonly Room room; [Resolved(canBeNull: true)] - private LoungeSubScreen? lounge { get; set; } + private IOnlinePlayLounge? lounge { get; set; } public override bool HandleNonPositionalInput => true; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs b/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs new file mode 100644 index 0000000000..8fa7d0751f --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs @@ -0,0 +1,32 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Game.Online.Rooms; + +namespace osu.Game.Screens.OnlinePlay.Lounge +{ + public interface IOnlinePlayLounge + { + /// + /// Attempts to join the given room. + /// + /// The room to join. + /// The password. + /// A delegate to invoke if the user joined the room. + /// A delegate to invoke if the user is not able join the room. + void Join(Room room, string? password, Action? onSuccess = null, Action? onFailure = null); + + /// + /// Clones the given room and opens it as a fresh (not-yet-created) one. + /// + /// The room to clone. + void Clone(Room room); + + /// + /// Closes the given room. + /// + /// The room to close. + void Close(Room room); + } +} diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index f3f4df166a..df17063fdf 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -21,6 +21,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input; using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Rulesets; @@ -33,7 +34,8 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Lounge { [Cached] - public abstract partial class LoungeSubScreen : OnlinePlaySubScreen + [Cached(typeof(IOnlinePlayLounge))] + public abstract partial class LoungeSubScreen : OnlinePlaySubScreen, IOnlinePlayLounge { public override string Title => "Lounge"; @@ -323,11 +325,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected abstract void TryJoin(Room room, string? password, Action onSuccess, Action onFailure); - /// - /// Copies a room and opens it as a fresh (not-yet-created) one. - /// - /// The room to copy. - public void OpenCopy(Room room) + public void Clone(Room room) { Debug.Assert(room.RoomID != null); @@ -363,6 +361,15 @@ namespace osu.Game.Screens.OnlinePlay.Lounge api.Queue(req); } + public void Close(Room room) + { + Debug.Assert(room.RoomID != null); + + var request = new ClosePlaylistRequest(room.RoomID.Value); + request.Success += RefreshRooms; + api.Queue(request); + } + /// /// Push a room as a new subscreen. /// diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 16462b90c1..8988c82dee 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -220,8 +220,6 @@ namespace osu.Game.Screens.OnlinePlay protected abstract string ScreenTitle { get; } - protected virtual RoomManager CreateRoomManager() => new RoomManager(); - protected abstract LoungeSubScreen CreateLounge(); ScreenStack IHasSubScreenStack.SubScreenStack => screenStack; diff --git a/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs b/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs index efd0b80ebf..262816ae89 100644 --- a/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Screens.OnlinePlay; using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Tests.Visual.Spectator; @@ -17,11 +16,6 @@ namespace osu.Game.Tests.Visual.Multiplayer ///
TestMultiplayerClient MultiplayerClient { get; } - /// - /// The cached . - /// - new TestMultiplayerRoomManager RoomManager { get; } - /// /// The cached . /// diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index dca1fc8f3c..d1497d5142 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -17,7 +17,6 @@ namespace osu.Game.Tests.Visual.Multiplayer public const int PLAYER_2_ID = 56; public TestMultiplayerClient MultiplayerClient => OnlinePlayDependencies.MultiplayerClient; - public new TestMultiplayerRoomManager RoomManager => OnlinePlayDependencies.RoomManager; public TestSpectatorClient SpectatorClient => OnlinePlayDependencies.SpectatorClient; protected new MultiplayerTestSceneDependencies OnlinePlayDependencies => (MultiplayerTestSceneDependencies)base.OnlinePlayDependencies; @@ -56,7 +55,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("join room", () => { SelectedRoom.Value = CreateRoom(); - API.Queue(new CreateRoomRequest(SelectedRoom.Value)); + MultiplayerClient.CreateRoom(SelectedRoom.Value).ConfigureAwait(false); }); AddUntilStep("wait for room join", () => RoomJoined); diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs index 88202d4327..24c33f2f49 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs @@ -3,7 +3,6 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; -using osu.Game.Screens.OnlinePlay; using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Tests.Visual.Spectator; @@ -16,19 +15,16 @@ namespace osu.Game.Tests.Visual.Multiplayer { public TestMultiplayerClient MultiplayerClient { get; } public TestSpectatorClient SpectatorClient { get; } - public new TestMultiplayerRoomManager RoomManager => (TestMultiplayerRoomManager)base.RoomManager; public MultiplayerTestSceneDependencies() { - MultiplayerClient = new TestMultiplayerClient(RoomManager); + MultiplayerClient = new TestMultiplayerClient(RequestsHandler); SpectatorClient = CreateSpectatorClient(); CacheAs(MultiplayerClient); CacheAs(SpectatorClient); } - protected override IRoomManager CreateRoomManager() => new TestMultiplayerRoomManager(RequestsHandler); - protected virtual TestSpectatorClient CreateSpectatorClient() => new TestSpectatorClient(); } } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 70e298f3e0..d514fc0d7e 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -10,6 +10,7 @@ using MessagePack; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; +using osu.Game.Beatmaps; using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; @@ -17,6 +18,7 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Mods; +using osu.Game.Tests.Visual.OnlinePlay; namespace osu.Game.Tests.Visual.Multiplayer { @@ -65,15 +67,15 @@ namespace osu.Game.Tests.Visual.Multiplayer [Resolved] private IAPIProvider api { get; set; } = null!; - private readonly TestMultiplayerRoomManager roomManager; - private MultiplayerPlaylistItem? currentItem => ServerRoom?.Playlist[currentIndex]; private int currentIndex; private long lastPlaylistItemId; - public TestMultiplayerClient(TestMultiplayerRoomManager roomManager) + private readonly TestRoomRequestsHandler apiRequestHandler; + + public TestMultiplayerClient(TestRoomRequestsHandler? apiRequestHandler = null) { - this.roomManager = roomManager; + this.apiRequestHandler = apiRequestHandler ?? new TestRoomRequestsHandler(); } public void Connect() => isConnected.Value = true; @@ -214,7 +216,7 @@ namespace osu.Game.Tests.Visual.Multiplayer roomId = clone(roomId); password = clone(password); - ServerAPIRoom = roomManager.ServerSideRooms.Single(r => r.RoomID == roomId); + ServerAPIRoom = ServerSideRooms.Single(r => r.RoomID == roomId); if (password != ServerAPIRoom.Password) throw new InvalidOperationException("Invalid password."); @@ -485,7 +487,15 @@ namespace osu.Game.Tests.Visual.Multiplayer protected override Task CreateRoom(MultiplayerRoom room) { - throw new NotImplementedException(); + Room apiRoom = new Room(room) + { + Type = room.Settings.MatchType == MatchType.Playlists + ? MatchType.HeadToHead + : room.Settings.MatchType + }; + + AddServerSideRoom(apiRoom, api.LocalUser.Value); + return JoinRoom(apiRoom.RoomID!.Value, room.Settings.Password); } private async Task changeMatchType(MatchType type) @@ -680,5 +690,18 @@ namespace osu.Game.Tests.Visual.Multiplayer isConnected.Value = false; return Task.CompletedTask; } + + #region API Room Handling + + public IReadOnlyList ServerSideRooms + => apiRequestHandler.ServerSideRooms; + + public void AddServerSideRoom(Room room, APIUser host) + => apiRequestHandler.AddServerSideRoom(room, host); + + public bool HandleRequest(APIRequest request, APIUser localUser, BeatmapManager beatmapManager) + => apiRequestHandler.HandleRequest(request, localUser, beatmapManager); + + #endregion } } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs deleted file mode 100644 index 59ac9a9749..0000000000 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs +++ /dev/null @@ -1,34 +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.Game.Online.API.Requests.Responses; -using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay.Components; -using osu.Game.Tests.Visual.OnlinePlay; - -namespace osu.Game.Tests.Visual.Multiplayer -{ - /// - /// A for use in multiplayer test scenes. - /// Should generally not be used by itself outside of a . - /// - public partial class TestMultiplayerRoomManager : RoomManager - { - private readonly TestRoomRequestsHandler requestsHandler; - - public TestMultiplayerRoomManager(TestRoomRequestsHandler requestsHandler) - { - this.requestsHandler = requestsHandler; - } - - public IReadOnlyList ServerSideRooms => requestsHandler.ServerSideRooms; - - /// - /// Adds a room to a local "server-side" list that's returned when a is fired. - /// - /// The room. - /// The host. - public void AddServerSideRoom(Room room, APIUser host) => requestsHandler.AddServerSideRoom(room, host); - } -} diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs index e2670c9ad8..203922c057 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs @@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay RequestsHandler = new TestRoomRequestsHandler(); OngoingOperationTracker = new OngoingOperationTracker(); AvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); - RoomManager = CreateRoomManager(); + RoomManager = new TestRoomManager(); UserLookupCache = new TestUserLookupCache(); BeatmapLookupCache = new BeatmapLookupCache(); @@ -80,7 +80,5 @@ namespace osu.Game.Tests.Visual.OnlinePlay if (instance is Drawable drawable) drawableComponents.Add(drawable); } - - protected virtual IRoomManager CreateRoomManager() => new TestRoomManager(); } } diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs index 60d169a46f..bff2753929 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs @@ -22,8 +22,14 @@ namespace osu.Game.Tests.Visual.OnlinePlay [Resolved] private IAPIProvider api { get; set; } = null!; + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + public void AddRooms(int count, RulesetInfo? ruleset = null, bool withPassword = false, bool withSpotlightRooms = false) { + // Can't reference Osu ruleset project here. + ruleset ??= rulesets.GetRuleset(0)!; + for (int i = 0; i < count; i++) { AddRoom(new Room @@ -33,12 +39,8 @@ namespace osu.Game.Tests.Visual.OnlinePlay Duration = TimeSpan.FromSeconds(10), Category = withSpotlightRooms && i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal, Password = withPassword ? @"password" : null, - PlaylistItemStats = ruleset == null - ? null - : new Room.RoomPlaylistItemStats { RulesetIDs = [ruleset.OnlineID] }, - Playlist = ruleset == null - ? Array.Empty() - : [new PlaylistItem(new BeatmapInfo { Metadata = new BeatmapMetadata() }) { RulesetID = ruleset.OnlineID }] + PlaylistItemStats = new Room.RoomPlaylistItemStats { RulesetIDs = [ruleset.OnlineID] }, + Playlist = [new PlaylistItem(new BeatmapInfo { Metadata = new BeatmapMetadata() }) { RulesetID = ruleset.OnlineID }] }); } } @@ -46,7 +48,11 @@ namespace osu.Game.Tests.Visual.OnlinePlay public void AddRoom(Room room) { room.RoomID = -currentRoomId; - api.Queue(new CreateRoomRequest(room)); + + var req = new CreateRoomRequest(room); + req.Success += AddOrUpdateRoom; + api.Queue(req); + currentRoomId++; } } From 7c38089c7559350de5080cdad9b55d0e5165d41b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 23 Jan 2025 16:22:52 +0900 Subject: [PATCH 159/801] Rename methods --- .../Online/Multiplayer/MultiplayerClient.cs | 37 +++++++------ .../Multiplayer/OnlineMultiplayerClient.cs | 54 +++++++++---------- .../Multiplayer/TestMultiplayerClient.cs | 6 +-- 3 files changed, 50 insertions(+), 47 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 7dfe974651..a8f314d372 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -171,7 +171,7 @@ namespace osu.Game.Online.Multiplayer throw new InvalidOperationException("Cannot join a multiplayer room while already in one."); var cancellationSource = joinCancellationSource = new CancellationTokenSource(); - await initRoom(room, r => CreateRoom(new MultiplayerRoom(room)), cancellationSource.Token).ConfigureAwait(false); + await initRoom(room, r => CreateRoomInternal(new MultiplayerRoom(room)), cancellationSource.Token).ConfigureAwait(false); } /// @@ -187,7 +187,7 @@ namespace osu.Game.Online.Multiplayer Debug.Assert(room.RoomID != null); var cancellationSource = joinCancellationSource = new CancellationTokenSource(); - await initRoom(room, r => JoinRoom(room.RoomID.Value, password ?? room.Password), cancellationSource.Token).ConfigureAwait(false); + await initRoom(room, r => JoinRoomInternal(room.RoomID.Value, password ?? room.Password), cancellationSource.Token).ConfigureAwait(false); } private async Task initRoom(Room room, Func> initFunc, CancellationToken cancellationToken) @@ -236,21 +236,6 @@ namespace osu.Game.Online.Multiplayer { } - /// - /// Creates the with the given settings. - /// - /// The room. - /// The joined - protected abstract Task CreateRoom(MultiplayerRoom room); - - /// - /// Joins the with a given ID. - /// - /// The room ID. - /// An optional password to use when joining the room. - /// The joined . - protected abstract Task JoinRoom(long roomId, string? password = null); - public Task LeaveRoom() { if (Room == null) @@ -279,6 +264,24 @@ namespace osu.Game.Online.Multiplayer }); } + /// + /// Creates the with the given settings. + /// + /// The room. + /// The joined + protected abstract Task CreateRoomInternal(MultiplayerRoom room); + + /// + /// Joins the with a given ID. + /// + /// The room ID. + /// An optional password to use when joining the room. + /// The joined . + protected abstract Task JoinRoomInternal(long roomId, string? password = null); + + /// + /// Leaves the currently-joined . + /// protected abstract Task LeaveRoomInternal(); public abstract Task InvitePlayer(int userId); diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 05f3e44405..068ba27789 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -75,7 +75,32 @@ namespace osu.Game.Online.Multiplayer } } - protected override async Task JoinRoom(long roomId, string? password = null) + protected override async Task CreateRoomInternal(MultiplayerRoom room) + { + if (!IsConnected.Value) + throw new OperationCanceledException(); + + Debug.Assert(connection != null); + + try + { + return await connection.InvokeAsync(nameof(IMultiplayerServer.CreateRoom), room).ConfigureAwait(false); + } + catch (HubException exception) + { + if (exception.GetHubExceptionMessage() == HubClientConnector.SERVER_SHUTDOWN_MESSAGE) + { + Debug.Assert(connector != null); + + await connector.Reconnect().ConfigureAwait(false); + return await CreateRoomInternal(room).ConfigureAwait(false); + } + + throw; + } + } + + protected override async Task JoinRoomInternal(long roomId, string? password = null) { if (!IsConnected.Value) throw new OperationCanceledException(); @@ -93,7 +118,7 @@ namespace osu.Game.Online.Multiplayer Debug.Assert(connector != null); await connector.Reconnect().ConfigureAwait(false); - return await JoinRoom(roomId, password).ConfigureAwait(false); + return await JoinRoomInternal(roomId, password).ConfigureAwait(false); } throw; @@ -266,31 +291,6 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.RemovePlaylistItem), playlistItemId); } - protected override async Task CreateRoom(MultiplayerRoom room) - { - if (!IsConnected.Value) - throw new OperationCanceledException(); - - Debug.Assert(connection != null); - - try - { - return await connection.InvokeAsync(nameof(IMultiplayerServer.CreateRoom), room).ConfigureAwait(false); - } - catch (HubException exception) - { - if (exception.GetHubExceptionMessage() == HubClientConnector.SERVER_SHUTDOWN_MESSAGE) - { - Debug.Assert(connector != null); - - await connector.Reconnect().ConfigureAwait(false); - return await CreateRoom(room).ConfigureAwait(false); - } - - throw; - } - } - public override Task DisconnectInternal() { if (connector == null) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index d514fc0d7e..359b223ad2 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -208,7 +208,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged(clone(userId), clone(user.BeatmapAvailability)); } - protected override async Task JoinRoom(long roomId, string? password = null) + protected override async Task JoinRoomInternal(long roomId, string? password = null) { if (RoomJoined || ServerAPIRoom != null) throw new InvalidOperationException("Already joined a room"); @@ -485,7 +485,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public override Task RemovePlaylistItem(long playlistItemId) => RemoveUserPlaylistItem(api.LocalUser.Value.OnlineID, clone(playlistItemId)); - protected override Task CreateRoom(MultiplayerRoom room) + protected override Task CreateRoomInternal(MultiplayerRoom room) { Room apiRoom = new Room(room) { @@ -495,7 +495,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }; AddServerSideRoom(apiRoom, api.LocalUser.Value); - return JoinRoom(apiRoom.RoomID!.Value, room.Settings.Password); + return JoinRoomInternal(apiRoom.RoomID!.Value, room.Settings.Password); } private async Task changeMatchType(MatchType type) From a198b0830affdab861037c0a90525946fa446b5d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 23 Jan 2025 17:18:01 +0900 Subject: [PATCH 160/801] Add comment indicating RoomManager shouldn't exist --- osu.Game/Screens/OnlinePlay/Components/RoomManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index 3abb4098fb..a1b61ea7a3 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -13,6 +13,7 @@ using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Components { + // Todo: This class should be inlined into the lounge. public partial class RoomManager : Component, IRoomManager { public event Action? RoomsUpdated; From b4e8a17f0386523e1fb15faf7e13ffd8aa0011c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 Jan 2025 09:57:23 +0100 Subject: [PATCH 161/801] Roll back windows build image to 2019 on android build job Per workaround suggested in https://github.com/actions/runner-images/issues/11402#issuecomment-2596473501. Applying this now as my hopes for a swift resolution without changes on our side are slim to none (read thread linked above in full to learn why). --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8645d728e..a88f1320cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,7 +96,7 @@ jobs: build-only-android: name: Build only (Android) - runs-on: windows-latest + runs-on: windows-2019 timeout-minutes: 60 steps: - name: Checkout From f2d8ea299777ad6168eb90d04a574d10bf083837 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 23 Jan 2025 18:25:55 +0900 Subject: [PATCH 162/801] Fix incorrect continuation --- .../Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 279b140d36..72b581eac1 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -472,7 +472,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { client.CreateRoom(room).ContinueWith(t => Schedule(() => { - if (t.IsCompleted) + if (t.IsCompletedSuccessfully) onSuccess(room); else if (t.IsFaulted) { From 6dbf466009f6ab12f2613eebb970a2a1d1e101b3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 23 Jan 2025 18:30:11 +0900 Subject: [PATCH 163/801] Fix incorrect exception handling In particular, when the exception is: `AggregateException { AggregateException { HubException } }`, then the existing code will only unwrap the first aggregate exception. The overlay's code was copied from the extension so both have been adjusted here. --- .../Online/Multiplayer/MultiplayerClientExtensions.cs | 9 +++------ .../Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs | 8 ++------ 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs b/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs index d846e7f566..1cc5a8e70a 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR; +using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Logging; namespace osu.Game.Online.Multiplayer @@ -16,12 +17,8 @@ namespace osu.Game.Online.Multiplayer { if (t.IsFaulted) { - Exception? exception = t.Exception; - - if (exception is AggregateException ae) - exception = ae.InnerException; - - Debug.Assert(exception != null); + Debug.Assert(t.Exception != null); + Exception exception = t.Exception.AsSingular(); if (exception.GetHubExceptionMessage() is string message) // Hub exceptions generally contain something we can show the user directly. diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 72b581eac1..2a5a83fadf 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -476,12 +476,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match onSuccess(room); else if (t.IsFaulted) { - Exception? exception = t.Exception; - - if (exception is AggregateException ae) - exception = ae.InnerException; - - Debug.Assert(exception != null); + Debug.Assert(t.Exception != null); + Exception exception = t.Exception.AsSingular(); if (exception.GetHubExceptionMessage() is string message) onError(message); From c67c0a7fc02d29a093821d00b95a249e73f01a4a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 16:07:18 +0900 Subject: [PATCH 164/801] Move `Selected` status to drawables Basically, I don't want bindables in `CarouselItem`. It means there needs to be a bind-unbind process on pooling. By moving these to the drawable and just updating every frame, we can simplify things a lot. --- osu.Game/Screens/SelectV2/CarouselItem.cs | 24 ++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/SelectV2/CarouselItem.cs b/osu.Game/Screens/SelectV2/CarouselItem.cs index 4636e8a32f..2cb96a3d7f 100644 --- a/osu.Game/Screens/SelectV2/CarouselItem.cs +++ b/osu.Game/Screens/SelectV2/CarouselItem.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Bindables; namespace osu.Game.Screens.SelectV2 { @@ -10,9 +9,9 @@ namespace osu.Game.Screens.SelectV2 /// Represents a single display item for display in a . /// This is used to house information related to the attached model that helps with display and tracking. /// - public abstract class CarouselItem : IComparable + public sealed class CarouselItem : IComparable { - public readonly BindableBool Selected = new BindableBool(); + public const float DEFAULT_HEIGHT = 40; /// /// The model this item is representing. @@ -20,16 +19,27 @@ namespace osu.Game.Screens.SelectV2 public readonly object Model; /// - /// The current Y position in the carousel. This is managed by and should not be set manually. + /// The current Y position in the carousel. + /// This is managed by and should not be set manually. /// public double CarouselYPosition { get; set; } /// - /// The height this item will take when displayed. + /// The height this item will take when displayed. Defaults to . /// - public abstract float DrawHeight { get; } + public float DrawHeight { get; set; } = DEFAULT_HEIGHT; - protected CarouselItem(object model) + /// + /// Whether this item should be a valid target for user group selection hotkeys. + /// + public bool IsGroupSelectionTarget { get; set; } + + /// + /// Whether this item is visible or collapsed (hidden). + /// + public bool IsVisible { get; set; } = true; + + public CarouselItem(object model) { Model = model; } From 980f6cf18e0d2177b9b8a63c5afcf803eea48e1d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 16:09:18 +0900 Subject: [PATCH 165/801] Make `CarouselItem` `sealed` and remove `BeatmapCarouselItem` concept Less abstraction is better. As far as I can tell, we don't need a custom model for this. If there's any tracking to be done, it should be done within `BeatmapCarousel`'s implementation (or a filter). --- .../SongSelect/TestSceneBeatmapCarouselV2.cs | 10 +- .../SelectV2/BeatmapCarouselFilterSorting.cs | 43 ++++----- .../Screens/SelectV2/BeatmapCarouselItem.cs | 48 ---------- .../Screens/SelectV2/BeatmapCarouselPanel.cs | 95 ++++++++++++------- osu.Game/Screens/SelectV2/ICarouselPanel.cs | 13 +++ 5 files changed, 99 insertions(+), 110 deletions(-) delete mode 100644 osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs index 984352b2f5..dee61bbcde 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs @@ -181,15 +181,15 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("visual item added", () => carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); AddStep("select middle beatmap", () => carousel.CurrentSelection = beatmapSets.ElementAt(beatmapSets.Count - 2)); - AddStep("scroll to selected item", () => scroll.ScrollTo(scroll.ChildrenOfType().Single(p => p.Item!.Selected.Value))); + AddStep("scroll to selected item", () => scroll.ScrollTo(scroll.ChildrenOfType().Single(p => p.Selected.Value))); AddUntilStep("wait for scroll finished", () => scroll.Current, () => Is.EqualTo(scroll.Target)); - AddStep("save selected screen position", () => positionBefore = carousel.ChildrenOfType().FirstOrDefault(p => p.Item!.Selected.Value)!.ScreenSpaceDrawQuad); + AddStep("save selected screen position", () => positionBefore = carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); AddStep("remove first beatmap", () => beatmapSets.Remove(beatmapSets.Last())); AddUntilStep("sorting finished", () => carousel.IsFiltering, () => Is.False); - AddAssert("select screen position unchanged", () => carousel.ChildrenOfType().Single(p => p.Item!.Selected.Value).ScreenSpaceDrawQuad, + AddAssert("select screen position unchanged", () => carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, () => Is.EqualTo(positionBefore)); } @@ -212,11 +212,11 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("wait for scroll finished", () => scroll.Current, () => Is.EqualTo(scroll.Target)); - AddStep("save selected screen position", () => positionBefore = carousel.ChildrenOfType().FirstOrDefault(p => p.Item!.Selected.Value)!.ScreenSpaceDrawQuad); + AddStep("save selected screen position", () => positionBefore = carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); AddStep("remove first beatmap", () => beatmapSets.Remove(beatmapSets.Last())); AddUntilStep("sorting finished", () => carousel.IsFiltering, () => Is.False); - AddAssert("select screen position unchanged", () => carousel.ChildrenOfType().Single(p => p.Item!.Selected.Value).ScreenSpaceDrawQuad, + AddAssert("select screen position unchanged", () => carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, () => Is.EqualTo(positionBefore)); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs index df41aa3e86..dd82bf3495 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs @@ -28,37 +28,32 @@ namespace osu.Game.Screens.SelectV2 return items.OrderDescending(Comparer.Create((a, b) => { - int comparison = 0; + int comparison; - if (a.Model is BeatmapInfo ab && b.Model is BeatmapInfo bb) + var ab = (BeatmapInfo)a.Model; + var bb = (BeatmapInfo)b.Model; + + switch (criteria.Sort) { - switch (criteria.Sort) - { - case SortMode.Artist: - comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.BeatmapSet!.Metadata.Artist, bb.BeatmapSet!.Metadata.Artist); - if (comparison == 0) - goto case SortMode.Title; - break; + case SortMode.Artist: + comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.BeatmapSet!.Metadata.Artist, bb.BeatmapSet!.Metadata.Artist); + if (comparison == 0) + goto case SortMode.Title; + break; - case SortMode.Difficulty: - comparison = ab.StarRating.CompareTo(bb.StarRating); - break; + case SortMode.Difficulty: + comparison = ab.StarRating.CompareTo(bb.StarRating); + break; - case SortMode.Title: - comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.BeatmapSet!.Metadata.Title, bb.BeatmapSet!.Metadata.Title); - break; + case SortMode.Title: + comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.BeatmapSet!.Metadata.Title, bb.BeatmapSet!.Metadata.Title); + break; - default: - throw new ArgumentOutOfRangeException(); - } + default: + throw new ArgumentOutOfRangeException(); } - if (comparison != 0) return comparison; - - if (a is BeatmapCarouselItem aItem && b is BeatmapCarouselItem bItem) - return aItem.ID.CompareTo(bItem.ID); - - return 0; + return comparison; })); }, cancellationToken).ConfigureAwait(false); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs deleted file mode 100644 index dd7aae3db9..0000000000 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs +++ /dev/null @@ -1,48 +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; -using osu.Game.Beatmaps; -using osu.Game.Database; - -namespace osu.Game.Screens.SelectV2 -{ - public class BeatmapCarouselItem : CarouselItem - { - public readonly Guid ID; - - /// - /// Whether this item has a header providing extra information for it. - /// When displaying items which don't have header, we should make sure enough information is included inline. - /// - public bool HasGroupHeader { get; set; } - - /// - /// Whether this item is a group header. - /// Group headers are generally larger in display. Setting this will account for the size difference. - /// - public bool IsGroupHeader { get; set; } - - public override float DrawHeight => IsGroupHeader ? 80 : 40; - - public BeatmapCarouselItem(object model) - : base(model) - { - ID = (Model as IHasGuidPrimaryKey)?.ID ?? Guid.NewGuid(); - } - - public override string? ToString() - { - switch (Model) - { - case BeatmapInfo bi: - return $"Difficulty: {bi.DifficultyName} ({bi.StarRating:N1}*)"; - - case BeatmapSetInfo si: - return $"{si.Metadata}"; - } - - return Model.ToString(); - } - } -} diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs index 27023b50be..da3e1b0964 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs @@ -21,27 +21,41 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private BeatmapCarousel carousel { get; set; } = null!; - public CarouselItem? Item - { - get => item; - set - { - item = value; - - selected.UnbindBindings(); - - if (item != null) - selected.BindTo(item.Selected); - } - } - - private readonly BindableBool selected = new BindableBool(); - private CarouselItem? item; + private Box activationFlash = null!; + private Box background = null!; + private OsuSpriteText text = null!; [BackgroundDependencyLoader] private void load() { - selected.BindValueChanged(value => + InternalChildren = new Drawable[] + { + background = new Box + { + Alpha = 0.8f, + RelativeSizeAxes = Axes.Both, + }, + activationFlash = new Box + { + Colour = Color4.White, + Blending = BlendingParameters.Additive, + Alpha = 0, + RelativeSizeAxes = Axes.Both, + }, + text = new OsuSpriteText + { + Padding = new MarginPadding(5), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + }; + + Selected.BindValueChanged(value => + { + activationFlash.FadeTo(value.NewValue ? 0.2f : 0, 500, Easing.OutQuint); + }); + + KeyboardSelected.BindValueChanged(value => { if (value.NewValue) { @@ -59,6 +73,8 @@ namespace osu.Game.Screens.SelectV2 { base.FreeAfterUse(); Item = null; + Selected.Value = false; + KeyboardSelected.Value = false; } protected override void PrepareForUse() @@ -72,31 +88,44 @@ namespace osu.Game.Screens.SelectV2 Size = new Vector2(500, Item.DrawHeight); Masking = true; - InternalChildren = new Drawable[] - { - new Box - { - Colour = (Item.Model is BeatmapInfo ? Color4.Aqua : Color4.Yellow).Darken(5), - RelativeSizeAxes = Axes.Both, - }, - new OsuSpriteText - { - Text = Item.ToString() ?? string.Empty, - Padding = new MarginPadding(5), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - } - }; + background.Colour = (Item.Model is BeatmapInfo ? Color4.Aqua : Color4.Yellow).Darken(5); + text.Text = getTextFor(Item.Model); this.FadeInFromZero(500, Easing.OutQuint); } + private string getTextFor(object item) + { + switch (item) + { + case BeatmapInfo bi: + return $"Difficulty: {bi.DifficultyName} ({bi.StarRating:N1}*)"; + + case BeatmapSetInfo si: + return $"{si.Metadata}"; + } + + return "unknown"; + } + protected override bool OnClick(ClickEvent e) { - carousel.CurrentSelection = Item!.Model; + if (carousel.CurrentSelection == Item!.Model) + carousel.ActivateSelection(); + else + carousel.CurrentSelection = Item!.Model; return true; } + public CarouselItem? Item { get; set; } + public BindableBool Selected { get; } = new BindableBool(); + public BindableBool KeyboardSelected { get; } = new BindableBool(); + public double DrawYPosition { get; set; } + + public void FlashFromActivation() + { + activationFlash.FadeOutFromOne(500, Easing.OutQuint); + } } } diff --git a/osu.Game/Screens/SelectV2/ICarouselPanel.cs b/osu.Game/Screens/SelectV2/ICarouselPanel.cs index 117feab621..c592734d8d 100644 --- a/osu.Game/Screens/SelectV2/ICarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/ICarouselPanel.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Bindables; using osu.Framework.Graphics; namespace osu.Game.Screens.SelectV2 @@ -10,6 +11,18 @@ namespace osu.Game.Screens.SelectV2 /// public interface ICarouselPanel { + /// + /// Whether this item has selection. + /// This is managed by and should not be set manually. + /// + BindableBool Selected { get; } + + /// + /// Whether this item has keyboard selection. + /// This is managed by and should not be set manually. + /// + BindableBool KeyboardSelected { get; } + /// /// The Y position which should be used for displaying this item within the carousel. This is managed by and should not be set manually. /// From ecef5e5d715b5f783ad630040131eb9791fd16fd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 16:10:42 +0900 Subject: [PATCH 166/801] Add set-difficulty tracking in `BeatmapCarouselFilterGrouping` Rather than tracking inside individual items, let's just maintain a single dictionary which is refreshed every time we regenerate filters. --- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 6cdd15d301..4f0767048a 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -13,6 +13,13 @@ namespace osu.Game.Screens.SelectV2 { public class BeatmapCarouselFilterGrouping : ICarouselFilter { + /// + /// Beatmap sets contain difficulties as related panels. This dictionary holds the relationships between set-difficulties to allow expanding them on selection. + /// + public IDictionary> SetItems => setItems; + + private readonly Dictionary> setItems = new Dictionary>(); + private readonly Func getCriteria; public BeatmapCarouselFilterGrouping(Func getCriteria) @@ -27,7 +34,10 @@ namespace osu.Game.Screens.SelectV2 if (criteria.SplitOutDifficulties) { foreach (var item in items) - ((BeatmapCarouselItem)item).HasGroupHeader = false; + { + item.IsVisible = true; + item.IsGroupSelectionTarget = true; + } return items; } @@ -44,14 +54,25 @@ namespace osu.Game.Screens.SelectV2 { // Add set header if (lastItem == null || (lastItem.Model is BeatmapInfo b2 && b2.BeatmapSet!.OnlineID != b.BeatmapSet!.OnlineID)) - newItems.Add(new BeatmapCarouselItem(b.BeatmapSet!) { IsGroupHeader = true }); + { + newItems.Add(new CarouselItem(b.BeatmapSet!) + { + DrawHeight = 80, + IsGroupSelectionTarget = true + }); + } + + if (!setItems.TryGetValue(b.BeatmapSet!, out var related)) + setItems[b.BeatmapSet!] = related = new HashSet(); + + related.Add(item); } newItems.Add(item); lastItem = item; - var beatmapCarouselItem = (BeatmapCarouselItem)item; - beatmapCarouselItem.HasGroupHeader = true; + item.IsGroupSelectionTarget = false; + item.IsVisible = false; } return newItems; From 2f94456a06dbdc50fcc4d87b4823e1baac27179b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 16:11:02 +0900 Subject: [PATCH 167/801] Add selection and activation flow --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 49 ++- osu.Game/Screens/SelectV2/Carousel.cs | 347 +++++++++++++++---- 2 files changed, 329 insertions(+), 67 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index d9c049bbae..e3bc487154 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -26,6 +26,8 @@ namespace osu.Game.Screens.SelectV2 private readonly LoadingLayer loading; + private readonly BeatmapCarouselFilterGrouping grouping; + public BeatmapCarousel() { DebounceDelay = 100; @@ -34,7 +36,7 @@ namespace osu.Game.Screens.SelectV2 Filters = new ICarouselFilter[] { new BeatmapCarouselFilterSorting(() => Criteria), - new BeatmapCarouselFilterGrouping(() => Criteria), + grouping = new BeatmapCarouselFilterGrouping(() => Criteria), }; AddInternal(carouselPanelPool); @@ -51,7 +53,50 @@ namespace osu.Game.Screens.SelectV2 protected override Drawable GetDrawableForDisplay(CarouselItem item) => carouselPanelPool.Get(); - protected override CarouselItem CreateCarouselItemForModel(BeatmapInfo model) => new BeatmapCarouselItem(model); + protected override void HandleItemDeselected(object? model) + { + base.HandleItemDeselected(model); + + var deselectedSet = model as BeatmapSetInfo ?? (model as BeatmapInfo)?.BeatmapSet; + + if (grouping.SetItems.TryGetValue(deselectedSet!, out var group)) + { + foreach (var i in group) + i.IsVisible = false; + } + } + + protected override void HandleItemSelected(object? model) + { + base.HandleItemSelected(model); + + // Selecting a set isn't valid – let's re-select the first difficulty. + if (model is BeatmapSetInfo setInfo) + { + CurrentSelection = setInfo.Beatmaps.First(); + return; + } + + var currentSelectionSet = (model as BeatmapInfo)?.BeatmapSet; + + if (currentSelectionSet == null) + return; + + if (grouping.SetItems.TryGetValue(currentSelectionSet, out var group)) + { + foreach (var i in group) + i.IsVisible = true; + } + } + + protected override void HandleItemActivated(CarouselItem item) + { + base.HandleItemActivated(item); + + // TODO: maybe this should be handled by the panel itself? + if (GetMaterialisedDrawableForItem(item) is BeatmapCarouselPanel drawable) + drawable.FlashFromActivation(); + } private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed) { diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index c042da167e..598a898686 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -28,7 +28,8 @@ namespace osu.Game.Screens.SelectV2 /// A highly efficient vertical list display that is used primarily for the song select screen, /// but flexible enough to be used for other use cases. ///
- public abstract partial class Carousel : CompositeDrawable + public abstract partial class Carousel : CompositeDrawable, IKeyBindingHandler + where T : notnull { #region Properties and methods for external usage @@ -80,26 +81,34 @@ namespace osu.Game.Screens.SelectV2 public int VisibleItems => scroll.Panels.Count; /// - /// The currently selected model. + /// The currently selected model. Generally of type T. /// /// - /// Setting this will ensure is set to true only on the matching . - /// Of note, if no matching item exists all items will be deselected while waiting for potential new item which matches. + /// A carousel may create panels for non-T types. + /// To keep things simple, we therefore avoid generic constraints on the current selection. + /// + /// The selection is never reset due to not existing. It can be set to anything. + /// If no matching carousel item exists, there will be no visually selected item while waiting for potential new item which matches. /// - public virtual object? CurrentSelection + public object? CurrentSelection { - get => currentSelection; - set + get => currentSelection.Model; + set => setSelection(value); + } + + /// + /// Activate the current selection, if a selection exists. + /// + public void ActivateSelection() + { + if (currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) { - if (currentSelectionCarouselItem != null) - currentSelectionCarouselItem.Selected.Value = false; - - currentSelection = value; - - currentSelectionCarouselItem = null; - currentSelectionYPosition = null; - updateSelection(); + CurrentSelection = currentKeyboardSelection.Model; + return; } + + if (currentSelection.CarouselItem != null) + HandleItemActivated(currentSelection.CarouselItem); } #endregion @@ -144,11 +153,42 @@ namespace osu.Game.Screens.SelectV2 protected abstract Drawable GetDrawableForDisplay(CarouselItem item); /// - /// Create an internal carousel representation for the provided model object. + /// Given a , find a drawable representation if it is currently displayed in the carousel. /// - /// The model. - /// A representing the model. - protected abstract CarouselItem CreateCarouselItemForModel(T model); + /// + /// This will only return a drawable if it is "on-screen". + /// + /// The item to find a related drawable representation. + /// The drawable representation if it exists. + protected Drawable? GetMaterialisedDrawableForItem(CarouselItem item) => + scroll.Panels.SingleOrDefault(p => ((ICarouselPanel)p).Item == item); + + /// + /// Called when an item is "selected". + /// + protected virtual void HandleItemSelected(object? model) + { + } + + /// + /// Called when an item is "deselected". + /// + protected virtual void HandleItemDeselected(object? model) + { + } + + /// + /// Called when an item is "activated". + /// + /// + /// An activated item should for instance: + /// - Open or close a folder + /// - Start gameplay on a beatmap difficulty. + /// + /// The carousel item which was activated. + protected virtual void HandleItemActivated(CarouselItem item) + { + } #endregion @@ -197,7 +237,7 @@ namespace osu.Game.Screens.SelectV2 // Copy must be performed on update thread for now (see ConfigureAwait above). // Could potentially be optimised in the future if it becomes an issue. - IEnumerable items = new List(Items.Select(CreateCarouselItemForModel)); + IEnumerable items = new List(Items.Select(m => new CarouselItem(m))); await Task.Run(async () => { @@ -210,7 +250,7 @@ namespace osu.Game.Screens.SelectV2 } log("Updating Y positions"); - await updateYPositions(items, cts.Token).ConfigureAwait(false); + updateYPositions(items, visibleHalfHeight, SpacingBetweenPanels); } catch (OperationCanceledException) { @@ -225,58 +265,231 @@ namespace osu.Game.Screens.SelectV2 carouselItems = items.ToList(); displayedRange = null; - updateSelection(); + // Need to call this to ensure correct post-selection logic is handled on the new items list. + HandleItemSelected(currentSelection.Model); + + refreshAfterSelection(); void log(string text) => Logger.Log($"Carousel[op {cts.GetHashCode().ToString()}] {stopwatch.ElapsedMilliseconds} ms: {text}"); } - private async Task updateYPositions(IEnumerable carouselItems, CancellationToken cancellationToken) => await Task.Run(() => + private static void updateYPositions(IEnumerable carouselItems, float offset, float spacing) { - float yPos = visibleHalfHeight; - foreach (var item in carouselItems) + updateItemYPosition(item, ref offset, spacing); + } + + private static void updateItemYPosition(CarouselItem item, ref float offset, float spacing) + { + item.CarouselYPosition = offset; + if (item.IsVisible) + offset += item.DrawHeight + spacing; + } + + #endregion + + #region Input handling + + public bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) { - item.CarouselYPosition = yPos; - yPos += item.DrawHeight + SpacingBetweenPanels; + case GlobalAction.Select: + ActivateSelection(); + return true; + + case GlobalAction.SelectNext: + selectNext(1, isGroupSelection: false); + return true; + + case GlobalAction.SelectNextGroup: + selectNext(1, isGroupSelection: true); + return true; + + case GlobalAction.SelectPrevious: + selectNext(-1, isGroupSelection: false); + return true; + + case GlobalAction.SelectPreviousGroup: + selectNext(-1, isGroupSelection: true); + return true; } - }, cancellationToken).ConfigureAwait(false); + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + + /// + /// Select the next valid selection relative to a current selection. + /// This is generally for keyboard based traversal. + /// + /// Positive for downwards, negative for upwards. + /// Whether the selection should traverse groups. Group selection updates the actual selection immediately, while non-group selection will only prepare a future keyboard selection. + /// Whether selection was possible. + private bool selectNext(int direction, bool isGroupSelection) + { + // Ensure sanity + Debug.Assert(direction != 0); + direction = direction > 0 ? 1 : -1; + + if (carouselItems == null || carouselItems.Count == 0) + return false; + + // If the user has a different keyboard selection and requests + // group selection, first transfer the keyboard selection to actual selection. + if (isGroupSelection && currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) + { + ActivateSelection(); + return true; + } + + CarouselItem? selectionItem = currentKeyboardSelection.CarouselItem; + int selectionIndex = currentKeyboardSelection.Index ?? -1; + + // To keep things simple, let's first handle the cases where there's no selection yet. + if (selectionItem == null || selectionIndex < 0) + { + // Start by selecting the first item. + selectionItem = carouselItems.First(); + selectionIndex = 0; + + // In the forwards case, immediately attempt selection of this panel. + // If selection fails, continue with standard logic to find the next valid selection. + if (direction > 0 && attemptSelection(selectionItem)) + return true; + + // In the backwards direction we can just allow the selection logic to go ahead and loop around to the last valid. + } + + Debug.Assert(selectionItem != null); + + // As a second special case, if we're group selecting backwards and the current selection isn't + // a group, base this selection operation from the closest previous group. + if (isGroupSelection && direction < 0) + { + while (!carouselItems[selectionIndex].IsGroupSelectionTarget) + selectionIndex--; + } + + CarouselItem? newItem; + + // Iterate over every item back to the current selection, finding the first valid item. + // The fail condition is when we reach the selection after a cyclic loop over every item. + do + { + selectionIndex += direction; + newItem = carouselItems[(selectionIndex + carouselItems.Count) % carouselItems.Count]; + + if (attemptSelection(newItem)) + return true; + } while (newItem != selectionItem); + + return false; + + bool attemptSelection(CarouselItem item) + { + if (!item.IsVisible || (isGroupSelection && !item.IsGroupSelectionTarget)) + return false; + + if (isGroupSelection) + setSelection(item.Model); + else + setKeyboardSelection(item.Model); + + return true; + } + } #endregion #region Selection handling - private object? currentSelection; - private CarouselItem? currentSelectionCarouselItem; - private double? currentSelectionYPosition; + private Selection currentKeyboardSelection = new Selection(); + private Selection currentSelection = new Selection(); - private void updateSelection() + private void setSelection(object? model) { - currentSelectionCarouselItem = null; + if (currentSelection.Model == model) + return; - if (carouselItems == null) return; + var previousSelection = currentSelection; - foreach (var item in carouselItems) + if (previousSelection.Model != null) + HandleItemDeselected(previousSelection.Model); + + currentSelection = currentKeyboardSelection = new Selection(model); + HandleItemSelected(currentSelection.Model); + + // ensure the selection hasn't changed in the handling of selection. + // if it's changed, avoid a second update of selection/scroll. + if (currentSelection.Model != model) + return; + + refreshAfterSelection(); + scrollToSelection(); + } + + private void setKeyboardSelection(object? model) + { + currentKeyboardSelection = new Selection(model); + + refreshAfterSelection(); + scrollToSelection(); + } + + /// + /// Call after a selection of items change to re-attach s to current s. + /// + private void refreshAfterSelection() + { + float yPos = visibleHalfHeight; + + // Invalidate display range as panel positions and visible status may have changed. + // Position transfer won't happen unless we invalidate this. + displayedRange = null; + + // The case where no items are available for display yet. + if (carouselItems == null) { - bool isSelected = item.Model == currentSelection; - - if (isSelected) - { - currentSelectionCarouselItem = item; - - if (currentSelectionYPosition != item.CarouselYPosition) - { - if (currentSelectionYPosition != null) - { - float adjustment = (float)(item.CarouselYPosition - currentSelectionYPosition.Value); - scroll.OffsetScrollPosition(adjustment); - } - - currentSelectionYPosition = item.CarouselYPosition; - } - } - - item.Selected.Value = isSelected; + currentKeyboardSelection = new Selection(); + currentSelection = new Selection(); + return; } + + float spacing = SpacingBetweenPanels; + int count = carouselItems.Count; + + Selection prevKeyboard = currentKeyboardSelection; + + // We are performing two important operations here: + // - Update all Y positions. After a selection occurs, panels may have changed visibility state and therefore Y positions. + // - Link selected models to CarouselItems. If a selection changed, this is where we find the relevant CarouselItems for further use. + for (int i = 0; i < count; i++) + { + var item = carouselItems[i]; + + updateItemYPosition(item, ref yPos, spacing); + + if (ReferenceEquals(item.Model, currentKeyboardSelection.Model)) + currentKeyboardSelection = new Selection(item.Model, item, item.CarouselYPosition, i); + + if (ReferenceEquals(item.Model, currentSelection.Model)) + currentSelection = new Selection(item.Model, item, item.CarouselYPosition, i); + } + + // If a keyboard selection is currently made, we want to keep the view stable around the selection. + // That means that we should offset the immediate scroll position by any change in Y position for the selection. + if (prevKeyboard.YPosition != null && currentKeyboardSelection.YPosition != prevKeyboard.YPosition) + scroll.OffsetScrollPosition((float)(currentKeyboardSelection.YPosition!.Value - prevKeyboard.YPosition.Value)); + } + + private void scrollToSelection() + { + if (currentKeyboardSelection.CarouselItem != null) + scroll.ScrollTo(currentKeyboardSelection.CarouselItem.CarouselYPosition - visibleHalfHeight); } #endregion @@ -285,7 +498,7 @@ namespace osu.Game.Screens.SelectV2 private DisplayRange? displayedRange; - private readonly CarouselItem carouselBoundsItem = new BoundsCarouselItem(); + private readonly CarouselItem carouselBoundsItem = new CarouselItem(new object()); /// /// The position of the lower visible bound with respect to the current scroll position. @@ -335,6 +548,9 @@ namespace osu.Game.Screens.SelectV2 float dist = Math.Abs(1f - posInScroll.Y / visibleHalfHeight); panel.X = offsetX(dist, visibleHalfHeight); + + c.Selected.Value = c.Item == currentSelection?.CarouselItem; + c.KeyboardSelected.Value = c.Item == currentKeyboardSelection?.CarouselItem; } } @@ -381,6 +597,8 @@ namespace osu.Game.Screens.SelectV2 ? new List() : carouselItems.GetRange(range.First, range.Last - range.First + 1); + toDisplay.RemoveAll(i => !i.IsVisible); + // Iterate over all panels which are already displayed and figure which need to be displayed / removed. foreach (var panel in scroll.Panels) { @@ -434,6 +652,15 @@ namespace osu.Game.Screens.SelectV2 #region Internal helper classes + /// + /// Bookkeeping for a current selection. + /// + /// The selected model. If null, there's no selection. + /// A related carousel item representation for the model. May be null if selection is not present as an item, or if has not been run yet. + /// The Y position of the selection as of the last run of . May be null if selection is not present as an item, or if has not been run yet. + /// The index of the selection as of the last run of . May be null if selection is not present as an item, or if has not been run yet. + private record Selection(object? Model = null, CarouselItem? CarouselItem = null, double? YPosition = null, int? Index = null); + private record DisplayRange(int First, int Last); /// @@ -573,16 +800,6 @@ namespace osu.Game.Screens.SelectV2 #endregion } - private class BoundsCarouselItem : CarouselItem - { - public override float DrawHeight => 0; - - public BoundsCarouselItem() - : base(new object()) - { - } - } - #endregion } } From 9ab045495d4dcb0d2d5afb52e4f4d69a5ed3074d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 16:24:04 +0900 Subject: [PATCH 168/801] Tidy up tests in preparation for adding more --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 187 ++++++++++++ .../SongSelect/TestSceneBeatmapCarouselV2.cs | 286 ------------------ .../TestSceneBeatmapCarouselV2Basics.cs | 119 ++++++++ 3 files changed, 306 insertions(+), 286 deletions(-) create mode 100644 osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs delete mode 100644 osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs create mode 100644 osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs new file mode 100644 index 0000000000..eaa29abf01 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -0,0 +1,187 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Resources; +using osuTK.Graphics; +using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel; + +namespace osu.Game.Tests.Visual.SongSelect +{ + public abstract partial class BeatmapCarouselV2TestScene : OsuManualInputManagerTestScene + { + protected readonly BindableList BeatmapSets = new BindableList(); + + protected BeatmapCarousel Carousel = null!; + + protected OsuScrollContainer Scroll => Carousel.ChildrenOfType>().Single(); + + [Cached(typeof(BeatmapStore))] + private BeatmapStore store; + + private OsuTextFlowContainer stats = null!; + + private int beatmapCount; + + protected BeatmapCarouselV2TestScene() + { + store = new TestBeatmapStore + { + BeatmapSets = { BindTarget = BeatmapSets } + }; + + BeatmapSets.BindCollectionChanged((_, _) => beatmapCount = BeatmapSets.Sum(s => s.Beatmaps.Count)); + + Scheduler.AddDelayed(updateStats, 100, true); + } + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("reset beatmaps", () => BeatmapSets.Clear()); + + CreateCarousel(); + + SortBy(new FilterCriteria { Sort = SortMode.Title }); + } + + protected void CreateCarousel() + { + AddStep("create components", () => + { + Box topBox; + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Relative, 1), + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 200), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 200), + }, + Content = new[] + { + new Drawable[] + { + topBox = new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Color4.Cyan, + RelativeSizeAxes = Axes.Both, + Alpha = 0.4f, + }, + }, + new Drawable[] + { + Carousel = new BeatmapCarousel + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 500, + RelativeSizeAxes = Axes.Y, + }, + }, + new[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Color4.Cyan, + RelativeSizeAxes = Axes.Both, + Alpha = 0.4f, + }, + topBox.CreateProxy(), + } + } + }, + stats = new OsuTextFlowContainer + { + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding(10), + TextAnchor = Anchor.CentreLeft, + }, + }; + }); + } + + protected void SortBy(FilterCriteria criteria) => AddStep($"sort by {criteria.Sort}", () => Carousel.Filter(criteria)); + + protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); + protected void WaitForSorting() => AddUntilStep("sorting finished", () => Carousel.IsFiltering, () => Is.False); + protected void WaitForScrolling() => AddUntilStep("scroll finished", () => Scroll.Current, () => Is.EqualTo(Scroll.Target)); + + /// + /// Add requested beatmap sets count to list. + /// + /// The count of beatmap sets to add. + /// If not null, the number of difficulties per set. If null, randomised difficulty count will be used. + protected void AddBeatmaps(int count, int? fixedDifficultiesPerSet = null) => AddStep($"add {count} beatmaps", () => + { + for (int i = 0; i < count; i++) + BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4))); + }); + + protected void RemoveLastBeatmap() => + AddStep("remove last beatmap", () => + { + if (BeatmapSets.Count == 0) return; + + BeatmapSets.Remove(BeatmapSets.Last()); + }); + + private void updateStats() + { + if (Carousel.IsNull()) + return; + + stats.Clear(); + createHeader("beatmap store"); + stats.AddParagraph($""" + sets: {BeatmapSets.Count} + beatmaps: {beatmapCount} + """); + createHeader("carousel"); + stats.AddParagraph($""" + sorting: {Carousel.IsFiltering} + tracked: {Carousel.ItemsTracked} + displayable: {Carousel.DisplayableItems} + displayed: {Carousel.VisibleItems} + selected: {Carousel.CurrentSelection} + """); + + void createHeader(string text) + { + stats.AddParagraph(string.Empty); + stats.AddParagraph(text, cp => + { + cp.Font = cp.Font.With(size: 18, weight: FontWeight.Bold); + }); + } + } + } +} diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs deleted file mode 100644 index dee61bbcde..0000000000 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs +++ /dev/null @@ -1,286 +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 NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions.ObjectExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Testing; -using osu.Framework.Utils; -using osu.Game.Beatmaps; -using osu.Game.Database; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Screens.Select; -using osu.Game.Screens.Select.Filter; -using osu.Game.Screens.SelectV2; -using osu.Game.Tests.Beatmaps; -using osu.Game.Tests.Resources; -using osuTK.Graphics; -using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel; - -namespace osu.Game.Tests.Visual.SongSelect -{ - [TestFixture] - public partial class TestSceneBeatmapCarouselV2 : OsuManualInputManagerTestScene - { - private readonly BindableList beatmapSets = new BindableList(); - - [Cached(typeof(BeatmapStore))] - private BeatmapStore store; - - private OsuTextFlowContainer stats = null!; - private BeatmapCarousel carousel = null!; - - private OsuScrollContainer scroll => carousel.ChildrenOfType>().Single(); - - private int beatmapCount; - - public TestSceneBeatmapCarouselV2() - { - store = new TestBeatmapStore - { - BeatmapSets = { BindTarget = beatmapSets } - }; - - beatmapSets.BindCollectionChanged((_, _) => - { - beatmapCount = beatmapSets.Sum(s => s.Beatmaps.Count); - }); - - Scheduler.AddDelayed(updateStats, 100, true); - } - - [SetUpSteps] - public void SetUpSteps() - { - AddStep("create components", () => - { - beatmapSets.Clear(); - - Box topBox; - Children = new Drawable[] - { - new GridContainer - { - RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.Relative, 1), - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.Absolute, 200), - new Dimension(), - new Dimension(GridSizeMode.Absolute, 200), - }, - Content = new[] - { - new Drawable[] - { - topBox = new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = Color4.Cyan, - RelativeSizeAxes = Axes.Both, - Alpha = 0.4f, - }, - }, - new Drawable[] - { - carousel = new BeatmapCarousel - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 500, - RelativeSizeAxes = Axes.Y, - }, - }, - new[] - { - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = Color4.Cyan, - RelativeSizeAxes = Axes.Both, - Alpha = 0.4f, - }, - topBox.CreateProxy(), - } - } - }, - stats = new OsuTextFlowContainer - { - AutoSizeAxes = Axes.Both, - Padding = new MarginPadding(10), - TextAnchor = Anchor.CentreLeft, - }, - }; - }); - - AddStep("sort by title", () => - { - carousel.Filter(new FilterCriteria { Sort = SortMode.Title }); - }); - } - - [Test] - public void TestBasic() - { - AddStep("add 10 beatmaps", () => - { - for (int i = 0; i < 10; i++) - beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); - }); - - AddStep("add 1 beatmap", () => beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4)))); - - AddStep("remove all beatmaps", () => beatmapSets.Clear()); - } - - [Test] - public void TestSorting() - { - AddStep("add 10 beatmaps", () => - { - for (int i = 0; i < 10; i++) - beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); - }); - - AddStep("sort by difficulty", () => - { - carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }); - }); - - AddStep("sort by artist", () => - { - carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }); - }); - } - - [Test] - public void TestScrollPositionMaintainedOnAddSecondSelected() - { - Quad positionBefore = default; - - AddStep("add 10 beatmaps", () => - { - for (int i = 0; i < 10; i++) - beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); - }); - - AddUntilStep("visual item added", () => carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); - - AddStep("select middle beatmap", () => carousel.CurrentSelection = beatmapSets.ElementAt(beatmapSets.Count - 2)); - AddStep("scroll to selected item", () => scroll.ScrollTo(scroll.ChildrenOfType().Single(p => p.Selected.Value))); - - AddUntilStep("wait for scroll finished", () => scroll.Current, () => Is.EqualTo(scroll.Target)); - - AddStep("save selected screen position", () => positionBefore = carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); - - AddStep("remove first beatmap", () => beatmapSets.Remove(beatmapSets.Last())); - AddUntilStep("sorting finished", () => carousel.IsFiltering, () => Is.False); - AddAssert("select screen position unchanged", () => carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, - () => Is.EqualTo(positionBefore)); - } - - [Test] - public void TestScrollPositionMaintainedOnAddLastSelected() - { - Quad positionBefore = default; - - AddStep("add 10 beatmaps", () => - { - for (int i = 0; i < 10; i++) - beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); - }); - - AddUntilStep("visual item added", () => carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); - - AddStep("scroll to last item", () => scroll.ScrollToEnd(false)); - - AddStep("select last beatmap", () => carousel.CurrentSelection = beatmapSets.First()); - - AddUntilStep("wait for scroll finished", () => scroll.Current, () => Is.EqualTo(scroll.Target)); - - AddStep("save selected screen position", () => positionBefore = carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); - - AddStep("remove first beatmap", () => beatmapSets.Remove(beatmapSets.Last())); - AddUntilStep("sorting finished", () => carousel.IsFiltering, () => Is.False); - AddAssert("select screen position unchanged", () => carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, - () => Is.EqualTo(positionBefore)); - } - - [Test] - public void TestAddRemoveOneByOne() - { - AddRepeatStep("add beatmaps", () => beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))), 20); - - AddRepeatStep("remove beatmaps", () => beatmapSets.RemoveAt(RNG.Next(0, beatmapSets.Count)), 20); - } - - [Test] - [Explicit] - public void TestInsane() - { - const int count = 200000; - - List generated = new List(); - - AddStep($"populate {count} test beatmaps", () => - { - generated.Clear(); - Task.Run(() => - { - for (int j = 0; j < count; j++) - generated.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); - }).ConfigureAwait(true); - }); - - AddUntilStep("wait for beatmaps populated", () => generated.Count, () => Is.GreaterThan(count / 3)); - AddUntilStep("this takes a while", () => generated.Count, () => Is.GreaterThan(count / 3 * 2)); - AddUntilStep("maybe they are done now", () => generated.Count, () => Is.EqualTo(count)); - - AddStep("add all beatmaps", () => beatmapSets.AddRange(generated)); - } - - private void updateStats() - { - if (carousel.IsNull()) - return; - - stats.Clear(); - createHeader("beatmap store"); - stats.AddParagraph($""" - sets: {beatmapSets.Count} - beatmaps: {beatmapCount} - """); - createHeader("carousel"); - stats.AddParagraph($""" - sorting: {carousel.IsFiltering} - tracked: {carousel.ItemsTracked} - displayable: {carousel.DisplayableItems} - displayed: {carousel.VisibleItems} - selected: {carousel.CurrentSelection} - """); - - void createHeader(string text) - { - stats.AddParagraph(string.Empty); - stats.AddParagraph(text, cp => - { - cp.Font = cp.Font.With(size: 18, weight: FontWeight.Bold); - }); - } - } - } -} diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs new file mode 100644 index 0000000000..8d801930fc --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs @@ -0,0 +1,119 @@ +// 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 NUnit.Framework; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Visual.SongSelect +{ + /// + /// Currently covers adding and removing of items and scrolling. + /// If we add more tests here, these two categories can likely be split out into separate scenes. + /// + [TestFixture] + public partial class TestSceneBeatmapCarouselV2Basics : BeatmapCarouselV2TestScene + { + [Test] + public void TestBasics() + { + AddBeatmaps(1); + AddBeatmaps(10); + RemoveLastBeatmap(); + AddStep("remove all beatmaps", () => BeatmapSets.Clear()); + } + + [Test] + public void TestAddRemoveOneByOne() + { + AddRepeatStep("add beatmaps", () => BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))), 20); + AddRepeatStep("remove beatmaps", () => BeatmapSets.RemoveAt(RNG.Next(0, BeatmapSets.Count)), 20); + } + + [Test] + public void TestSorting() + { + AddBeatmaps(10); + SortBy(new FilterCriteria { Sort = SortMode.Difficulty }); + SortBy(new FilterCriteria { Sort = SortMode.Artist }); + } + + [Test] + public void TestScrollPositionMaintainedOnAddSecondSelected() + { + Quad positionBefore = default; + + AddBeatmaps(10); + WaitForDrawablePanels(); + + AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2)); + AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType().Single(p => p.Selected.Value))); + + WaitForScrolling(); + + AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); + + RemoveLastBeatmap(); + WaitForSorting(); + + AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, + () => Is.EqualTo(positionBefore)); + } + + [Test] + public void TestScrollPositionMaintainedOnAddLastSelected() + { + Quad positionBefore = default; + + AddBeatmaps(10); + WaitForDrawablePanels(); + + AddStep("scroll to last item", () => Scroll.ScrollToEnd(false)); + + AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.First()); + + WaitForScrolling(); + + AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); + + RemoveLastBeatmap(); + WaitForSorting(); + AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, + () => Is.EqualTo(positionBefore)); + } + + [Test] + [Explicit] + public void TestPerformanceWithManyBeatmaps() + { + const int count = 200000; + + List generated = new List(); + + AddStep($"populate {count} test beatmaps", () => + { + generated.Clear(); + Task.Run(() => + { + for (int j = 0; j < count; j++) + generated.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); + }).ConfigureAwait(true); + }); + + AddUntilStep("wait for beatmaps populated", () => generated.Count, () => Is.GreaterThan(count / 3)); + AddUntilStep("this takes a while", () => generated.Count, () => Is.GreaterThan(count / 3 * 2)); + AddUntilStep("maybe they are done now", () => generated.Count, () => Is.EqualTo(count)); + + AddStep("add all beatmaps", () => BeatmapSets.AddRange(generated)); + } + } +} From ffca90779fcc9a781fb6a5c6063c0f1baa927f81 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 16:48:03 +0900 Subject: [PATCH 169/801] Fix sort direction being flipped --- .../Visual/SongSelect/BeatmapCarouselV2TestScene.cs | 10 ++++++---- .../SongSelect/TestSceneBeatmapCarouselV2Basics.cs | 10 +++++----- .../Screens/SelectV2/BeatmapCarouselFilterSorting.cs | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index eaa29abf01..3aa9f60181 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual.SongSelect [SetUpSteps] public void SetUpSteps() { - AddStep("reset beatmaps", () => BeatmapSets.Clear()); + RemoveAllBeatmaps(); CreateCarousel(); @@ -146,12 +146,14 @@ namespace osu.Game.Tests.Visual.SongSelect BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4))); }); - protected void RemoveLastBeatmap() => - AddStep("remove last beatmap", () => + protected void RemoveAllBeatmaps() => AddStep("clear all beatmaps", () => BeatmapSets.Clear()); + + protected void RemoveFirstBeatmap() => + AddStep("remove first beatmap", () => { if (BeatmapSets.Count == 0) return; - BeatmapSets.Remove(BeatmapSets.Last()); + BeatmapSets.Remove(BeatmapSets.First()); }); private void updateStats() diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs index 8d801930fc..748831bf7b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs @@ -28,8 +28,8 @@ namespace osu.Game.Tests.Visual.SongSelect { AddBeatmaps(1); AddBeatmaps(10); - RemoveLastBeatmap(); - AddStep("remove all beatmaps", () => BeatmapSets.Clear()); + RemoveFirstBeatmap(); + RemoveAllBeatmaps(); } [Test] @@ -62,7 +62,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); - RemoveLastBeatmap(); + RemoveFirstBeatmap(); WaitForSorting(); AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, @@ -79,13 +79,13 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("scroll to last item", () => Scroll.ScrollToEnd(false)); - AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.First()); + AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.Last()); WaitForScrolling(); AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); - RemoveLastBeatmap(); + RemoveFirstBeatmap(); WaitForSorting(); AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, () => Is.EqualTo(positionBefore)); diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs index dd82bf3495..0298616aa8 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.SelectV2 { var criteria = getCriteria(); - return items.OrderDescending(Comparer.Create((a, b) => + return items.Order(Comparer.Create((a, b) => { int comparison; From eaea053c7d8824d26ba43821bc4e46bb9ba227a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 17:19:09 +0900 Subject: [PATCH 170/801] Add test coverage of various selection examples Where possible I've tried to match the test and method names of `TestSceneBeatmapCarousel` for easy coverage comparison. --- .../TestSceneBeatmapCarouselV2Selection.cs | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs new file mode 100644 index 0000000000..305774b7d3 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs @@ -0,0 +1,216 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Screens.SelectV2; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.SongSelect +{ + [TestFixture] + public partial class TestSceneBeatmapCarouselV2Selection : BeatmapCarouselV2TestScene + { + /// + /// Keyboard selection via up and down arrows doesn't actually change the selection until + /// the select key is pressed. + /// + [Test] + public void TestKeyboardSelectionKeyRepeat() + { + AddBeatmaps(10); + WaitForDrawablePanels(); + checkNoSelection(); + + select(); + checkNoSelection(); + + AddStep("press down arrow", () => InputManager.PressKey(Key.Down)); + checkSelectionIterating(false); + + AddStep("press up arrow", () => InputManager.PressKey(Key.Up)); + checkSelectionIterating(false); + + AddStep("release down arrow", () => InputManager.ReleaseKey(Key.Down)); + checkSelectionIterating(false); + + AddStep("release up arrow", () => InputManager.ReleaseKey(Key.Up)); + checkSelectionIterating(false); + + select(); + checkHasSelection(); + } + + /// + /// Keyboard selection via left and right arrows moves between groups, updating the selection + /// immediately. + /// + [Test] + public void TestGroupSelectionKeyRepeat() + { + AddBeatmaps(10); + WaitForDrawablePanels(); + checkNoSelection(); + + AddStep("press right arrow", () => InputManager.PressKey(Key.Right)); + checkSelectionIterating(true); + + AddStep("press left arrow", () => InputManager.PressKey(Key.Left)); + checkSelectionIterating(true); + + AddStep("release right arrow", () => InputManager.ReleaseKey(Key.Right)); + checkSelectionIterating(true); + + AddStep("release left arrow", () => InputManager.ReleaseKey(Key.Left)); + checkSelectionIterating(false); + } + + [Test] + public void TestCarouselRemembersSelection() + { + AddBeatmaps(10); + WaitForDrawablePanels(); + + selectNextGroup(); + + object? selection = null; + + AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model); + + checkHasSelection(); + AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null); + AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); + + RemoveAllBeatmaps(); + AddUntilStep("no drawable selection", getSelectedPanel, () => Is.Null); + + AddBeatmaps(10); + WaitForDrawablePanels(); + + checkHasSelection(); + AddAssert("no drawable selection", getSelectedPanel, () => Is.Null); + + AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!)); + + AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); + AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); + + BeatmapCarouselPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); + } + + [Test] + public void TestTraversalBeyondStart() + { + const int total_set_count = 200; + + AddBeatmaps(total_set_count); + WaitForDrawablePanels(); + + selectNextGroup(); + waitForSelection(0, 0); + selectPrevGroup(); + waitForSelection(total_set_count - 1, 0); + } + + [Test] + public void TestTraversalBeyondEnd() + { + const int total_set_count = 200; + + AddBeatmaps(total_set_count); + WaitForDrawablePanels(); + + selectPrevGroup(); + waitForSelection(total_set_count - 1, 0); + selectNextGroup(); + waitForSelection(0, 0); + } + + [Test] + public void TestKeyboardSelection() + { + AddBeatmaps(10, 3); + WaitForDrawablePanels(); + + selectNextPanel(); + selectNextPanel(); + selectNextPanel(); + selectNextPanel(); + checkNoSelection(); + + select(); + waitForSelection(3, 0); + + selectNextPanel(); + waitForSelection(3, 0); + + select(); + waitForSelection(3, 1); + + selectNextPanel(); + waitForSelection(3, 1); + + select(); + waitForSelection(3, 2); + + selectNextPanel(); + waitForSelection(3, 2); + + select(); + waitForSelection(4, 0); + } + + [Test] + public void TestEmptyTraversal() + { + selectNextPanel(); + checkNoSelection(); + + selectNextGroup(); + checkNoSelection(); + + selectPrevPanel(); + checkNoSelection(); + + selectPrevGroup(); + checkNoSelection(); + } + + private void waitForSelection(int set, int? diff = null) + { + AddUntilStep($"selected is set{set}{(diff.HasValue ? $" diff{diff.Value}" : "")}", () => + { + if (diff != null) + return ReferenceEquals(Carousel.CurrentSelection, BeatmapSets[set].Beatmaps[diff.Value]); + + return BeatmapSets[set].Beatmaps.Contains(Carousel.CurrentSelection); + }); + } + + private void selectNextPanel() => AddStep("select next panel", () => InputManager.Key(Key.Down)); + private void selectPrevPanel() => AddStep("select prev panel", () => InputManager.Key(Key.Up)); + private void selectNextGroup() => AddStep("select next group", () => InputManager.Key(Key.Right)); + private void selectPrevGroup() => AddStep("select prev group", () => InputManager.Key(Key.Left)); + + private void select() => AddStep("select", () => InputManager.Key(Key.Enter)); + + private void checkNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null); + private void checkHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null); + + private void checkSelectionIterating(bool isIterating) + { + object? selection = null; + + for (int i = 0; i < 3; i++) + { + AddStep("store selection", () => selection = Carousel.CurrentSelection); + if (isIterating) + AddUntilStep("selection changed", () => Carousel.CurrentSelection != selection); + else + AddUntilStep("selection not changed", () => Carousel.CurrentSelection == selection); + } + } + } +} From e9d6411e615ba85a2989511a9f374682b20d25cf Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 23 Jan 2025 19:10:11 +0900 Subject: [PATCH 171/801] Clean up error handling --- .../Match/MultiplayerMatchSettingsOverlay.cs | 58 +++++++++---------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 2a5a83fadf..eda3bace40 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -463,9 +463,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match .ContinueWith(t => Schedule(() => { if (t.IsCompletedSuccessfully) - onSuccess(room); + onSuccess(); else - onError(t.Exception?.AsSingular().Message ?? "Error changing settings."); + onError(t.Exception, "Error changing settings"); })); } else @@ -473,26 +473,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match client.CreateRoom(room).ContinueWith(t => Schedule(() => { if (t.IsCompletedSuccessfully) - onSuccess(room); - else if (t.IsFaulted) - { - Debug.Assert(t.Exception != null); - Exception exception = t.Exception.AsSingular(); - - if (exception.GetHubExceptionMessage() is string message) - onError(message); - else - onError($"Error creating room: {exception}"); - } + onSuccess(); else - onError("Error creating room."); + onError(t.Exception, "Error creating room"); })); } } private void hideError() => ErrorText.FadeOut(50); - private void onSuccess(Room room) => Schedule(() => + private void onSuccess() => Schedule(() => { Debug.Assert(applyingSettingsOperation != null); @@ -502,28 +492,34 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match applyingSettingsOperation = null; }); - private void onError(string text) => Schedule(() => + private void onError(Exception? exception, string description) { - Debug.Assert(applyingSettingsOperation != null); + if (exception is AggregateException aggregateException) + exception = aggregateException.AsSingular(); - // see https://github.com/ppy/osu-web/blob/2c97aaeb64fb4ed97c747d8383a35b30f57428c7/app/Models/Multiplayer/PlaylistItem.php#L48. - const string not_found_prefix = "beatmaps not found:"; + string message = exception?.GetHubExceptionMessage() ?? $"{description} ({exception?.Message})"; - if (text.StartsWith(not_found_prefix, StringComparison.Ordinal)) + Schedule(() => { - ErrorText.Text = "The selected beatmap is not available online."; - room.Playlist.SingleOrDefault()?.MarkInvalid(); - } - else - { - ErrorText.Text = text; - } + Debug.Assert(applyingSettingsOperation != null); - ErrorText.FadeIn(50); + // see https://github.com/ppy/osu-web/blob/2c97aaeb64fb4ed97c747d8383a35b30f57428c7/app/Models/Multiplayer/PlaylistItem.php#L48. + const string not_found_prefix = "beatmaps not found:"; - applyingSettingsOperation.Dispose(); - applyingSettingsOperation = null; - }); + if (message.StartsWith(not_found_prefix, StringComparison.Ordinal)) + { + ErrorText.Text = "The selected beatmap is not available online."; + room.Playlist.SingleOrDefault()?.MarkInvalid(); + } + else + ErrorText.Text = message; + + ErrorText.FadeIn(50); + + applyingSettingsOperation.Dispose(); + applyingSettingsOperation = null; + }); + } protected override void Dispose(bool isDisposing) { From 8f17a44976439ba30c8ee13f1200d72821847c5a Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Thu, 23 Jan 2025 10:29:04 +0000 Subject: [PATCH 172/801] Remove unused default value --- .../Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs index 4f7023059f..b77176b49d 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data /// The ratio of between this and the previous . In the /// case where one or both of the is undefined, this will have a value of 1. /// - public double HitObjectIntervalRatio = 1; + public double HitObjectIntervalRatio; /// public double Interval { get; private set; } From 2feab314267ae017cce1334ed8e83ba4fdfc0ec7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 22:41:20 +0900 Subject: [PATCH 173/801] Adjust inline commentary based on review feedback --- osu.Game/Screens/SelectV2/Carousel.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 598a898686..8194ddaaed 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -366,8 +366,8 @@ namespace osu.Game.Screens.SelectV2 Debug.Assert(selectionItem != null); - // As a second special case, if we're group selecting backwards and the current selection isn't - // a group, base this selection operation from the closest previous group. + // As a second special case, if we're group selecting backwards and the current selection isn't a group, + // make sure to go back to the group header this item belongs to, so that the block below doesn't find it and stop too early. if (isGroupSelection && direction < 0) { while (!carouselItems[selectionIndex].IsGroupSelectionTarget) @@ -423,8 +423,8 @@ namespace osu.Game.Screens.SelectV2 currentSelection = currentKeyboardSelection = new Selection(model); HandleItemSelected(currentSelection.Model); - // ensure the selection hasn't changed in the handling of selection. - // if it's changed, avoid a second update of selection/scroll. + // `HandleItemSelected` can alter `CurrentSelection`, which will recursively call `setSelection()` again. + // if that happens, the rest of this method should be a no-op. if (currentSelection.Model != model) return; From 0716b73d2aa43f6343c700a3b9bb0eb451542f26 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 22:44:39 +0900 Subject: [PATCH 174/801] `ActivateSelection` -> `TryActivateSelection` --- osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs | 2 +- osu.Game/Screens/SelectV2/Carousel.cs | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs index da3e1b0964..9219656365 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs @@ -111,7 +111,7 @@ namespace osu.Game.Screens.SelectV2 protected override bool OnClick(ClickEvent e) { if (carousel.CurrentSelection == Item!.Model) - carousel.ActivateSelection(); + carousel.TryActivateSelection(); else carousel.CurrentSelection = Item!.Model; return true; diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 8194ddaaed..6899c10451 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -97,9 +97,10 @@ namespace osu.Game.Screens.SelectV2 } /// - /// Activate the current selection, if a selection exists. + /// Activate the current selection, if a selection exists and matches keyboard selection. + /// If keyboard selection does not match selection, this will transfer the selection on first invocation. /// - public void ActivateSelection() + public void TryActivateSelection() { if (currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) { @@ -295,7 +296,7 @@ namespace osu.Game.Screens.SelectV2 switch (e.Action) { case GlobalAction.Select: - ActivateSelection(); + TryActivateSelection(); return true; case GlobalAction.SelectNext: @@ -342,7 +343,7 @@ namespace osu.Game.Screens.SelectV2 // group selection, first transfer the keyboard selection to actual selection. if (isGroupSelection && currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) { - ActivateSelection(); + TryActivateSelection(); return true; } From d5369d3508c4ae9227a5f5858536153f947ee600 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 23:53:09 +0900 Subject: [PATCH 175/801] Add regions to `BeatmapCarousel` --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 97 ++++++++++++-------- 1 file changed, 61 insertions(+), 36 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index e3bc487154..540eedbd92 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -22,8 +22,6 @@ namespace osu.Game.Screens.SelectV2 { private IBindableList detachedBeatmaps = null!; - private readonly DrawablePool carouselPanelPool = new DrawablePool(100); - private readonly LoadingLayer loading; private readonly BeatmapCarouselFilterGrouping grouping; @@ -39,19 +37,60 @@ namespace osu.Game.Screens.SelectV2 grouping = new BeatmapCarouselFilterGrouping(() => Criteria), }; - AddInternal(carouselPanelPool); - AddInternal(loading = new LoadingLayer(dimBackground: true)); } [BackgroundDependencyLoader] private void load(BeatmapStore beatmapStore, CancellationToken? cancellationToken) + { + setupPools(); + setupBeatmaps(beatmapStore, cancellationToken); + } + + #region Beatmap source hookup + + private void setupBeatmaps(BeatmapStore beatmapStore, CancellationToken? cancellationToken) { detachedBeatmaps = beatmapStore.GetBeatmapSets(cancellationToken); detachedBeatmaps.BindCollectionChanged(beatmapSetsChanged, true); } - protected override Drawable GetDrawableForDisplay(CarouselItem item) => carouselPanelPool.Get(); + private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed) + { + // TODO: moving management of BeatmapInfo tracking to BeatmapStore might be something we want to consider. + // right now we are managing this locally which is a bit of added overhead. + IEnumerable? newBeatmapSets = changed.NewItems?.Cast(); + IEnumerable? beatmapSetInfos = changed.OldItems?.Cast(); + + switch (changed.Action) + { + case NotifyCollectionChangedAction.Add: + Items.AddRange(newBeatmapSets!.SelectMany(s => s.Beatmaps)); + break; + + case NotifyCollectionChangedAction.Remove: + + foreach (var set in beatmapSetInfos!) + { + foreach (var beatmap in set.Beatmaps) + Items.RemoveAll(i => i is BeatmapInfo bi && beatmap.Equals(bi)); + } + + break; + + case NotifyCollectionChangedAction.Move: + case NotifyCollectionChangedAction.Replace: + throw new NotImplementedException(); + + case NotifyCollectionChangedAction.Reset: + Items.Clear(); + break; + } + } + + #endregion + + #region Selection handling protected override void HandleItemDeselected(object? model) { @@ -98,38 +137,9 @@ namespace osu.Game.Screens.SelectV2 drawable.FlashFromActivation(); } - private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed) - { - // TODO: moving management of BeatmapInfo tracking to BeatmapStore might be something we want to consider. - // right now we are managing this locally which is a bit of added overhead. - IEnumerable? newBeatmapSets = changed.NewItems?.Cast(); - IEnumerable? beatmapSetInfos = changed.OldItems?.Cast(); + #endregion - switch (changed.Action) - { - case NotifyCollectionChangedAction.Add: - Items.AddRange(newBeatmapSets!.SelectMany(s => s.Beatmaps)); - break; - - case NotifyCollectionChangedAction.Remove: - - foreach (var set in beatmapSetInfos!) - { - foreach (var beatmap in set.Beatmaps) - Items.RemoveAll(i => i is BeatmapInfo bi && beatmap.Equals(bi)); - } - - break; - - case NotifyCollectionChangedAction.Move: - case NotifyCollectionChangedAction.Replace: - throw new NotImplementedException(); - - case NotifyCollectionChangedAction.Reset: - Items.Clear(); - break; - } - } + #region Filtering public FilterCriteria Criteria { get; private set; } = new FilterCriteria(); @@ -139,5 +149,20 @@ namespace osu.Game.Screens.SelectV2 loading.Show(); FilterAsync().ContinueWith(_ => Schedule(() => loading.Hide())); } + + #endregion + + #region Drawable pooling + + private readonly DrawablePool carouselPanelPool = new DrawablePool(100); + + private void setupPools() + { + AddInternal(carouselPanelPool); + } + + protected override Drawable GetDrawableForDisplay(CarouselItem item) => carouselPanelPool.Get(); + + #endregion } } From f4270ab3b994dad45acfc9c735da1a52c323e0ee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 23:58:51 +0900 Subject: [PATCH 176/801] Simplify selection handling logic --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 32 +++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 540eedbd92..aca71efe93 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -92,19 +92,6 @@ namespace osu.Game.Screens.SelectV2 #region Selection handling - protected override void HandleItemDeselected(object? model) - { - base.HandleItemDeselected(model); - - var deselectedSet = model as BeatmapSetInfo ?? (model as BeatmapInfo)?.BeatmapSet; - - if (grouping.SetItems.TryGetValue(deselectedSet!, out var group)) - { - foreach (var i in group) - i.IsVisible = false; - } - } - protected override void HandleItemSelected(object? model) { base.HandleItemSelected(model); @@ -116,15 +103,24 @@ namespace osu.Game.Screens.SelectV2 return; } - var currentSelectionSet = (model as BeatmapInfo)?.BeatmapSet; + if (model is BeatmapInfo beatmapInfo) + setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, true); + } - if (currentSelectionSet == null) - return; + protected override void HandleItemDeselected(object? model) + { + base.HandleItemDeselected(model); - if (grouping.SetItems.TryGetValue(currentSelectionSet, out var group)) + if (model is BeatmapInfo beatmapInfo) + setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, false); + } + + private void setVisibilityOfSetItems(BeatmapSetInfo set, bool visible) + { + if (grouping.SetItems.TryGetValue(set, out var group)) { foreach (var i in group) - i.IsVisible = true; + i.IsVisible = visible; } } From 13c64b59af7a6e809ffdb2632973f10ef14ff722 Mon Sep 17 00:00:00 2001 From: Layendan Date: Thu, 23 Jan 2025 15:36:20 -0700 Subject: [PATCH 177/801] Inherit menu items from parent class --- .../OnlinePlay/Lounge/Components/DrawableRoom.cs | 2 +- .../OnlinePlay/Lounge/DrawableLoungeRoom.cs | 16 ++-------------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 7fefa0a1a8..7463e05c96 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -340,7 +340,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } - public MenuItem[] ContextMenuItems + public virtual MenuItem[] ContextMenuItems { get { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index da04152bd3..700cc09eb6 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -158,7 +158,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge public Popover GetPopover() => new PasswordEntryPopover(Room); - public new MenuItem[] ContextMenuItems + public override MenuItem[] ContextMenuItems { get { @@ -170,19 +170,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge }) }; - if (Room.RoomID.HasValue) - { - items.AddRange([ - new OsuMenuItem("View in browser", MenuItemType.Standard, () => - { - game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value)); - }), - new OsuMenuItem("Copy link", MenuItemType.Standard, () => - { - game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value)); - }) - ]); - } + items.AddRange(base.ContextMenuItems); if (Room.Type == MatchType.Playlists && Room.Host?.Id == api.LocalUser.Value.Id && Room.StartDate?.AddMinutes(5) >= DateTimeOffset.Now && !Room.HasEnded) { From b0a7237fd6c397f2412a4e209af40094788bcc30 Mon Sep 17 00:00:00 2001 From: Layendan Date: Thu, 23 Jan 2025 15:37:30 -0700 Subject: [PATCH 178/801] Fix formatting --- osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 700cc09eb6..47630ce1ff 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -39,7 +39,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge /// /// A with lounge-specific interactions such as selection and hover sounds. /// - public partial class DrawableLoungeRoom : DrawableRoom, IFilterable, IHasContextMenu, IHasPopover, IKeyBindingHandler + public partial class DrawableLoungeRoom : DrawableRoom, IFilterable, IHasPopover, IKeyBindingHandler { private const float transition_duration = 60; private const float selection_border_width = 4; @@ -59,9 +59,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge [Resolved] private IAPIProvider api { get; set; } = null!; - [Resolved] - private OsuGame? game { get; set; } - private readonly BindableWithCurrent selectedRoom = new BindableWithCurrent(); private Sample? sampleSelect; private Sample? sampleJoin; From d326f23576176fb02700cb9a3cfc989374f44664 Mon Sep 17 00:00:00 2001 From: Layendan Date: Thu, 23 Jan 2025 15:39:18 -0700 Subject: [PATCH 179/801] Remove unused method --- osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 47630ce1ff..f2afbcef71 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -236,8 +236,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge Room.PropertyChanged -= onRoomPropertyChanged; } - private string formatRoomUrl(long id) => $@"{api.WebsiteRootUrl}/multiplayer/rooms/{id}"; - public partial class PasswordEntryPopover : OsuPopover { private readonly Room room; From 61a818e4eddc8805a6584095656cf70511e945e5 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 23 Jan 2025 21:22:35 -0500 Subject: [PATCH 180/801] Hide Discord RPC error messages away from user attention --- osu.Desktop/DiscordRichPresence.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 7dd9250ab6..6afb3e319d 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -82,7 +82,7 @@ namespace osu.Desktop }; client.OnReady += onReady; - client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network, LogLevel.Error); + client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network); try { From 5cc8181bad679ab8f1171531493f47c856c0633b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 15:29:49 +0900 Subject: [PATCH 181/801] Expose `GameplayStartTime` in `IGameplayClock` --- .../TestSceneClicksPerSecondCalculator.cs | 1 + osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 8 ++++---- osu.Game/Screens/Play/GameplayClockContainer.cs | 2 ++ osu.Game/Screens/Play/IGameplayClock.cs | 5 +++++ .../Play/MasterGameplayClockContainer.cs | 17 ++++++++--------- 5 files changed, 20 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs index db06329d74..55d57d7a65 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs @@ -120,6 +120,7 @@ namespace osu.Game.Tests.Visual.Gameplay public double FramesPerSecond => throw new NotImplementedException(); public FrameTimeInfo TimeInfo => throw new NotImplementedException(); public double StartTime => throw new NotImplementedException(); + public double GameplayStartTime => throw new NotImplementedException(); public IAdjustableAudioComponent AdjustmentsFromMods => adjustableAudioComponent; diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 92258f3fc9..50111e64a8 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.UI private readonly Bindable waitingOnFrames = new Bindable(); - private readonly double gameplayStartTime; + public double GameplayStartTime { get; } private IGameplayClock? parentGameplayClock; @@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.UI framedClock = new FramedClock(manualClock = new ManualClock()); - this.gameplayStartTime = gameplayStartTime; + GameplayStartTime = gameplayStartTime; } [BackgroundDependencyLoader(true)] @@ -257,8 +257,8 @@ namespace osu.Game.Rulesets.UI return; } - if (manualClock.CurrentTime < gameplayStartTime) - manualClock.CurrentTime = proposedTime = Math.Min(gameplayStartTime, proposedTime); + if (manualClock.CurrentTime < GameplayStartTime) + manualClock.CurrentTime = proposedTime = Math.Min(GameplayStartTime, proposedTime); else if (Math.Abs(manualClock.CurrentTime - proposedTime) > sixty_frame_time * 1.2f) { proposedTime = proposedTime > manualClock.CurrentTime diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 255877e0aa..2afdcfaebb 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -39,6 +39,8 @@ namespace osu.Game.Screens.Play /// public double StartTime { get; protected set; } + public double GameplayStartTime { get; protected set; } + public IAdjustableAudioComponent AdjustmentsFromMods { get; } = new AudioAdjustments(); private readonly BindableBool isPaused = new BindableBool(true); diff --git a/osu.Game/Screens/Play/IGameplayClock.cs b/osu.Game/Screens/Play/IGameplayClock.cs index ad28e343ff..bef7362aa9 100644 --- a/osu.Game/Screens/Play/IGameplayClock.cs +++ b/osu.Game/Screens/Play/IGameplayClock.cs @@ -18,6 +18,11 @@ namespace osu.Game.Screens.Play /// double StartTime { get; } + /// + /// The time from which actual gameplay should start. When intro time is skipped, this will be the seeked location. + /// + double GameplayStartTime { get; } + /// /// All adjustments applied to this clock which come from mods. /// diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 3851806788..0b47d8ed85 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -57,8 +57,6 @@ namespace osu.Game.Screens.Play private Track track; - private readonly double skipTargetTime; - [Resolved] private MusicController musicController { get; set; } = null!; @@ -66,16 +64,16 @@ namespace osu.Game.Screens.Play /// Create a new master gameplay clock container. /// /// The beatmap to be used for time and metadata references. - /// The latest time which should be used when introducing gameplay. Will be used when skipping forward. - public MasterGameplayClockContainer(WorkingBeatmap beatmap, double skipTargetTime) + /// The latest time which should be used when introducing gameplay. Will be used when skipping forward. + public MasterGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime) : base(beatmap.Track, applyOffsets: true, requireDecoupling: true) { this.beatmap = beatmap; - this.skipTargetTime = skipTargetTime; track = beatmap.Track; StartTime = findEarliestStartTime(); + GameplayStartTime = gameplayStartTime; } private double findEarliestStartTime() @@ -84,7 +82,7 @@ namespace osu.Game.Screens.Play // generally this is either zero, or some point earlier than zero in the case of storyboards, lead-ins etc. // start with the originally provided latest time (if before zero). - double time = Math.Min(0, skipTargetTime); + double time = Math.Min(0, GameplayStartTime); // if a storyboard is present, it may dictate the appropriate start time by having events in negative time space. // this is commonly used to display an intro before the audio track start. @@ -119,10 +117,10 @@ namespace osu.Game.Screens.Play ///
public void Skip() { - if (GameplayClock.CurrentTime > skipTargetTime - MINIMUM_SKIP_TIME) + if (GameplayClock.CurrentTime > GameplayStartTime - MINIMUM_SKIP_TIME) return; - double skipTarget = skipTargetTime - MINIMUM_SKIP_TIME; + double skipTarget = GameplayStartTime - MINIMUM_SKIP_TIME; if (StartTime < -10000 && GameplayClock.CurrentTime < 0 && skipTarget > 6000) // double skip exception for storyboards with very long intros @@ -187,7 +185,8 @@ namespace osu.Game.Screens.Play } else { - Logger.Log($"Playback discrepancy detected ({playbackDiscrepancyCount} of allowed {allowed_playback_discrepancies}): {elapsedGameplayClockTime:N1} vs {elapsedValidationTime:N1}"); + Logger.Log( + $"Playback discrepancy detected ({playbackDiscrepancyCount} of allowed {allowed_playback_discrepancies}): {elapsedGameplayClockTime:N1} vs {elapsedValidationTime:N1}"); } elapsedValidationTime = null; From fb10996951a1821d06f6ffe5a092763cb1e44bca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 15:30:02 +0900 Subject: [PATCH 182/801] Consume `GameplayStartTime` for more lenient offset adjustments --- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index e988760834..503e9ad15e 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -291,7 +291,7 @@ namespace osu.Game.Screens.Play.PlayerSettings Debug.Assert(gameplayClock != null); // TODO: the blocking conditions should probably display a message. - if (!player.IsBreakTime.Value && gameplayClock.CurrentTime - gameplayClock.StartTime > 10000) + if (!player.IsBreakTime.Value && gameplayClock.CurrentTime - gameplayClock.GameplayStartTime > 10000) return false; if (gameplayClock.IsPaused.Value) From ee78e1b2234bd7c1a94a7be58d48c9b82ce88923 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 15:33:39 +0900 Subject: [PATCH 183/801] Add safeties against attempting to apply previous play while offset adjust is not allowed This should theoretically not be possible, but while we are sharing this control's implementation between gameplay and non-gameplay usages, let's ensure nothing weird can occur. --- .../Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index e988760834..9465624b02 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -245,6 +245,9 @@ namespace osu.Game.Screens.Play.PlayerSettings Text = BeatmapOffsetControlStrings.CalibrateUsingLastPlay, Action = () => { + if (Current.Disabled) + return; + Current.Value = lastPlayBeatmapOffset - lastPlayAverage; lastAppliedScore.Value = ReferenceScore.Value; }, @@ -277,6 +280,9 @@ namespace osu.Game.Screens.Play.PlayerSettings protected override void Update() { base.Update(); + + if (useAverageButton != null) + useAverageButton.Enabled.Value = allowOffsetAdjust; Current.Disabled = !allowOffsetAdjust; } From 8f8a6455b4dfde594d78234c1cd3ca346337570f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 15:34:03 +0900 Subject: [PATCH 184/801] Bypass offset disallowed status when handling realm callbacks Hopefully don't need to overthink this one. --- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 9465624b02..c7367ea8c6 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -121,7 +121,11 @@ namespace osu.Game.Screens.Play.PlayerSettings // At the point we reach here, it's not guaranteed that all realm writes have taken place (there may be some in-flight). // We are only aware of writes that originated from our own flow, so if we do see one that's active we can avoid handling the feedback value arriving. if (realmWriteTask == null) + { + Current.Disabled = false; + Current.Disabled = allowOffsetAdjust; Current.Value = val; + } if (realmWriteTask?.IsCompleted == true) { From 05b1002e9d4f7de5d5db4ef28784dd3b8bf57c99 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 15:57:13 +0900 Subject: [PATCH 185/801] Adjust layout and code quality slightly --- .../OnlinePlay/Lounge/Components/DrawableRoom.cs | 14 ++++---------- .../OnlinePlay/Lounge/DrawableLoungeRoom.cs | 11 ++++------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 7463e05c96..4402d1cf5c 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -349,23 +349,17 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components if (Room.RoomID.HasValue) { items.AddRange([ - new OsuMenuItem("View in browser", MenuItemType.Standard, () => - { - game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value)); - }), - new OsuMenuItem("Copy link", MenuItemType.Standard, () => - { - game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value)); - }) + new OsuMenuItem("View in browser", MenuItemType.Standard, () => game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value))), + new OsuMenuItem("Copy link", MenuItemType.Standard, () => game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value))) ]); } return items.ToArray(); + + string formatRoomUrl(long id) => $@"{api.WebsiteRootUrl}/multiplayer/rooms/{id}"; } } - private string formatRoomUrl(long id) => $@"{api.WebsiteRootUrl}/multiplayer/rooms/{id}"; - protected virtual UpdateableBeatmapBackgroundSprite CreateBackground() => new UpdateableBeatmapBackgroundSprite(); protected virtual IEnumerable CreateBottomDetails() diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index f2afbcef71..1cabb22e30 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -159,16 +159,13 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { get { - var items = new List - { - new OsuMenuItem("Create copy", MenuItemType.Standard, () => - { - lounge?.OpenCopy(Room); - }) - }; + var items = new List(); items.AddRange(base.ContextMenuItems); + items.Add(new OsuMenuItemSpacer()); + items.Add(new OsuMenuItem("Create copy", MenuItemType.Standard, () => lounge?.OpenCopy(Room))); + if (Room.Type == MatchType.Playlists && Room.Host?.Id == api.LocalUser.Value.Id && Room.StartDate?.AddMinutes(5) >= DateTimeOffset.Now && !Room.HasEnded) { items.Add(new OsuMenuItem("Close playlist", MenuItemType.Destructive, () => From 28a59f4e29bce5a14c8672de6a7ed8b5bb417fcc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 16:45:14 +0900 Subject: [PATCH 186/801] Move line to correct location --- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index c7367ea8c6..ace001f635 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -123,8 +123,8 @@ namespace osu.Game.Screens.Play.PlayerSettings if (realmWriteTask == null) { Current.Disabled = false; - Current.Disabled = allowOffsetAdjust; Current.Value = val; + Current.Disabled = allowOffsetAdjust; } if (realmWriteTask?.IsCompleted == true) From 721b2dfbbaed488fbc65cd44b91506bc073703eb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 17:16:51 +0900 Subject: [PATCH 187/801] Fix average button not correctly becoming disabled where it previously would --- .../PlayerSettings/BeatmapOffsetControl.cs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index ace001f635..e0b0a1b0ab 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -138,15 +138,15 @@ namespace osu.Game.Screens.Play.PlayerSettings ReferenceScore.BindValueChanged(scoreChanged, true); } + // the last play graph is relative to the offset at the point of the last play, so we need to factor that out for some usages. + private double adjustmentSinceLastPlay => lastPlayBeatmapOffset - Current.Value; + private void currentChanged(ValueChangedEvent offset) { Scheduler.AddOnce(updateOffset); void updateOffset() { - // the last play graph is relative to the offset at the point of the last play, so we need to factor that out. - double adjustmentSinceLastPlay = lastPlayBeatmapOffset - Current.Value; - // Negative is applied here because the play graph is considering a hit offset, not track (as we currently use for clocks). lastPlayGraph?.UpdateOffset(-adjustmentSinceLastPlay); @@ -157,11 +157,6 @@ namespace osu.Game.Screens.Play.PlayerSettings return; } - if (useAverageButton != null) - { - useAverageButton.Enabled.Value = !Precision.AlmostEquals(lastPlayAverage, adjustmentSinceLastPlay, Current.Precision / 2); - } - realmWriteTask = realm.WriteAsync(r => { var setInfo = r.Find(beatmap.Value.BeatmapSetInfo.ID); @@ -255,7 +250,6 @@ namespace osu.Game.Screens.Play.PlayerSettings Current.Value = lastPlayBeatmapOffset - lastPlayAverage; lastAppliedScore.Value = ReferenceScore.Value; }, - Enabled = { Value = !Precision.AlmostEquals(lastPlayAverage, 0, Current.Precision / 2) } }, globalOffsetText = new LinkFlowContainer { @@ -285,9 +279,12 @@ namespace osu.Game.Screens.Play.PlayerSettings { base.Update(); + bool allow = allowOffsetAdjust; + if (useAverageButton != null) - useAverageButton.Enabled.Value = allowOffsetAdjust; - Current.Disabled = !allowOffsetAdjust; + useAverageButton.Enabled.Value = allow && !Precision.AlmostEquals(lastPlayAverage, adjustmentSinceLastPlay, Current.Precision / 2); + + Current.Disabled = !allow; } private bool allowOffsetAdjust From 17b1739ae49b549692c61eaddae35682b9e9053b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 18:00:05 +0900 Subject: [PATCH 188/801] Combine countless update methods all called together into a single method --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 52 +++++++------------ 1 file changed, 19 insertions(+), 33 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index edb44a7666..9915560a95 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -355,11 +355,11 @@ namespace osu.Game.Screens.OnlinePlay.Match public override void OnResuming(ScreenTransitionEvent e) { base.OnResuming(e); + updateBeatmap(); + updateSpecifics(); + beginHandlingTrack(); - Scheduler.AddOnce(updateMods); - Scheduler.AddOnce(updateRuleset); - Scheduler.AddOnce(updateUserStyle); } protected bool ExitConfirmed { get; private set; } @@ -448,9 +448,7 @@ namespace osu.Game.Screens.OnlinePlay.Match updateUserMods(); updateBeatmap(); - updateMods(); - updateRuleset(); - updateUserStyle(); + updateSpecifics(); if (!item.AllowedMods.Any()) { @@ -501,43 +499,31 @@ namespace osu.Game.Screens.OnlinePlay.Match UserModsSelectOverlay.Beatmap.Value = Beatmap.Value; } - private void updateMods() + private void updateSpecifics() { if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) return; var rulesetInstance = GetGameplayRuleset().CreateInstance(); Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray(); - } - - private void updateRuleset() - { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) - return; Ruleset.Value = GetGameplayRuleset(); - } - private void updateUserStyle() - { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) - return; - - if (UserStyleDisplayContainer == null) - return; - - PlaylistItem gameplayItem = SelectedItem.Value.With(ruleset: GetGameplayRuleset().OnlineID, beatmap: new Optional(GetGameplayBeatmap())); - PlaylistItem? currentItem = UserStyleDisplayContainer.SingleOrDefault()?.Item; - - if (gameplayItem.Equals(currentItem)) - return; - - UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem) + if (UserStyleDisplayContainer != null) { - AllowReordering = false, - AllowEditing = true, - RequestEdit = _ => OpenStyleSelection() - }; + PlaylistItem gameplayItem = SelectedItem.Value.With(ruleset: GetGameplayRuleset().OnlineID, beatmap: new Optional(GetGameplayBeatmap())); + PlaylistItem? currentItem = UserStyleDisplayContainer.SingleOrDefault()?.Item; + + if (gameplayItem.Equals(currentItem)) + return; + + UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem) + { + AllowReordering = false, + AllowEditing = true, + RequestEdit = _ => OpenStyleSelection() + }; + } } protected virtual APIMod[] GetGameplayMods() From 92429b2ed9e8f7a658196659656aeb9ec7dcd14d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 18:34:04 +0900 Subject: [PATCH 189/801] Adjust comments on `ICarouselPanel` to imply external use --- osu.Game/Screens/SelectV2/ICarouselPanel.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/SelectV2/ICarouselPanel.cs b/osu.Game/Screens/SelectV2/ICarouselPanel.cs index c592734d8d..2776fdec6c 100644 --- a/osu.Game/Screens/SelectV2/ICarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/ICarouselPanel.cs @@ -3,33 +3,33 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Pooling; namespace osu.Game.Screens.SelectV2 { /// /// An interface to be attached to any s which are used for display inside a . + /// Importantly, all properties in this interface are managed by and should not be written to elsewhere. /// public interface ICarouselPanel { /// - /// Whether this item has selection. - /// This is managed by and should not be set manually. + /// Whether this item has selection. Should be read from to update the visual state. /// BindableBool Selected { get; } /// - /// Whether this item has keyboard selection. - /// This is managed by and should not be set manually. + /// Whether this item has keyboard selection. Should be read from to update the visual state. /// BindableBool KeyboardSelected { get; } /// - /// The Y position which should be used for displaying this item within the carousel. This is managed by and should not be set manually. + /// The Y position used internally for positioning in the carousel. /// double DrawYPosition { get; set; } /// - /// The carousel item this drawable is representing. This is managed by and should not be set manually. + /// The carousel item this drawable is representing. Will be set before is called. /// CarouselItem? Item { get; set; } } From 9366bfbf0d317e18884086f5532c5cf12443f904 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 18:40:48 +0900 Subject: [PATCH 190/801] Move activation drawable flow portion to `ICarouselPanel` --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 9 --------- osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs | 2 +- osu.Game/Screens/SelectV2/Carousel.cs | 3 +++ osu.Game/Screens/SelectV2/ICarouselPanel.cs | 5 +++++ 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index aca71efe93..630f7b6583 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -124,15 +124,6 @@ namespace osu.Game.Screens.SelectV2 } } - protected override void HandleItemActivated(CarouselItem item) - { - base.HandleItemActivated(item); - - // TODO: maybe this should be handled by the panel itself? - if (GetMaterialisedDrawableForItem(item) is BeatmapCarouselPanel drawable) - drawable.FlashFromActivation(); - } - #endregion #region Filtering diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs index 9219656365..398ec7bf4c 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs @@ -123,7 +123,7 @@ namespace osu.Game.Screens.SelectV2 public double DrawYPosition { get; set; } - public void FlashFromActivation() + public void Activated() { activationFlash.FadeOutFromOne(500, Easing.OutQuint); } diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 6899c10451..6ff27c6198 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -109,7 +109,10 @@ namespace osu.Game.Screens.SelectV2 } if (currentSelection.CarouselItem != null) + { + (GetMaterialisedDrawableForItem(currentSelection.CarouselItem) as ICarouselPanel)?.Activated(); HandleItemActivated(currentSelection.CarouselItem); + } } #endregion diff --git a/osu.Game/Screens/SelectV2/ICarouselPanel.cs b/osu.Game/Screens/SelectV2/ICarouselPanel.cs index 2776fdec6c..a956bb22a3 100644 --- a/osu.Game/Screens/SelectV2/ICarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/ICarouselPanel.cs @@ -23,6 +23,11 @@ namespace osu.Game.Screens.SelectV2 ///
BindableBool KeyboardSelected { get; } + /// + /// Called when the panel is activated. Should be used to update the panel's visual state. + /// + void Activated(); + /// /// The Y position used internally for positioning in the carousel. /// From 15b6e28ebe888b1a87574891be1a0db3b04093b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 21 Jan 2025 12:16:36 +0100 Subject: [PATCH 191/801] Remove dependence of blueprint containers on `IPositionSnapProvider` --- .../Edit/CatchBlueprintContainer.cs | 29 +++++++ .../Edit/CatchHitObjectComposer.cs | 20 ++--- .../Editor/TestSceneManiaBeatSnapGrid.cs | 6 -- .../Blueprints/HoldNoteSelectionBlueprint.cs | 3 +- .../Edit/ManiaBlueprintContainer.cs | 25 +++++- .../Components/PathControlPointVisualiser.cs | 8 +- .../Edit/OsuBlueprintContainer.cs | 67 ++++++++++++++- .../Edit/OsuHitObjectComposer.cs | 80 ++++++++--------- .../Edit/TaikoBlueprintContainer.cs | 25 +++++- .../SkinEditor/SkinBlueprintContainer.cs | 5 ++ osu.Game/Rulesets/Edit/HitObjectComposer.cs | 32 +------ .../Edit/ScrollingHitObjectComposer.cs | 17 ++++ .../Compose/Components/BlueprintContainer.cs | 86 +++---------------- .../Components/ComposeBlueprintContainer.cs | 6 +- .../Components/EditorBlueprintContainer.cs | 29 +++---- .../Compose/Components/Timeline/Timeline.cs | 4 +- .../Timeline/TimelineBlueprintContainer.cs | 17 ++++ 17 files changed, 263 insertions(+), 196 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs b/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs index 3979d30616..47035b0227 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs @@ -1,16 +1,22 @@ // 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 osu.Framework.Input.Events; using osu.Game.Rulesets.Catch.Edit.Blueprints; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit.Compose.Components; +using osuTK; namespace osu.Game.Rulesets.Catch.Edit { public partial class CatchBlueprintContainer : ComposeBlueprintContainer { + public new CatchHitObjectComposer Composer => (CatchHitObjectComposer)base.Composer; + public CatchBlueprintContainer(CatchHitObjectComposer composer) : base(composer) { @@ -36,5 +42,28 @@ namespace osu.Game.Rulesets.Catch.Edit } protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield); + + protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints) + { + Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + + // The final movement position, relative to movementBlueprintOriginalPosition. + Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; + + // Retrieve a snapped position. + var gridSnapResult = Composer.FindSnappedPositionAndTime(movePosition); + gridSnapResult.ScreenSpacePosition.X = movePosition.X; + var distanceSnapResult = Composer.TryDistanceSnap(gridSnapResult.ScreenSpacePosition); + + var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS + ? distanceSnapResult + : gridSnapResult; + + var referenceBlueprint = blueprints.First().blueprint; + bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); + if (moved) + ApplySnapResultTime(result, referenceBlueprint.Item.StartTime); + return moved; + } } } diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index 7bb5539963..9618eb28a9 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.Edit { public partial class CatchHitObjectComposer : ScrollingHitObjectComposer, IKeyBindingHandler { - private const float distance_snap_radius = 50; + public const float DISTANCE_SNAP_RADIUS = 50; private CatchDistanceSnapGrid distanceSnapGrid = null!; @@ -135,22 +135,12 @@ namespace osu.Game.Rulesets.Catch.Edit DistanceSnapProvider.HandleToggleViaKey(key); } - public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) + public SnapResult? TryDistanceSnap(Vector2 screenSpacePosition) { - var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType); + if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(screenSpacePosition) is SnapResult snapResult) + return snapResult; - result.ScreenSpacePosition.X = screenSpacePosition.X; - - if (snapType.HasFlag(SnapType.RelativeGrids)) - { - if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult && - Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius) - { - result = snapResult; - } - } - - return result; + return null; } private PalpableCatchHitObject? getLastSnappableHitObject(double time) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs index 127beed83e..19ff13e216 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs @@ -20,7 +20,6 @@ using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Tests.Visual; -using osuTK; namespace osu.Game.Rulesets.Mania.Tests.Editor { @@ -100,10 +99,5 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { set => InternalChild = value; } - - public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) - { - throw new NotImplementedException(); - } } } diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs index 915706c044..ff29154f87 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; -using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; @@ -24,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints private EditorBeatmap? editorBeatmap { get; set; } [Resolved] - private IPositionSnapProvider? positionSnapProvider { get; set; } + private ManiaHitObjectComposer? positionSnapProvider { get; set; } private EditBodyPiece body = null!; private EditHoldNoteEndPiece head = null!; diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs index d0eb8c1e6e..4eb54e6366 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs @@ -1,17 +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.Linq; +using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit.Compose.Components; +using osuTK; namespace osu.Game.Rulesets.Mania.Edit { public partial class ManiaBlueprintContainer : ComposeBlueprintContainer { - public ManiaBlueprintContainer(HitObjectComposer composer) + public new ManiaHitObjectComposer Composer => (ManiaHitObjectComposer)base.Composer; + + public ManiaBlueprintContainer(ManiaHitObjectComposer composer) : base(composer) { } @@ -33,5 +39,22 @@ namespace osu.Game.Rulesets.Mania.Edit protected override SelectionHandler CreateSelectionHandler() => new ManiaSelectionHandler(); protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield); + + protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints) + { + Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + + // The final movement position, relative to movementBlueprintOriginalPosition. + Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; + + // Retrieve a snapped position. + var result = Composer.FindSnappedPositionAndTime(movePosition); + + var referenceBlueprint = blueprints.First().blueprint; + bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); + if (moved) + ApplySnapResultTime(result, referenceBlueprint.Item.StartTime); + return moved; + } } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index f98117c0fa..bac5f0101c 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public Action> SplitControlPointsRequested; [Resolved(CanBeNull = true)] - private IPositionSnapProvider positionSnapProvider { get; set; } + private OsuHitObjectComposer positionSnapProvider { get; set; } [Resolved(CanBeNull = true)] private IDistanceSnapProvider distanceSnapProvider { get; set; } @@ -433,7 +433,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { // Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex])); - SnapResult result = positionSnapProvider?.FindSnappedPositionAndTime(newHeadPosition); + SnapResult result = positionSnapProvider?.TrySnapToNearbyObjects(newHeadPosition) + ?? positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition) + ?? positionSnapProvider?.TrySnapToPositionGrid(newHeadPosition); Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position; @@ -453,7 +455,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } else { - SnapResult result = positionSnapProvider?.FindSnappedPositionAndTime(Parent!.ToScreenSpace(e.MousePosition), SnapType.GlobalGrids); + SnapResult result = positionSnapProvider?.TrySnapToPositionGrid(Parent!.ToScreenSpace(e.MousePosition)); Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? Parent!.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs index 54c54fca17..235368e552 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs @@ -1,6 +1,9 @@ // 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 osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; @@ -8,12 +11,15 @@ using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders; using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit.Compose.Components; +using osuTK; namespace osu.Game.Rulesets.Osu.Edit { public partial class OsuBlueprintContainer : ComposeBlueprintContainer { - public OsuBlueprintContainer(HitObjectComposer composer) + public new OsuHitObjectComposer Composer => (OsuHitObjectComposer)base.Composer; + + public OsuBlueprintContainer(OsuHitObjectComposer composer) : base(composer) { } @@ -36,5 +42,64 @@ namespace osu.Game.Rulesets.Osu.Edit return base.CreateHitObjectBlueprintFor(hitObject); } + + protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints) + { + Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + + for (int i = 0; i < blueprints.Count; i++) + { + if (checkSnappingBlueprintToNearbyObjects(blueprints[i].blueprint, distanceTravelled, blueprints[i].originalSnapPositions)) + return true; + } + + // if no positional snapping could be performed, try unrestricted snapping from the earliest + // item in the selection. + + // The final movement position, relative to movementBlueprintOriginalPosition. + Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; + + // Retrieve a snapped position. + var result = Composer.TrySnapToDistanceGrid(movePosition) ?? Composer.TrySnapToPositionGrid(movePosition) ?? new SnapResult(movePosition, null); + + var referenceBlueprint = blueprints.First().blueprint; + bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); + if (moved) + ApplySnapResultTime(result, referenceBlueprint.Item.StartTime); + return moved; + } + + /// + /// Check for positional snap for given blueprint. + /// + /// The blueprint to check for snapping. + /// Distance travelled since start of dragging action. + /// The snap positions of blueprint before start of dragging action. + /// Whether an object to snap to was found. + private bool checkSnappingBlueprintToNearbyObjects(SelectionBlueprint blueprint, Vector2 distanceTravelled, Vector2[] originalPositions) + { + var currentPositions = blueprint.ScreenSpaceSnapPoints; + + for (int i = 0; i < originalPositions.Length; i++) + { + Vector2 originalPosition = originalPositions[i]; + var testPosition = originalPosition + distanceTravelled; + + var positionalResult = Composer.TrySnapToNearbyObjects(testPosition); + + if (positionalResult == null || positionalResult.ScreenSpacePosition == testPosition) continue; + + var delta = positionalResult.ScreenSpacePosition - currentPositions[i]; + + // attempt to move the objects, and apply any time based snapping if we can. + if (SelectionHandler.HandleMovement(new MoveSelectionEvent(blueprint, delta))) + { + ApplySnapResultTime(positionalResult, blueprint.Item.StartTime); + return true; + } + } + + return false; + } } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index aad3d0c93b..06a74fb631 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; @@ -222,56 +223,55 @@ namespace osu.Game.Rulesets.Osu.Edit } } - public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) + [CanBeNull] + public SnapResult TrySnapToNearbyObjects(Vector2 screenSpacePosition) { - if (snapType.HasFlag(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) - { - // In the case of snapping to nearby objects, a time value is not provided. - // This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap - // this could result in unexpected behaviour when distance snapping is turned on and a user attempts to place an object that is - // BOTH on a valid distance snap ring, and also at the same position as a previous object. - // - // We want to ensure that in this particular case, the time-snapping component of distance snap is still applied. - // The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over - // the time value if the proposed positions are roughly the same. - if (snapType.HasFlag(SnapType.RelativeGrids) && DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) - { - (Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition)); - if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1)) - snapResult.Time = distanceSnappedTime; - } + if (!snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) + return null; + if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null) return snapResult; - } - SnapResult result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType); + // In the case of snapping to nearby objects, a time value is not provided. + // This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap + // this could result in unexpected behaviour when distance snapping is turned on and a user attempts to place an object that is + // BOTH on a valid distance snap ring, and also at the same position as a previous object. + // + // We want to ensure that in this particular case, the time-snapping component of distance snap is still applied. + // The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over + // the time value if the proposed positions are roughly the same. + (Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition)); + if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1)) + snapResult.Time = distanceSnappedTime; - if (snapType.HasFlag(SnapType.RelativeGrids)) - { - if (DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) - { - (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition)); + return snapResult; + } - result.ScreenSpacePosition = distanceSnapGrid.ToScreenSpace(pos); - result.Time = time; - } - } + [CanBeNull] + public SnapResult TrySnapToDistanceGrid(Vector2 screenSpacePosition) + { + if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null) + return null; - if (snapType.HasFlag(SnapType.GlobalGrids)) - { - if (rectangularGridSnapToggle.Value == TernaryState.True) - { - Vector2 pos = positionSnapGrid.GetSnappedPosition(positionSnapGrid.ToLocalSpace(result.ScreenSpacePosition)); + var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); + (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition)); + return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, playfield); + } - // A grid which doesn't perfectly fit the playfield can produce a position that is outside of the playfield. - // We need to clamp the position to the playfield bounds to ensure that the snapped position is always in bounds. - pos = Vector2.Clamp(pos, Vector2.Zero, OsuPlayfield.BASE_SIZE); + [CanBeNull] + public SnapResult TrySnapToPositionGrid(Vector2 screenSpacePosition) + { + if (rectangularGridSnapToggle.Value != TernaryState.True) + return null; - result.ScreenSpacePosition = positionSnapGrid.ToScreenSpace(pos); - } - } + Vector2 pos = positionSnapGrid.GetSnappedPosition(positionSnapGrid.ToLocalSpace(screenSpacePosition)); - return result; + // A grid which doesn't perfectly fit the playfield can produce a position that is outside of the playfield. + // We need to clamp the position to the playfield bounds to ensure that the snapped position is always in bounds. + pos = Vector2.Clamp(pos, Vector2.Zero, OsuPlayfield.BASE_SIZE); + + var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); + return new SnapResult(positionSnapGrid.ToScreenSpace(pos), null, playfield); } private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult) diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs index 027723c02c..f0c3eec044 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs @@ -1,16 +1,22 @@ // 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 osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Edit.Blueprints; using osu.Game.Screens.Edit.Compose.Components; +using osuTK; namespace osu.Game.Rulesets.Taiko.Edit { public partial class TaikoBlueprintContainer : ComposeBlueprintContainer { - public TaikoBlueprintContainer(HitObjectComposer composer) + public new TaikoHitObjectComposer Composer => (TaikoHitObjectComposer)base.Composer; + + public TaikoBlueprintContainer(TaikoHitObjectComposer composer) : base(composer) { } @@ -19,5 +25,22 @@ namespace osu.Game.Rulesets.Taiko.Edit public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject) => new TaikoSelectionBlueprint(hitObject); + + protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints) + { + Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + + // The final movement position, relative to movementBlueprintOriginalPosition. + Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; + + // Retrieve a snapped position. + var result = Composer.FindSnappedPositionAndTime(movePosition); + + var referenceBlueprint = blueprints.First().blueprint; + bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); + if (moved) + ApplySnapResultTime(result, referenceBlueprint.Item.StartTime); + return moved; + } } } diff --git a/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs b/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs index 3f8d9f80d4..8f831a6f18 100644 --- a/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs +++ b/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs @@ -111,6 +111,11 @@ namespace osu.Game.Overlays.SkinEditor SelectedItems.AddRange(targetComponents.SelectMany(list => list).Except(SelectedItems).ToArray()); } + protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints) + { + throw new System.NotImplementedException(); + } + /// /// Move the current selection spatially by the specified delta, in screen coordinates (ie. the same coordinates as the blueprints). /// diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 15b60114af..b38b0291e8 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -376,7 +376,7 @@ namespace osu.Game.Rulesets.Edit /// /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic. /// - protected virtual ComposeBlueprintContainer CreateBlueprintContainer() => new ComposeBlueprintContainer(this); + protected abstract ComposeBlueprintContainer CreateBlueprintContainer(); protected virtual Drawable CreateHitObjectInspector() => new HitObjectInspector(); @@ -566,28 +566,6 @@ namespace osu.Game.Rulesets.Edit /// The most relevant . protected virtual Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => drawableRulesetWrapper.Playfield; - public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) - { - var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); - double? targetTime = null; - - if (snapType.HasFlag(SnapType.GlobalGrids)) - { - if (playfield is ScrollingPlayfield scrollingPlayfield) - { - targetTime = scrollingPlayfield.TimeAtScreenSpacePosition(screenSpacePosition); - - // apply beat snapping - targetTime = BeatSnapProvider.SnapTime(targetTime.Value); - - // convert back to screen space - screenSpacePosition = scrollingPlayfield.ScreenSpacePositionAtTime(targetTime.Value); - } - } - - return new SnapResult(screenSpacePosition, targetTime, playfield); - } - #endregion } @@ -596,7 +574,7 @@ namespace osu.Game.Rulesets.Edit /// Generally used to access certain methods without requiring a generic type for . ///
[Cached] - public abstract partial class HitObjectComposer : CompositeDrawable, IPositionSnapProvider + public abstract partial class HitObjectComposer : CompositeDrawable { public const float TOOLBOX_CONTRACTED_SIZE_LEFT = 60; public const float TOOLBOX_CONTRACTED_SIZE_RIGHT = 120; @@ -639,11 +617,5 @@ namespace osu.Game.Rulesets.Edit /// The time instant to seek to, in milliseconds. /// The ruleset-specific description of objects to select at the given timestamp. public virtual void SelectFromTimestamp(double timestamp, string objectDescription) { } - - #region IPositionSnapProvider - - public abstract SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All); - - #endregion } } diff --git a/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs b/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs index e7161ce36c..3671724042 100644 --- a/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs @@ -117,6 +117,23 @@ namespace osu.Game.Rulesets.Edit } } + public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) + { + var scrollingPlayfield = PlayfieldAtScreenSpacePosition(screenSpacePosition) as ScrollingPlayfield; + if (scrollingPlayfield == null) + return new SnapResult(screenSpacePosition, null); + + double? targetTime = scrollingPlayfield.TimeAtScreenSpacePosition(screenSpacePosition); + + // apply beat snapping + targetTime = BeatSnapProvider.SnapTime(targetTime.Value); + + // convert back to screen space + screenSpacePosition = scrollingPlayfield.ScreenSpacePositionAtTime(targetTime.Value); + + return new SnapResult(screenSpacePosition, targetTime, scrollingPlayfield); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 4a321f4a81..dc04561242 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -43,9 +43,6 @@ namespace osu.Game.Screens.Edit.Compose.Components private readonly Dictionary> blueprintMap = new Dictionary>(); - [Resolved(canBeNull: true)] - private IPositionSnapProvider snapProvider { get; set; } - [Resolved(CanBeNull = true)] private IEditorChangeHandler changeHandler { get; set; } @@ -333,19 +330,19 @@ namespace osu.Game.Screens.Edit.Compose.Components protected void RemoveBlueprintFor(T item) { - if (!blueprintMap.Remove(item, out var blueprint)) + if (!blueprintMap.Remove(item, out var blueprintToRemove)) return; - blueprint.Deselect(); - blueprint.Selected -= OnBlueprintSelected; - blueprint.Deselected -= OnBlueprintDeselected; + blueprintToRemove.Deselect(); + blueprintToRemove.Selected -= OnBlueprintSelected; + blueprintToRemove.Deselected -= OnBlueprintDeselected; - SelectionBlueprints.Remove(blueprint, true); + SelectionBlueprints.Remove(blueprintToRemove, true); - if (movementBlueprints?.Contains(blueprint) == true) + if (movementBlueprints?.Any(m => m.blueprint == blueprintToRemove) == true) finishSelectionMovement(); - OnBlueprintRemoved(blueprint.Item); + OnBlueprintRemoved(blueprintToRemove.Item); } /// @@ -538,8 +535,7 @@ namespace osu.Game.Screens.Edit.Compose.Components #region Selection Movement - private Vector2[][] movementBlueprintsOriginalPositions; - private SelectionBlueprint[] movementBlueprints; + private (SelectionBlueprint blueprint, Vector2[] originalSnapPositions)[] movementBlueprints; /// /// Whether a blueprint is currently being dragged. @@ -572,8 +568,7 @@ namespace osu.Game.Screens.Edit.Compose.Components return false; // Movement is tracked from the blueprint of the earliest item, since it only makes sense to distance snap from that item - movementBlueprints = SortForMovement(SelectionHandler.SelectedBlueprints).ToArray(); - movementBlueprintsOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSnapPoints).ToArray(); + movementBlueprints = SortForMovement(SelectionHandler.SelectedBlueprints).Select(b => (b, b.ScreenSpaceSnapPoints)).ToArray(); return true; } @@ -594,68 +589,10 @@ namespace osu.Game.Screens.Edit.Compose.Components if (movementBlueprints == null) return false; - Debug.Assert(movementBlueprintsOriginalPositions != null); - - Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; - - if (snapProvider != null) - { - for (int i = 0; i < movementBlueprints.Length; i++) - { - if (checkSnappingBlueprintToNearbyObjects(movementBlueprints[i], distanceTravelled, movementBlueprintsOriginalPositions[i])) - return true; - } - } - - // if no positional snapping could be performed, try unrestricted snapping from the earliest - // item in the selection. - - // The final movement position, relative to movementBlueprintOriginalPosition. - Vector2 movePosition = movementBlueprintsOriginalPositions.First().First() + distanceTravelled; - - // Retrieve a snapped position. - var result = snapProvider?.FindSnappedPositionAndTime(movePosition, ~SnapType.NearbyObjects); - - if (result == null) - { - return SelectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprints.First(), movePosition - movementBlueprints.First().ScreenSpaceSelectionPoint)); - } - - return ApplySnapResult(movementBlueprints, result); + return TryMoveBlueprints(e, movementBlueprints); } - /// - /// Check for positional snap for given blueprint. - /// - /// The blueprint to check for snapping. - /// Distance travelled since start of dragging action. - /// The snap positions of blueprint before start of dragging action. - /// Whether an object to snap to was found. - private bool checkSnappingBlueprintToNearbyObjects(SelectionBlueprint blueprint, Vector2 distanceTravelled, Vector2[] originalPositions) - { - var currentPositions = blueprint.ScreenSpaceSnapPoints; - - for (int i = 0; i < originalPositions.Length; i++) - { - Vector2 originalPosition = originalPositions[i]; - var testPosition = originalPosition + distanceTravelled; - - var positionalResult = snapProvider.FindSnappedPositionAndTime(testPosition, SnapType.NearbyObjects); - - if (positionalResult.ScreenSpacePosition == testPosition) continue; - - var delta = positionalResult.ScreenSpacePosition - currentPositions[i]; - - // attempt to move the objects, and abort any time based snapping if we can. - if (SelectionHandler.HandleMovement(new MoveSelectionEvent(blueprint, delta))) - return true; - } - - return false; - } - - protected virtual bool ApplySnapResult(SelectionBlueprint[] blueprints, SnapResult result) => - SelectionHandler.HandleMovement(new MoveSelectionEvent(blueprints.First(), result.ScreenSpacePosition - blueprints.First().ScreenSpaceSelectionPoint)); + protected abstract bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints); /// /// Finishes the current movement of selected blueprints. @@ -666,7 +603,6 @@ namespace osu.Game.Screens.Edit.Compose.Components if (movementBlueprints == null) return false; - movementBlueprintsOriginalPositions = null; movementBlueprints = null; return true; diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 15bbddd97e..27d6656c69 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// A blueprint container generally displayed as an overlay to a ruleset's playfield. /// - public partial class ComposeBlueprintContainer : EditorBlueprintContainer + public abstract partial class ComposeBlueprintContainer : EditorBlueprintContainer { private readonly Container placementBlueprintContainer; @@ -52,7 +52,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => editorScreen?.MainContent.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos); - public ComposeBlueprintContainer(HitObjectComposer composer) + protected ComposeBlueprintContainer(HitObjectComposer composer) : base(composer) { placementBlueprintContainer = new Container @@ -340,7 +340,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void updatePlacementTimeAndPosition() { - var snapResult = Composer.FindSnappedPositionAndTime(InputManager.CurrentState.Mouse.Position, CurrentPlacement.SnapType); + SnapResult snapResult = new SnapResult(InputManager.CurrentState.Mouse.Position, null); // Composer.FindSnappedPositionAndTime(InputManager.CurrentState.Mouse.Position, CurrentPlacement.SnapType); TODO // if no time was found from positional snapping, we should still quantize to the beat. snapResult.Time ??= Beatmap.SnapTime(EditorClock.CurrentTime, null); diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs index 7b046251e0..f1811dd84f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs @@ -17,7 +17,7 @@ using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Screens.Edit.Compose.Components { - public partial class EditorBlueprintContainer : BlueprintContainer + public abstract partial class EditorBlueprintContainer : BlueprintContainer { [Resolved] protected EditorClock EditorClock { get; private set; } @@ -73,27 +73,22 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override IEnumerable> SortForMovement(IReadOnlyList> blueprints) => blueprints.OrderBy(b => b.Item.StartTime); - protected override bool ApplySnapResult(SelectionBlueprint[] blueprints, SnapResult result) + protected void ApplySnapResultTime(SnapResult result, double referenceTime) { - if (!base.ApplySnapResult(blueprints, result)) - return false; + if (!result.Time.HasValue) + return; - if (result.Time.HasValue) + // Apply the start time at the newly snapped-to position + double offset = result.Time.Value - referenceTime; + + if (offset != 0) { - // Apply the start time at the newly snapped-to position - double offset = result.Time.Value - blueprints.First().Item.StartTime; - - if (offset != 0) + Beatmap.PerformOnSelection(obj => { - Beatmap.PerformOnSelection(obj => - { - obj.StartTime += offset; - Beatmap.Update(obj); - }); - } + obj.StartTime += offset; + Beatmap.Update(obj); + }); } - - return true; } protected override void AddBlueprintFor(HitObject item) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 5f46b3d937..cbf49e62e7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -22,7 +22,7 @@ using osuTK.Input; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { [Cached] - public partial class Timeline : ZoomableScrollContainer, IPositionSnapProvider + public partial class Timeline : ZoomableScrollContainer { private const float timeline_height = 80; @@ -332,7 +332,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline return (float)(time / editorClock.TrackLength * Content.DrawWidth); } - public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) + public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) { double time = TimeAtPosition(Content.ToLocalSpace(screenSpacePosition).X); return new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(time)); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 2b5667ff9c..011ff17b30 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -107,6 +107,23 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline return base.OnDragStart(e); } + protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints) + { + Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + + // The final movement position, relative to movementBlueprintOriginalPosition. + Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; + + // Retrieve a snapped position. + var result = timeline?.FindSnappedPositionAndTime(movePosition) ?? new SnapResult(movePosition, null); + + var referenceBlueprint = blueprints.First().blueprint; + bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); + if (moved) + ApplySnapResultTime(result, referenceBlueprint.Item.StartTime); + return moved; + } + private float dragTimeAccumulated; protected override void Update() From a6987f5c95373ac90c8305b39442847f15e42d06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 21 Jan 2025 13:49:29 +0100 Subject: [PATCH 192/801] Remove dependence of placement blueprints on `IPositionSnapProvider` --- .../BananaShowerPlacementBlueprint.cs | 8 +++-- .../Blueprints/CatchPlacementBlueprint.cs | 7 +++-- .../Blueprints/FruitPlacementBlueprint.cs | 14 +++++++-- .../JuiceStreamPlacementBlueprint.cs | 13 +++++++-- .../Edit/CatchHitObjectComposer.cs | 1 + .../Blueprints/HoldNotePlacementBlueprint.cs | 6 ++-- .../Blueprints/ManiaPlacementBlueprint.cs | 21 ++++++++++---- .../Edit/Blueprints/NotePlacementBlueprint.cs | 7 +++-- .../Edit/ManiaHitObjectComposer.cs | 1 + .../Edit/Blueprints/GridPlacementBlueprint.cs | 12 ++++---- .../HitCircles/HitCirclePlacementBlueprint.cs | 15 ++++++++-- .../Sliders/SliderPlacementBlueprint.cs | 29 +++++++++++++++---- .../Spinners/SpinnerPlacementBlueprint.cs | 8 +++++ .../Edit/OsuHitObjectComposer.cs | 1 + .../Edit/Blueprints/HitPlacementBlueprint.cs | 10 +++++-- .../Blueprints/TaikoSpanPlacementBlueprint.cs | 16 ++++++---- .../Edit/TaikoHitObjectComposer.cs | 2 ++ .../Edit/HitObjectPlacementBlueprint.cs | 2 +- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 14 ++------- .../Components/ComposeBlueprintContainer.cs | 7 +---- 20 files changed, 137 insertions(+), 57 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs index 6902f78172..85b7624f1b 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs @@ -7,6 +7,7 @@ using osu.Framework.Utils; using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Edit; +using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Catch.Edit.Blueprints @@ -59,11 +60,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints return base.OnMouseDown(e); } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { + var result = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); + base.UpdateTimeAndPosition(result); - if (!(result.Time is double time)) return; + if (!(result.Time is double time)) return result; switch (PlacementActive) { @@ -78,6 +81,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints HitObject.StartTime = Math.Min(placementStartTime, placementEndTime); HitObject.EndTime = Math.Max(placementStartTime, placementEndTime); + return result; } } } diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs index aa862375c5..90b7fa172c 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Rulesets.Catch.Edit.Blueprints { - public partial class CatchPlacementBlueprint : HitObjectPlacementBlueprint + public abstract partial class CatchPlacementBlueprint : HitObjectPlacementBlueprint where THitObject : CatchHitObject, new() { protected new THitObject HitObject => (THitObject)base.HitObject; @@ -19,7 +19,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints [Resolved] private Playfield playfield { get; set; } = null!; - public CatchPlacementBlueprint() + [Resolved] + protected CatchHitObjectComposer? Composer { get; private set; } + + protected CatchPlacementBlueprint() : base(new THitObject()) { } diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs index 72592891fb..83f75771ad 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs @@ -5,6 +5,7 @@ using osu.Framework.Input.Events; using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Edit; +using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Catch.Edit.Blueprints @@ -41,11 +42,20 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints return true; } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - base.UpdateTimeAndPosition(result); + var gridSnapResult = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); + gridSnapResult.ScreenSpacePosition.X = screenSpacePosition.X; + var distanceSnapResult = Composer?.TryDistanceSnap(gridSnapResult.ScreenSpacePosition); + + var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS + ? distanceSnapResult + : gridSnapResult; + + UpdateTimeAndPosition(result); HitObject.X = ToLocalSpace(result.ScreenSpacePosition).X; + return result; } } } diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs index 21cc260462..292175353a 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs @@ -83,8 +83,16 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints return base.OnMouseDown(e); } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { + var gridSnapResult = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); + gridSnapResult.ScreenSpacePosition.X = screenSpacePosition.X; + var distanceSnapResult = Composer?.TryDistanceSnap(gridSnapResult.ScreenSpacePosition); + + var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS + ? distanceSnapResult + : gridSnapResult; + switch (PlacementActive) { case PlacementState.Waiting: @@ -99,7 +107,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints break; default: - return; + return result; } // Make sure the up-to-date position is used for outlines. @@ -113,6 +121,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints ApplyDefaultsToHitObject(); scrollingPath.UpdatePathFrom(HitObjectContainer, HitObject); nestedOutlineContainer.UpdateNestedObjectsFrom(HitObjectContainer, HitObject); + return result; } private double positionToTime(float relativeYPosition) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index 9618eb28a9..dfe9dc9dd8 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -23,6 +23,7 @@ using osuTK; namespace osu.Game.Rulesets.Catch.Edit { + [Cached] public partial class CatchHitObjectComposer : ScrollingHitObjectComposer, IKeyBindingHandler { public const float DISTANCE_SNAP_RADIUS = 50; diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs index 13cfc5f691..094c59da46 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs @@ -98,9 +98,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints private double originalStartTime; - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - base.UpdateTimeAndPosition(result); + var result = base.UpdateTimeAndPosition(screenSpacePosition, fallbackTime); if (PlacementActive == PlacementState.Active) { @@ -121,6 +121,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints if (result.Time is double startTime) originalStartTime = HitObject.StartTime = startTime; } + + return result; } } } diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index a68bd5d6d6..359a952755 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -1,8 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - +using System; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; @@ -20,13 +20,18 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints { protected new T HitObject => (T)base.HitObject; - private Column column; + [Resolved] + private ManiaHitObjectComposer? composer { get; set; } - public Column Column + private Column? column; + + public Column? Column { get => column; set { + ArgumentNullException.ThrowIfNull(value); + if (value == column) return; @@ -53,9 +58,11 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints return true; } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - base.UpdateTimeAndPosition(result); + var result = composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); + + UpdateTimeAndPosition(result); if (result.Playfield is Column col) { @@ -76,6 +83,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints if (PlacementActive == PlacementState.Waiting) Column = col; } + + return result; } private float getNoteHeight(Column resultPlayfield) => diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs index 422215db57..a8cccfb067 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs @@ -8,6 +8,7 @@ using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Objects; +using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Mania.Edit.Blueprints @@ -35,15 +36,17 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints }; } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double referenceTime) { - base.UpdateTimeAndPosition(result); + var result = base.UpdateTimeAndPosition(screenSpacePosition, referenceTime); if (result.Playfield != null) { piece.Width = result.Playfield.DrawWidth; piece.Position = ToLocalSpace(result.ScreenSpacePosition); } + + return result; } protected override bool OnMouseDown(MouseDownEvent e) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 9062c32b7b..bc20456722 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -19,6 +19,7 @@ using osuTK; namespace osu.Game.Rulesets.Mania.Edit { + [Cached] public partial class ManiaHitObjectComposer : ScrollingHitObjectComposer { private DrawableManiaEditorRuleset drawableRuleset = null!; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs index 163b42bcfd..d3e780df9a 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints public partial class GridPlacementBlueprint : PlacementBlueprint { [Resolved] - private HitObjectComposer? hitObjectComposer { get; set; } + private OsuHitObjectComposer? hitObjectComposer { get; set; } private OsuGridToolboxGroup gridToolboxGroup = null!; private Vector2 originalOrigin; @@ -95,12 +95,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints base.OnDragEnd(e); } - public override SnapType SnapType => ~SnapType.GlobalGrids; - - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double referenceTime) { if (State.Value == Visibility.Hidden) - return; + return new SnapResult(screenSpacePosition, referenceTime); + + var result = hitObjectComposer?.TrySnapToNearbyObjects(screenSpacePosition) ?? new SnapResult(screenSpacePosition, referenceTime); var pos = ToLocalSpace(result.ScreenSpacePosition); @@ -120,6 +120,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints gridToolboxGroup.SetGridFromPoints(gridToolboxGroup.StartPosition.Value, pos); } } + + return result; } protected override void PopOut() diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs index 78a0e36dc2..dad7bd5f0e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs @@ -1,10 +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 osu.Framework.Allocation; using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Objects; +using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles @@ -15,6 +17,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles private readonly HitCirclePiece circlePiece; + [Resolved] + private OsuHitObjectComposer? composer { get; set; } + public HitCirclePlacementBlueprint() : base(new HitCircle()) { @@ -45,10 +50,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles return base.OnMouseDown(e); } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - base.UpdateTimeAndPosition(result); + var result = composer?.TrySnapToNearbyObjects(screenSpacePosition) + ?? composer?.TrySnapToDistanceGrid(screenSpacePosition) + ?? composer?.TrySnapToPositionGrid(screenSpacePosition) + ?? new SnapResult(screenSpacePosition, fallbackTime); + + UpdateTimeAndPosition(result); HitObject.Position = ToLocalSpace(result.ScreenSpacePosition); + return result; } } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 4f2f6516a8..f5fe00e8b6 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -25,6 +25,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { public new Slider HitObject => (Slider)base.HitObject; + [Resolved] + private OsuHitObjectComposer? composer { get; set; } + private SliderBodyPiece bodyPiece = null!; private HitCirclePiece headCirclePiece = null!; private HitCirclePiece tailCirclePiece = null!; @@ -40,9 +43,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private int currentSegmentLength; private bool usingCustomSegmentType; - [Resolved] - private IPositionSnapProvider? positionSnapProvider { get; set; } - [Resolved] private IDistanceSnapProvider? distanceSnapProvider { get; set; } @@ -106,9 +106,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders [Resolved] private EditorBeatmap editorBeatmap { get; set; } = null!; - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - base.UpdateTimeAndPosition(result); + var result = composer?.TrySnapToNearbyObjects(screenSpacePosition) + ?? composer?.TrySnapToDistanceGrid(screenSpacePosition) + ?? composer?.TrySnapToPositionGrid(screenSpacePosition) + ?? new SnapResult(screenSpacePosition, fallbackTime); + + UpdateTimeAndPosition(result); switch (state) { @@ -131,6 +136,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders updateCursor(); break; } + + return result; } protected override bool OnMouseDown(MouseDownEvent e) @@ -375,7 +382,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private Vector2 getCursorPosition() { - var result = positionSnapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.ControlPoints ? SnapType.GlobalGrids : SnapType.All); + SnapResult? result = null; + var mousePosition = inputManager.CurrentState.Mouse.Position; + + if (state != SliderPlacementState.ControlPoints) + { + result ??= composer?.TrySnapToNearbyObjects(mousePosition); + result ??= composer?.TrySnapToDistanceGrid(mousePosition); + } + + result ??= composer?.TrySnapToPositionGrid(mousePosition); + return ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position; } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs index 17d2dcd75c..6c4847cada 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs @@ -9,6 +9,7 @@ using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; +using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners @@ -70,5 +71,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners ? Math.Max(HitObject.StartTime, EditorClock.CurrentTime) : Math.Max(HitObject.StartTime + beatSnapProvider.GetBeatLengthAtTime(HitObject.StartTime), beatSnapProvider.SnapTime(EditorClock.CurrentTime)); } + + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) + { + var result = new SnapResult(screenSpacePosition, fallbackTime); + UpdateTimeAndPosition(result); + return result; + } } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 06a74fb631..faed599fa5 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -32,6 +32,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Edit { + [Cached] public partial class OsuHitObjectComposer : HitObjectComposer { public OsuHitObjectComposer(Ruleset ruleset) diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs index 7f45123bd6..b887fac42a 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Taiko.Objects; @@ -16,6 +17,9 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints public new Hit HitObject => (Hit)base.HitObject; + [Resolved] + private TaikoHitObjectComposer? composer { get; set; } + public HitPlacementBlueprint() : base(new Hit()) { @@ -40,10 +44,12 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints return true; } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { + var result = composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); piece.Position = ToLocalSpace(result.ScreenSpacePosition); - base.UpdateTimeAndPosition(result); + UpdateTimeAndPosition(result); + return result; } } } diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs index de3a4d96eb..7263c1ef2c 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs @@ -1,9 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Framework.Utils; @@ -26,12 +25,15 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints private readonly IHasDuration spanPlacementObject; + [Resolved] + private TaikoHitObjectComposer? composer { get; set; } + protected override bool IsValidForPlacement => Precision.DefinitelyBigger(spanPlacementObject.Duration, 0); public TaikoSpanPlacementBlueprint(HitObject hitObject) : base(hitObject) { - spanPlacementObject = hitObject as IHasDuration; + spanPlacementObject = (hitObject as IHasDuration)!; RelativeSizeAxes = Axes.Both; @@ -79,9 +81,11 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints EndPlacement(true); } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - base.UpdateTimeAndPosition(result); + var result = composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); + + UpdateTimeAndPosition(result); if (PlacementActive == PlacementState.Active) { @@ -116,6 +120,8 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints originalPosition = ToLocalSpace(result.ScreenSpacePosition); } } + + return result; } } } diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs index d97a854ff7..54031f0c9f 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; @@ -12,6 +13,7 @@ using osu.Game.Screens.Edit.Compose.Components; namespace osu.Game.Rulesets.Taiko.Edit { + [Cached] public partial class TaikoHitObjectComposer : ScrollingHitObjectComposer { protected override bool ApplyHorizontalCentering => false; diff --git a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs index 4df2a52743..0bfda94f44 100644 --- a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Edit /// Updates the time and position of this based on the provided snap information. /// /// The snap result information. - public override void UpdateTimeAndPosition(SnapResult result) + public void UpdateTimeAndPosition(SnapResult result) { if (PlacementActive == PlacementState.Waiting) { diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index 52b8a5c796..f2d501d1c4 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -7,6 +7,7 @@ using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Objects; +using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Edit @@ -75,18 +76,7 @@ namespace osu.Game.Rulesets.Edit PlacementActive = PlacementState.Finished; } - /// - /// Determines which objects to snap to for the snap result in . - /// - public virtual SnapType SnapType => SnapType.All; - - /// - /// Updates the time and position of this based on the provided snap information. - /// - /// The snap result information. - public virtual void UpdateTimeAndPosition(SnapResult result) - { - } + public abstract SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime); public bool OnPressed(KeyBindingPressEvent e) { diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 27d6656c69..de1f589135 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -340,12 +340,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void updatePlacementTimeAndPosition() { - SnapResult snapResult = new SnapResult(InputManager.CurrentState.Mouse.Position, null); // Composer.FindSnappedPositionAndTime(InputManager.CurrentState.Mouse.Position, CurrentPlacement.SnapType); TODO - - // if no time was found from positional snapping, we should still quantize to the beat. - snapResult.Time ??= Beatmap.SnapTime(EditorClock.CurrentTime, null); - - CurrentPlacement.UpdateTimeAndPosition(snapResult); + CurrentPlacement.UpdateTimeAndPosition(InputManager.CurrentState.Mouse.Position, Beatmap.SnapTime(EditorClock.CurrentTime, null)); } #endregion From 32d341a46855d9116aa12ed8f79e1864e3bb6b78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 21 Jan 2025 13:49:48 +0100 Subject: [PATCH 193/801] Remove `IPositionSnapProvider` --- .../Rulesets/Edit/IPositionSnapProvider.cs | 23 ------------- osu.Game/Rulesets/Edit/SnapType.cs | 32 ------------------- 2 files changed, 55 deletions(-) delete mode 100644 osu.Game/Rulesets/Edit/IPositionSnapProvider.cs delete mode 100644 osu.Game/Rulesets/Edit/SnapType.cs diff --git a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs deleted file mode 100644 index 002a0aafe6..0000000000 --- a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs +++ /dev/null @@ -1,23 +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 osuTK; - -namespace osu.Game.Rulesets.Edit -{ - /// - /// A snap provider which given a proposed position for a hit object, potentially offers a more correct position and time value inferred from the context of the beatmap. - /// - [Cached] - public interface IPositionSnapProvider - { - /// - /// Given a position, find a valid time and position snap. - /// - /// The screen-space position to be snapped. - /// The type of snapping to apply. - /// The time and position post-snapping. - SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All); - } -} diff --git a/osu.Game/Rulesets/Edit/SnapType.cs b/osu.Game/Rulesets/Edit/SnapType.cs deleted file mode 100644 index cf743f6ace..0000000000 --- a/osu.Game/Rulesets/Edit/SnapType.cs +++ /dev/null @@ -1,32 +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; - -namespace osu.Game.Rulesets.Edit -{ - [Flags] - public enum SnapType - { - None = 0, - - /// - /// Snapping to visible nearby objects. - /// - NearbyObjects = 1 << 0, - - /// - /// Grids which are global to the playfield. - /// - GlobalGrids = 1 << 1, - - /// - /// Grids which are relative to other nearby hit objects. - /// - RelativeGrids = 1 << 2, - - AllGrids = RelativeGrids | GlobalGrids, - - All = NearbyObjects | GlobalGrids | RelativeGrids, - } -} From 269ade178e4513d2873c4caf6e9aacafc9118097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 21 Jan 2025 14:59:12 +0100 Subject: [PATCH 194/801] Fix tests --- .../Editor/CatchPlacementBlueprintTestScene.cs | 9 ++++----- .../Editor/ManiaPlacementBlueprintTestScene.cs | 6 ++---- .../Edit/Blueprints/GridPlacementBlueprint.cs | 6 +++--- .../HitCircles/HitCirclePlacementBlueprint.cs | 2 +- .../Sliders/Components/PathControlPointVisualiser.cs | 2 +- .../Blueprints/Sliders/SliderPlacementBlueprint.cs | 2 +- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 4 +++- .../Overlays/SkinEditor/SkinBlueprintContainer.cs | 7 ++++++- osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs | 2 +- osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs | 11 ++++------- 10 files changed, 26 insertions(+), 25 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs index 0578010c25..a327e6d4c9 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs @@ -12,7 +12,6 @@ using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; using osu.Game.Rulesets.Catch.Objects.Drawables; -using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Tests.Visual; @@ -71,11 +70,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor contentContainer.Playfield.HitObjectContainer.Add(hitObject); } - protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint) + protected override void UpdatePlacementTimeAndPosition() { - var result = base.SnapForBlueprint(blueprint); - result.Time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(result.ScreenSpacePosition) / TIME_SNAP) * TIME_SNAP; - return result; + var position = InputManager.CurrentState.Mouse.Position; + double time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(position) / TIME_SNAP) * TIME_SNAP; + CurrentBlueprint.UpdateTimeAndPosition(position, time); } } } diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs index 5e633c3161..0f913a6a7d 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs @@ -9,7 +9,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; -using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.UI; @@ -47,12 +46,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor }); } - protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint) + protected override void UpdatePlacementTimeAndPosition() { double time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position); var pos = column.ScreenSpacePositionAtTime(time); - - return new SnapResult(pos, time, column); + CurrentBlueprint.UpdateTimeAndPosition(pos, time); } protected override Container CreateHitObjectContainer() => new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both }; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs index d3e780df9a..d9edc8dbd4 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs @@ -95,12 +95,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints base.OnDragEnd(e); } - public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double referenceTime) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { if (State.Value == Visibility.Hidden) - return new SnapResult(screenSpacePosition, referenceTime); + return new SnapResult(screenSpacePosition, fallbackTime); - var result = hitObjectComposer?.TrySnapToNearbyObjects(screenSpacePosition) ?? new SnapResult(screenSpacePosition, referenceTime); + var result = hitObjectComposer?.TrySnapToNearbyObjects(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); var pos = ToLocalSpace(result.ScreenSpacePosition); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs index dad7bd5f0e..53784a7f08 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - var result = composer?.TrySnapToNearbyObjects(screenSpacePosition) + var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime) ?? composer?.TrySnapToDistanceGrid(screenSpacePosition) ?? composer?.TrySnapToPositionGrid(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index bac5f0101c..a3bb0b868a 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -433,7 +433,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { // Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex])); - SnapResult result = positionSnapProvider?.TrySnapToNearbyObjects(newHeadPosition) + SnapResult result = positionSnapProvider?.TrySnapToNearbyObjects(newHeadPosition, oldStartTime) ?? positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition) ?? positionSnapProvider?.TrySnapToPositionGrid(newHeadPosition); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index f5fe00e8b6..fd72f18b12 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - var result = composer?.TrySnapToNearbyObjects(screenSpacePosition) + var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime) ?? composer?.TrySnapToDistanceGrid(screenSpacePosition) ?? composer?.TrySnapToPositionGrid(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index faed599fa5..7a93a26e45 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -225,11 +225,13 @@ namespace osu.Game.Rulesets.Osu.Edit } [CanBeNull] - public SnapResult TrySnapToNearbyObjects(Vector2 screenSpacePosition) + public SnapResult TrySnapToNearbyObjects(Vector2 screenSpacePosition, double? fallbackTime = null) { if (!snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) return null; + snapResult.Time ??= fallbackTime; + if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null) return snapResult; diff --git a/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs b/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs index 8f831a6f18..df8cb33a71 100644 --- a/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs +++ b/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs @@ -113,7 +113,12 @@ namespace osu.Game.Overlays.SkinEditor protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints) { - throw new System.NotImplementedException(); + Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + + // The final movement position, relative to movementBlueprintOriginalPosition. + var referenceBlueprint = blueprints.First().blueprint; + Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; + return SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, movePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); } /// diff --git a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs index 0bfda94f44..3119680272 100644 --- a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Edit /// Updates the time and position of this based on the provided snap information. /// /// The snap result information. - public void UpdateTimeAndPosition(SnapResult result) + protected void UpdateTimeAndPosition(SnapResult result) { if (PlacementActive == PlacementState.Waiting) { diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs index aa8aff3adc..baf614d1c8 100644 --- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs @@ -28,7 +28,7 @@ namespace osu.Game.Tests.Visual base.Content.Add(HitObjectContainer = CreateHitObjectContainer().With(c => c.Clock = new FramedClock(new StopwatchClock()))); base.Content.Add(new MouseMovementInterceptor { - MouseMoved = updatePlacementTimeAndPosition, + MouseMoved = UpdatePlacementTimeAndPosition, }); } @@ -93,13 +93,10 @@ namespace osu.Game.Tests.Visual if (CurrentBlueprint.PlacementActive == PlacementBlueprint.PlacementState.Finished) ResetPlacement(); - updatePlacementTimeAndPosition(); + UpdatePlacementTimeAndPosition(); } - private void updatePlacementTimeAndPosition() => CurrentBlueprint.UpdateTimeAndPosition(SnapForBlueprint(CurrentBlueprint)); - - protected virtual SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint) => - new SnapResult(InputManager.CurrentState.Mouse.Position, null); + protected virtual void UpdatePlacementTimeAndPosition() => CurrentBlueprint.UpdateTimeAndPosition(InputManager.CurrentState.Mouse.Position, 0); public override void Add(Drawable drawable) { @@ -108,7 +105,7 @@ namespace osu.Game.Tests.Visual if (drawable is HitObjectPlacementBlueprint blueprint) { blueprint.Show(); - blueprint.UpdateTimeAndPosition(SnapForBlueprint(blueprint)); + UpdatePlacementTimeAndPosition(); } } From 0164a2e4dca86fed1f3ea016eb9b1e4084eebba1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 19:02:31 +0900 Subject: [PATCH 195/801] Move pool item preparation / cleanup duties to `Carousel` --- osu.Game/Screens/SelectV2/Carousel.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 6ff27c6198..648c2d090a 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -540,11 +540,13 @@ namespace osu.Game.Screens.SelectV2 { var c = (ICarouselPanel)panel; + // panel in the process of expiring, ignore it. + if (c.Item == null) + continue; + if (panel.Depth != c.DrawYPosition) scroll.Panels.ChangeChildDepth(panel, (float)c.DrawYPosition); - Debug.Assert(c.Item != null); - if (c.DrawYPosition != c.Item.CarouselYPosition) c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed); @@ -631,7 +633,9 @@ namespace osu.Game.Screens.SelectV2 if (drawable is not ICarouselPanel carouselPanel) throw new InvalidOperationException($"Carousel panel drawables must implement {typeof(ICarouselPanel)}"); + carouselPanel.DrawYPosition = item.CarouselYPosition; carouselPanel.Item = item; + scroll.Add(drawable); } @@ -650,6 +654,12 @@ namespace osu.Game.Screens.SelectV2 { panel.FinishTransforms(); panel.Expire(); + + var carouselPanel = (ICarouselPanel)panel; + + carouselPanel.Item = null; + carouselPanel.Selected.Value = false; + carouselPanel.KeyboardSelected.Value = false; } #endregion From 175eb82ccfed30fed57bbbeea02d687eb0a4794c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 19:02:47 +0900 Subject: [PATCH 196/801] Split out beatmaps and set panels into two separate classes --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 2 +- .../TestSceneBeatmapCarouselV2Basics.cs | 10 +- .../TestSceneBeatmapCarouselV2Selection.cs | 2 +- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 20 ++- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 2 +- ...eatmapCarouselPanel.cs => BeatmapPanel.cs} | 58 +++------ osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 114 ++++++++++++++++++ 7 files changed, 158 insertions(+), 50 deletions(-) rename osu.Game/Screens/SelectV2/{BeatmapCarouselPanel.cs => BeatmapPanel.cs} (69%) create mode 100644 osu.Game/Screens/SelectV2/BeatmapSetPanel.cs diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 3aa9f60181..4c85cf8fcd 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -131,7 +131,7 @@ namespace osu.Game.Tests.Visual.SongSelect protected void SortBy(FilterCriteria criteria) => AddStep($"sort by {criteria.Sort}", () => Carousel.Filter(criteria)); - protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); + protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); protected void WaitForSorting() => AddUntilStep("sorting finished", () => Carousel.IsFiltering, () => Is.False); protected void WaitForScrolling() => AddUntilStep("scroll finished", () => Scroll.Current, () => Is.EqualTo(Scroll.Target)); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs index 748831bf7b..3a516ea762 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs @@ -56,16 +56,16 @@ namespace osu.Game.Tests.Visual.SongSelect WaitForDrawablePanels(); AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2)); - AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType().Single(p => p.Selected.Value))); + AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType().Single(p => p.Selected.Value))); WaitForScrolling(); - AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); + AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); RemoveFirstBeatmap(); WaitForSorting(); - AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, + AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, () => Is.EqualTo(positionBefore)); } @@ -83,11 +83,11 @@ namespace osu.Game.Tests.Visual.SongSelect WaitForScrolling(); - AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); + AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); RemoveFirstBeatmap(); WaitForSorting(); - AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, + AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, () => Is.EqualTo(positionBefore)); } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs index 305774b7d3..3c42969d8c 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); - BeatmapCarouselPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); + BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); } [Test] diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 630f7b6583..bb13c7449d 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -141,14 +141,28 @@ namespace osu.Game.Screens.SelectV2 #region Drawable pooling - private readonly DrawablePool carouselPanelPool = new DrawablePool(100); + private readonly DrawablePool beatmapPanelPool = new DrawablePool(100); + private readonly DrawablePool setPanelPool = new DrawablePool(100); private void setupPools() { - AddInternal(carouselPanelPool); + AddInternal(beatmapPanelPool); + AddInternal(setPanelPool); } - protected override Drawable GetDrawableForDisplay(CarouselItem item) => carouselPanelPool.Get(); + protected override Drawable GetDrawableForDisplay(CarouselItem item) + { + switch (item.Model) + { + case BeatmapInfo: + return beatmapPanelPool.Get(); + + case BeatmapSetInfo: + return setPanelPool.Get(); + } + + throw new InvalidOperationException(); + } #endregion } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 4f0767048a..0658263a8c 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -57,7 +57,7 @@ namespace osu.Game.Screens.SelectV2 { newItems.Add(new CarouselItem(b.BeatmapSet!) { - DrawHeight = 80, + DrawHeight = BeatmapSetPanel.HEIGHT, IsGroupSelectionTarget = true }); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs similarity index 69% rename from osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs rename to osu.Game/Screens/SelectV2/BeatmapPanel.cs index 398ec7bf4c..4a9e406def 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapPanel.cs @@ -16,22 +16,25 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public partial class BeatmapCarouselPanel : PoolableDrawable, ICarouselPanel + public partial class BeatmapPanel : PoolableDrawable, ICarouselPanel { [Resolved] private BeatmapCarousel carousel { get; set; } = null!; private Box activationFlash = null!; - private Box background = null!; private OsuSpriteText text = null!; [BackgroundDependencyLoader] private void load() { + Size = new Vector2(500, CarouselItem.DEFAULT_HEIGHT); + Masking = true; + InternalChildren = new Drawable[] { - background = new Box + new Box { + Colour = Color4.Aqua.Darken(5), Alpha = 0.8f, RelativeSizeAxes = Axes.Both, }, @@ -69,63 +72,40 @@ namespace osu.Game.Screens.SelectV2 }); } - protected override void FreeAfterUse() - { - base.FreeAfterUse(); - Item = null; - Selected.Value = false; - KeyboardSelected.Value = false; - } - protected override void PrepareForUse() { base.PrepareForUse(); Debug.Assert(Item != null); + var beatmap = (BeatmapInfo)Item.Model; - DrawYPosition = Item.CarouselYPosition; - - Size = new Vector2(500, Item.DrawHeight); - Masking = true; - - background.Colour = (Item.Model is BeatmapInfo ? Color4.Aqua : Color4.Yellow).Darken(5); - text.Text = getTextFor(Item.Model); + text.Text = $"Difficulty: {beatmap.DifficultyName} ({beatmap.StarRating:N1}*)"; this.FadeInFromZero(500, Easing.OutQuint); } - private string getTextFor(object item) - { - switch (item) - { - case BeatmapInfo bi: - return $"Difficulty: {bi.DifficultyName} ({bi.StarRating:N1}*)"; - - case BeatmapSetInfo si: - return $"{si.Metadata}"; - } - - return "unknown"; - } - protected override bool OnClick(ClickEvent e) { - if (carousel.CurrentSelection == Item!.Model) - carousel.TryActivateSelection(); - else + if (carousel.CurrentSelection != Item!.Model) + { carousel.CurrentSelection = Item!.Model; + return true; + } + + carousel.TryActivateSelection(); return true; } + #region ICarouselPanel + public CarouselItem? Item { get; set; } public BindableBool Selected { get; } = new BindableBool(); public BindableBool KeyboardSelected { get; } = new BindableBool(); public double DrawYPosition { get; set; } - public void Activated() - { - activationFlash.FadeOutFromOne(500, Easing.OutQuint); - } + public void Activated() => activationFlash.FadeOutFromOne(500, Easing.OutQuint); + + #endregion } } diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs new file mode 100644 index 0000000000..0b95f94365 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -0,0 +1,114 @@ +// 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.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Sprites; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapSetPanel : PoolableDrawable, ICarouselPanel + { + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 2; + + [Resolved] + private BeatmapCarousel carousel { get; set; } = null!; + + private Box activationFlash = null!; + private OsuSpriteText text = null!; + + [BackgroundDependencyLoader] + private void load() + { + Size = new Vector2(500, HEIGHT); + Masking = true; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = Color4.Yellow.Darken(5), + Alpha = 0.8f, + RelativeSizeAxes = Axes.Both, + }, + activationFlash = new Box + { + Colour = Color4.White, + Blending = BlendingParameters.Additive, + Alpha = 0, + RelativeSizeAxes = Axes.Both, + }, + text = new OsuSpriteText + { + Padding = new MarginPadding(5), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + }; + + Selected.BindValueChanged(value => + { + activationFlash.FadeTo(value.NewValue ? 0.2f : 0, 500, Easing.OutQuint); + }); + + KeyboardSelected.BindValueChanged(value => + { + if (value.NewValue) + { + BorderThickness = 5; + BorderColour = Color4.Pink; + } + else + { + BorderThickness = 0; + } + }); + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + Debug.Assert(Item != null); + Debug.Assert(Item.IsGroupSelectionTarget); + + var beatmapSetInfo = (BeatmapSetInfo)Item.Model; + + text.Text = $"{beatmapSetInfo.Metadata}"; + + this.FadeInFromZero(500, Easing.OutQuint); + } + + protected override bool OnClick(ClickEvent e) + { + carousel.CurrentSelection = Item!.Model; + return true; + } + + #region ICarouselPanel + + public CarouselItem? Item { get; set; } + public BindableBool Selected { get; } = new BindableBool(); + public BindableBool KeyboardSelected { get; } = new BindableBool(); + + public double DrawYPosition { get; set; } + + public void Activated() + { + // sets should never be activated. + throw new InvalidOperationException(); + } + + #endregion + } +} From da762384f8450c709c8319ec2a82ba32d29528f8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 20:20:18 +0900 Subject: [PATCH 197/801] Fix breakage from reordering co-reliant variable sets (and guard against it) --- osu.Game/Screens/Play/MasterGameplayClockContainer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 0b47d8ed85..c20d461526 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -72,17 +72,17 @@ namespace osu.Game.Screens.Play track = beatmap.Track; - StartTime = findEarliestStartTime(); GameplayStartTime = gameplayStartTime; + StartTime = findEarliestStartTime(gameplayStartTime, beatmap); } - private double findEarliestStartTime() + private static double findEarliestStartTime(double gameplayStartTime, WorkingBeatmap beatmap) { // here we are trying to find the time to start playback from the "zero" point. // generally this is either zero, or some point earlier than zero in the case of storyboards, lead-ins etc. // start with the originally provided latest time (if before zero). - double time = Math.Min(0, GameplayStartTime); + double time = Math.Min(0, gameplayStartTime); // if a storyboard is present, it may dictate the appropriate start time by having events in negative time space. // this is commonly used to display an intro before the audio track start. From 589035c5348aa16c586c7d28ae04cc598e3410c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 Jan 2025 12:34:05 +0100 Subject: [PATCH 198/801] Simplify code --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 7a93a26e45..2a7ec79e55 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -230,8 +230,6 @@ namespace osu.Game.Rulesets.Osu.Edit if (!snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) return null; - snapResult.Time ??= fallbackTime; - if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null) return snapResult; @@ -244,8 +242,9 @@ namespace osu.Game.Rulesets.Osu.Edit // The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over // the time value if the proposed positions are roughly the same. (Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition)); - if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1)) - snapResult.Time = distanceSnappedTime; + snapResult.Time = Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1) + ? distanceSnappedTime + : fallbackTime; return snapResult; } From b04144df5489465e59b5305f65cd8b450f84fbc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 Jan 2025 12:50:46 +0100 Subject: [PATCH 199/801] Fix behavioural change in interaction between grid & distance snap --- .../HitCircles/HitCirclePlacementBlueprint.cs | 9 +++++---- .../Components/PathControlPointVisualiser.cs | 15 ++++++++++----- .../Sliders/SliderPlacementBlueprint.cs | 9 +++++---- .../Edit/OsuBlueprintContainer.cs | 6 +++++- .../Edit/OsuHitObjectComposer.cs | 2 +- 5 files changed, 26 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs index 53784a7f08..0e1ede4d4c 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs @@ -52,10 +52,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime) - ?? composer?.TrySnapToDistanceGrid(screenSpacePosition) - ?? composer?.TrySnapToPositionGrid(screenSpacePosition) - ?? new SnapResult(screenSpacePosition, fallbackTime); + var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime); + result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition); + if (composer?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? screenSpacePosition, result?.Time ?? fallbackTime) is SnapResult gridSnapResult) + result = gridSnapResult; + result ??= new SnapResult(screenSpacePosition, fallbackTime); UpdateTimeAndPosition(result); HitObject.Position = ToLocalSpace(result.ScreenSpacePosition); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index a3bb0b868a..189bb005a7 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -9,6 +9,7 @@ using System.Collections.Specialized; using System.Diagnostics; using System.Linq; using Humanizer; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -48,6 +49,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public Action> SplitControlPointsRequested; [Resolved(CanBeNull = true)] + [CanBeNull] private OsuHitObjectComposer positionSnapProvider { get; set; } [Resolved(CanBeNull = true)] @@ -433,14 +435,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { // Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex])); - SnapResult result = positionSnapProvider?.TrySnapToNearbyObjects(newHeadPosition, oldStartTime) - ?? positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition) - ?? positionSnapProvider?.TrySnapToPositionGrid(newHeadPosition); - Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position; + var result = positionSnapProvider?.TrySnapToNearbyObjects(newHeadPosition, oldStartTime); + result ??= positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition); + if (positionSnapProvider?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? newHeadPosition, result?.Time ?? oldStartTime) is SnapResult gridSnapResult) + result = gridSnapResult; + result ??= new SnapResult(newHeadPosition, oldStartTime); + + Vector2 movementDelta = Parent!.ToLocalSpace(result.ScreenSpacePosition) - hitObject.Position; hitObject.Position += movementDelta; - hitObject.StartTime = result?.Time ?? hitObject.StartTime; + hitObject.StartTime = result.Time ?? hitObject.StartTime; for (int i = 1; i < hitObject.Path.ControlPoints.Count; i++) { diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index fd72f18b12..2d38e83b2e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -108,10 +108,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime) - ?? composer?.TrySnapToDistanceGrid(screenSpacePosition) - ?? composer?.TrySnapToPositionGrid(screenSpacePosition) - ?? new SnapResult(screenSpacePosition, fallbackTime); + var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime); + result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition); + if (composer?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? screenSpacePosition, result?.Time ?? fallbackTime) is SnapResult gridSnapResult) + result = gridSnapResult; + result ??= new SnapResult(screenSpacePosition, fallbackTime); UpdateTimeAndPosition(result); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs index 235368e552..5eff95adec 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs @@ -60,7 +60,11 @@ namespace osu.Game.Rulesets.Osu.Edit Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; // Retrieve a snapped position. - var result = Composer.TrySnapToDistanceGrid(movePosition) ?? Composer.TrySnapToPositionGrid(movePosition) ?? new SnapResult(movePosition, null); + var result = Composer.TrySnapToNearbyObjects(movePosition); + result ??= Composer.TrySnapToDistanceGrid(movePosition); + if (Composer.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? movePosition, result?.Time) is SnapResult gridSnapResult) + result = gridSnapResult; + result ??= new SnapResult(movePosition, null); var referenceBlueprint = blueprints.First().blueprint; bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 2a7ec79e55..194276baf9 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -261,7 +261,7 @@ namespace osu.Game.Rulesets.Osu.Edit } [CanBeNull] - public SnapResult TrySnapToPositionGrid(Vector2 screenSpacePosition) + public SnapResult TrySnapToPositionGrid(Vector2 screenSpacePosition, double? fallbackTime = null) { if (rectangularGridSnapToggle.Value != TernaryState.True) return null; From daec91f61d5410ad2ab879443aea0ce7a757c01c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 Jan 2025 13:05:38 +0100 Subject: [PATCH 200/801] Refactor further to avoid weird non-virtual common method --- .../Edit/Blueprints/BananaShowerPlacementBlueprint.cs | 2 +- .../Edit/Blueprints/FruitPlacementBlueprint.cs | 2 +- .../Edit/Blueprints/ManiaPlacementBlueprint.cs | 2 +- .../HitCircles/HitCirclePlacementBlueprint.cs | 2 +- .../Blueprints/Sliders/SliderPlacementBlueprint.cs | 2 +- .../Blueprints/Spinners/SpinnerPlacementBlueprint.cs | 8 -------- .../Edit/Blueprints/HitPlacementBlueprint.cs | 2 +- .../Edit/Blueprints/TaikoSpanPlacementBlueprint.cs | 2 +- osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs | 10 ++++++---- 9 files changed, 13 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs index 85b7624f1b..971c98cafd 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints { var result = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); - base.UpdateTimeAndPosition(result); + base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime); if (!(result.Time is double time)) return result; diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs index 83f75771ad..96cfbcb046 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints ? distanceSnapResult : gridSnapResult; - UpdateTimeAndPosition(result); + base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime); HitObject.X = ToLocalSpace(result.ScreenSpacePosition).X; return result; diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index 359a952755..423f14b092 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints { var result = composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); - UpdateTimeAndPosition(result); + base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime); if (result.Playfield is Column col) { diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs index 0e1ede4d4c..93d79a50ab 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs @@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles result = gridSnapResult; result ??= new SnapResult(screenSpacePosition, fallbackTime); - UpdateTimeAndPosition(result); + base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime); HitObject.Position = ToLocalSpace(result.ScreenSpacePosition); return result; } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 2d38e83b2e..1012578375 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders result = gridSnapResult; result ??= new SnapResult(screenSpacePosition, fallbackTime); - UpdateTimeAndPosition(result); + base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime); switch (state) { diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs index 6c4847cada..17d2dcd75c 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs @@ -9,7 +9,6 @@ using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; -using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners @@ -71,12 +70,5 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners ? Math.Max(HitObject.StartTime, EditorClock.CurrentTime) : Math.Max(HitObject.StartTime + beatSnapProvider.GetBeatLengthAtTime(HitObject.StartTime), beatSnapProvider.SnapTime(EditorClock.CurrentTime)); } - - public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) - { - var result = new SnapResult(screenSpacePosition, fallbackTime); - UpdateTimeAndPosition(result); - return result; - } } } diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs index b887fac42a..ce2a674e92 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints { var result = composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); piece.Position = ToLocalSpace(result.ScreenSpacePosition); - UpdateTimeAndPosition(result); + base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime); return result; } } diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs index 7263c1ef2c..3d5c95e1e8 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs @@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints { var result = composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); - UpdateTimeAndPosition(result); + base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime); if (PlacementActive == PlacementState.Active) { diff --git a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs index 3119680272..6720540ec2 100644 --- a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose; +using osuTK; namespace osu.Game.Rulesets.Edit { @@ -87,14 +88,13 @@ namespace osu.Game.Rulesets.Edit } /// - /// Updates the time and position of this based on the provided snap information. + /// Updates the time and position of this . /// - /// The snap result information. - protected void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double time) { if (PlacementActive == PlacementState.Waiting) { - HitObject.StartTime = result.Time ?? EditorClock.CurrentTime; + HitObject.StartTime = time; if (HitObject is IHasComboInformation comboInformation) comboInformation.UpdateComboInformation(getPreviousHitObject() as IHasComboInformation); @@ -129,6 +129,8 @@ namespace osu.Game.Rulesets.Edit for (int i = 0; i < hasRepeats.NodeSamples.Count; i++) hasRepeats.NodeSamples[i] = HitObject.Samples.Select(o => o.With()).ToList(); } + + return new SnapResult(screenSpacePosition, time); } /// From b0136f98a9f19bd61d3e0519cc200184908b31fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 Jan 2025 14:24:16 +0100 Subject: [PATCH 201/801] Fix test failures --- osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 4c85cf8fcd..281be924a1 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -131,7 +131,7 @@ namespace osu.Game.Tests.Visual.SongSelect protected void SortBy(FilterCriteria criteria) => AddStep($"sort by {criteria.Sort}", () => Carousel.Filter(criteria)); - protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); + protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); protected void WaitForSorting() => AddUntilStep("sorting finished", () => Carousel.IsFiltering, () => Is.False); protected void WaitForScrolling() => AddUntilStep("scroll finished", () => Scroll.Current, () => Is.EqualTo(Scroll.Target)); From 82c5f37c2cf005e330a5525892246d5a70358174 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 22:45:05 +0900 Subject: [PATCH 202/801] Remove selection animation on set panel --- osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index 0b95f94365..483869cad2 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -56,11 +56,6 @@ namespace osu.Game.Screens.SelectV2 } }; - Selected.BindValueChanged(value => - { - activationFlash.FadeTo(value.NewValue ? 0.2f : 0, 500, Easing.OutQuint); - }); - KeyboardSelected.BindValueChanged(value => { if (value.NewValue) From 55ab3c72f6acce20144a12ae6258138742969fe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 Jan 2025 15:15:50 +0100 Subject: [PATCH 203/801] Remove unused field --- osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index 483869cad2..37e8b88f71 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -24,7 +24,6 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private BeatmapCarousel carousel { get; set; } = null!; - private Box activationFlash = null!; private OsuSpriteText text = null!; [BackgroundDependencyLoader] @@ -41,13 +40,6 @@ namespace osu.Game.Screens.SelectV2 Alpha = 0.8f, RelativeSizeAxes = Axes.Both, }, - activationFlash = new Box - { - Colour = Color4.White, - Blending = BlendingParameters.Additive, - Alpha = 0, - RelativeSizeAxes = Axes.Both, - }, text = new OsuSpriteText { Padding = new MarginPadding(5), From 79df094f17b65c5276d317bc84563d0afbe21e67 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 24 Jan 2025 23:20:04 +0900 Subject: [PATCH 204/801] Add unique samples for friend online/offline notifications --- osu.Game/Online/FriendPresenceNotifier.cs | 4 ++-- .../Notifications/FriendOfflineNotification.cs | 10 ++++++++++ .../Overlays/Notifications/FriendOnlineNotification.cs | 10 ++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 osu.Game/Overlays/Notifications/FriendOfflineNotification.cs create mode 100644 osu.Game/Overlays/Notifications/FriendOnlineNotification.cs diff --git a/osu.Game/Online/FriendPresenceNotifier.cs b/osu.Game/Online/FriendPresenceNotifier.cs index 75b487384a..229ad4f734 100644 --- a/osu.Game/Online/FriendPresenceNotifier.cs +++ b/osu.Game/Online/FriendPresenceNotifier.cs @@ -167,7 +167,7 @@ namespace osu.Game.Online APIUser? singleUser = onlineAlertQueue.Count == 1 ? onlineAlertQueue.Single() : null; - notifications.Post(new SimpleNotification + notifications.Post(new FriendOnlineNotification { Transient = true, IsImportant = false, @@ -204,7 +204,7 @@ namespace osu.Game.Online return; } - notifications.Post(new SimpleNotification + notifications.Post(new FriendOfflineNotification { Transient = true, IsImportant = false, diff --git a/osu.Game/Overlays/Notifications/FriendOfflineNotification.cs b/osu.Game/Overlays/Notifications/FriendOfflineNotification.cs new file mode 100644 index 0000000000..147fd4ba6f --- /dev/null +++ b/osu.Game/Overlays/Notifications/FriendOfflineNotification.cs @@ -0,0 +1,10 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Overlays.Notifications +{ + public partial class FriendOfflineNotification : SimpleNotification + { + public override string PopInSampleName => "UI/notification-friend-offline"; + } +} diff --git a/osu.Game/Overlays/Notifications/FriendOnlineNotification.cs b/osu.Game/Overlays/Notifications/FriendOnlineNotification.cs new file mode 100644 index 0000000000..6a5cf3b517 --- /dev/null +++ b/osu.Game/Overlays/Notifications/FriendOnlineNotification.cs @@ -0,0 +1,10 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Overlays.Notifications +{ + public partial class FriendOnlineNotification : SimpleNotification + { + public override string PopInSampleName => "UI/notification-friend-online"; + } +} From 354126b7f7684a052389fff61715118a6fe3d885 Mon Sep 17 00:00:00 2001 From: ThePooN Date: Fri, 24 Jan 2025 18:14:55 +0100 Subject: [PATCH 205/801] =?UTF-8?q?=F0=9F=94=A7=20Specify=20we're=20not=20?= =?UTF-8?q?using=20non-exempt=20encryption?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- osu.iOS/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.iOS/Info.plist b/osu.iOS/Info.plist index 70747fc9c8..120e8caecc 100644 --- a/osu.iOS/Info.plist +++ b/osu.iOS/Info.plist @@ -153,6 +153,8 @@ Editor + ITSAppUsesNonExemptEncryption + LSApplicationCategoryType public.app-category.music-games LSSupportsOpeningDocumentsInPlace From ab4162e2aafc4e246ba070870e4967ab7a6e00cb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 25 Jan 2025 19:27:21 +0900 Subject: [PATCH 206/801] Various refactorings and cleanups --- .../TestSceneMultiplayerLoungeSubScreen.cs | 28 +++++-------------- .../TestScenePlaylistsLoungeSubScreen.cs | 28 +++---------------- .../Multiplayer/IMultiplayerLoungeServer.cs | 5 ++++ .../Online/Multiplayer/MultiplayerClient.cs | 3 +- osu.Game/Online/Rooms/CreateRoomRequest.cs | 2 ++ osu.Game/Online/Rooms/JoinRoomRequest.cs | 2 ++ .../OnlinePlay/Lounge/DrawableLoungeRoom.cs | 2 +- .../OnlinePlay/Lounge/IOnlinePlayLounge.cs | 6 ++-- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 6 ++-- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 3 ++ .../Multiplayer/MultiplayerLoungeSubScreen.cs | 2 +- .../Playlists/PlaylistsLoungeSubScreen.cs | 2 +- 12 files changed, 34 insertions(+), 55 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs index 4a259149e2..eb649acd2d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs @@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestJoinRoomWithoutPassword() { - addRoom(false); + AddStep("add room", () => RoomManager.AddRooms(1, withPassword: false)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("join room", () => InputManager.Key(Key.Enter)); @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestPopoverHidesOnBackButton() { - addRoom(true); + AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); @@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestPopoverHidesOnLeavingScreen() { - addRoom(true); + AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); @@ -86,7 +86,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - addRoom(true); + AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); @@ -105,7 +105,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - addRoom(true); + AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); @@ -124,7 +124,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - addRoom(true); + AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); @@ -139,7 +139,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - addRoom(true); + AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); @@ -149,20 +149,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("room joined", () => MultiplayerClient.RoomJoined); } - private void addRoom(bool withPassword) - { - int initialRoomCount = 0; - - AddStep("add room", () => - { - initialRoomCount = roomsContainer.Rooms.Count; - RoomManager.AddRooms(1, withPassword: withPassword); - loungeScreen.RefreshRooms(); - }); - - AddUntilStep("wait for room to appear", () => roomsContainer.Rooms.Count == initialRoomCount + 1); - } - protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies(); } } diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs index 0897a3b2f5..53c7873de5 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs @@ -35,12 +35,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestManyRooms() { - AddStep("add rooms", () => - { - RoomManager.AddRooms(500); - loungeScreen.RefreshRooms(); - }); - + AddStep("add rooms", () => RoomManager.AddRooms(500)); AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 500); } @@ -49,12 +44,7 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("reset mouse", () => InputManager.ReleaseButton(MouseButton.Left)); - AddStep("add rooms", () => - { - RoomManager.AddRooms(30); - loungeScreen.RefreshRooms(); - }); - + AddStep("add rooms", () => RoomManager.AddRooms(30)); AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30); AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0])); @@ -71,12 +61,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestScrollSelectedIntoView() { - AddStep("add rooms", () => - { - RoomManager.AddRooms(30); - loungeScreen.RefreshRooms(); - }); - + AddStep("add rooms", () => RoomManager.AddRooms(30)); AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30); AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0])); @@ -90,12 +75,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestEnteringRoomTakesLeaseOnSelection() { - AddStep("add rooms", () => - { - RoomManager.AddRooms(1); - loungeScreen.RefreshRooms(); - }); - + AddStep("add rooms", () => RoomManager.AddRooms(1)); AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 1); AddAssert("selected room is not disabled", () => !loungeScreen.SelectedRoom.Disabled); diff --git a/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs index c5eb6f9b36..0ee9fa54cd 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs @@ -10,6 +10,11 @@ namespace osu.Game.Online.Multiplayer /// public interface IMultiplayerLoungeServer { + /// + /// Request to create a multiplayer room. + /// + /// The room to create. + /// The created multiplayer room. Task CreateRoom(MultiplayerRoom room); /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index a8f314d372..6749ed9535 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -168,7 +168,7 @@ namespace osu.Game.Online.Multiplayer public async Task CreateRoom(Room room) { if (Room != null) - throw new InvalidOperationException("Cannot join a multiplayer room while already in one."); + throw new InvalidOperationException("Cannot create a multiplayer room while already in one."); var cancellationSource = joinCancellationSource = new CancellationTokenSource(); await initRoom(room, r => CreateRoomInternal(new MultiplayerRoom(room)), cancellationSource.Token).ConfigureAwait(false); @@ -212,6 +212,7 @@ namespace osu.Game.Online.Multiplayer APIRoom.RoomID = joinedRoom.RoomID; APIRoom.Playlist = joinedRoom.Playlist.Select(item => new PlaylistItem(item)).ToArray(); APIRoom.CurrentPlaylistItem = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId); + // The server will null out the end date upon the host joining the room, but the null value is never communicated to the client. APIRoom.EndDate = null; Debug.Assert(LocalUser != null); diff --git a/osu.Game/Online/Rooms/CreateRoomRequest.cs b/osu.Game/Online/Rooms/CreateRoomRequest.cs index 9773bb5e7d..5b2ea77aad 100644 --- a/osu.Game/Online/Rooms/CreateRoomRequest.cs +++ b/osu.Game/Online/Rooms/CreateRoomRequest.cs @@ -15,6 +15,8 @@ namespace osu.Game.Online.Rooms public CreateRoomRequest(Room room) { Room = room; + + // Also copy back to the source model, since it is likely to have been stored elsewhere. Success += r => Room.CopyFrom(r); } diff --git a/osu.Game/Online/Rooms/JoinRoomRequest.cs b/osu.Game/Online/Rooms/JoinRoomRequest.cs index 13e7ac8c84..610e887242 100644 --- a/osu.Game/Online/Rooms/JoinRoomRequest.cs +++ b/osu.Game/Online/Rooms/JoinRoomRequest.cs @@ -16,6 +16,8 @@ namespace osu.Game.Online.Rooms { Room = room; Password = password; + + // Also copy back to the source model, since it is likely to have been stored elsewhere. Success += r => Room.CopyFrom(r); } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 032a231ad3..5de35ef101 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -162,7 +162,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { new OsuMenuItem("Create copy", MenuItemType.Standard, () => { - lounge?.Clone(Room); + lounge?.OpenCopy(Room); }) }; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs b/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs index 8fa7d0751f..73ab84af13 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs @@ -18,10 +18,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge void Join(Room room, string? password, Action? onSuccess = null, Action? onFailure = null); /// - /// Clones the given room and opens it as a fresh (not-yet-created) one. + /// Copies the given room and opens it as a fresh (not-yet-created) one. /// - /// The room to clone. - void Clone(Room room); + /// The room to copy. + void OpenCopy(Room room); /// /// Closes the given room. diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index df17063fdf..0e08e398a4 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -309,7 +309,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge joiningRoomOperation = ongoingOperationTracker?.BeginOperation(); - TryJoin(room, password, r => + JoinInternal(room, password, r => { Open(room); joiningRoomOperation?.Dispose(); @@ -323,9 +323,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge }); }); - protected abstract void TryJoin(Room room, string? password, Action onSuccess, Action onFailure); + protected abstract void JoinInternal(Room room, string? password, Action onSuccess, Action onFailure); - public void Clone(Room room) + public void OpenCopy(Room room) { Debug.Assert(room.RoomID != null); diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index d37f3b877c..80b3961f44 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -353,6 +353,9 @@ namespace osu.Game.Screens.OnlinePlay.Match return base.OnExiting(e); } + /// + /// Parts from the current room. + /// protected abstract void PartRoom(); private bool ensureExitConfirmed() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index e901ecbdce..873a9cde88 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -81,7 +81,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override ListingPollingComponent CreatePollingComponent() => new MultiplayerListingPollingComponent(); - protected override void TryJoin(Room room, string? password, Action onSuccess, Action onFailure) + protected override void JoinInternal(Room room, string? password, Action onSuccess, Action onFailure) { client.JoinRoom(room, password).ContinueWith(result => { diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs index 92415e0eb1..6ed367328c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs @@ -60,7 +60,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return criteria; } - protected override void TryJoin(Room room, string? password, Action onSuccess, Action onFailure) + protected override void JoinInternal(Room room, string? password, Action onSuccess, Action onFailure) { var joinRoomRequest = new JoinRoomRequest(room, password); From dac7d21302cbd9b7094ba7fc0d5989a9f254d46d Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 25 Jan 2025 18:12:44 -0500 Subject: [PATCH 207/801] Be explicit on nullability in `RequiresPortraitOrientation` Co-authored-by: Dean Herbert --- osu.Game/Screens/Play/Player.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b3274766b2..92c483b24a 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -68,7 +68,16 @@ namespace osu.Game.Screens.Play public override bool HideMenuCursorOnNonMouseInput => true; - public override bool RequiresPortraitOrientation => DrawableRuleset?.RequiresPortraitOrientation == true; + public override bool RequiresPortraitOrientation + { + get + { + if (!LoadedBeatmapSuccessfully) + return false; + + return DrawableRuleset!.RequiresPortraitOrientation; + } + } protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered; From 8151c3095ddfc6389516054c4ae66ead80f5b605 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 25 Jan 2025 18:21:20 -0500 Subject: [PATCH 208/801] Revert unnecessary inheritance Everyone is right, too much inheritance and polymorphism backfires very badly. --- .../Skinning/TestSceneColumnHitObjectArea.cs | 10 +++--- .../Mods/ManiaModWithPlayfieldCover.cs | 4 +-- osu.Game.Rulesets.Mania/UI/Column.cs | 6 +++- .../UI/Components/ColumnHitObjectArea.cs | 15 ++++---- .../Components/HitPositionPaddedContainer.cs | 35 ++++++------------- osu.Game.Rulesets.Mania/UI/Stage.cs | 12 ++++--- 6 files changed, 39 insertions(+), 43 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs index d4bbc8acb6..bf67d2d6a9 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs @@ -28,18 +28,20 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { RelativeSizeAxes = Axes.Both, Width = 0.5f, - Child = new ColumnHitObjectArea(new HitObjectContainer()) + Child = new ColumnHitObjectArea { - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, + Child = new HitObjectContainer(), } }, new ColumnTestContainer(1, ManiaAction.Key2) { RelativeSizeAxes = Axes.Both, Width = 0.5f, - Child = new ColumnHitObjectArea(new HitObjectContainer()) + Child = new ColumnHitObjectArea { - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, + Child = new HitObjectContainer(), } } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs index b6e6ee7481..1bc16112c5 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs @@ -5,9 +5,9 @@ using System; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI; -using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Mania.Mods foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns)) { HitObjectContainer hoc = column.HitObjectContainer; - ColumnHitObjectArea hocParent = (ColumnHitObjectArea)hoc.Parent!; + Container hocParent = (Container)hoc.Parent!; hocParent.Remove(hoc, false); hocParent.Add(CreateCover(hoc).With(c => diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 99d952ef1f..81f4d79281 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -67,7 +67,11 @@ namespace osu.Game.Rulesets.Mania.UI Width = COLUMN_WIDTH; hitPolicy = new OrderedHitPolicy(HitObjectContainer); - HitObjectArea = new ColumnHitObjectArea(HitObjectContainer) { RelativeSizeAxes = Axes.Both }; + HitObjectArea = new ColumnHitObjectArea + { + RelativeSizeAxes = Axes.Both, + Child = HitObjectContainer, + }; } [Resolved] diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs index 2d719ef764..46b6ef86f7 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs @@ -3,7 +3,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; @@ -17,25 +16,29 @@ namespace osu.Game.Rulesets.Mania.UI.Components private readonly Drawable hitTarget; - public ColumnHitObjectArea(HitObjectContainer hitObjectContainer) - : base(hitObjectContainer) + protected override Container Content => content; + + private readonly Container content; + + public ColumnHitObjectArea() { AddRangeInternal(new[] { UnderlayElements = new Container { RelativeSizeAxes = Axes.Both, - Depth = 2, }, hitTarget = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.HitTarget), _ => new DefaultHitTarget()) { RelativeSizeAxes = Axes.X, - Depth = 1 + }, + content = new Container + { + RelativeSizeAxes = Axes.Both, }, Explosions = new Container { RelativeSizeAxes = Axes.Both, - Depth = -1, } }); } diff --git a/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs b/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs index f550e3b241..ae91be1c67 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs @@ -4,52 +4,37 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.UI.Components { - public partial class HitPositionPaddedContainer : SkinReloadableDrawable + public partial class HitPositionPaddedContainer : Container { protected readonly IBindable Direction = new Bindable(); - public HitPositionPaddedContainer(Drawable child) - { - InternalChild = child; - } - - internal void Add(Drawable drawable) - { - base.AddInternal(drawable); - } - - internal void Remove(Drawable drawable, bool disposeImmediately = true) - { - base.RemoveInternal(drawable, disposeImmediately); - } + [Resolved] + private ISkinSource skin { get; set; } = null!; [BackgroundDependencyLoader] private void load(IScrollingInfo scrollingInfo) { Direction.BindTo(scrollingInfo.Direction); - Direction.BindValueChanged(onDirectionChanged, true); - } + Direction.BindValueChanged(onDirectionChanged); + + skin.SourceChanged += onSkinChanged; - protected override void SkinChanged(ISkinSource skin) - { - base.SkinChanged(skin); UpdateHitPosition(); } - private void onDirectionChanged(ValueChangedEvent direction) - { - UpdateHitPosition(); - } + private void onSkinChanged() => UpdateHitPosition(); + private void onDirectionChanged(ValueChangedEvent direction) => UpdateHitPosition(); protected virtual void UpdateHitPosition() { - float hitPosition = CurrentSkin.GetConfig( + float hitPosition = skin.GetConfig( new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value ?? Stage.HIT_TARGET_POSITION; diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index 2d73e7bcbe..fb9671c14d 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -103,12 +103,13 @@ namespace osu.Game.Rulesets.Mania.UI Width = 1366, // Bar lines should only be masked on the vertical axis BypassAutoSizeAxes = Axes.Both, Masking = true, - Child = barLineContainer = new HitPositionPaddedContainer(HitObjectContainer) + Child = barLineContainer = new HitPositionPaddedContainer { Name = "Bar lines", Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.Y, + Child = HitObjectContainer, } }, columnFlow = new ColumnFlow(definition) @@ -119,12 +120,13 @@ namespace osu.Game.Rulesets.Mania.UI { RelativeSizeAxes = Axes.Both }, - new HitPositionPaddedContainer(judgements = new JudgementContainer - { - RelativeSizeAxes = Axes.Both, - }) + new HitPositionPaddedContainer { RelativeSizeAxes = Axes.Both, + Child = judgements = new JudgementContainer + { + RelativeSizeAxes = Axes.Both, + }, }, topLevelContainer = new Container { RelativeSizeAxes = Axes.Both } } From ffc37cece0483c9bcdea0962abc8bfbe1dd9b0f1 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 25 Jan 2025 18:50:54 -0500 Subject: [PATCH 209/801] Avoid extra unnecessary DI Co-authored-by: Dean Herbert --- .../UI/DrawableManiaRuleset.cs | 2 +- .../UI/ManiaPlayfieldAdjustmentContainer.cs | 11 ++++------ .../Edit/DrawableEditorRulesetWrapper.cs | 22 +++++++++---------- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 1 - 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index a186d9aa7d..e33cf092c3 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -161,7 +161,7 @@ namespace osu.Game.Rulesets.Mania.UI /// The scroll time. public static double ComputeScrollTime(double scrollSpeed) => MAX_TIME_RANGE / scrollSpeed; - public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer(); + public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer(this); protected override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages); diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs index b0203643b0..feb75b9f1e 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.UI; @@ -16,8 +15,11 @@ namespace osu.Game.Rulesets.Mania.UI private readonly DrawSizePreservingFillContainer scalingContainer; - public ManiaPlayfieldAdjustmentContainer() + private readonly DrawableManiaRuleset drawableManiaRuleset; + + public ManiaPlayfieldAdjustmentContainer(DrawableManiaRuleset drawableManiaRuleset) { + this.drawableManiaRuleset = drawableManiaRuleset; InternalChild = scalingContainer = new DrawSizePreservingFillContainer { Anchor = Anchor.Centre, @@ -30,9 +32,6 @@ namespace osu.Game.Rulesets.Mania.UI }; } - [Resolved] - private DrawableRuleset drawableRuleset { get; set; } = null!; - protected override void Update() { base.Update(); @@ -40,8 +39,6 @@ namespace osu.Game.Rulesets.Mania.UI float aspectRatio = DrawWidth / DrawHeight; bool isPortrait = aspectRatio < 1f; - var drawableManiaRuleset = (DrawableManiaRuleset)drawableRuleset; - if (isPortrait && drawableManiaRuleset.Beatmap.Stages.Count == 1) { // Scale playfield up by 25% to become playable on mobile devices, diff --git a/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs b/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs index 573eb8c42f..174b278d89 100644 --- a/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs +++ b/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs @@ -19,16 +19,16 @@ namespace osu.Game.Rulesets.Edit internal partial class DrawableEditorRulesetWrapper : CompositeDrawable where TObject : HitObject { - public Playfield Playfield => DrawableRuleset.Playfield; + public Playfield Playfield => drawableRuleset.Playfield; - public readonly DrawableRuleset DrawableRuleset; + private readonly DrawableRuleset drawableRuleset; [Resolved] private EditorBeatmap beatmap { get; set; } = null!; public DrawableEditorRulesetWrapper(DrawableRuleset drawableRuleset) { - DrawableRuleset = drawableRuleset; + this.drawableRuleset = drawableRuleset; RelativeSizeAxes = Axes.Both; @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Edit [BackgroundDependencyLoader] private void load() { - DrawableRuleset.FrameStablePlayback = false; + drawableRuleset.FrameStablePlayback = false; Playfield.DisplayJudgements.Value = false; } @@ -67,27 +67,27 @@ namespace osu.Game.Rulesets.Edit private void regenerateAutoplay() { - var autoplayMod = DrawableRuleset.Mods.OfType().Single(); - DrawableRuleset.SetReplayScore(autoplayMod.CreateScoreFromReplayData(DrawableRuleset.Beatmap, DrawableRuleset.Mods)); + var autoplayMod = drawableRuleset.Mods.OfType().Single(); + drawableRuleset.SetReplayScore(autoplayMod.CreateScoreFromReplayData(drawableRuleset.Beatmap, drawableRuleset.Mods)); } private void addHitObject(HitObject hitObject) { - DrawableRuleset.AddHitObject((TObject)hitObject); - DrawableRuleset.Playfield.PostProcess(); + drawableRuleset.AddHitObject((TObject)hitObject); + drawableRuleset.Playfield.PostProcess(); } private void removeHitObject(HitObject hitObject) { - DrawableRuleset.RemoveHitObject((TObject)hitObject); - DrawableRuleset.Playfield.PostProcess(); + drawableRuleset.RemoveHitObject((TObject)hitObject); + drawableRuleset.Playfield.PostProcess(); } public override bool PropagatePositionalInputSubTree => false; public override bool PropagateNonPositionalInputSubTree => false; - public PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => DrawableRuleset.CreatePlayfieldAdjustmentContainer(); + public PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => drawableRuleset.CreatePlayfieldAdjustmentContainer(); protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 8882d55b42..15b60114af 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -133,7 +133,6 @@ namespace osu.Game.Rulesets.Edit if (DrawableRuleset is IDrawableScrollingRuleset scrollingRuleset) dependencies.CacheAs(scrollingRuleset.ScrollingInfo); - dependencies.CacheAs(drawableRulesetWrapper.DrawableRuleset); dependencies.CacheAs(Playfield); InternalChildren = new[] From bb7daae08063fb06e16934b7542a14b65a1f189d Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 25 Jan 2025 19:08:01 -0500 Subject: [PATCH 210/801] Simplify orientation locking code magnificently --- osu.Game/Mobile/OrientationManager.cs | 30 ++++++++++----------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/osu.Game/Mobile/OrientationManager.cs b/osu.Game/Mobile/OrientationManager.cs index 0f9b56d434..964b40e2af 100644 --- a/osu.Game/Mobile/OrientationManager.cs +++ b/osu.Game/Mobile/OrientationManager.cs @@ -50,30 +50,22 @@ namespace osu.Game.Mobile bool lockCurrentOrientation = localUserPlaying.Value == LocalUserPlayingState.Playing; bool lockToPortraitOnPhone = requiresPortraitOrientation.Value; - if (lockCurrentOrientation) + if (IsTablet) { - if (!IsTablet && lockToPortraitOnPhone && !IsCurrentOrientationPortrait) - SetAllowedOrientations(GameOrientation.Portrait); - else if (!IsTablet && !lockToPortraitOnPhone && IsCurrentOrientationPortrait) - SetAllowedOrientations(GameOrientation.Landscape); - else - { - // if the orientation is already portrait/landscape according to the game's specifications, - // then use Locked instead of Portrait/Landscape to handle the case where the device is - // in landscape-left or reverse-portrait. + if (lockCurrentOrientation) SetAllowedOrientations(GameOrientation.Locked); - } - - return; + else + SetAllowedOrientations(null); } - - if (!IsTablet && lockToPortraitOnPhone) + else { - SetAllowedOrientations(GameOrientation.Portrait); - return; + if (lockToPortraitOnPhone) + SetAllowedOrientations(GameOrientation.Portrait); + else if (lockCurrentOrientation) + SetAllowedOrientations(GameOrientation.Locked); + else + SetAllowedOrientations(null); } - - SetAllowedOrientations(null); } /// From c18128e97419ea1f7c9a4086f1b19de8f9c6022e Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 25 Jan 2025 20:01:12 -0500 Subject: [PATCH 211/801] Remove `OrientationManager` and the entire mobile namespace --- osu.Android/AndroidOrientationManager.cs | 39 ------------ osu.Android/OsuGameAndroid.cs | 32 +++++++++- osu.Game/Mobile/GameOrientation.cs | 34 ----------- osu.Game/Mobile/OrientationManager.cs | 77 ------------------------ osu.Game/OsuGame.cs | 63 ++++++++----------- osu.Game/Utils/MobileUtils.cs | 49 +++++++++++++++ osu.iOS/IOSOrientationManager.cs | 41 ------------- osu.iOS/OsuGameIOS.cs | 33 +++++++++- 8 files changed, 138 insertions(+), 230 deletions(-) delete mode 100644 osu.Android/AndroidOrientationManager.cs delete mode 100644 osu.Game/Mobile/GameOrientation.cs delete mode 100644 osu.Game/Mobile/OrientationManager.cs create mode 100644 osu.Game/Utils/MobileUtils.cs delete mode 100644 osu.iOS/IOSOrientationManager.cs diff --git a/osu.Android/AndroidOrientationManager.cs b/osu.Android/AndroidOrientationManager.cs deleted file mode 100644 index 76d2fc24cb..0000000000 --- a/osu.Android/AndroidOrientationManager.cs +++ /dev/null @@ -1,39 +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 Android.Content.PM; -using Android.Content.Res; -using osu.Framework.Allocation; -using osu.Game.Mobile; - -namespace osu.Android -{ - public partial class AndroidOrientationManager : OrientationManager - { - [Resolved] - private OsuGameActivity gameActivity { get; set; } = null!; - - protected override bool IsCurrentOrientationPortrait => gameActivity.Resources!.Configuration!.Orientation == Orientation.Portrait; - protected override bool IsTablet => gameActivity.IsTablet; - - protected override void SetAllowedOrientations(GameOrientation? orientation) - => gameActivity.RequestedOrientation = orientation == null ? gameActivity.DefaultOrientation : toScreenOrientation(orientation.Value); - - private static ScreenOrientation toScreenOrientation(GameOrientation orientation) - { - if (orientation == GameOrientation.Locked) - return ScreenOrientation.Locked; - - if (orientation == GameOrientation.Portrait) - return ScreenOrientation.Portrait; - - if (orientation == GameOrientation.Landscape) - return ScreenOrientation.Landscape; - - if (orientation == GameOrientation.FullPortrait) - return ScreenOrientation.SensorPortrait; - - return ScreenOrientation.SensorLandscape; - } - } -} diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 4143c8cae6..0f2451f0a0 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -3,11 +3,13 @@ using System; using Android.App; +using Android.Content.PM; using Microsoft.Maui.Devices; using osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Platform; using osu.Game; +using osu.Game.Screens; using osu.Game.Updater; using osu.Game.Utils; @@ -71,7 +73,35 @@ namespace osu.Android protected override void LoadComplete() { base.LoadComplete(); - LoadComponentAsync(new AndroidOrientationManager(), Add); + UserPlayingState.BindValueChanged(_ => updateOrientation()); + } + + protected override void ScreenChanged(IOsuScreen? current, IOsuScreen? newScreen) + { + base.ScreenChanged(current, newScreen); + + if (newScreen != null) + updateOrientation(); + } + + private void updateOrientation() + { + var orientation = MobileUtils.GetOrientation(this, (IOsuScreen)ScreenStack.CurrentScreen, gameActivity.IsTablet); + + switch (orientation) + { + case MobileUtils.Orientation.Locked: + gameActivity.RequestedOrientation = ScreenOrientation.Locked; + break; + + case MobileUtils.Orientation.Portrait: + gameActivity.RequestedOrientation = ScreenOrientation.Portrait; + break; + + case MobileUtils.Orientation.Default: + gameActivity.RequestedOrientation = gameActivity.DefaultOrientation; + break; + } } public override void SetHost(GameHost host) diff --git a/osu.Game/Mobile/GameOrientation.cs b/osu.Game/Mobile/GameOrientation.cs deleted file mode 100644 index 0022c8fefb..0000000000 --- a/osu.Game/Mobile/GameOrientation.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Mobile -{ - public enum GameOrientation - { - /// - /// Lock the game orientation. - /// - Locked, - - /// - /// Display the game in regular portrait orientation. - /// - Portrait, - - /// - /// Display the game in landscape-right orientation. - /// - Landscape, - - /// - /// Display the game in landscape-right/landscape-left orientations. - /// - FullLandscape, - - /// - /// Display the game in portrait/portrait-upside-down orientations. - /// This is exclusive to tablet mobile devices. - /// - FullPortrait, - } -} diff --git a/osu.Game/Mobile/OrientationManager.cs b/osu.Game/Mobile/OrientationManager.cs deleted file mode 100644 index 964b40e2af..0000000000 --- a/osu.Game/Mobile/OrientationManager.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.Bindables; -using osu.Framework.Graphics; -using osu.Game.Screens.Play; - -namespace osu.Game.Mobile -{ - /// - /// A that manages the device orientations a game can display in. - /// - public abstract partial class OrientationManager : Component - { - /// - /// Whether the current orientation of the game is portrait. - /// - protected abstract bool IsCurrentOrientationPortrait { get; } - - /// - /// Whether the mobile device is considered a tablet. - /// - protected abstract bool IsTablet { get; } - - [Resolved] - private OsuGame game { get; set; } = null!; - - [Resolved] - private ILocalUserPlayInfo localUserPlayInfo { get; set; } = null!; - - private IBindable requiresPortraitOrientation = null!; - private IBindable localUserPlaying = null!; - - protected override void LoadComplete() - { - base.LoadComplete(); - - requiresPortraitOrientation = game.RequiresPortraitOrientation.GetBoundCopy(); - requiresPortraitOrientation.BindValueChanged(_ => updateOrientations()); - - localUserPlaying = localUserPlayInfo.PlayingState.GetBoundCopy(); - localUserPlaying.BindValueChanged(_ => updateOrientations()); - - updateOrientations(); - } - - private void updateOrientations() - { - bool lockCurrentOrientation = localUserPlaying.Value == LocalUserPlayingState.Playing; - bool lockToPortraitOnPhone = requiresPortraitOrientation.Value; - - if (IsTablet) - { - if (lockCurrentOrientation) - SetAllowedOrientations(GameOrientation.Locked); - else - SetAllowedOrientations(null); - } - else - { - if (lockToPortraitOnPhone) - SetAllowedOrientations(GameOrientation.Portrait); - else if (lockCurrentOrientation) - SetAllowedOrientations(GameOrientation.Locked); - else - SetAllowedOrientations(null); - } - } - - /// - /// Sets the allowed orientations the device can rotate to. - /// - /// The allowed orientations, or null to return back to default. - protected abstract void SetAllowedOrientations(GameOrientation? orientation); - } -} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index cc6613da89..89aba818a3 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -173,25 +173,14 @@ namespace osu.Game /// public readonly IBindable OverlayActivationMode = new Bindable(); - /// - /// On mobile phones, this specifies whether the device should be set and locked to portrait orientation. - /// Tablet devices are unaffected by this property. - /// - /// - /// Implementations can be viewed in mobile projects. - /// - public IBindable RequiresPortraitOrientation => requiresPortraitOrientation; - - private readonly Bindable requiresPortraitOrientation = new BindableBool(); - /// /// Whether the back button is currently displayed. /// private readonly IBindable backButtonVisibility = new Bindable(); - IBindable ILocalUserPlayInfo.PlayingState => playingState; + IBindable ILocalUserPlayInfo.PlayingState => UserPlayingState; - private readonly Bindable playingState = new Bindable(); + protected readonly Bindable UserPlayingState = new Bindable(); protected OsuScreenStack ScreenStack; @@ -319,7 +308,7 @@ namespace osu.Game protected override UserInputManager CreateUserInputManager() { var userInputManager = base.CreateUserInputManager(); - (userInputManager as OsuUserInputManager)?.PlayingState.BindTo(playingState); + (userInputManager as OsuUserInputManager)?.PlayingState.BindTo(UserPlayingState); return userInputManager; } @@ -414,7 +403,7 @@ namespace osu.Game // Transfer any runtime changes back to configuration file. SkinManager.CurrentSkinInfo.ValueChanged += skin => configSkin.Value = skin.NewValue.ID.ToString(); - playingState.BindValueChanged(p => + UserPlayingState.BindValueChanged(p => { BeatmapManager.PauseImports = p.NewValue != LocalUserPlayingState.NotPlaying; SkinManager.PauseImports = p.NewValue != LocalUserPlayingState.NotPlaying; @@ -1555,7 +1544,7 @@ namespace osu.Game GlobalCursorDisplay.ShowCursor = (ScreenStack.CurrentScreen as IOsuScreen)?.CursorVisible ?? false; } - private void screenChanged(IScreen current, IScreen newScreen) + protected virtual void ScreenChanged([CanBeNull] IOsuScreen current, [CanBeNull] IOsuScreen newScreen) { SentrySdk.ConfigureScope(scope => { @@ -1571,10 +1560,10 @@ namespace osu.Game switch (current) { case Player player: - player.PlayingState.UnbindFrom(playingState); + player.PlayingState.UnbindFrom(UserPlayingState); // reset for sanity. - playingState.Value = LocalUserPlayingState.NotPlaying; + UserPlayingState.Value = LocalUserPlayingState.NotPlaying; break; } @@ -1591,7 +1580,7 @@ namespace osu.Game break; case Player player: - player.PlayingState.BindTo(playingState); + player.PlayingState.BindTo(UserPlayingState); break; default: @@ -1599,32 +1588,32 @@ namespace osu.Game break; } - if (current is IOsuScreen currentOsuScreen) + if (current != null) { - backButtonVisibility.UnbindFrom(currentOsuScreen.BackButtonVisibility); - OverlayActivationMode.UnbindFrom(currentOsuScreen.OverlayActivationMode); - configUserActivity.UnbindFrom(currentOsuScreen.Activity); + backButtonVisibility.UnbindFrom(current.BackButtonVisibility); + OverlayActivationMode.UnbindFrom(current.OverlayActivationMode); + configUserActivity.UnbindFrom(current.Activity); } - if (newScreen is IOsuScreen newOsuScreen) + // Bind to new screen. + if (newScreen != null) { - backButtonVisibility.BindTo(newOsuScreen.BackButtonVisibility); - OverlayActivationMode.BindTo(newOsuScreen.OverlayActivationMode); - configUserActivity.BindTo(newOsuScreen.Activity); + backButtonVisibility.BindTo(newScreen.BackButtonVisibility); + OverlayActivationMode.BindTo(newScreen.OverlayActivationMode); + configUserActivity.BindTo(newScreen.Activity); - GlobalCursorDisplay.MenuCursor.HideCursorOnNonMouseInput = newOsuScreen.HideMenuCursorOnNonMouseInput; + // Handle various configuration updates based on new screen settings. + GlobalCursorDisplay.MenuCursor.HideCursorOnNonMouseInput = newScreen.HideMenuCursorOnNonMouseInput; - requiresPortraitOrientation.Value = newOsuScreen.RequiresPortraitOrientation; - - if (newOsuScreen.HideOverlaysOnEnter) + if (newScreen.HideOverlaysOnEnter) CloseAllOverlays(); else Toolbar.Show(); - if (newOsuScreen.ShowFooter) + if (newScreen.ShowFooter) { BackButton.Hide(); - ScreenFooter.SetButtons(newOsuScreen.CreateFooterButtons()); + ScreenFooter.SetButtons(newScreen.CreateFooterButtons()); ScreenFooter.Show(); } else @@ -1632,16 +1621,16 @@ namespace osu.Game ScreenFooter.SetButtons(Array.Empty()); ScreenFooter.Hide(); } - } - skinEditor.SetTarget((OsuScreen)newScreen); + skinEditor.SetTarget((OsuScreen)newScreen); + } } - private void screenPushed(IScreen lastScreen, IScreen newScreen) => screenChanged(lastScreen, newScreen); + private void screenPushed(IScreen lastScreen, IScreen newScreen) => ScreenChanged((OsuScreen)lastScreen, (OsuScreen)newScreen); private void screenExited(IScreen lastScreen, IScreen newScreen) { - screenChanged(lastScreen, newScreen); + ScreenChanged((OsuScreen)lastScreen, (OsuScreen)newScreen); if (newScreen == null) Exit(); diff --git a/osu.Game/Utils/MobileUtils.cs b/osu.Game/Utils/MobileUtils.cs new file mode 100644 index 0000000000..6e59efb71c --- /dev/null +++ b/osu.Game/Utils/MobileUtils.cs @@ -0,0 +1,49 @@ +// 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.Screens; +using osu.Game.Screens.Play; + +namespace osu.Game.Utils +{ + public static class MobileUtils + { + /// + /// Determines the correct state which a mobile device should be put into for the given information. + /// + /// Information about whether the user is currently playing. + /// The current screen which the user is at. + /// Whether the user is playing on a mobile tablet device instead of a phone. + public static Orientation GetOrientation(ILocalUserPlayInfo userPlayInfo, IOsuScreen currentScreen, bool isTablet) + { + bool lockCurrentOrientation = userPlayInfo.PlayingState.Value == LocalUserPlayingState.Playing; + bool lockToPortraitOnPhone = currentScreen.RequiresPortraitOrientation; + + if (lockToPortraitOnPhone && !isTablet) + return Orientation.Portrait; + + if (lockCurrentOrientation) + return Orientation.Locked; + + return Orientation.Default; + } + + public enum Orientation + { + /// + /// Lock the game orientation. + /// + Locked, + + /// + /// Lock the game to portrait orientation (does not include upside-down portrait). + /// + Portrait, + + /// + /// Use the application's default settings. + /// + Default, + } + } +} diff --git a/osu.iOS/IOSOrientationManager.cs b/osu.iOS/IOSOrientationManager.cs deleted file mode 100644 index 6d5bb990c2..0000000000 --- a/osu.iOS/IOSOrientationManager.cs +++ /dev/null @@ -1,41 +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.Game.Mobile; -using UIKit; - -namespace osu.iOS -{ - public partial class IOSOrientationManager : OrientationManager - { - private readonly AppDelegate appDelegate; - - protected override bool IsCurrentOrientationPortrait => appDelegate.CurrentOrientation.IsPortrait(); - protected override bool IsTablet => UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad; - - public IOSOrientationManager(AppDelegate appDelegate) - { - this.appDelegate = appDelegate; - } - - protected override void SetAllowedOrientations(GameOrientation? orientation) - => appDelegate.Orientations = orientation == null ? null : toUIInterfaceOrientationMask(orientation.Value); - - private UIInterfaceOrientationMask toUIInterfaceOrientationMask(GameOrientation orientation) - { - if (orientation == GameOrientation.Locked) - return (UIInterfaceOrientationMask)(1 << (int)appDelegate.CurrentOrientation); - - if (orientation == GameOrientation.Portrait) - return UIInterfaceOrientationMask.Portrait; - - if (orientation == GameOrientation.Landscape) - return UIInterfaceOrientationMask.LandscapeRight; - - if (orientation == GameOrientation.FullPortrait) - return UIInterfaceOrientationMask.Portrait | UIInterfaceOrientationMask.PortraitUpsideDown; - - return UIInterfaceOrientationMask.Landscape; - } - } -} diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index ed47a1e8b8..a5a42c1e66 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -8,8 +8,10 @@ using osu.Framework.Graphics; using osu.Framework.iOS; using osu.Framework.Platform; using osu.Game; +using osu.Game.Screens; using osu.Game.Updater; using osu.Game.Utils; +using UIKit; namespace osu.iOS { @@ -28,7 +30,36 @@ namespace osu.iOS protected override void LoadComplete() { base.LoadComplete(); - LoadComponentAsync(new IOSOrientationManager(appDelegate), Add); + UserPlayingState.BindValueChanged(_ => updateOrientation()); + } + + protected override void ScreenChanged(IOsuScreen? current, IOsuScreen? newScreen) + { + base.ScreenChanged(current, newScreen); + + if (newScreen != null) + updateOrientation(); + } + + private void updateOrientation() + { + bool iPad = UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad; + var orientation = MobileUtils.GetOrientation(this, (IOsuScreen)ScreenStack.CurrentScreen, iPad); + + switch (orientation) + { + case MobileUtils.Orientation.Locked: + appDelegate.Orientations = (UIInterfaceOrientationMask)(1 << (int)appDelegate.CurrentOrientation); + break; + + case MobileUtils.Orientation.Portrait: + appDelegate.Orientations = UIInterfaceOrientationMask.Portrait; + break; + + case MobileUtils.Orientation.Default: + appDelegate.Orientations = null; + break; + } } protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier(); From 4d7b0710275f2e41317d988f516322cc2c06c45f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 25 Jan 2025 23:58:56 -0500 Subject: [PATCH 212/801] Specifiy second-factor authentication code text box with `Code` type --- osu.Game/Overlays/Login/SecondFactorAuthForm.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs index 3022233e9c..506cb70d09 100644 --- a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs +++ b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Game.Graphics; @@ -62,6 +63,7 @@ namespace osu.Game.Overlays.Login }, codeTextBox = new OsuTextBox { + InputProperties = new TextInputProperties(TextInputType.Code), PlaceholderText = "Enter code", RelativeSizeAxes = Axes.X, TabbableContentContainer = this, From a7aa553445738068eb8075043cb64187ed6b73dc Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Sun, 26 Jan 2025 16:21:07 +0000 Subject: [PATCH 213/801] Fix incorrect `startTime` calculation --- .../Difficulty/Preprocessing/TaikoDifficultyHitObject.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 0c668797cd..486841b995 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -118,13 +118,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing noteObjects.Add(this); } - double startTime = hitObject.StartTime * clockRate; - // Retrieve the timing point at the note's start time - TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(startTime); + TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(hitObject.StartTime); // Calculate the slider velocity at the note's start time. - double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, startTime, clockRate); + double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, hitObject.StartTime, clockRate); CurrentSliderVelocity = currentSliderVelocity; EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity; From 13c956c2482ee8ff81e83f283de9f17910ad189d Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Sun, 26 Jan 2025 20:15:13 +0000 Subject: [PATCH 214/801] Account for floating point errors --- .../Difficulty/Preprocessing/TaikoDifficultyHitObject.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 486841b995..f9ca2707ab 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -118,11 +118,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing noteObjects.Add(this); } + // Using `hitObject.StartTime` causes floating point error differences + double normalizedStartTime = StartTime * clockRate; + // Retrieve the timing point at the note's start time - TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(hitObject.StartTime); + TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(normalizedStartTime); // Calculate the slider velocity at the note's start time. - double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, hitObject.StartTime, clockRate); + double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, normalizedStartTime, clockRate); CurrentSliderVelocity = currentSliderVelocity; EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity; From 836a9e5c2518dab2d130e6148c17568f02bcd819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Jan 2025 09:40:20 +0100 Subject: [PATCH 215/801] Remove explicit beatmap set from list of bundled beatmap sets --- osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs index 3aa34a5580..61aa9ef921 100644 --- a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs +++ b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs @@ -345,7 +345,6 @@ namespace osu.Game.Beatmaps.Drawables "1971951 James Landino - Shiba Paradise.osz", "1972518 Toromaru - Sleight of Hand.osz", "1982302 KINEMA106 - INVITE.osz", - "1983475 KNOWER - The Government Knows.osz", "2010165 Junk - Yellow Smile (bms edit).osz", "2022737 Andora - Euphoria (feat. WaMi).osz", "2025023 tephe - Genjitsu Escape.osz", From e24af4b341d36e13c1b897a43a5d7d2d13fd94c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Jan 2025 09:40:53 +0100 Subject: [PATCH 216/801] Add inline comments for sets that are not marked FA but should be --- osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs index 61aa9ef921..16e143f9dc 100644 --- a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs +++ b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs @@ -292,7 +292,7 @@ namespace osu.Game.Beatmaps.Drawables "1407228 II-L - VANGUARD-1.osz", "1422686 II-L - VANGUARD-2.osz", "1429217 Street - Phi.osz", - "1442235 2ToneDisco x Cosmicosmo - Shoelaces (feat. Puniden).osz", + "1442235 2ToneDisco x Cosmicosmo - Shoelaces (feat. Puniden).osz", // set is not marked as FA, but track is listed in https://osu.ppy.sh/beatmaps/artists/157 "1447478 Cres. - End Time.osz", "1449942 m108 - Crescent Sakura.osz", "1463778 MuryokuP - A tree without a branch.osz", @@ -336,8 +336,8 @@ namespace osu.Game.Beatmaps.Drawables "1854710 Blaster & Extra Terra - Spacecraft (Cut Ver.).osz", "1859322 Hino Isuka - Delightness Brightness.osz", "1884102 Maduk - Go (feat. Lachi) (Cut Ver.).osz", - "1884578 Neko Hacker - People People feat. Nanahira.osz", - "1897902 uma vs. Morimori Atsushi - Re: End of a Dream.osz", + "1884578 Neko Hacker - People People feat. Nanahira.osz", // set is not marked as FA, but track is listed in https://osu.ppy.sh/beatmaps/artists/266 + "1897902 uma vs. Morimori Atsushi - Re: End of a Dream.osz", // set is not marked as FA, but track is listed in https://osu.ppy.sh/beatmaps/artists/108 "1905582 KINEMA106 - Fly Away (Cut Ver.).osz", "1934686 ARForest - Rainbow Magic!!.osz", "1963076 METAROOM - S.N.U.F.F.Y.osz", From 01ae1a58f12d2268c3aa12cda499824cad0e184e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Jan 2025 10:25:22 +0100 Subject: [PATCH 217/801] Catch and display user-friendly errors regarding corrupted audio files Addresses lack of user feedback as indicated by https://github.com/ppy/osu/issues/31693. --- osu.Game/Screens/Edit/Setup/ResourcesSection.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 408292c2d0..2eda232b9f 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Localisation; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Localisation; @@ -97,7 +98,17 @@ namespace osu.Game.Screens.Edit.Setup if (!source.Exists) return false; - var tagSource = TagLib.File.Create(source.FullName); + TagLib.File? tagSource; + + try + { + tagSource = TagLib.File.Create(source.FullName); + } + catch (Exception e) + { + Logger.Error(e, "The selected audio track appears to be corrupted. Please select another one."); + return false; + } changeResource(source, applyToAllDifficulties, @"audio", metadata => metadata.AudioFile, From be9c96c041b4dc9179b00a1ec13e3eaf0f7b414f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Jan 2025 10:25:53 +0100 Subject: [PATCH 218/801] Fix infinite loop when switching audio tracks fails on an existing beatmap Bit ugly, but appears to work in practice... --- .../Screens/Edit/Setup/ResourcesSection.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 2eda232b9f..cab6eddaa4 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -203,16 +203,40 @@ namespace osu.Game.Screens.Edit.Setup editor?.Save(); } + // to avoid scaring users, both background & audio choosers use fake `FileInfo`s with user-friendly filenames + // when displaying an imported beatmap rather than the actual SHA-named file in storage. + // however, that means that when a background or audio file is chosen that is broken or doesn't exist on disk when switching away from the fake files, + // the rollback could enter an infinite loop, because the fake `FileInfo`s *also* don't exist on disk - at least not in the fake location they indicate. + // to circumvent this issue, just allow rollback to proceed always without actually running any of the change logic to ensure visual consistency. + // note that this means that `Change{BackgroundImage,AudioTrack}()` are required to not have made any modifications to the beatmap files + // (or at least cleaned them up properly themselves) if they return `false`. + private bool rollingBackBackgroundChange; + private bool rollingBackAudioChange; + private void backgroundChanged(ValueChangedEvent file) { + if (rollingBackBackgroundChange) + return; + if (file.NewValue == null || !ChangeBackgroundImage(file.NewValue, backgroundChooser.ApplyToAllDifficulties.Value)) + { + rollingBackBackgroundChange = true; backgroundChooser.Current.Value = file.OldValue; + rollingBackBackgroundChange = false; + } } private void audioTrackChanged(ValueChangedEvent file) { + if (rollingBackAudioChange) + return; + if (file.NewValue == null || !ChangeAudioTrack(file.NewValue, audioTrackChooser.ApplyToAllDifficulties.Value)) + { + rollingBackAudioChange = true; audioTrackChooser.Current.Value = file.OldValue; + rollingBackAudioChange = false; + } } } } From ca979d35423265017435e4cd44b3c3e5c3a92630 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 27 Jan 2025 18:32:12 +0900 Subject: [PATCH 219/801] Adjust xmldocs --- osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs index 8142873fd5..499e84ce80 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs @@ -38,13 +38,13 @@ namespace osu.Game.Online.Multiplayer public MatchUserState? MatchState { get; set; } /// - /// Any ruleset applicable only to the local user. + /// If not-null, a local override for this user's ruleset selection. /// [Key(5)] public int? RulesetId; /// - /// Any beatmap applicable only to the local user. + /// If not-null, a local override for this user's beatmap selection. /// [Key(6)] public int? BeatmapId; From fc73037d9f0373f8914e389efc1202900580195f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 27 Jan 2025 18:45:52 +0900 Subject: [PATCH 220/801] Add pill displaying current freestyle status --- .../Lounge/Components/DrawableRoom.cs | 5 ++ .../Lounge/Components/FreeStyleStatusPill.cs | 64 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index c39ca347c7..7bc0b612f1 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -169,6 +169,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft }, + new FreeStyleStatusPill(Room) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft + }, endDateInfo = new EndDateInfo(Room) { Anchor = Anchor.CentreLeft, diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs new file mode 100644 index 0000000000..1f3149d788 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs @@ -0,0 +1,64 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.ComponentModel; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Online.Rooms; +using osuTK.Graphics; + +namespace osu.Game.Screens.OnlinePlay.Lounge.Components +{ + public class FreeStyleStatusPill : OnlinePlayPill + { + private readonly Room room; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + protected override FontUsage Font => base.Font.With(weight: FontWeight.SemiBold); + + public FreeStyleStatusPill(Room room) + { + this.room = room; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Pill.Background.Alpha = 1; + Pill.Background.Colour = colours.Yellow; + + TextFlow.Text = "Freestyle"; + TextFlow.Colour = Color4.Black; + + room.PropertyChanged += onRoomPropertyChanged; + updateFreeStyleStatus(); + } + + private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + switch (e.PropertyName) + { + case nameof(Room.CurrentPlaylistItem): + case nameof(Room.Playlist): + updateFreeStyleStatus(); + break; + } + } + + private void updateFreeStyleStatus() + { + PlaylistItem? currentItem = room.Playlist.GetCurrentItem() ?? room.CurrentPlaylistItem; + Alpha = currentItem?.FreeStyle == true ? 1 : 0; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + room.PropertyChanged -= onRoomPropertyChanged; + } + } +} From bb8f58f6d6db344499f50e64f1463cc8ca84e35c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Jan 2025 12:28:53 +0100 Subject: [PATCH 221/801] Work around rare sharpcompress failure to extract certain archives Closes https://github.com/ppy/osu/issues/31667. See https://github.com/ppy/osu/issues/31667#issuecomment-2615483900 for explanation. For whatever it's worth, I see rejecting this change and telling upstream to fix it as an equally agreeable outcome, but after I spent an hour+ tracking this down, writing this diff was nothing in comparison. --- osu.Game/IO/Archives/ZipArchiveReader.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/osu.Game/IO/Archives/ZipArchiveReader.cs b/osu.Game/IO/Archives/ZipArchiveReader.cs index 6bb2a314e7..8b9ecc7462 100644 --- a/osu.Game/IO/Archives/ZipArchiveReader.cs +++ b/osu.Game/IO/Archives/ZipArchiveReader.cs @@ -9,6 +9,7 @@ using System.IO; using System.Linq; using System.Text; using Microsoft.Toolkit.HighPerformance; +using osu.Framework.Extensions; using osu.Framework.IO.Stores; using SharpCompress.Archives.Zip; using SharpCompress.Common; @@ -54,12 +55,22 @@ namespace osu.Game.IO.Archives if (entry == null) return null; - var owner = MemoryAllocator.Default.Allocate((int)entry.Size); - using (Stream s = entry.OpenEntryStream()) - s.ReadExactly(owner.Memory.Span); + { + if (entry.Size > 0) + { + var owner = MemoryAllocator.Default.Allocate((int)entry.Size); + s.ReadExactly(owner.Memory.Span); + return new MemoryOwnerMemoryStream(owner); + } - return new MemoryOwnerMemoryStream(owner); + // due to a sharpcompress bug (https://github.com/adamhathcock/sharpcompress/issues/88), + // in rare instances the `ZipArchiveEntry` will not contain a correct `Size` but instead report 0. + // this would lead to the block above reading nothing, and the game basically seeing an archive full of empty files. + // since the bug is years old now, and this is a rather rare situation anyways (reported once in years), + // work around this locally by falling back to reading as many bytes as possible and using a standard non-pooled memory stream. + return new MemoryStream(s.ReadAllRemainingBytesToArray()); + } } public override void Dispose() From 71b89c390fe7d672ec8f1f61bbea31352315a4fb Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Mon, 27 Jan 2025 12:54:22 +0000 Subject: [PATCH 222/801] Rename class, rename children to hit objects and groups, make fields un-settable --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 12 ++++---- .../Data/SamePatternsGroupedHitObjects.cs | 22 +++++++------- ...ects.cs => SameRhythmHitObjectGrouping.cs} | 30 +++++++++---------- .../Rhythm/TaikoDifficultyHitObjectRhythm.cs | 2 +- .../TaikoRhythmDifficultyPreprocessor.cs | 10 +++---- 5 files changed, 38 insertions(+), 38 deletions(-) rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/{SameRhythmGroupedHitObjects.cs => SameRhythmHitObjectGrouping.cs} (65%) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs index 8accc6124c..f4686f2fe3 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators return difficulty; } - private static double evaluateDifficultyOf(SameRhythmGroupedHitObjects sameRhythmGroupedHitObjects, double hitWindow) + private static double evaluateDifficultyOf(SameRhythmHitObjectGrouping sameRhythmGroupedHitObjects, double hitWindow) { double intervalDifficulty = ratioDifficulty(sameRhythmGroupedHitObjects.HitObjectIntervalRatio); double? previousInterval = sameRhythmGroupedHitObjects.Previous?.HitObjectInterval; @@ -47,9 +47,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators intervalDifficulty *= repeatedIntervalPenalty(sameRhythmGroupedHitObjects, hitWindow); // If a previous interval exists and there are multiple hit objects in the sequence: - if (previousInterval != null && sameRhythmGroupedHitObjects.Children.Count > 1) + if (previousInterval != null && sameRhythmGroupedHitObjects.HitObjects.Count > 1) { - double expectedDurationFromPrevious = (double)previousInterval * sameRhythmGroupedHitObjects.Children.Count; + double expectedDurationFromPrevious = (double)previousInterval * sameRhythmGroupedHitObjects.HitObjects.Count; double durationDifference = sameRhythmGroupedHitObjects.Duration - expectedDurationFromPrevious; if (durationDifference > 0) @@ -75,11 +75,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// /// Determines if the changes in hit object intervals is consistent based on a given threshold. /// - private static double repeatedIntervalPenalty(SameRhythmGroupedHitObjects sameRhythmGroupedHitObjects, double hitWindow, double threshold = 0.1) + private static double repeatedIntervalPenalty(SameRhythmHitObjectGrouping sameRhythmGroupedHitObjects, double hitWindow, double threshold = 0.1) { double longIntervalPenalty = sameInterval(sameRhythmGroupedHitObjects, 3); - double shortIntervalPenalty = sameRhythmGroupedHitObjects.Children.Count < 6 + double shortIntervalPenalty = sameRhythmGroupedHitObjects.HitObjects.Count < 6 ? sameInterval(sameRhythmGroupedHitObjects, 4) : 1.0; // Returns a non-penalty if there are 6 or more notes within an interval. @@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators return Math.Min(longIntervalPenalty, shortIntervalPenalty) * durationPenalty; - double sameInterval(SameRhythmGroupedHitObjects startObject, int intervalCount) + double sameInterval(SameRhythmHitObjectGrouping startObject, int intervalCount) { List intervals = new List(); var currentObject = startObject; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs index cb22b2ef82..938cb4670f 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs @@ -7,34 +7,34 @@ using System.Linq; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data { /// - /// Represents grouped by their 's interval. + /// Represents grouped by their 's interval. /// public class SamePatternsGroupedHitObjects { - public IReadOnlyList Children { get; } + public IReadOnlyList Groups { get; } public SamePatternsGroupedHitObjects? Previous { get; } /// - /// The between children within this group. - /// If there is only one child, this will have the value of the first child's . + /// The between groups . + /// If there is only one group, this will have the value of the first group's . /// - public double ChildrenInterval => Children.Count > 1 ? Children[1].Interval : Children[0].Interval; + public double GroupInterval => Groups.Count > 1 ? Groups[1].Interval : Groups[0].Interval; /// - /// The ratio of between this and the previous . In the + /// The ratio of between this and the previous . In the /// case where there is no previous , this will have a value of 1. /// - public double IntervalRatio => ChildrenInterval / Previous?.ChildrenInterval ?? 1.0d; + public double IntervalRatio => GroupInterval / Previous?.GroupInterval ?? 1.0d; - public TaikoDifficultyHitObject FirstHitObject => Children[0].FirstHitObject; + public TaikoDifficultyHitObject FirstHitObject => Groups[0].FirstHitObject; - public IEnumerable AllHitObjects => Children.SelectMany(child => child.Children); + public IEnumerable AllHitObjects => Groups.SelectMany(hitObject => hitObject.HitObjects); - public SamePatternsGroupedHitObjects(SamePatternsGroupedHitObjects? previous, List children) + public SamePatternsGroupedHitObjects(SamePatternsGroupedHitObjects? previous, List groups) { Previous = previous; - Children = children; + Groups = groups; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjectGrouping.cs similarity index 65% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjectGrouping.cs index b77176b49d..9caa9b9958 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjectGrouping.cs @@ -10,46 +10,46 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data /// /// Represents a group of s with no rhythm variation. /// - public class SameRhythmGroupedHitObjects : IHasInterval + public class SameRhythmHitObjectGrouping : IHasInterval { - public List Children { get; private set; } + public readonly List HitObjects; - public TaikoDifficultyHitObject FirstHitObject => Children[0]; + public TaikoDifficultyHitObject FirstHitObject => HitObjects[0]; - public SameRhythmGroupedHitObjects? Previous; + public readonly SameRhythmHitObjectGrouping? Previous; /// /// of the first hit object. /// - public double StartTime => Children[0].StartTime; + public double StartTime => HitObjects[0].StartTime; /// /// The interval between the first and final hit object within this group. /// - public double Duration => Children[^1].StartTime - Children[0].StartTime; + public double Duration => HitObjects[^1].StartTime - HitObjects[0].StartTime; /// - /// The interval in ms of each hit object in this . This is only defined if there is - /// more than two hit objects in this . + /// The interval in ms of each hit object in this . This is only defined if there is + /// more than two hit objects in this . /// - public double? HitObjectInterval; + public readonly double? HitObjectInterval; /// - /// The ratio of between this and the previous . In the + /// The ratio of between this and the previous . In the /// case where one or both of the is undefined, this will have a value of 1. /// - public double HitObjectIntervalRatio; + public readonly double HitObjectIntervalRatio; /// - public double Interval { get; private set; } + public double Interval { get; } - public SameRhythmGroupedHitObjects(SameRhythmGroupedHitObjects? previous, List children) + public SameRhythmHitObjectGrouping(SameRhythmHitObjectGrouping? previous, List hitObjects) { Previous = previous; - Children = children; + HitObjects = hitObjects; // Calculate the average interval between hitobjects, or null if there are fewer than two - HitObjectInterval = Children.Count < 2 ? null : Duration / (Children.Count - 1); + HitObjectInterval = HitObjects.Count < 2 ? null : Duration / (HitObjects.Count - 1); // Calculate the ratio between this group's interval and the previous group's interval HitObjectIntervalRatio = Previous?.HitObjectInterval != null && HitObjectInterval != null diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs index 351015ae08..3503a836fa 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm /// /// The group of hit objects with consistent rhythm that this object belongs to. /// - public SameRhythmGroupedHitObjects? SameRhythmGroupedHitObjects; + public SameRhythmHitObjectGrouping? SameRhythmGroupedHitObjects; /// /// The larger pattern of rhythm groups that this object is part of. diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs index cd56d835dc..3ebc0c25b7 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm foreach (var rhythmGroup in rhythmGroups) { - foreach (var hitObject in rhythmGroup.Children) + foreach (var hitObject in rhythmGroup.HitObjects) { hitObject.Rhythm.SameRhythmGroupedHitObjects = rhythmGroup; hitObject.HitObjectInterval = rhythmGroup.HitObjectInterval; @@ -33,21 +33,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm } } - private static List createSameRhythmGroupedHitObjects(List hitObjects) + private static List createSameRhythmGroupedHitObjects(List hitObjects) { - var rhythmGroups = new List(); + var rhythmGroups = new List(); var groups = IntervalGroupingUtils.GroupByInterval(hitObjects); foreach (var group in groups) { var previous = rhythmGroups.Count > 0 ? rhythmGroups[^1] : null; - rhythmGroups.Add(new SameRhythmGroupedHitObjects(previous, group)); + rhythmGroups.Add(new SameRhythmHitObjectGrouping(previous, group)); } return rhythmGroups; } - private static List createSamePatternGroupedHitObjects(List rhythmGroups) + private static List createSamePatternGroupedHitObjects(List rhythmGroups) { var patternGroups = new List(); var groups = IntervalGroupingUtils.GroupByInterval(rhythmGroups); From f3c17f1c2b73e4f12fd00b130bd8326ca17a74e6 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Mon, 27 Jan 2025 12:56:33 +0000 Subject: [PATCH 223/801] Use correct English --- .../Difficulty/Preprocessing/TaikoDifficultyHitObject.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index f9ca2707ab..d6a2d5874e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -119,13 +119,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing } // Using `hitObject.StartTime` causes floating point error differences - double normalizedStartTime = StartTime * clockRate; + double normalisedStartTime = StartTime * clockRate; // Retrieve the timing point at the note's start time - TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(normalizedStartTime); + TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(normalisedStartTime); // Calculate the slider velocity at the note's start time. - double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, normalizedStartTime, clockRate); + double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, normalisedStartTime, clockRate); CurrentSliderVelocity = currentSliderVelocity; EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity; From 1aa1137b09cc649b1e99d2f0eb18b846feb249ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Jan 2025 21:22:51 +0900 Subject: [PATCH 224/801] Remove "Accuracy" and "Stack Leniency" from osu!catch editor setup --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 3 +- .../Edit/Setup/CatchDifficultySection.cs | 125 ++++++++++++++++++ 2 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Catch/Edit/Setup/CatchDifficultySection.cs diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 5bd7a0ff00..d253b9893f 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -14,6 +14,7 @@ using osu.Game.Graphics; using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Difficulty; using osu.Game.Rulesets.Catch.Edit; +using osu.Game.Rulesets.Catch.Edit.Setup; using osu.Game.Rulesets.Catch.Mods; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Replays; @@ -228,7 +229,7 @@ namespace osu.Game.Rulesets.Catch public override IEnumerable CreateEditorSetupSections() => [ new MetadataSection(), - new DifficultySection(), + new CatchDifficultySection(), new FillFlowContainer { AutoSizeAxes = Axes.Y, diff --git a/osu.Game.Rulesets.Catch/Edit/Setup/CatchDifficultySection.cs b/osu.Game.Rulesets.Catch/Edit/Setup/CatchDifficultySection.cs new file mode 100644 index 0000000000..6ae60c4d24 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Edit/Setup/CatchDifficultySection.cs @@ -0,0 +1,125 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Screens.Edit.Setup; + +namespace osu.Game.Rulesets.Catch.Edit.Setup +{ + public partial class CatchDifficultySection : SetupSection + { + private FormSliderBar circleSizeSlider { get; set; } = null!; + private FormSliderBar healthDrainSlider { get; set; } = null!; + private FormSliderBar approachRateSlider { get; set; } = null!; + private FormSliderBar baseVelocitySlider { get; set; } = null!; + private FormSliderBar tickRateSlider { get; set; } = null!; + + public override LocalisableString Title => EditorSetupStrings.DifficultyHeader; + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + circleSizeSlider = new FormSliderBar + { + Caption = BeatmapsetsStrings.ShowStatsCs, + HintText = EditorSetupStrings.CircleSizeDescription, + Current = new BindableFloat(Beatmap.Difficulty.CircleSize) + { + Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, + MinValue = 0, + MaxValue = 10, + Precision = 0.1f, + }, + TransferValueOnCommit = true, + TabbableContentContainer = this, + }, + healthDrainSlider = new FormSliderBar + { + Caption = BeatmapsetsStrings.ShowStatsDrain, + HintText = EditorSetupStrings.DrainRateDescription, + Current = new BindableFloat(Beatmap.Difficulty.DrainRate) + { + Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, + MinValue = 0, + MaxValue = 10, + Precision = 0.1f, + }, + TransferValueOnCommit = true, + TabbableContentContainer = this, + }, + approachRateSlider = new FormSliderBar + { + Caption = BeatmapsetsStrings.ShowStatsAr, + HintText = EditorSetupStrings.ApproachRateDescription, + Current = new BindableFloat(Beatmap.Difficulty.ApproachRate) + { + Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, + MinValue = 0, + MaxValue = 10, + Precision = 0.1f, + }, + TransferValueOnCommit = true, + TabbableContentContainer = this, + }, + baseVelocitySlider = new FormSliderBar + { + Caption = EditorSetupStrings.BaseVelocity, + HintText = EditorSetupStrings.BaseVelocityDescription, + Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier) + { + Default = 1.4, + MinValue = 0.4, + MaxValue = 3.6, + Precision = 0.01f, + }, + TransferValueOnCommit = true, + TabbableContentContainer = this, + }, + tickRateSlider = new FormSliderBar + { + Caption = EditorSetupStrings.TickRate, + HintText = EditorSetupStrings.TickRateDescription, + Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate) + { + Default = 1, + MinValue = 1, + MaxValue = 4, + Precision = 1, + }, + TransferValueOnCommit = true, + TabbableContentContainer = this, + }, + }; + + foreach (var item in Children.OfType>()) + item.Current.ValueChanged += _ => updateValues(); + + foreach (var item in Children.OfType>()) + item.Current.ValueChanged += _ => updateValues(); + } + + private void updateValues() + { + // for now, update these on commit rather than making BeatmapMetadata bindables. + // after switching database engines we can reconsider if switching to bindables is a good direction. + Beatmap.Difficulty.CircleSize = circleSizeSlider.Current.Value; + Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value; + Beatmap.Difficulty.ApproachRate = approachRateSlider.Current.Value; + Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value; + Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value; + + Beatmap.UpdateAllHitObjects(); + Beatmap.SaveState(); + } + } +} From 017d38af3d0f8af13155cf049e27c5371fd6f3bf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Jan 2025 21:29:17 +0900 Subject: [PATCH 225/801] Change friend online notifications' icon and colours The previous choices made it seem like potentially destructive actions were being performed. I've gone with neutral colours and more suiting icons to attempt to avoid this. --- Addresses concerns in https://github.com/ppy/osu/discussions/31621#discussioncomment-11948377. I chose this design even though it wasn't the #1 most popular because I personally feel that using green/red doesn't work great for these. --- osu.Game/Online/FriendPresenceNotifier.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/FriendPresenceNotifier.cs b/osu.Game/Online/FriendPresenceNotifier.cs index 75b487384a..bc2bf344b0 100644 --- a/osu.Game/Online/FriendPresenceNotifier.cs +++ b/osu.Game/Online/FriendPresenceNotifier.cs @@ -171,9 +171,9 @@ namespace osu.Game.Online { Transient = true, IsImportant = false, - Icon = FontAwesome.Solid.UserPlus, + Icon = FontAwesome.Solid.User, Text = $"Online: {string.Join(@", ", onlineAlertQueue.Select(u => u.Username))}", - IconColour = colours.Green, + IconColour = colours.GrayD, Activated = () => { if (singleUser != null) @@ -208,9 +208,9 @@ namespace osu.Game.Online { Transient = true, IsImportant = false, - Icon = FontAwesome.Solid.UserMinus, + Icon = FontAwesome.Solid.UserSlash, Text = $"Offline: {string.Join(@", ", offlineAlertQueue.Select(u => u.Username))}", - IconColour = colours.Red + IconColour = colours.Gray3 }); offlineAlertQueue.Clear(); From a3a08832b41fd9a46c50142eb0f05b0720a20f78 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Jan 2025 21:31:51 +0900 Subject: [PATCH 226/801] Add keywords to make lighten-during-breaks setting discoverable to stable users See https://github.com/ppy/osu/discussions/31671. --- .../Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs index 048351b4cb..830ccec279 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs @@ -35,7 +35,8 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay new SettingsCheckbox { LabelText = GameplaySettingsStrings.LightenDuringBreaks, - Current = config.GetBindable(OsuSetting.LightenDuringBreaks) + Current = config.GetBindable(OsuSetting.LightenDuringBreaks), + Keywords = new[] { "dim", "level" } }, new SettingsCheckbox { From 6c4b4166ac21324abf6b467c87a166c032cb5933 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 17:09:42 +0900 Subject: [PATCH 227/801] Add fail cases to unstable rate incremental testing --- osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs index 03dc91b5d4..18ac5b4964 100644 --- a/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs +++ b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs @@ -36,6 +36,10 @@ namespace osu.Game.Tests.NonVisual.Ranking .Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null)) .ToList(); + // Add some red herrings + events.Insert(4, new HitEvent(200, 1.0, HitResult.Meh, new HitObject { HitWindows = HitWindows.Empty }, null, null)); + events.Insert(8, new HitEvent(-100, 1.0, HitResult.Miss, new HitObject(), null, null)); + HitEventExtensions.UnstableRateCalculationResult result = null; for (int i = 0; i < events.Count; i++) @@ -57,6 +61,10 @@ namespace osu.Game.Tests.NonVisual.Ranking .Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null)) .ToList(); + // Add some red herrings + events.Insert(4, new HitEvent(200, 1.0, HitResult.Meh, new HitObject { HitWindows = HitWindows.Empty }, null, null)); + events.Insert(8, new HitEvent(-100, 1.0, HitResult.Miss, new HitObject(), null, null)); + HitEventExtensions.UnstableRateCalculationResult result = null; for (int i = 0; i < events.Count; i++) From d8ec3b77e4b29ff95f90873bf0ae83fb4041c460 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 17:06:13 +0900 Subject: [PATCH 228/801] Fix incremental unstable rate calculation not matching expectations The `EventCount` variable wasn't factoring in that some results do not affect unstable rate. It would therefore become more incorrect as the play continued. Closes https://github.com/ppy/osu/issues/31712. --- osu.Game/Rulesets/Scoring/HitEventExtensions.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index 269342460f..fed0c3b51b 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -28,11 +28,12 @@ namespace osu.Game.Rulesets.Scoring result ??= new UnstableRateCalculationResult(); // Handle rewinding in the simplest way possible. - if (hitEvents.Count < result.EventCount + 1) + if (hitEvents.Count < result.LastProcessedIndex + 1) result = new UnstableRateCalculationResult(); - for (int i = result.EventCount; i < hitEvents.Count; i++) + for (int i = result.LastProcessedIndex + 1; i < hitEvents.Count; i++) { + result.LastProcessedIndex = i; HitEvent e = hitEvents[i]; if (!AffectsUnstableRate(e)) @@ -84,6 +85,11 @@ namespace osu.Game.Rulesets.Scoring /// public class UnstableRateCalculationResult { + /// + /// The last result index processed. For internal incremental calculation use. + /// + public int LastProcessedIndex = -1; + /// /// Total events processed. For internal incremental calculation use. /// From fd1d90cbd93ffc0cc9be5c3d18035e78613e0d06 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 11:55:35 +0900 Subject: [PATCH 229/801] Update framework Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 7ae16b8b70..d2682fc024 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 7b0a027d39..309a9dcc87 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 4c83ef83eeb9b372fdbc31a624a6688f0428dca2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 17:34:03 +0900 Subject: [PATCH 230/801] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index bfb6e51f93..bc4c42484d 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From bf40f071eb0d17fa54957ac9c3436afe12749506 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 17:40:52 +0900 Subject: [PATCH 231/801] Code quality pass --- osu.Game/Online/FriendPresenceNotifier.cs | 89 +++++++++++-------- .../FriendOfflineNotification.cs | 10 --- .../Notifications/FriendOnlineNotification.cs | 10 --- 3 files changed, 52 insertions(+), 57 deletions(-) delete mode 100644 osu.Game/Overlays/Notifications/FriendOfflineNotification.cs delete mode 100644 osu.Game/Overlays/Notifications/FriendOnlineNotification.cs diff --git a/osu.Game/Online/FriendPresenceNotifier.cs b/osu.Game/Online/FriendPresenceNotifier.cs index 229ad4f734..70d532dfeb 100644 --- a/osu.Game/Online/FriendPresenceNotifier.cs +++ b/osu.Game/Online/FriendPresenceNotifier.cs @@ -31,15 +31,6 @@ namespace osu.Game.Online [Resolved] private MetadataClient metadataClient { get; set; } = null!; - [Resolved] - private ChannelManager channelManager { get; set; } = null!; - - [Resolved] - private ChatOverlay chatOverlay { get; set; } = null!; - - [Resolved] - private OsuColour colours { get; set; } = null!; - [Resolved] private OsuConfigManager config { get; set; } = null!; @@ -165,26 +156,7 @@ namespace osu.Game.Online return; } - APIUser? singleUser = onlineAlertQueue.Count == 1 ? onlineAlertQueue.Single() : null; - - notifications.Post(new FriendOnlineNotification - { - Transient = true, - IsImportant = false, - Icon = FontAwesome.Solid.UserPlus, - Text = $"Online: {string.Join(@", ", onlineAlertQueue.Select(u => u.Username))}", - IconColour = colours.Green, - Activated = () => - { - if (singleUser != null) - { - channelManager.OpenPrivateChannel(singleUser); - chatOverlay.Show(); - } - - return true; - } - }); + notifications.Post(new FriendOnlineNotification(onlineAlertQueue)); onlineAlertQueue.Clear(); lastOnlineAlertTime = null; @@ -204,17 +176,60 @@ namespace osu.Game.Online return; } - notifications.Post(new FriendOfflineNotification - { - Transient = true, - IsImportant = false, - Icon = FontAwesome.Solid.UserMinus, - Text = $"Offline: {string.Join(@", ", offlineAlertQueue.Select(u => u.Username))}", - IconColour = colours.Red - }); + notifications.Post(new FriendOfflineNotification(offlineAlertQueue)); offlineAlertQueue.Clear(); lastOfflineAlertTime = null; } + + public partial class FriendOnlineNotification : SimpleNotification + { + private readonly ICollection users; + + public FriendOnlineNotification(ICollection users) + { + this.users = users; + Transient = true; + IsImportant = false; + Icon = FontAwesome.Solid.User; + Text = $"Online: {string.Join(@", ", users.Select(u => u.Username))}"; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours, ChannelManager channelManager, ChatOverlay chatOverlay) + { + IconColour = colours.GrayD; + Activated = () => + { + APIUser? singleUser = users.Count == 1 ? users.Single() : null; + + if (singleUser != null) + { + channelManager.OpenPrivateChannel(singleUser); + chatOverlay.Show(); + } + + return true; + }; + } + + public override string PopInSampleName => "UI/notification-friend-online"; + } + + private partial class FriendOfflineNotification : SimpleNotification + { + public FriendOfflineNotification(ICollection users) + { + Transient = true; + IsImportant = false; + Icon = FontAwesome.Solid.UserSlash; + Text = $"Offline: {string.Join(@", ", users.Select(u => u.Username))}"; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) => IconColour = colours.Gray3; + + public override string PopInSampleName => "UI/notification-friend-offline"; + } } } diff --git a/osu.Game/Overlays/Notifications/FriendOfflineNotification.cs b/osu.Game/Overlays/Notifications/FriendOfflineNotification.cs deleted file mode 100644 index 147fd4ba6f..0000000000 --- a/osu.Game/Overlays/Notifications/FriendOfflineNotification.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Overlays.Notifications -{ - public partial class FriendOfflineNotification : SimpleNotification - { - public override string PopInSampleName => "UI/notification-friend-offline"; - } -} diff --git a/osu.Game/Overlays/Notifications/FriendOnlineNotification.cs b/osu.Game/Overlays/Notifications/FriendOnlineNotification.cs deleted file mode 100644 index 6a5cf3b517..0000000000 --- a/osu.Game/Overlays/Notifications/FriendOnlineNotification.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Overlays.Notifications -{ - public partial class FriendOnlineNotification : SimpleNotification - { - public override string PopInSampleName => "UI/notification-friend-online"; - } -} From e8d20fb4020083d85a31a16492ae3c92d2b6382d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 18:16:04 +0900 Subject: [PATCH 232/801] Fix skin `SourceChanged` event never being unbound --- .../UI/Components/HitPositionPaddedContainer.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs b/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs index ae91be1c67..72daf4b21d 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Skinning; @@ -22,15 +23,12 @@ namespace osu.Game.Rulesets.Mania.UI.Components private void load(IScrollingInfo scrollingInfo) { Direction.BindTo(scrollingInfo.Direction); - Direction.BindValueChanged(onDirectionChanged); + Direction.BindValueChanged(_ => UpdateHitPosition(), true); skin.SourceChanged += onSkinChanged; - - UpdateHitPosition(); } private void onSkinChanged() => UpdateHitPosition(); - private void onDirectionChanged(ValueChangedEvent direction) => UpdateHitPosition(); protected virtual void UpdateHitPosition() { @@ -42,5 +40,13 @@ namespace osu.Game.Rulesets.Mania.UI.Components ? new MarginPadding { Top = hitPosition } : new MarginPadding { Bottom = hitPosition }; } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (skin.IsNotNull()) + skin.SourceChanged -= onSkinChanged; + } } } From d3f9804ef1de2ee9e9f75df9321183bb9439da8c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 18:45:02 +0900 Subject: [PATCH 233/801] Combine more methods to simplify flow --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 33 ++++--------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 9915560a95..3e0d94e992 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -277,7 +277,7 @@ namespace osu.Game.Screens.OnlinePlay.Match UserRuleset.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); - beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateBeatmap()); + beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateSpecifics()); userModsSelectOverlayRegistration = overlayManager?.RegisterBlockingOverlay(UserModsSelectOverlay); @@ -346,7 +346,7 @@ namespace osu.Game.Screens.OnlinePlay.Match public override void OnSuspending(ScreenTransitionEvent e) { // Should be a noop in most cases, but let's ensure beyond doubt that the beatmap is in a correct state. - updateBeatmap(); + updateSpecifics(); onLeaving(); base.OnSuspending(e); @@ -356,7 +356,6 @@ namespace osu.Game.Screens.OnlinePlay.Match { base.OnResuming(e); - updateBeatmap(); updateSpecifics(); beginHandlingTrack(); @@ -446,8 +445,6 @@ namespace osu.Game.Screens.OnlinePlay.Match if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) return; - updateUserMods(); - updateBeatmap(); updateSpecifics(); if (!item.AllowedMods.Any()) @@ -471,42 +468,26 @@ namespace osu.Game.Screens.OnlinePlay.Match UserStyleSection?.Hide(); } - private void updateUserMods() + private void updateSpecifics() { if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) return; + var rulesetInstance = GetGameplayRuleset().CreateInstance(); + // Remove any user mods that are no longer allowed. - Ruleset rulesetInstance = GetGameplayRuleset().CreateInstance(); Mod[] allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); Mod[] newUserMods = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); - - if (newUserMods.SequenceEqual(UserMods.Value)) - return; - - UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList(); - } - - private void updateBeatmap() - { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) - return; + if (!newUserMods.SequenceEqual(UserMods.Value)) + UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList(); // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info int beatmapId = GetGameplayBeatmap().OnlineID; var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == beatmapId); Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); UserModsSelectOverlay.Beatmap.Value = Beatmap.Value; - } - private void updateSpecifics() - { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) - return; - - var rulesetInstance = GetGameplayRuleset().CreateInstance(); Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray(); - Ruleset.Value = GetGameplayRuleset(); if (UserStyleDisplayContainer != null) From 05200e897057c06dc7a4e9ad0cedfbccaf6c9738 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 19:05:28 +0900 Subject: [PATCH 234/801] Add missing `partial` --- .../Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs index 1f3149d788..1c0135fb89 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs @@ -10,7 +10,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Lounge.Components { - public class FreeStyleStatusPill : OnlinePlayPill + public partial class FreeStyleStatusPill : OnlinePlayPill { private readonly Room room; From c70ff1108527a58903067eaf39cfa5a7d778b486 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 19:06:14 +0900 Subject: [PATCH 235/801] Remove new bindables from `RoomSubScreen` --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 23 +++---------------- .../Multiplayer/MultiplayerMatchSubScreen.cs | 11 +-------- .../Playlists/PlaylistsRoomSubScreen.cs | 16 +++++++++---- 3 files changed, 16 insertions(+), 34 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 3e0d94e992..d9e22efec5 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -69,18 +69,6 @@ namespace osu.Game.Screens.OnlinePlay.Match /// protected readonly Bindable> UserMods = new Bindable>(Array.Empty()); - /// - /// 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 UserBeatmap = 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 UserRuleset = new Bindable(); - [Resolved(CanBeNull = true)] private IOverlayManager? overlayManager { get; set; } @@ -273,8 +261,6 @@ namespace osu.Game.Screens.OnlinePlay.Match SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); UserMods.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); - UserBeatmap.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); - UserRuleset.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateSpecifics()); @@ -507,14 +493,11 @@ namespace osu.Game.Screens.OnlinePlay.Match } } - protected virtual APIMod[] GetGameplayMods() - => UserMods.Value.Select(m => new APIMod(m)).Concat(SelectedItem.Value!.RequiredMods).ToArray(); + protected virtual APIMod[] GetGameplayMods() => UserMods.Value.Select(m => new APIMod(m)).Concat(SelectedItem.Value!.RequiredMods).ToArray(); - protected virtual RulesetInfo GetGameplayRuleset() - => Rulesets.GetRuleset(UserRuleset.Value?.OnlineID ?? SelectedItem.Value!.RulesetID)!; + protected virtual RulesetInfo GetGameplayRuleset() => Rulesets.GetRuleset(SelectedItem.Value!.RulesetID)!; - protected virtual IBeatmapInfo GetGameplayBeatmap() - => UserBeatmap.Value ?? SelectedItem.Value!.Beatmap; + protected virtual IBeatmapInfo GetGameplayBeatmap() => SelectedItem.Value!.Beatmap; protected abstract void OpenStyleSelection(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index b5fe8bf631..7f946a6997 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -388,7 +388,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return; } - updateCurrentItem(); + SelectedItem.Value = Room.Playlist.SingleOrDefault(i => i.ID == client.Room.Settings.PlaylistItemId); addItemButton.Alpha = localUserCanAddItem ? 1 : 0; @@ -400,15 +400,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private bool localUserCanAddItem => client.IsHost || Room.QueueMode != QueueMode.HostOnly; - private void updateCurrentItem() - { - Debug.Assert(client.Room != null); - - SelectedItem.Value = Room.Playlist.SingleOrDefault(i => i.ID == client.Room.Settings.PlaylistItemId); - UserBeatmap.Value = client.LocalUser?.BeatmapId == null ? null : UserBeatmap.Value; - UserRuleset.Value = client.LocalUser?.RulesetId == null ? null : UserRuleset.Value; - } - private void handleRoomLost() => Schedule(() => { Logger.Log($"{this} exiting due to loss of room or connection"); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index d1b90b18e7..2c74767f42 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -11,11 +11,13 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Framework.Screens; +using osu.Game.Beatmaps; using osu.Game.Graphics.Cursor; using osu.Game.Input; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; +using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match.Components; @@ -46,6 +48,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private FillFlowContainer progressSection = null!; private DrawableRoomPlaylist drawablePlaylist = null!; + private readonly Bindable userBeatmap = new Bindable(); + private readonly Bindable userRuleset = new Bindable(); + public PlaylistsRoomSubScreen(Room room) : base(room, false) // Editing is temporarily not allowed. { @@ -78,10 +83,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private void onSelectedItemChanged(ValueChangedEvent item) { // Simplest for now. - UserBeatmap.Value = null; - UserRuleset.Value = null; + userBeatmap.Value = null; + userRuleset.Value = null; } + protected override IBeatmapInfo GetGameplayBeatmap() => userBeatmap.Value ?? base.GetGameplayBeatmap(); + protected override RulesetInfo GetGameplayRuleset() => userRuleset.Value ?? base.GetGameplayRuleset(); + private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e) { switch (e.PropertyName) @@ -313,8 +321,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists this.Push(new PlaylistsRoomStyleSelect(Room, item) { - Beatmap = { BindTarget = UserBeatmap }, - Ruleset = { BindTarget = UserRuleset } + Beatmap = { BindTarget = userBeatmap }, + Ruleset = { BindTarget = userRuleset } }); } From facc9a4dc3d2e0d8b3741cddd0536e7775817d86 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 19:15:28 +0900 Subject: [PATCH 236/801] Fix reference hashsets getting emptied before used --- osu.Game/Online/FriendPresenceNotifier.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/FriendPresenceNotifier.cs b/osu.Game/Online/FriendPresenceNotifier.cs index 70d532dfeb..a73c705d76 100644 --- a/osu.Game/Online/FriendPresenceNotifier.cs +++ b/osu.Game/Online/FriendPresenceNotifier.cs @@ -156,7 +156,7 @@ namespace osu.Game.Online return; } - notifications.Post(new FriendOnlineNotification(onlineAlertQueue)); + notifications.Post(new FriendOnlineNotification(onlineAlertQueue.ToArray())); onlineAlertQueue.Clear(); lastOnlineAlertTime = null; @@ -176,7 +176,7 @@ namespace osu.Game.Online return; } - notifications.Post(new FriendOfflineNotification(offlineAlertQueue)); + notifications.Post(new FriendOfflineNotification(offlineAlertQueue.ToArray())); offlineAlertQueue.Clear(); lastOfflineAlertTime = null; From 07bff222008fb729e9a17824dd0e17a206df1c88 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 19:30:55 +0900 Subject: [PATCH 237/801] Fix delay before difficulty panel displays fully --- .../Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs | 2 +- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 6 ++++-- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 13a282dd52..249cad8ca3 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -161,7 +161,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { new Drawable[] { - new DrawableRoomPlaylistItem(playlistItem) + new DrawableRoomPlaylistItem(playlistItem, true) { RelativeSizeAxes = Axes.X, AllowReordering = false, diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 7a773bb116..1e1e79d256 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -74,7 +74,7 @@ namespace osu.Game.Screens.OnlinePlay public bool IsSelectedItem => SelectedItem.Value?.ID == Item.ID; - private readonly DelayedLoadWrapper onScreenLoader = new DelayedLoadWrapper(Empty) { RelativeSizeAxes = Axes.Both }; + private readonly DelayedLoadWrapper onScreenLoader; private readonly IBindable valid = new Bindable(); private IBeatmapInfo? beatmap; @@ -120,9 +120,11 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(CanBeNull = true)] private ManageCollectionsDialog? manageCollectionsDialog { get; set; } - public DrawableRoomPlaylistItem(PlaylistItem item) + public DrawableRoomPlaylistItem(PlaylistItem item, bool loadImmediately = false) : base(item) { + onScreenLoader = new DelayedLoadWrapper(Empty, timeBeforeLoad: loadImmediately ? 0 : 500) { RelativeSizeAxes = Axes.Both }; + Item = item; valid.BindTo(item.Valid); diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index d9e22efec5..8f286c0f16 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -484,7 +484,7 @@ namespace osu.Game.Screens.OnlinePlay.Match if (gameplayItem.Equals(currentItem)) return; - UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem) + UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true) { AllowReordering = false, AllowEditing = true, From a6814d1a8a5c86fae6eb0a587c13c2196b523434 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 19:48:04 +0900 Subject: [PATCH 238/801] Make multiplayer change room settings more obvious as to what it does "Edit" felt really weird. --- osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs index 0c993f4abf..0eb8cc3706 100644 --- a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs @@ -49,8 +49,10 @@ namespace osu.Game.Screens.OnlinePlay.Match ButtonsContainer.Add(editButton = new PurpleRoundedButton { RelativeSizeAxes = Axes.Y, - Size = new Vector2(100, 1), - Text = CommonStrings.ButtonsEdit, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(120, 0.7f), + Text = "Change settings", Action = () => OnEdit?.Invoke() }); } From e8d0d2a1d9ebaa21bd408a8976902b40827e6cc4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 19:56:36 +0900 Subject: [PATCH 239/801] Combine more methods to simplify flow futher --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 81 +++++++++---------- .../Multiplayer/MultiplayerMatchSubScreen.cs | 3 - 2 files changed, 36 insertions(+), 48 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 8f286c0f16..428f0e9ed8 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -259,8 +259,8 @@ namespace osu.Game.Screens.OnlinePlay.Match { base.LoadComplete(); - SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); - UserMods.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); + SelectedItem.BindValueChanged(_ => updateSpecifics()); + UserMods.BindValueChanged(_ => updateSpecifics()); beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateSpecifics()); @@ -426,35 +426,7 @@ namespace osu.Game.Screens.OnlinePlay.Match /// The screen to enter. protected abstract Screen CreateGameplayScreen(PlaylistItem selectedItem); - protected void OnSelectedItemChanged() - { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) - return; - - updateSpecifics(); - - if (!item.AllowedMods.Any()) - { - UserModsSection?.Hide(); - UserModsSelectOverlay.Hide(); - UserModsSelectOverlay.IsValidMod = _ => false; - } - else - { - UserModsSection?.Show(); - - var rulesetInstance = GetGameplayRuleset().CreateInstance(); - var allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance)); - UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); - } - - if (item.FreeStyle) - UserStyleSection?.Show(); - else - UserStyleSection?.Hide(); - } - - private void updateSpecifics() + private void updateSpecifics() => Scheduler.AddOnce(() => { if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) return; @@ -476,22 +448,41 @@ namespace osu.Game.Screens.OnlinePlay.Match Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray(); Ruleset.Value = GetGameplayRuleset(); - if (UserStyleDisplayContainer != null) + if (!item.AllowedMods.Any()) { - PlaylistItem gameplayItem = SelectedItem.Value.With(ruleset: GetGameplayRuleset().OnlineID, beatmap: new Optional(GetGameplayBeatmap())); - PlaylistItem? currentItem = UserStyleDisplayContainer.SingleOrDefault()?.Item; - - if (gameplayItem.Equals(currentItem)) - return; - - UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true) - { - AllowReordering = false, - AllowEditing = true, - RequestEdit = _ => OpenStyleSelection() - }; + UserModsSection?.Hide(); + UserModsSelectOverlay.Hide(); + UserModsSelectOverlay.IsValidMod = _ => false; } - } + else + { + UserModsSection?.Show(); + UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); + } + + if (item.FreeStyle) + { + UserStyleSection?.Show(); + + if (UserStyleDisplayContainer != null) + { + PlaylistItem gameplayItem = SelectedItem.Value.With(ruleset: GetGameplayRuleset().OnlineID, beatmap: new Optional(GetGameplayBeatmap())); + PlaylistItem? currentItem = UserStyleDisplayContainer.SingleOrDefault()?.Item; + + if (gameplayItem.Equals(currentItem)) + return; + + UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true) + { + AllowReordering = false, + AllowEditing = item.FreeStyle, + RequestEdit = _ => OpenStyleSelection() + }; + } + } + else + UserStyleSection?.Hide(); + }); protected virtual APIMod[] GetGameplayMods() => UserMods.Value.Select(m => new APIMod(m)).Concat(SelectedItem.Value!.RequiredMods).ToArray(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 7f946a6997..f882fb7f89 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -392,9 +392,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer addItemButton.Alpha = localUserCanAddItem ? 1 : 0; - // Forcefully update the selected item so that the user state is applied. - Scheduler.AddOnce(OnSelectedItemChanged); - Activity.Value = new UserActivity.InLobby(Room); } From bc930e8fd32eab12f1bcdf6e57236433ad7ebe40 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 20:02:01 +0900 Subject: [PATCH 240/801] Minimal clean-up to get things bearable I plan to do a full refactor of `RoomSubScreen` at first opportunity. --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 59 ++++++++++--------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 428f0e9ed8..c9c9c3eca7 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -49,18 +50,18 @@ namespace osu.Game.Screens.OnlinePlay.Match /// A container that provides controls for selection of user mods. /// This will be shown/hidden automatically when applicable. /// - protected Drawable? UserModsSection; + protected Drawable UserModsSection = null!; /// /// A container that provides controls for selection of the user style. /// This will be shown/hidden automatically when applicable. /// - protected Drawable? UserStyleSection; + protected Drawable UserStyleSection = null!; /// /// A container that will display the user's style. /// - protected Container? UserStyleDisplayContainer; + protected Container UserStyleDisplayContainer = null!; private Sample? sampleStart; @@ -448,40 +449,44 @@ namespace osu.Game.Screens.OnlinePlay.Match Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray(); Ruleset.Value = GetGameplayRuleset(); - if (!item.AllowedMods.Any()) + bool freeMod = item.AllowedMods.Any(); + bool freeStyle = item.FreeStyle; + + // For now, the game can never be in a state where freemod and freestyle are on at the same time. + // This will change, but due to the current implementation if this was to occur drawables will overlap so let's assert. + Debug.Assert(!freeMod || !freeStyle); + + if (freeMod) { - UserModsSection?.Hide(); + UserModsSection.Show(); + UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); + } + else + { + UserModsSection.Hide(); UserModsSelectOverlay.Hide(); UserModsSelectOverlay.IsValidMod = _ => false; } - else - { - UserModsSection?.Show(); - UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); - } - if (item.FreeStyle) + if (freeStyle) { - UserStyleSection?.Show(); + UserStyleSection.Show(); - if (UserStyleDisplayContainer != null) + PlaylistItem gameplayItem = SelectedItem.Value.With(ruleset: GetGameplayRuleset().OnlineID, beatmap: new Optional(GetGameplayBeatmap())); + PlaylistItem? currentItem = UserStyleDisplayContainer.SingleOrDefault()?.Item; + + if (gameplayItem.Equals(currentItem)) + return; + + UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true) { - PlaylistItem gameplayItem = SelectedItem.Value.With(ruleset: GetGameplayRuleset().OnlineID, beatmap: new Optional(GetGameplayBeatmap())); - PlaylistItem? currentItem = UserStyleDisplayContainer.SingleOrDefault()?.Item; - - if (gameplayItem.Equals(currentItem)) - return; - - UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true) - { - AllowReordering = false, - AllowEditing = item.FreeStyle, - RequestEdit = _ => OpenStyleSelection() - }; - } + AllowReordering = false, + AllowEditing = freeStyle, + RequestEdit = _ => OpenStyleSelection() + }; } else - UserStyleSection?.Hide(); + UserStyleSection.Hide(); }); protected virtual APIMod[] GetGameplayMods() => UserMods.Value.Select(m => new APIMod(m)).Concat(SelectedItem.Value!.RequiredMods).ToArray(); From ca7a36d3d6739d8aee75d937cd7544ea7a071983 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 29 Jan 2025 23:32:44 +0900 Subject: [PATCH 241/801] Remove unused usings --- osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs index 0eb8cc3706..08bcf32edf 100644 --- a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Game.Beatmaps.Drawables; using osu.Game.Online.API; using osu.Game.Online.Rooms; -using osu.Game.Resources.Localisation.Web; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Match.Components; using osuTK; From 46144960e50fc49867bccaed2ee035e983a05718 Mon Sep 17 00:00:00 2001 From: "Rian (Reza Mouna Hendrian)" <52914632+Rian8337@users.noreply.github.com> Date: Thu, 30 Jan 2025 03:06:05 +0800 Subject: [PATCH 242/801] Remove unnecessary strain sorting in difficult slider count (#31724) --- osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 89adda302c..6f1b680211 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -53,13 +53,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills if (sliderStrains.Count == 0) return 0; - double[] sortedStrains = sliderStrains.OrderDescending().ToArray(); - - double maxSliderStrain = sortedStrains.Max(); + double maxSliderStrain = sliderStrains.Max(); if (maxSliderStrain == 0) return 0; - return sortedStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxSliderStrain * 12.0 - 6.0)))); + return sliderStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxSliderStrain * 12.0 - 6.0)))); } } } From bad2959d5ba6f74d3ab76d32a6110ebccedde922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 Jan 2025 08:54:42 +0100 Subject: [PATCH 243/801] Change mirror mod direction setting tooltip to hopefully be less confusing See https://github.com/ppy/osu/issues/29720, https://discord.com/channels/188630481301012481/188630652340404224/1334294048541904906. This removes the tooltip due to being zero or negative information, and also changes the description of the setting to not contain the word "mirror", which will hopefully quash the "this is where I would place a mirror to my screen to achieve what I want" interpretation which seems to be a minority interpretation. The first time this was complained about I figured this was probably a one guy issue, but now it's happened twice, and I never want to see this conversation again. --- osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs index 6d01808fb5..4af88caee4 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override LocalisableString Description => "Flip objects on the chosen axes."; public override Type[] IncompatibleMods => new[] { typeof(ModHardRock) }; - [SettingSource("Mirrored axes", "Choose which axes objects are mirrored over.")] + [SettingSource("Flipped axes")] public Bindable Reflection { get; } = new Bindable(); public void ApplyToHitObject(HitObject hitObject) From ec99fc114103f5fd2bc696bcf1bd75ad3bd37241 Mon Sep 17 00:00:00 2001 From: Marvin Helstein Date: Thu, 30 Jan 2025 10:15:16 +0200 Subject: [PATCH 244/801] Move `ApplySelectionOrder` override from `EditorBlueprintContainer` to `ComposeBlueprintContainer` --- .../Edit/Compose/Components/ComposeBlueprintContainer.cs | 5 +++++ .../Edit/Compose/Components/EditorBlueprintContainer.cs | 5 ----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index de1f589135..e82f6395d0 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.Collections.Generic; using System.Linq; using Humanizer; @@ -52,6 +53,10 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => editorScreen?.MainContent.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos); + protected override IEnumerable> ApplySelectionOrder(IEnumerable> blueprints) => + base.ApplySelectionOrder(blueprints) + .OrderBy(b => Math.Min(Math.Abs(EditorClock.CurrentTime - b.Item.GetEndTime()), Math.Abs(EditorClock.CurrentTime - b.Item.StartTime))); + protected ComposeBlueprintContainer(HitObjectComposer composer) : base(composer) { diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs index f1811dd84f..e67644baaa 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs @@ -3,7 +3,6 @@ #nullable disable -using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; @@ -126,10 +125,6 @@ namespace osu.Game.Screens.Edit.Compose.Components return true; } - protected override IEnumerable> ApplySelectionOrder(IEnumerable> blueprints) => - base.ApplySelectionOrder(blueprints) - .OrderBy(b => Math.Min(Math.Abs(EditorClock.CurrentTime - b.Item.GetEndTime()), Math.Abs(EditorClock.CurrentTime - b.Item.StartTime))); - protected override SelectionBlueprintContainer CreateSelectionBlueprintContainer() => new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }; protected override SelectionHandler CreateSelectionHandler() => new EditorSelectionHandler(); From 31c4461fbb1167d3a1c93910b0f5c4263ab348dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 17 Oct 2024 11:04:39 +0200 Subject: [PATCH 245/801] Abstract out `WizardOverlay` for multi-step wizard type screens To be used in the editor, for the beatmap submission wizard. I've recently been on record for hating "abstract" as a rationale to do anything, but seeing this commit ~3 months after I originally made it, it still feels okay to do for me in this particular case. I think the abstraction is loose enough, makes sense from a code reuse and UX consistency standpoint, and doesn't seem to leak any particular implementation details. That said, it is both a huge diffstat and also potentially controversial, which is why I'm PRing first separately. --- .../Overlays/FirstRunSetup/ScreenBeatmaps.cs | 2 +- .../Overlays/FirstRunSetup/ScreenBehaviour.cs | 2 +- .../FirstRunSetup/ScreenImportFromStable.cs | 2 +- .../Overlays/FirstRunSetup/ScreenUIScale.cs | 2 +- .../Overlays/FirstRunSetup/ScreenWelcome.cs | 2 +- osu.Game/Overlays/FirstRunSetupOverlay.cs | 268 +--------------- osu.Game/Overlays/WizardOverlay.cs | 288 ++++++++++++++++++ ...FirstRunSetupScreen.cs => WizardScreen.cs} | 4 +- 8 files changed, 305 insertions(+), 265 deletions(-) create mode 100644 osu.Game/Overlays/WizardOverlay.cs rename osu.Game/Overlays/{FirstRunSetup/FirstRunSetupScreen.cs => WizardScreen.cs} (96%) diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs index da60951ab6..392b170ad2 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs @@ -21,7 +21,7 @@ using Realms; namespace osu.Game.Overlays.FirstRunSetup { [LocalisableDescription(typeof(FirstRunSetupBeatmapScreenStrings), nameof(FirstRunSetupBeatmapScreenStrings.Header))] - public partial class ScreenBeatmaps : FirstRunSetupScreen + public partial class ScreenBeatmaps : WizardScreen { private ProgressRoundedButton downloadBundledButton = null!; private ProgressRoundedButton downloadTutorialButton = null!; diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs b/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs index d31ce7ea18..a583ba5f6b 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs @@ -20,7 +20,7 @@ using osu.Game.Overlays.Settings.Sections; namespace osu.Game.Overlays.FirstRunSetup { [LocalisableDescription(typeof(FirstRunSetupOverlayStrings), nameof(FirstRunSetupOverlayStrings.Behaviour))] - public partial class ScreenBehaviour : FirstRunSetupScreen + public partial class ScreenBehaviour : WizardScreen { private SearchContainer searchContainer; diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs index 5eb38b6e11..5bdcd8e850 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs @@ -31,7 +31,7 @@ using osuTK; namespace osu.Game.Overlays.FirstRunSetup { [LocalisableDescription(typeof(FirstRunOverlayImportFromStableScreenStrings), nameof(FirstRunOverlayImportFromStableScreenStrings.Header))] - public partial class ScreenImportFromStable : FirstRunSetupScreen + public partial class ScreenImportFromStable : WizardScreen { private static readonly Vector2 button_size = new Vector2(400, 50); diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs index d0eefa55c5..fc64408775 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs @@ -32,7 +32,7 @@ using osuTK; namespace osu.Game.Overlays.FirstRunSetup { [LocalisableDescription(typeof(GraphicsSettingsStrings), nameof(GraphicsSettingsStrings.UIScaling))] - public partial class ScreenUIScale : FirstRunSetupScreen + public partial class ScreenUIScale : WizardScreen { [BackgroundDependencyLoader] private void load(OsuConfigManager config) diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs b/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs index 68c6c78986..93cf555bc9 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs @@ -23,7 +23,7 @@ using osuTK; namespace osu.Game.Overlays.FirstRunSetup { [LocalisableDescription(typeof(FirstRunSetupOverlayStrings), nameof(FirstRunSetupOverlayStrings.WelcomeTitle))] - public partial class ScreenWelcome : FirstRunSetupScreen + public partial class ScreenWelcome : WizardScreen { [BackgroundDependencyLoader] private void load(FrameworkConfigManager frameworkConfig) diff --git a/osu.Game/Overlays/FirstRunSetupOverlay.cs b/osu.Game/Overlays/FirstRunSetupOverlay.cs index 1a302cf51d..c2e89f32f1 100644 --- a/osu.Game/Overlays/FirstRunSetupOverlay.cs +++ b/osu.Game/Overlays/FirstRunSetupOverlay.cs @@ -1,38 +1,22 @@ // 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.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Events; -using osu.Framework.Localisation; -using osu.Framework.Screens; -using osu.Framework.Threading; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; -using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Overlays.FirstRunSetup; -using osu.Game.Overlays.Mods; using osu.Game.Overlays.Notifications; using osu.Game.Screens; -using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; namespace osu.Game.Overlays { [Cached] - public partial class FirstRunSetupOverlay : ShearedOverlayContainer + public partial class FirstRunSetupOverlay : WizardOverlay { [Resolved] private IPerformFromScreenRunner performer { get; set; } = null!; @@ -43,28 +27,8 @@ namespace osu.Game.Overlays [Resolved] private OsuConfigManager config { get; set; } = null!; - private ScreenStack? stack; - - public ShearedButton? NextButton => DisplayedFooterContent?.NextButton; - private readonly Bindable showFirstRunSetup = new Bindable(); - private int? currentStepIndex; - - /// - /// The currently displayed screen, if any. - /// - public FirstRunSetupScreen? CurrentScreen => (FirstRunSetupScreen?)stack?.CurrentScreen; - - private readonly List steps = new List(); - - private Container screenContent = null!; - - private Container content = null!; - - private LoadingSpinner loading = null!; - private ScheduledDelegate? loadingShowDelegate; - public FirstRunSetupOverlay() : base(OverlayColourScheme.Purple) { @@ -73,67 +37,15 @@ namespace osu.Game.Overlays [BackgroundDependencyLoader(permitNulls: true)] private void load(OsuColour colours, LegacyImportManager? legacyImportManager) { - steps.Add(typeof(ScreenWelcome)); - steps.Add(typeof(ScreenUIScale)); - steps.Add(typeof(ScreenBeatmaps)); + AddStep(); + AddStep(); + AddStep(); if (legacyImportManager?.SupportsImportFromStable == true) - steps.Add(typeof(ScreenImportFromStable)); - steps.Add(typeof(ScreenBehaviour)); + AddStep(); + AddStep(); Header.Title = FirstRunSetupOverlayStrings.FirstRunSetupTitle; Header.Description = FirstRunSetupOverlayStrings.FirstRunSetupDescription; - - MainAreaContent.AddRange(new Drawable[] - { - content = new PopoverContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Bottom = 20 }, - Child = new GridContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] - { - new Dimension(), - new Dimension(minSize: 640, maxSize: 800), - new Dimension(), - }, - Content = new[] - { - new[] - { - Empty(), - new InputBlockingContainer - { - Masking = true, - CornerRadius = 14, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourProvider.Background6, - }, - loading = new LoadingSpinner(), - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Vertical = 20 }, - Child = screenContent = new Container { RelativeSizeAxes = Axes.Both, }, - }, - }, - }, - Empty(), - }, - } - } - }, - }); } protected override void LoadComplete() @@ -145,55 +57,6 @@ namespace osu.Game.Overlays if (showFirstRunSetup.Value) Show(); } - [Resolved] - private ScreenFooter footer { get; set; } = null!; - - public new FirstRunSetupFooterContent? DisplayedFooterContent => base.DisplayedFooterContent as FirstRunSetupFooterContent; - - public override VisibilityContainer CreateFooterContent() - { - var footerContent = new FirstRunSetupFooterContent - { - ShowNextStep = showNextStep, - }; - - footerContent.OnLoadComplete += _ => updateButtons(); - return footerContent; - } - - public override bool OnBackButton() - { - if (currentStepIndex == 0) - return false; - - Debug.Assert(stack != null); - - stack.CurrentScreen.Exit(); - currentStepIndex--; - - updateButtons(); - return true; - } - - public override bool OnPressed(KeyBindingPressEvent e) - { - if (!e.Repeat) - { - switch (e.Action) - { - case GlobalAction.Select: - DisplayedFooterContent?.NextButton.TriggerClick(); - return true; - - case GlobalAction.Back: - footer.BackButton.TriggerClick(); - return false; - } - } - - return base.OnPressed(e); - } - public override void Show() { // if we are valid for display, only do so after reaching the main menu. @@ -207,24 +70,11 @@ namespace osu.Game.Overlays }, new[] { typeof(MainMenu) }); } - protected override void PopIn() - { - base.PopIn(); - - content.ScaleTo(0.99f) - .ScaleTo(1, 400, Easing.OutQuint); - - if (currentStepIndex == null) - showFirstStep(); - } - protected override void PopOut() { base.PopOut(); - content.ScaleTo(0.99f, 400, Easing.OutQuint); - - if (currentStepIndex != null) + if (CurrentStepIndex != null) { notificationOverlay.Post(new SimpleNotification { @@ -237,112 +87,14 @@ namespace osu.Game.Overlays }, }); } - else - { - stack?.FadeOut(100) - .Expire(); - } } - private void showFirstStep() + protected override void ShowNextStep() { - Debug.Assert(currentStepIndex == null); + base.ShowNextStep(); - screenContent.Child = stack = new ScreenStack - { - RelativeSizeAxes = Axes.Both, - }; - - currentStepIndex = -1; - showNextStep(); - } - - private void showNextStep() - { - Debug.Assert(currentStepIndex != null); - Debug.Assert(stack != null); - - currentStepIndex++; - - if (currentStepIndex < steps.Count) - { - var nextScreen = (Screen)Activator.CreateInstance(steps[currentStepIndex.Value])!; - - loadingShowDelegate = Scheduler.AddDelayed(() => loading.Show(), 200); - nextScreen.OnLoadComplete += _ => - { - loadingShowDelegate?.Cancel(); - loading.Hide(); - }; - - stack.Push(nextScreen); - } - else - { + if (CurrentStepIndex == null) showFirstRunSetup.Value = false; - currentStepIndex = null; - Hide(); - } - - updateButtons(); - } - - private void updateButtons() => DisplayedFooterContent?.UpdateButtons(currentStepIndex, steps); - - public partial class FirstRunSetupFooterContent : VisibilityContainer - { - public ShearedButton NextButton { get; private set; } = null!; - - public Action? ShowNextStep; - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - RelativeSizeAxes = Axes.Both; - - InternalChild = NextButton = new ShearedButton(0) - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Margin = new MarginPadding { Right = 12f }, - RelativeSizeAxes = Axes.X, - Width = 1, - Text = FirstRunSetupOverlayStrings.GetStarted, - DarkerColour = colourProvider.Colour2, - LighterColour = colourProvider.Colour1, - Action = () => ShowNextStep?.Invoke(), - }; - } - - public void UpdateButtons(int? currentStep, IReadOnlyList steps) - { - NextButton.Enabled.Value = currentStep != null; - - if (currentStep == null) - return; - - bool isFirstStep = currentStep == 0; - bool isLastStep = currentStep == steps.Count - 1; - - if (isFirstStep) - NextButton.Text = FirstRunSetupOverlayStrings.GetStarted; - else - { - NextButton.Text = isLastStep - ? CommonStrings.Finish - : LocalisableString.Interpolate($@"{CommonStrings.Next} ({steps[currentStep.Value + 1].GetLocalisableDescription()})"); - } - } - - protected override void PopIn() - { - this.FadeIn(); - } - - protected override void PopOut() - { - this.Delay(400).FadeOut(); - } } } } diff --git a/osu.Game/Overlays/WizardOverlay.cs b/osu.Game/Overlays/WizardOverlay.cs new file mode 100644 index 0000000000..38701efc96 --- /dev/null +++ b/osu.Game/Overlays/WizardOverlay.cs @@ -0,0 +1,288 @@ +// 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.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Framework.Screens; +using osu.Framework.Threading; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; +using osu.Game.Localisation; +using osu.Game.Overlays.Mods; +using osu.Game.Screens.Footer; + +namespace osu.Game.Overlays +{ + public partial class WizardOverlay : ShearedOverlayContainer + { + private ScreenStack? stack; + + public ShearedButton? NextButton => DisplayedFooterContent?.NextButton; + + protected int? CurrentStepIndex { get; private set; } + + /// + /// The currently displayed screen, if any. + /// + public WizardScreen? CurrentScreen => (WizardScreen?)stack?.CurrentScreen; + + private readonly List steps = new List(); + + private Container screenContent = null!; + + private Container content = null!; + + private LoadingSpinner loading = null!; + private ScheduledDelegate? loadingShowDelegate; + + protected WizardOverlay(OverlayColourScheme scheme) + : base(scheme) + { + } + + [BackgroundDependencyLoader] + private void load() + { + MainAreaContent.AddRange(new Drawable[] + { + content = new PopoverContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Bottom = 20 }, + Child = new GridContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(minSize: 640, maxSize: 800), + new Dimension(), + }, + Content = new[] + { + new[] + { + Empty(), + new InputBlockingContainer + { + Masking = true, + CornerRadius = 14, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourProvider.Background6, + }, + loading = new LoadingSpinner(), + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Vertical = 20 }, + Child = screenContent = new Container { RelativeSizeAxes = Axes.Both, }, + }, + }, + }, + Empty(), + }, + } + } + }, + }); + } + + [Resolved] + private ScreenFooter footer { get; set; } = null!; + + public new WizardFooterContent? DisplayedFooterContent => base.DisplayedFooterContent as WizardFooterContent; + + public override VisibilityContainer CreateFooterContent() + { + var footerContent = new WizardFooterContent + { + ShowNextStep = ShowNextStep, + }; + + footerContent.OnLoadComplete += _ => updateButtons(); + return footerContent; + } + + public override bool OnBackButton() + { + if (CurrentStepIndex == 0) + return false; + + Debug.Assert(stack != null); + + stack.CurrentScreen.Exit(); + CurrentStepIndex--; + + updateButtons(); + return true; + } + + public override bool OnPressed(KeyBindingPressEvent e) + { + if (!e.Repeat) + { + switch (e.Action) + { + case GlobalAction.Select: + DisplayedFooterContent?.NextButton.TriggerClick(); + return true; + + case GlobalAction.Back: + footer.BackButton.TriggerClick(); + return false; + } + } + + return base.OnPressed(e); + } + + protected override void PopIn() + { + base.PopIn(); + + content.ScaleTo(0.99f) + .ScaleTo(1, 400, Easing.OutQuint); + + if (CurrentStepIndex == null) + showFirstStep(); + } + + protected override void PopOut() + { + base.PopOut(); + + content.ScaleTo(0.99f, 400, Easing.OutQuint); + + if (CurrentStepIndex == null) + { + stack?.FadeOut(100) + .Expire(); + } + } + + protected void AddStep() + where T : WizardScreen + { + steps.Add(typeof(T)); + } + + private void showFirstStep() + { + Debug.Assert(CurrentStepIndex == null); + + screenContent.Child = stack = new ScreenStack + { + RelativeSizeAxes = Axes.Both, + }; + + CurrentStepIndex = -1; + ShowNextStep(); + } + + protected virtual void ShowNextStep() + { + Debug.Assert(CurrentStepIndex != null); + Debug.Assert(stack != null); + + CurrentStepIndex++; + + if (CurrentStepIndex < steps.Count) + { + var nextScreen = (Screen)Activator.CreateInstance(steps[CurrentStepIndex.Value])!; + + loadingShowDelegate = Scheduler.AddDelayed(() => loading.Show(), 200); + nextScreen.OnLoadComplete += _ => + { + loadingShowDelegate?.Cancel(); + loading.Hide(); + }; + + stack.Push(nextScreen); + } + else + { + CurrentStepIndex = null; + Hide(); + } + + updateButtons(); + } + + private void updateButtons() => DisplayedFooterContent?.UpdateButtons(CurrentStepIndex, steps); + + public partial class WizardFooterContent : VisibilityContainer + { + public ShearedButton NextButton { get; private set; } = null!; + + public Action? ShowNextStep; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + RelativeSizeAxes = Axes.Both; + + InternalChild = NextButton = new ShearedButton(0) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding { Right = 12f }, + RelativeSizeAxes = Axes.X, + Width = 1, + Text = FirstRunSetupOverlayStrings.GetStarted, + DarkerColour = colourProvider.Colour2, + LighterColour = colourProvider.Colour1, + Action = () => ShowNextStep?.Invoke(), + }; + } + + public void UpdateButtons(int? currentStep, IReadOnlyList steps) + { + NextButton.Enabled.Value = currentStep != null; + + if (currentStep == null) + return; + + bool isFirstStep = currentStep == 0; + bool isLastStep = currentStep == steps.Count - 1; + + if (isFirstStep) + NextButton.Text = FirstRunSetupOverlayStrings.GetStarted; + else + { + NextButton.Text = isLastStep + ? CommonStrings.Finish + : LocalisableString.Interpolate($@"{CommonStrings.Next} ({steps[currentStep.Value + 1].GetLocalisableDescription()})"); + } + } + + protected override void PopIn() + { + this.FadeIn(); + } + + protected override void PopOut() + { + this.Delay(400).FadeOut(); + } + } + } +} diff --git a/osu.Game/Overlays/FirstRunSetup/FirstRunSetupScreen.cs b/osu.Game/Overlays/WizardScreen.cs similarity index 96% rename from osu.Game/Overlays/FirstRunSetup/FirstRunSetupScreen.cs rename to osu.Game/Overlays/WizardScreen.cs index 76921718f2..7f3b1fe7f4 100644 --- a/osu.Game/Overlays/FirstRunSetup/FirstRunSetupScreen.cs +++ b/osu.Game/Overlays/WizardScreen.cs @@ -13,9 +13,9 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osuTK; -namespace osu.Game.Overlays.FirstRunSetup +namespace osu.Game.Overlays { - public abstract partial class FirstRunSetupScreen : Screen + public abstract partial class WizardScreen : Screen { private const float offset = 100; From 749704344c5fbb0d46b153d98e60798e331a3965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 Jan 2025 13:11:05 +0100 Subject: [PATCH 246/801] Move implicit slider path segment handling logic to Bezier converter The logic in `LegacyBeatmapEncoder` that was supposed to handle the lazer-exclusive feature of supporting multiple slider segment types in a single slider was interfering rather badly with the Bezier converter. Generally it was a bit difficult to follow, too. The nice thing about `BezierConverter` is that it is *guaranteed* to only output Bezier control points. In light of this, the same double-up- -the-control-point logic that was supposed to make multiple slider segment types backwards-compatible with stable can be placed in the Bezier conversion logic, and be *much* more understandable, too. --- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 59 +++++-------------- osu.Game/Rulesets/Objects/BezierConverter.cs | 4 ++ 2 files changed, 19 insertions(+), 44 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 6c855e1346..07e88ab956 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -447,60 +447,31 @@ namespace osu.Game.Beatmaps.Formats private void addPathData(TextWriter writer, IHasPath pathData, Vector2 position) { - PathType? lastType = null; - for (int i = 0; i < pathData.Path.ControlPoints.Count; i++) { PathControlPoint point = pathData.Path.ControlPoints[i]; + // Note that lazer's encoding format supports specifying multiple curve types for a slider path, which is not supported by stable. + // Backwards compatibility with stable is handled by `LegacyBeatmapExporter` and `BezierConverter.ConvertToModernBezier()`. if (point.Type != null) { - // We've reached a new (explicit) segment! - - // Explicit segments have a new format in which the type is injected into the middle of the control point string. - // To preserve compatibility with osu-stable as much as possible, explicit segments with the same type are converted to use implicit segments by duplicating the control point. - // One exception are consecutive perfect curves, which aren't supported in osu!stable and can lead to decoding issues if encoded as implicit segments - bool needsExplicitSegment = point.Type != lastType || point.Type == PathType.PERFECT_CURVE || i == pathData.Path.ControlPoints.Count - 1; - - // Another exception to this is when the last two control points of the last segment were duplicated. This is not a scenario supported by osu!stable. - // Lazer does not add implicit segments for the last two control points of _any_ explicit segment, so an explicit segment is forced in order to maintain consistency with the decoder. - if (i > 1) + switch (point.Type?.Type) { - // We need to use the absolute control point position to determine equality, otherwise floating point issues may arise. - Vector2 p1 = position + pathData.Path.ControlPoints[i - 1].Position; - Vector2 p2 = position + pathData.Path.ControlPoints[i - 2].Position; + case SplineType.BSpline: + writer.Write(point.Type.Value.Degree > 0 ? $"B{point.Type.Value.Degree}|" : "B|"); + break; - if ((int)p1.X == (int)p2.X && (int)p1.Y == (int)p2.Y) - needsExplicitSegment = true; - } + case SplineType.Catmull: + writer.Write("C|"); + break; - if (needsExplicitSegment) - { - switch (point.Type?.Type) - { - case SplineType.BSpline: - writer.Write(point.Type.Value.Degree > 0 ? $"B{point.Type.Value.Degree}|" : "B|"); - break; + case SplineType.PerfectCurve: + writer.Write("P|"); + break; - case SplineType.Catmull: - writer.Write("C|"); - break; - - case SplineType.PerfectCurve: - writer.Write("P|"); - break; - - case SplineType.Linear: - writer.Write("L|"); - break; - } - - lastType = point.Type; - } - else - { - // New segment with the same type - duplicate the control point - writer.Write(FormattableString.Invariant($"{position.X + point.Position.X}:{position.Y + point.Position.Y}|")); + case SplineType.Linear: + writer.Write("L|"); + break; } } diff --git a/osu.Game/Rulesets/Objects/BezierConverter.cs b/osu.Game/Rulesets/Objects/BezierConverter.cs index 638975630e..384c686167 100644 --- a/osu.Game/Rulesets/Objects/BezierConverter.cs +++ b/osu.Game/Rulesets/Objects/BezierConverter.cs @@ -136,6 +136,7 @@ namespace osu.Game.Rulesets.Objects { for (int j = 0; j < segment.Length - 1; j++) { + if (result.Count > 0 && j == 0) result.Add(new PathControlPoint(segment[j])); result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.BEZIER : null)); } } @@ -147,6 +148,7 @@ namespace osu.Game.Rulesets.Objects { for (int j = 0; j < segment.Length - 1; j++) { + if (result.Count > 0 && j == 0) result.Add(new PathControlPoint(segment[j])); result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.BEZIER : null)); } } @@ -158,6 +160,7 @@ namespace osu.Game.Rulesets.Objects for (int j = 0; j < circleResult.Length - 1; j++) { + if (result.Count > 0 && j == 0) result.Add(new PathControlPoint(circleResult[j])); result.Add(new PathControlPoint(circleResult[j], j == 0 ? PathType.BEZIER : null)); } @@ -170,6 +173,7 @@ namespace osu.Game.Rulesets.Objects for (int j = 0; j < bSplineResult.Length - 1; j++) { + if (result.Count > 0 && j == 0) result.Add(new PathControlPoint(bSplineResult[j])); result.Add(new PathControlPoint(bSplineResult[j], j == 0 ? PathType.BEZIER : null)); } From 64b67252a2edc1b762c4f4cca311738effe2df68 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 30 Jan 2025 08:22:28 -0500 Subject: [PATCH 247/801] Enable NRT on `Column` --- osu.Game.Rulesets.Mania/ManiaInputManager.cs | 2 +- osu.Game.Rulesets.Mania/UI/Column.cs | 19 +++++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaInputManager.cs b/osu.Game.Rulesets.Mania/ManiaInputManager.cs index 36ccf68d76..e8c993a91b 100644 --- a/osu.Game.Rulesets.Mania/ManiaInputManager.cs +++ b/osu.Game.Rulesets.Mania/ManiaInputManager.cs @@ -8,7 +8,7 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mania { - [Cached] // Used for touch input, see ColumnTouchInputArea. + [Cached] // Used for touch input, see Column.OnTouchDown/OnTouchUp. public partial class ManiaInputManager : RulesetInputManager { public ManiaInputManager(RulesetInfo ruleset, int variant) diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 81f4d79281..5425965897 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -1,10 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; @@ -45,11 +44,11 @@ namespace osu.Game.Rulesets.Mania.UI internal readonly Container TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }; - private DrawablePool hitExplosionPool; + private DrawablePool hitExplosionPool = null!; private readonly OrderedHitPolicy hitPolicy; public Container UnderlayElements => HitObjectArea.UnderlayElements; - private GameplaySampleTriggerSource sampleTriggerSource; + private GameplaySampleTriggerSource sampleTriggerSource = null!; /// /// Whether this is a special (ie. scratch) column. @@ -75,7 +74,7 @@ namespace osu.Game.Rulesets.Mania.UI } [Resolved] - private ISkinSource skin { get; set; } + private ISkinSource skin { get; set; } = null!; [BackgroundDependencyLoader] private void load(GameHost host) @@ -136,7 +135,7 @@ namespace osu.Game.Rulesets.Mania.UI base.Dispose(isDisposing); - if (skin != null) + if (skin.IsNotNull()) skin.SourceChanged -= onSourceChanged; } @@ -187,14 +186,14 @@ namespace osu.Game.Rulesets.Mania.UI #region Touch Input - [Resolved(canBeNull: true)] - private ManiaInputManager maniaInputManager { get; set; } + [Resolved] + private ManiaInputManager? maniaInputManager { get; set; } private int touchActivationCount; protected override bool OnTouchDown(TouchDownEvent e) { - maniaInputManager.KeyBindingContainer.TriggerPressed(Action.Value); + maniaInputManager?.KeyBindingContainer.TriggerPressed(Action.Value); touchActivationCount++; return true; } @@ -204,7 +203,7 @@ namespace osu.Game.Rulesets.Mania.UI touchActivationCount--; if (touchActivationCount == 0) - maniaInputManager.KeyBindingContainer.TriggerReleased(Action.Value); + maniaInputManager?.KeyBindingContainer.TriggerReleased(Action.Value); } #endregion From 261a7e537b0451f34725c376af345ff8fdd131f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 Jan 2025 14:42:44 +0100 Subject: [PATCH 248/801] Fix distance snap time part ceasing to work when grid snap is also active As pointed out in https://github.com/ppy/osu/pull/31655#discussion_r1935536934. --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 194276baf9..e08968e1aa 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -273,7 +273,7 @@ namespace osu.Game.Rulesets.Osu.Edit pos = Vector2.Clamp(pos, Vector2.Zero, OsuPlayfield.BASE_SIZE); var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); - return new SnapResult(positionSnapGrid.ToScreenSpace(pos), null, playfield); + return new SnapResult(positionSnapGrid.ToScreenSpace(pos), fallbackTime, playfield); } private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult) From 2ee480c442436bb442b8b6171e2f42b86c3cbfa8 Mon Sep 17 00:00:00 2001 From: James Wilson Date: Thu, 30 Jan 2025 13:58:38 +0000 Subject: [PATCH 249/801] Clamp `estimateImproperlyFollowedDifficultSliders` between 0 and `attributes.AimDifficultSliderCount` (#31736) --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index f191180630..dc2df39cdb 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -161,7 +161,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty { // We add tick misses here since they too mean that the player didn't follow the slider properly // We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly - estimateImproperlyFollowedDifficultSliders = Math.Min(countSliderEndsDropped + countSliderTickMiss, attributes.AimDifficultSliderCount); + estimateImproperlyFollowedDifficultSliders = Math.Clamp(countSliderEndsDropped + countSliderTickMiss, 0, attributes.AimDifficultSliderCount); } double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / attributes.AimDifficultSliderCount, 3) + attributes.SliderFactor; From b4f63da048e16c9f0fd0d339ea13f33637dade9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 Jan 2025 15:23:22 +0100 Subject: [PATCH 250/801] Move control point double-up logic to `LegacyBeatmapExporter` Done for two reasons: - During review it was requested for the logic to be moved out of `BezierConverter` as `BezierConverter` was intended to produce "lazer style" sliders with per-control-point curve types, as a future usability / code layering concern. - It is also relevant for encode-decode stability. With how the logic was structured between the Bezier converter and the legacy beatmap encoder, the encoder would leave behind per-control-point Bezier curve specs that stable ignored, but subsequent encodes and decodes in lazer would end up multiplying the doubled-up control points ad nauseam. Instead, it is sufficient to only specify the curve type for the head control point as Bezier, not specify any further curve types later on, and instead just keep the double-up-control-point for new implicit segment logic which is enough to make stable cooperate (and also as close to outputting the slider exactly as stable would have produced it as we've ever been) --- osu.Game/Database/LegacyBeatmapExporter.cs | 32 ++++++++++++++------ osu.Game/Rulesets/Objects/BezierConverter.cs | 4 --- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 24e752da31..9bb90ab461 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -120,18 +120,30 @@ namespace osu.Game.Database if (BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1 && hasPath.Path.ControlPoints[0].Type!.Value.Degree == null) continue; - var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints); - - // Truncate control points to integer positions - foreach (var pathControlPoint in newControlPoints) - { - pathControlPoint.Position = new Vector2( - (float)Math.Floor(pathControlPoint.Position.X), - (float)Math.Floor(pathControlPoint.Position.Y)); - } + var convertedToBezier = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints); hasPath.Path.ControlPoints.Clear(); - hasPath.Path.ControlPoints.AddRange(newControlPoints); + + for (int i = 0; i < convertedToBezier.Count; i++) + { + var convertedPoint = convertedToBezier[i]; + + // Truncate control points to integer positions + var position = new Vector2( + (float)Math.Floor(convertedPoint.Position.X), + (float)Math.Floor(convertedPoint.Position.Y)); + + // stable only supports a single curve type specification per slider. + // we exploit the fact that the converted-to-Bézier path only has Bézier segments, + // and thus we specify the Bézier curve type once ever at the start of the slider. + hasPath.Path.ControlPoints.Add(new PathControlPoint(position, i == 0 ? PathType.BEZIER : null)); + + // however, the Bézier path as output by the converter has multiple segments. + // `LegacyBeatmapEncoder` will attempt to encode this by emitting per-control-point curve type specs which don't do anything for stable. + // instead, stable expects control points that start a segment to be present in the path twice in succession. + if (convertedPoint.Type == PathType.BEZIER) + hasPath.Path.ControlPoints.Add(new PathControlPoint(position)); + } } // Encode to legacy format diff --git a/osu.Game/Rulesets/Objects/BezierConverter.cs b/osu.Game/Rulesets/Objects/BezierConverter.cs index 384c686167..638975630e 100644 --- a/osu.Game/Rulesets/Objects/BezierConverter.cs +++ b/osu.Game/Rulesets/Objects/BezierConverter.cs @@ -136,7 +136,6 @@ namespace osu.Game.Rulesets.Objects { for (int j = 0; j < segment.Length - 1; j++) { - if (result.Count > 0 && j == 0) result.Add(new PathControlPoint(segment[j])); result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.BEZIER : null)); } } @@ -148,7 +147,6 @@ namespace osu.Game.Rulesets.Objects { for (int j = 0; j < segment.Length - 1; j++) { - if (result.Count > 0 && j == 0) result.Add(new PathControlPoint(segment[j])); result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.BEZIER : null)); } } @@ -160,7 +158,6 @@ namespace osu.Game.Rulesets.Objects for (int j = 0; j < circleResult.Length - 1; j++) { - if (result.Count > 0 && j == 0) result.Add(new PathControlPoint(circleResult[j])); result.Add(new PathControlPoint(circleResult[j], j == 0 ? PathType.BEZIER : null)); } @@ -173,7 +170,6 @@ namespace osu.Game.Rulesets.Objects for (int j = 0; j < bSplineResult.Length - 1; j++) { - if (result.Count > 0 && j == 0) result.Add(new PathControlPoint(bSplineResult[j])); result.Add(new PathControlPoint(bSplineResult[j], j == 0 ? PathType.BEZIER : null)); } From 4a164b7b149ff7c8f78d15f904fcb61673ac9ff8 Mon Sep 17 00:00:00 2001 From: Joppe27 Date: Sun, 11 Dec 2022 02:17:50 +0100 Subject: [PATCH 251/801] Add legacy taiko swell --- .../Objects/Drawables/DrawableSwell.cs | 113 ++------------ .../Objects/ISkinnableSwell.cs | 22 +++ .../Argon/TaikoArgonSkinTransformer.cs | 2 +- .../Skinning/Default/DefaultSwell.cs | 142 ++++++++++++++++++ .../Skinning/Legacy/LegacySwell.cs | 136 +++++++++++++++++ .../Skinning/Legacy/LegacySwellCirclePiece.cs | 23 +++ .../Legacy/TaikoLegacySkinTransformer.cs | 10 +- .../TaikoSkinComponents.cs | 1 + 8 files changed, 348 insertions(+), 101 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 28617b35f6..cba044959c 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -6,14 +6,9 @@ using System; using System.Linq; using JetBrains.Annotations; -using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; -using osuTK.Graphics; -using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Skinning.Default; @@ -25,11 +20,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public partial class DrawableSwell : DrawableTaikoHitObject { - private const float target_ring_thick_border = 1.4f; - private const float target_ring_thin_border = 1f; - private const float target_ring_scale = 5f; - private const float inner_ring_alpha = 0.65f; - /// /// Offset away from the start time of the swell at which the ring starts appearing. /// @@ -37,10 +27,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private Vector2 baseSize; + private readonly SkinnableDrawable spinnerBody; + private readonly Container ticks; - private readonly Container bodyContainer; - private readonly CircularContainer targetRing; - private readonly CircularContainer expandingRing; private double? lastPressHandleTime; @@ -61,82 +50,17 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { FillMode = FillMode.Fit; - Content.Add(bodyContainer = new Container + Content.Add(spinnerBody = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Swell), _ => new DefaultSwell()) { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Depth = 1, - Children = new Drawable[] - { - expandingRing = new CircularContainer - { - Name = "Expanding ring", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Alpha = 0, - RelativeSizeAxes = Axes.Both, - Blending = BlendingParameters.Additive, - Masking = true, - Children = new[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = inner_ring_alpha, - } - } - }, - targetRing = new CircularContainer - { - Name = "Target ring (thick border)", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - BorderThickness = target_ring_thick_border, - Blending = BlendingParameters.Additive, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - }, - new CircularContainer - { - Name = "Target ring (thin border)", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - BorderThickness = target_ring_thin_border, - BorderColour = Color4.White, - Children = new[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - } - } - } - } - } - } }); AddInternal(ticks = new Container { RelativeSizeAxes = Axes.Both }); } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - expandingRing.Colour = colours.YellowLight; - targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); - } - - protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Swell), + protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.SwellCirclePiece), _ => new SwellCirclePiece { // to allow for rotation transform @@ -208,16 +132,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables int numHits = ticks.Count(r => r.IsHit); - float completion = (float)numHits / HitObject.RequiredHits; - - expandingRing - .FadeTo(expandingRing.Alpha + Math.Clamp(completion / 16, 0.1f, 0.6f), 50) - .Then() - .FadeTo(completion / 8, 2000, Easing.OutQuint); - - MainPiece.Drawable.RotateTo((float)(completion * HitObject.Duration / 8), 4000, Easing.OutQuint); - - expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); + (spinnerBody.Drawable as ISkinnableSwell)?.OnUserInput(this, numHits, MainPiece); if (numHits == HitObject.RequiredHits) ApplyMaxResult(); @@ -252,24 +167,24 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { base.UpdateStartTimeStateTransforms(); - using (BeginDelayedSequence(-ring_appear_offset)) - targetRing.ScaleTo(target_ring_scale, 400, Easing.OutQuint); + (spinnerBody.Drawable as ISkinnableSwell)?.ApplyPassiveTransforms(this, MainPiece); } protected override void UpdateHitStateTransforms(ArmedState state) { - const double transition_duration = 300; - switch (state) { case ArmedState.Idle: - expandingRing.FadeTo(0); + HandleUserInput = true; break; case ArmedState.Miss: case ArmedState.Hit: - this.FadeOut(transition_duration, Easing.Out); - bodyContainer.ScaleTo(1.4f, transition_duration); + // Postpone drawable hitobject expiration until it has animated/faded out. Inputs on the object are disallowed during this delay. + LifetimeEnd = Time.Current + 1200; + HandleUserInput = false; + + (spinnerBody.Drawable as ISkinnableSwell)?.OnHitObjectEnd(state, MainPiece); break; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs new file mode 100644 index 0000000000..18feff5bb9 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs @@ -0,0 +1,22 @@ +// 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.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Taiko.Objects +{ + public interface ISkinnableSwell + { + void OnUserInput(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece); + + void OnHitObjectEnd(ArmedState state, SkinnableDrawable mainPiece); + + /// + /// Applies passive transforms on HitObject start. Gets called every time DrawableTaikoHitobject + /// changes state. This happens on creation, and when the object is completed (as in hit or missed). + /// + void ApplyPassiveTransforms(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece); + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index bfc9e8648d..cfd30dd628 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon case TaikoSkinComponents.TaikoExplosionOk: return new ArgonHitExplosion(taikoComponent.Component); - case TaikoSkinComponents.Swell: + case TaikoSkinComponents.SwellCirclePiece: return new ArgonSwellCirclePiece(); } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs new file mode 100644 index 0000000000..e525e9873d --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs @@ -0,0 +1,142 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Skinning; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Taiko.Skinning.Default +{ + public partial class DefaultSwell : Container, ISkinnableSwell + { + private const float target_ring_thick_border = 1.4f; + private const float target_ring_thin_border = 1f; + private const float target_ring_scale = 5f; + private const float inner_ring_alpha = 0.65f; + + private readonly Container bodyContainer; + private readonly CircularContainer targetRing; + private readonly CircularContainer expandingRing; + + public DefaultSwell() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + RelativeSizeAxes = Axes.Both; + + Content.Add(bodyContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Depth = 1, + Children = new Drawable[] + { + expandingRing = new CircularContainer + { + Name = "Expanding ring", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + Masking = true, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = inner_ring_alpha, + } + } + }, + targetRing = new CircularContainer + { + Name = "Target ring (thick border)", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderThickness = target_ring_thick_border, + Blending = BlendingParameters.Additive, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + }, + new CircularContainer + { + Name = "Target ring (thin border)", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderThickness = target_ring_thin_border, + BorderColour = Color4.White, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } + } + } + } + } + } + }); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + expandingRing.Colour = colours.YellowLight; + targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); + } + + public void OnUserInput(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) + { + float completion = (float)numHits / swell.HitObject.RequiredHits; + + mainPiece.Drawable.RotateTo((float)(completion * swell.HitObject.Duration / 8), 4000, Easing.OutQuint); + + expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); + + expandingRing + .FadeTo(expandingRing.Alpha + Math.Clamp(completion / 16, 0.1f, 0.6f), 50) + .Then() + .FadeTo(completion / 8, 2000, Easing.OutQuint); + } + + public void OnHitObjectEnd(ArmedState state, SkinnableDrawable mainPiece) + { + const double transition_duration = 300; + + bodyContainer.FadeOut(transition_duration, Easing.OutQuad); + bodyContainer.ScaleTo(1.4f, transition_duration); + mainPiece.FadeOut(transition_duration, Easing.OutQuad); + } + + public void ApplyPassiveTransforms(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) + { + if (swell.IsHit == false) + expandingRing.FadeTo(0); + + const double ring_appear_offset = 100; + + targetRing.Delay(ring_appear_offset).ScaleTo(target_ring_scale, 400, Easing.OutQuint); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs new file mode 100644 index 0000000000..240ec71f94 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -0,0 +1,136 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Game.Skinning; +using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Framework.Audio.Sample; +using osu.Game.Audio; +using osuTK; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Taiko.Skinning.Legacy +{ + public partial class LegacySwell : Container, ISkinnableSwell + { + private Container bodyContainer = null!; + private Sprite spinnerCircle = null!; + private Sprite shrinkingRing = null!; + private Sprite clearAnimation = null!; + private ISample? clearSample; + private LegacySpriteText remainingHitsCountdown = null!; + + private bool samplePlayed; + + public LegacySwell() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin, SkinManager skinManager) + { + Child = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(200f, 100f), + + Children = new Drawable[] + { + bodyContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + + Children = new Drawable[] + { + spinnerCircle = new Sprite + { + Texture = skin.GetTexture("spinner-circle"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(0.8f), + }, + shrinkingRing = new Sprite + { + Texture = skin.GetTexture("spinner-approachcircle") ?? skinManager.DefaultClassicSkin.GetTexture("spinner-approachcircle"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = Vector2.One, + }, + remainingHitsCountdown = new LegacySpriteText(LegacyFont.Combo) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(0f, 165f), + Scale = Vector2.One, + }, + } + }, + clearAnimation = new Sprite + { + // File extension is included here because of a GetTexture limitation, see #21543 + Texture = skin.GetTexture("spinner-osu.png"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(0f, -165f), + Scale = new Vector2(0.3f), + Alpha = 0, + }, + } + }; + + clearSample = skin.GetSample(new SampleInfo("spinner-osu")); + } + + public void OnUserInput(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) + { + remainingHitsCountdown.Text = $"{swell.HitObject.RequiredHits - numHits}"; + spinnerCircle.RotateTo(180f * numHits, 1000, Easing.OutQuint); + } + + public void OnHitObjectEnd(ArmedState state, SkinnableDrawable mainPiece) + { + const double clear_transition_duration = 300; + + bodyContainer.FadeOut(clear_transition_duration, Easing.OutQuad); + + if (state == ArmedState.Hit) + { + if (!samplePlayed) + { + clearSample?.Play(); + samplePlayed = true; + } + + clearAnimation + .FadeIn(clear_transition_duration, Easing.InQuad) + .ScaleTo(0.8f, clear_transition_duration, Easing.InQuad) + .Delay(700).FadeOut(200, Easing.OutQuad); + } + } + + public void ApplyPassiveTransforms(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) + { + if (swell.IsHit == false) + { + remainingHitsCountdown.Text = $"{swell.HitObject.RequiredHits}"; + samplePlayed = false; + } + + const double body_transition_duration = 100; + + mainPiece.FadeOut(body_transition_duration); + bodyContainer.FadeIn(body_transition_duration); + shrinkingRing.ResizeTo(0.1f, swell.HitObject.Duration); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs new file mode 100644 index 0000000000..40501d1d40 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Rulesets.Taiko.Skinning.Legacy +{ + internal partial class LegacySwellCirclePiece : Sprite + { + [BackgroundDependencyLoader] + private void load(ISkinSource skin, SkinManager skinManager) + { + Texture = skin.GetTexture("spinner-warning") ?? skinManager.DefaultClassicSkin.GetTexture("spinner-circle"); + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + Scale = skin.GetTexture("spinner-warning") != null ? Vector2.One : new Vector2(0.18f); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index 5bdb824f1c..243d975216 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -66,7 +66,15 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy return this.GetAnimation("sliderscorepoint", false, false); case TaikoSkinComponents.Swell: - // todo: support taiko legacy swell (https://github.com/ppy/osu/issues/13601). + if (GetTexture("spinner-circle") != null) + return new LegacySwell(); + + return null; + + case TaikoSkinComponents.SwellCirclePiece: + if (GetTexture("spinner-circle") != null) + return new LegacySwellCirclePiece(); + return null; case TaikoSkinComponents.HitTarget: diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index 28133ffcb2..aa7e4686d8 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -11,6 +11,7 @@ namespace osu.Game.Rulesets.Taiko DrumRollBody, DrumRollTick, Swell, + SwellCirclePiece, HitTarget, PlayfieldBackgroundLeft, PlayfieldBackgroundRight, From fe84e6e5f53d5a3264b1fcbe68fb698b7c039f48 Mon Sep 17 00:00:00 2001 From: Joppe27 Date: Sun, 11 Dec 2022 02:19:06 +0100 Subject: [PATCH 252/801] Adjust existing test to accommodate swell size --- osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs index c130b5f366..286b16aa34 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs @@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning { Anchor = Anchor.Centre, Origin = Anchor.Centre, + Scale = new osuTK.Vector2(0.5f), })); } From 988450a2c4f8244d1ef1bc572d711ee781eaaa09 Mon Sep 17 00:00:00 2001 From: Joppe27 Date: Sun, 11 Dec 2022 02:23:48 +0100 Subject: [PATCH 253/801] Add test for expire delay Delaying the expiry of the drawable hitobject can potentially be dangerous and gameplay-altering when user inputs are accidentally handled. This is why I found a test necessary. --- .../TestSceneDrawableSwellExpireDelay.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs new file mode 100644 index 0000000000..ad78ed3b20 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using NUnit.Framework; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Replays; +using osu.Game.Rulesets.Taiko.Tests.Judgements; + +namespace osu.Game.Rulesets.Taiko.Tests.Skinning +{ + public partial class TestSceneDrawableSwellExpireDelay : JudgementTest + { + [Test] + public void TestExpireDelay() + { + const double swell_start = 1000; + const double swell_duration = 1000; + + Swell swell = new Swell + { + StartTime = swell_start, + Duration = swell_duration, + }; + + Hit hit = new Hit { StartTime = swell_start + swell_duration + 50 }; + + List frames = new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(2100, TaikoAction.LeftCentre), + }; + + PerformTest(frames, CreateBeatmap(swell, hit)); + + AssertResult(0, HitResult.Ok); + } + } +} From e2196e8b9b97f447863b61124f7bd3454a505e60 Mon Sep 17 00:00:00 2001 From: Joppe27 Date: Tue, 13 Dec 2022 19:32:05 +0100 Subject: [PATCH 254/801] Rename methods and skin component + add comments --- .../Objects/Drawables/DrawableSwell.cs | 13 ++++++++----- osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs | 10 +++------- .../Skinning/Default/DefaultSwell.cs | 6 +++--- .../Skinning/Legacy/LegacySwell.cs | 6 +++--- .../Skinning/Legacy/TaikoLegacySkinTransformer.cs | 2 +- osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs | 2 +- 6 files changed, 19 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index cba044959c..54a609f7d3 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { FillMode = FillMode.Fit; - Content.Add(spinnerBody = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Swell), _ => new DefaultSwell()) + Content.Add(spinnerBody = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.SwellBody), _ => new DefaultSwell()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables int numHits = ticks.Count(r => r.IsHit); - (spinnerBody.Drawable as ISkinnableSwell)?.OnUserInput(this, numHits, MainPiece); + (spinnerBody.Drawable as ISkinnableSwell)?.AnimateSwellProgress(this, numHits, MainPiece); if (numHits == HitObject.RequiredHits) ApplyMaxResult(); @@ -167,7 +167,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { base.UpdateStartTimeStateTransforms(); - (spinnerBody.Drawable as ISkinnableSwell)?.ApplyPassiveTransforms(this, MainPiece); + (spinnerBody.Drawable as ISkinnableSwell)?.AnimateSwellStart(this, MainPiece); } protected override void UpdateHitStateTransforms(ArmedState state) @@ -175,16 +175,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables switch (state) { case ArmedState.Idle: + // Only for rewind support. Reallows user inputs if swell is rewound from being hit/missed to being idle. HandleUserInput = true; break; case ArmedState.Miss: case ArmedState.Hit: + const int clear_animation_duration = 1200; + // Postpone drawable hitobject expiration until it has animated/faded out. Inputs on the object are disallowed during this delay. - LifetimeEnd = Time.Current + 1200; + LifetimeEnd = Time.Current + clear_animation_duration; HandleUserInput = false; - (spinnerBody.Drawable as ISkinnableSwell)?.OnHitObjectEnd(state, MainPiece); + (spinnerBody.Drawable as ISkinnableSwell)?.AnimateSwellCompletion(state, MainPiece); break; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs index 18feff5bb9..3cdb3566fb 100644 --- a/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs @@ -9,14 +9,10 @@ namespace osu.Game.Rulesets.Taiko.Objects { public interface ISkinnableSwell { - void OnUserInput(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece); + void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece); - void OnHitObjectEnd(ArmedState state, SkinnableDrawable mainPiece); + void AnimateSwellCompletion(ArmedState state, SkinnableDrawable mainPiece); - /// - /// Applies passive transforms on HitObject start. Gets called every time DrawableTaikoHitobject - /// changes state. This happens on creation, and when the object is completed (as in hit or missed). - /// - void ApplyPassiveTransforms(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece); + void AnimateSwellStart(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece); } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs index e525e9873d..cec07d8769 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs @@ -106,7 +106,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); } - public void OnUserInput(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) + public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) { float completion = (float)numHits / swell.HitObject.RequiredHits; @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default .FadeTo(completion / 8, 2000, Easing.OutQuint); } - public void OnHitObjectEnd(ArmedState state, SkinnableDrawable mainPiece) + public void AnimateSwellCompletion(ArmedState state, SkinnableDrawable mainPiece) { const double transition_duration = 300; @@ -129,7 +129,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default mainPiece.FadeOut(transition_duration, Easing.OutQuad); } - public void ApplyPassiveTransforms(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) + public void AnimateSwellStart(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) { if (swell.IsHit == false) expandingRing.FadeTo(0); diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 240ec71f94..fdddea2df5 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -91,13 +91,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy clearSample = skin.GetSample(new SampleInfo("spinner-osu")); } - public void OnUserInput(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) + public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) { remainingHitsCountdown.Text = $"{swell.HitObject.RequiredHits - numHits}"; spinnerCircle.RotateTo(180f * numHits, 1000, Easing.OutQuint); } - public void OnHitObjectEnd(ArmedState state, SkinnableDrawable mainPiece) + public void AnimateSwellCompletion(ArmedState state, SkinnableDrawable mainPiece) { const double clear_transition_duration = 300; @@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } } - public void ApplyPassiveTransforms(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) + public void AnimateSwellStart(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) { if (swell.IsHit == false) { diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index 243d975216..b9ebed6b80 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy case TaikoSkinComponents.DrumRollTick: return this.GetAnimation("sliderscorepoint", false, false); - case TaikoSkinComponents.Swell: + case TaikoSkinComponents.SwellBody: if (GetTexture("spinner-circle") != null) return new LegacySwell(); diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index aa7e4686d8..0145fb6482 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Taiko RimHit, DrumRollBody, DrumRollTick, - Swell, + SwellBody, SwellCirclePiece, HitTarget, PlayfieldBackgroundLeft, From cf2d0e6911539a23f9f9ae41160b06b1bb52e91f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 31 Jan 2025 16:22:37 +0900 Subject: [PATCH 255/801] Fix results screen sounds persisting after exit --- osu.Game/Screens/Ranking/ResultsScreen.cs | 107 ++++++++++++---------- 1 file changed, 57 insertions(+), 50 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 5e91171051..95dbfb2712 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -64,6 +64,7 @@ namespace osu.Game.Screens.Ranking private Drawable bottomPanel = null!; private Container detachedPanelContainer = null!; + private AudioContainer audioContainer = null!; private bool lastFetchCompleted; @@ -100,76 +101,80 @@ namespace osu.Game.Screens.Ranking popInSample = audio.Samples.Get(@"UI/overlay-pop-in"); - InternalChild = new PopoverContainer + InternalChild = audioContainer = new AudioContainer { RelativeSizeAxes = Axes.Both, - Child = new GridContainer + Child = new PopoverContainer { RelativeSizeAxes = Axes.Both, - Content = new[] + Child = new GridContainer { - new Drawable[] + RelativeSizeAxes = Axes.Both, + Content = new[] { - VerticalScrollContent = new VerticalScrollContainer + new Drawable[] { - RelativeSizeAxes = Axes.Both, - ScrollbarVisible = false, - Child = new Container + VerticalScrollContent = new VerticalScrollContainer { RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + StatisticsPanel = createStatisticsPanel().With(panel => + { + panel.RelativeSizeAxes = Axes.Both; + panel.Score.BindTarget = SelectedScore; + }), + ScorePanelList = new ScorePanelList + { + RelativeSizeAxes = Axes.Both, + SelectedScore = { BindTarget = SelectedScore }, + PostExpandAction = () => StatisticsPanel.ToggleVisibility() + }, + detachedPanelContainer = new Container + { + RelativeSizeAxes = Axes.Both + }, + } + } + }, + }, + new[] + { + bottomPanel = new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = TwoLayerButton.SIZE_EXTENDED.Y, + Alpha = 0, Children = new Drawable[] { - StatisticsPanel = createStatisticsPanel().With(panel => - { - panel.RelativeSizeAxes = Axes.Both; - panel.Score.BindTarget = SelectedScore; - }), - ScorePanelList = new ScorePanelList + new Box { RelativeSizeAxes = Axes.Both, - SelectedScore = { BindTarget = SelectedScore }, - PostExpandAction = () => StatisticsPanel.ToggleVisibility() + Colour = Color4Extensions.FromHex("#333") }, - detachedPanelContainer = new Container + buttons = new FillFlowContainer { - RelativeSizeAxes = Axes.Both + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5), + Direction = FillDirection.Horizontal }, } } - }, - }, - new[] - { - bottomPanel = new Container - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - Height = TwoLayerButton.SIZE_EXTENDED.Y, - Alpha = 0, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#333") - }, - buttons = new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(5), - Direction = FillDirection.Horizontal - }, - } } + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize) } - }, - RowDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.AutoSize) } } }; @@ -330,6 +335,8 @@ namespace osu.Game.Screens.Ranking if (!skipExitTransition) this.FadeOut(100); + + audioContainer.Volume.Value = 0; return false; } From 20280cd1959d0ceecff45f1e11a7aff3cedd5768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 31 Jan 2025 09:01:42 +0100 Subject: [PATCH 256/801] Do not double up first control point of path --- osu.Game/Database/LegacyBeatmapExporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 9bb90ab461..8f94fc9e63 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -141,7 +141,7 @@ namespace osu.Game.Database // however, the Bézier path as output by the converter has multiple segments. // `LegacyBeatmapEncoder` will attempt to encode this by emitting per-control-point curve type specs which don't do anything for stable. // instead, stable expects control points that start a segment to be present in the path twice in succession. - if (convertedPoint.Type == PathType.BEZIER) + if (convertedPoint.Type == PathType.BEZIER && i > 0) hasPath.Path.ControlPoints.Add(new PathControlPoint(position)); } } From 8718483c702e7a69a2314d9fd515297615cb6920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 21 Jan 2025 15:18:59 +0100 Subject: [PATCH 257/801] Avoid moving already placed objects temporally when "limit distance snap to current time" is active --- .../HitCircles/HitCirclePlacementBlueprint.cs | 16 +++++++++++++++- .../Components/PathControlPointVisualiser.cs | 11 ++++++++++- .../Sliders/SliderPlacementBlueprint.cs | 12 ++++++++++-- .../Edit/OsuBlueprintContainer.cs | 15 +++++++++++++-- .../Edit/OsuHitObjectComposer.cs | 4 ++-- .../Editing/TestSceneDistanceSnapGrid.cs | 2 +- .../Components/CircularDistanceSnapGrid.cs | 9 +++------ .../Compose/Components/DistanceSnapGrid.cs | 19 +++++-------------- 8 files changed, 59 insertions(+), 29 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs index 93d79a50ab..61ed30259a 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs @@ -2,10 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Input.Events; +using osu.Game.Configuration; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit; using osuTK; using osuTK.Input; @@ -20,12 +23,23 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles [Resolved] private OsuHitObjectComposer? composer { get; set; } + [Resolved] + private EditorClock? editorClock { get; set; } + + private Bindable limitedDistanceSnap { get; set; } = null!; + public HitCirclePlacementBlueprint() : base(new HitCircle()) { InternalChild = circlePiece = new HitCirclePiece(); } + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + limitedDistanceSnap = config.GetBindable(OsuSetting.EditorLimitedDistanceSnap); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -53,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime); - result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition); + result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition, limitedDistanceSnap.Value && editorClock != null ? editorClock.CurrentTime : null); if (composer?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? screenSpacePosition, result?.Time ?? fallbackTime) is SnapResult gridSnapResult) result = gridSnapResult; result ??= new SnapResult(screenSpacePosition, fallbackTime); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 189bb005a7..b9938209ae 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -21,6 +21,7 @@ using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -55,6 +56,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components [Resolved(CanBeNull = true)] private IDistanceSnapProvider distanceSnapProvider { get; set; } + private Bindable limitedDistanceSnap { get; set; } = null!; + public PathControlPointVisualiser(T hitObject, bool allowSelection) { this.hitObject = hitObject; @@ -69,6 +72,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components }; } + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + limitedDistanceSnap = config.GetBindable(OsuSetting.EditorLimitedDistanceSnap); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -437,7 +446,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex])); var result = positionSnapProvider?.TrySnapToNearbyObjects(newHeadPosition, oldStartTime); - result ??= positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition); + result ??= positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition, limitedDistanceSnap.Value ? oldStartTime : null); if (positionSnapProvider?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? newHeadPosition, result?.Time ?? oldStartTime) is SnapResult gridSnapResult) result = gridSnapResult; result ??= new SnapResult(newHeadPosition, oldStartTime); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 1012578375..21817045c4 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -5,10 +5,12 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -49,6 +51,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders [Resolved] private FreehandSliderToolboxGroup? freehandToolboxGroup { get; set; } + [Resolved] + private EditorClock? editorClock { get; set; } + + private Bindable limitedDistanceSnap { get; set; } = null!; + private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 }; protected override bool IsValidForPlacement => HitObject.Path.HasValidLength; @@ -63,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } [BackgroundDependencyLoader] - private void load() + private void load(OsuConfigManager config) { InternalChildren = new Drawable[] { @@ -74,6 +81,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders }; state = SliderPlacementState.Initial; + limitedDistanceSnap = config.GetBindable(OsuSetting.EditorLimitedDistanceSnap); } protected override void LoadComplete() @@ -109,7 +117,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime); - result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition); + result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition, limitedDistanceSnap.Value && editorClock != null ? editorClock.CurrentTime : null); if (composer?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? screenSpacePosition, result?.Time ?? fallbackTime) is SnapResult gridSnapResult) result = gridSnapResult; result ??= new SnapResult(screenSpacePosition, fallbackTime); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs index 5eff95adec..9d82046c23 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs @@ -3,7 +3,10 @@ using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Input.Events; +using osu.Game.Configuration; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; @@ -17,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Edit { public partial class OsuBlueprintContainer : ComposeBlueprintContainer { + private Bindable limitedDistanceSnap { get; set; } = null!; + public new OsuHitObjectComposer Composer => (OsuHitObjectComposer)base.Composer; public OsuBlueprintContainer(OsuHitObjectComposer composer) @@ -24,6 +29,12 @@ namespace osu.Game.Rulesets.Osu.Edit { } + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + limitedDistanceSnap = config.GetBindable(OsuSetting.EditorLimitedDistanceSnap); + } + protected override SelectionHandler CreateSelectionHandler() => new OsuSelectionHandler(); public override HitObjectSelectionBlueprint? CreateHitObjectBlueprintFor(HitObject hitObject) @@ -58,15 +69,15 @@ namespace osu.Game.Rulesets.Osu.Edit // The final movement position, relative to movementBlueprintOriginalPosition. Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; + var referenceBlueprint = blueprints.First().blueprint; // Retrieve a snapped position. var result = Composer.TrySnapToNearbyObjects(movePosition); - result ??= Composer.TrySnapToDistanceGrid(movePosition); + result ??= Composer.TrySnapToDistanceGrid(movePosition, limitedDistanceSnap.Value ? referenceBlueprint.Item.StartTime : null); if (Composer.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? movePosition, result?.Time) is SnapResult gridSnapResult) result = gridSnapResult; result ??= new SnapResult(movePosition, null); - var referenceBlueprint = blueprints.First().blueprint; bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); if (moved) ApplySnapResultTime(result, referenceBlueprint.Item.StartTime); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index e08968e1aa..563d0b1e3e 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -250,13 +250,13 @@ namespace osu.Game.Rulesets.Osu.Edit } [CanBeNull] - public SnapResult TrySnapToDistanceGrid(Vector2 screenSpacePosition) + public SnapResult TrySnapToDistanceGrid(Vector2 screenSpacePosition, double? fixedTime = null) { if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null) return null; var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); - (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition)); + (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition), fixedTime); return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, playfield); } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs index c1a788cd22..818862d958 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs @@ -181,7 +181,7 @@ namespace osu.Game.Tests.Visual.Editing } } - public override (Vector2 position, double time) GetSnappedPosition(Vector2 screenSpacePosition) + public override (Vector2 position, double time) GetSnappedPosition(Vector2 screenSpacePosition, double? fixedTime = null) => (Vector2.Zero, 0); } diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs index bd750dac76..e84c2ebc35 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs @@ -16,9 +16,6 @@ namespace osu.Game.Screens.Edit.Compose.Components { public abstract partial class CircularDistanceSnapGrid : DistanceSnapGrid { - [Resolved] - private EditorClock editorClock { get; set; } = null!; - protected CircularDistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null) : base(referenceObject, startPosition, startTime, endTime) { @@ -76,7 +73,7 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - public override (Vector2 position, double time) GetSnappedPosition(Vector2 position) + public override (Vector2 position, double time) GetSnappedPosition(Vector2 position, double? fixedTime = null) { if (MaxIntervals == 0) return (StartPosition, StartTime); @@ -100,8 +97,8 @@ namespace osu.Game.Screens.Edit.Compose.Components if (travelLength < DistanceBetweenTicks) travelLength = DistanceBetweenTicks; - float snappedDistance = LimitedDistanceSnap.Value - ? SnapProvider.DurationToDistance(ReferenceObject, editorClock.CurrentTime - ReferenceObject.GetEndTime()) + float snappedDistance = fixedTime != null + ? SnapProvider.DurationToDistance(ReferenceObject, fixedTime.Value - ReferenceObject.GetEndTime()) // When interacting with the resolved snap provider, the distance spacing multiplier should first be removed // to allow for snapping at a non-multiplied ratio. : SnapProvider.FindSnappedDistance(ReferenceObject, travelLength / distanceSpacingMultiplier, DistanceSnapTarget.End); diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs index 7003d632ca..aaf58e0f7a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs @@ -10,7 +10,6 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Layout; -using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -61,18 +60,6 @@ namespace osu.Game.Screens.Edit.Compose.Components [Resolved] private BindableBeatDivisor beatDivisor { get; set; } - /// - /// When enabled, distance snap should only snap to the current time (as per the editor clock). - /// This is to emulate stable behaviour. - /// - protected Bindable LimitedDistanceSnap { get; private set; } - - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - LimitedDistanceSnap = config.GetBindable(OsuSetting.EditorLimitedDistanceSnap); - } - private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); protected readonly HitObject ReferenceObject; @@ -143,8 +130,12 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Snaps a position to this grid. /// /// The original position in coordinate space local to this . + /// + /// Whether the snap operation should be temporally constrained to a particular time instant, + /// thus fixing the possible positions to a set distance from the . + /// /// A tuple containing the snapped position in coordinate space local to this and the respective time value. - public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position); + public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position, double? fixedTime = null); /// /// Retrieves the applicable colour for a beat index. From 4fd8a4dc5a6f0453767175aa706ea331bbfca7c6 Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Fri, 31 Jan 2025 16:55:39 +0800 Subject: [PATCH 258/801] Merge taiko swell components Per , taking a variation of the "Make all swell main pieces implement ISkinnableSwellPart" path. Should clean the interface up enough for further refactors. --- .../Objects/Drawables/DrawableSwell.cs | 25 ++++-------- .../Objects/ISkinnableSwell.cs | 7 ++-- .../Skinning/Argon/ArgonSwell.cs | 20 ++++++++++ .../Argon/TaikoArgonSkinTransformer.cs | 4 +- .../Skinning/Default/DefaultSwell.cs | 26 +++++++++---- ...wellSymbolPiece.cs => SwellCirclePiece.cs} | 0 .../Skinning/Legacy/LegacySwell.cs | 38 +++++++++++-------- .../Legacy/TaikoLegacySkinTransformer.cs | 6 --- .../TaikoSkinComponents.cs | 1 - 9 files changed, 75 insertions(+), 52 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs rename osu.Game.Rulesets.Taiko/Skinning/Default/{SwellSymbolPiece.cs => SwellCirclePiece.cs} (100%) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 54a609f7d3..18d76d02a1 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -27,8 +27,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private Vector2 baseSize; - private readonly SkinnableDrawable spinnerBody; - private readonly Container ticks; private double? lastPressHandleTime; @@ -50,24 +48,17 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { FillMode = FillMode.Fit; - Content.Add(spinnerBody = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.SwellBody), _ => new DefaultSwell()) + AddInternal(ticks = new Container { RelativeSizeAxes = Axes.Both }); + } + + protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.SwellBody), + _ => new DefaultSwell { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, }); - AddInternal(ticks = new Container { RelativeSizeAxes = Axes.Both }); - } - - protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.SwellCirclePiece), - _ => new SwellCirclePiece - { - // to allow for rotation transform - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }); - protected override void RecreatePieces() { base.RecreatePieces(); @@ -132,7 +123,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables int numHits = ticks.Count(r => r.IsHit); - (spinnerBody.Drawable as ISkinnableSwell)?.AnimateSwellProgress(this, numHits, MainPiece); + (MainPiece.Drawable as ISkinnableSwell)?.AnimateSwellProgress(this, numHits); if (numHits == HitObject.RequiredHits) ApplyMaxResult(); @@ -167,7 +158,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { base.UpdateStartTimeStateTransforms(); - (spinnerBody.Drawable as ISkinnableSwell)?.AnimateSwellStart(this, MainPiece); + (MainPiece.Drawable as ISkinnableSwell)?.AnimateSwellStart(this); } protected override void UpdateHitStateTransforms(ArmedState state) @@ -187,7 +178,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables LifetimeEnd = Time.Current + clear_animation_duration; HandleUserInput = false; - (spinnerBody.Drawable as ISkinnableSwell)?.AnimateSwellCompletion(state, MainPiece); + (MainPiece.Drawable as ISkinnableSwell)?.AnimateSwellCompletion(state); break; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs index 3cdb3566fb..9bd169acd7 100644 --- a/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs @@ -3,16 +3,15 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects.Drawables; -using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Objects { public interface ISkinnableSwell { - void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece); + void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits); - void AnimateSwellCompletion(ArmedState state, SkinnableDrawable mainPiece); + void AnimateSwellCompletion(ArmedState state); - void AnimateSwellStart(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece); + void AnimateSwellStart(DrawableTaikoHitObject swell); } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs new file mode 100644 index 0000000000..65cd936e38 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Game.Rulesets.Taiko.Skinning.Default; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + public partial class ArgonSwell : DefaultSwell + { + protected override Drawable CreateCentreCircle() + { + return new ArgonSwellCirclePiece() + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index cfd30dd628..b588a22d12 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -68,8 +68,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon case TaikoSkinComponents.TaikoExplosionOk: return new ArgonHitExplosion(taikoComponent.Component); - case TaikoSkinComponents.SwellCirclePiece: - return new ArgonSwellCirclePiece(); + case TaikoSkinComponents.SwellBody: + return new ArgonSwell(); } break; diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs index cec07d8769..bdb444db90 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs @@ -11,7 +11,6 @@ using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; -using osu.Game.Skinning; using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Skinning.Default @@ -26,6 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default private readonly Container bodyContainer; private readonly CircularContainer targetRing; private readonly CircularContainer expandingRing; + private readonly Drawable centreCircle; public DefaultSwell() { @@ -35,6 +35,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default Content.Add(bodyContainer = new Container { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Depth = 1, Children = new Drawable[] @@ -94,11 +96,21 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default } } } - } + }, + centreCircle = CreateCentreCircle(), } }); } + protected virtual Drawable CreateCentreCircle() + { + return new SwellCirclePiece() + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -106,11 +118,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); } - public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) + public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits) { float completion = (float)numHits / swell.HitObject.RequiredHits; - mainPiece.Drawable.RotateTo((float)(completion * swell.HitObject.Duration / 8), 4000, Easing.OutQuint); + centreCircle.RotateTo((float)(completion * swell.HitObject.Duration / 8), 4000, Easing.OutQuint); expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); @@ -120,16 +132,16 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default .FadeTo(completion / 8, 2000, Easing.OutQuint); } - public void AnimateSwellCompletion(ArmedState state, SkinnableDrawable mainPiece) + public void AnimateSwellCompletion(ArmedState state) { const double transition_duration = 300; bodyContainer.FadeOut(transition_duration, Easing.OutQuad); bodyContainer.ScaleTo(1.4f, transition_duration); - mainPiece.FadeOut(transition_duration, Easing.OutQuad); + centreCircle.FadeOut(transition_duration, Easing.OutQuad); } - public void AnimateSwellStart(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) + public void AnimateSwellStart(DrawableTaikoHitObject swell) { if (swell.IsHit == false) expandingRing.FadeTo(0); diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/SwellSymbolPiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/SwellCirclePiece.cs similarity index 100% rename from osu.Game.Rulesets.Taiko/Skinning/Default/SwellSymbolPiece.cs rename to osu.Game.Rulesets.Taiko/Skinning/Default/SwellCirclePiece.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index fdddea2df5..e487c5e051 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -18,6 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy public partial class LegacySwell : Container, ISkinnableSwell { private Container bodyContainer = null!; + private Sprite warning = null!; private Sprite spinnerCircle = null!; private Sprite shrinkingRing = null!; private Sprite clearAnimation = null!; @@ -40,14 +41,21 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Position = new Vector2(200f, 100f), Children = new Drawable[] { + warning = new Sprite + { + Texture = skin.GetTexture("spinner-warning") ?? skinManager.DefaultClassicSkin.GetTexture("spinner-circle"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = skin.GetTexture("spinner-warning") != null ? Vector2.One : new Vector2(0.18f), + }, bodyContainer = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, + Position = new Vector2(200f, 100f), Alpha = 0, Children = new Drawable[] @@ -73,31 +81,31 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy Position = new Vector2(0f, 165f), Scale = Vector2.One, }, + clearAnimation = new Sprite + { + // File extension is included here because of a GetTexture limitation, see #21543 + Texture = skin.GetTexture("spinner-osu.png"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(0f, -165f), + Scale = new Vector2(0.3f), + Alpha = 0, + }, } }, - clearAnimation = new Sprite - { - // File extension is included here because of a GetTexture limitation, see #21543 - Texture = skin.GetTexture("spinner-osu.png"), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Position = new Vector2(0f, -165f), - Scale = new Vector2(0.3f), - Alpha = 0, - }, } }; clearSample = skin.GetSample(new SampleInfo("spinner-osu")); } - public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) + public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits) { remainingHitsCountdown.Text = $"{swell.HitObject.RequiredHits - numHits}"; spinnerCircle.RotateTo(180f * numHits, 1000, Easing.OutQuint); } - public void AnimateSwellCompletion(ArmedState state, SkinnableDrawable mainPiece) + public void AnimateSwellCompletion(ArmedState state) { const double clear_transition_duration = 300; @@ -118,7 +126,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } } - public void AnimateSwellStart(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) + public void AnimateSwellStart(DrawableTaikoHitObject swell) { if (swell.IsHit == false) { @@ -128,7 +136,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy const double body_transition_duration = 100; - mainPiece.FadeOut(body_transition_duration); + warning.FadeOut(body_transition_duration); bodyContainer.FadeIn(body_transition_duration); shrinkingRing.ResizeTo(0.1f, swell.HitObject.Duration); } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index b9ebed6b80..8fa4551fd4 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -71,12 +71,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy return null; - case TaikoSkinComponents.SwellCirclePiece: - if (GetTexture("spinner-circle") != null) - return new LegacySwellCirclePiece(); - - return null; - case TaikoSkinComponents.HitTarget: if (GetTexture("taikobigcircle") != null) return new TaikoLegacyHitTarget(); diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index 0145fb6482..05c6316a05 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -11,7 +11,6 @@ namespace osu.Game.Rulesets.Taiko DrumRollBody, DrumRollTick, SwellBody, - SwellCirclePiece, HitTarget, PlayfieldBackgroundLeft, PlayfieldBackgroundRight, From 2a5540b39251c19f46a2965f0226f45d7a085f3e Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Fri, 31 Jan 2025 17:51:35 +0800 Subject: [PATCH 259/801] remove ISkinnableSwell This commit removes ISkinnableSwell for taiko swell animations. In place of it, an event named UpdateHitProgress is added to DrawableSwell, and the skin swells are converted to listen to said event and ApplyCustomUpdateState, like how spinner skinning is implemented for std. --- .../Objects/Drawables/DrawableSwell.cs | 6 +- .../Objects/ISkinnableSwell.cs | 17 ----- .../Skinning/Default/DefaultSwell.cs | 70 +++++++++++------ .../Skinning/Legacy/LegacySwell.cs | 75 ++++++++++++------- 4 files changed, 101 insertions(+), 67 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 18d76d02a1..e0276db911 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -38,6 +38,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// public bool MustAlternate { get; internal set; } = true; + public event Action UpdateHitProgress; + public DrawableSwell() : this(null) { @@ -123,7 +125,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables int numHits = ticks.Count(r => r.IsHit); - (MainPiece.Drawable as ISkinnableSwell)?.AnimateSwellProgress(this, numHits); + UpdateHitProgress?.Invoke(numHits, HitObject.RequiredHits); if (numHits == HitObject.RequiredHits) ApplyMaxResult(); @@ -158,7 +160,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { base.UpdateStartTimeStateTransforms(); - (MainPiece.Drawable as ISkinnableSwell)?.AnimateSwellStart(this); } protected override void UpdateHitStateTransforms(ArmedState state) @@ -178,7 +179,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables LifetimeEnd = Time.Current + clear_animation_duration; HandleUserInput = false; - (MainPiece.Drawable as ISkinnableSwell)?.AnimateSwellCompletion(state); break; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs deleted file mode 100644 index 9bd169acd7..0000000000 --- a/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs +++ /dev/null @@ -1,17 +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.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Taiko.Objects.Drawables; - -namespace osu.Game.Rulesets.Taiko.Objects -{ - public interface ISkinnableSwell - { - void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits); - - void AnimateSwellCompletion(ArmedState state); - - void AnimateSwellStart(DrawableTaikoHitObject swell); - } -} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs index bdb444db90..852116cbfe 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -15,13 +16,15 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Skinning.Default { - public partial class DefaultSwell : Container, ISkinnableSwell + public partial class DefaultSwell : Container { private const float target_ring_thick_border = 1.4f; private const float target_ring_thin_border = 1f; private const float target_ring_scale = 5f; private const float inner_ring_alpha = 0.65f; + private DrawableSwell drawableSwell = null!; + private readonly Container bodyContainer; private readonly CircularContainer targetRing; private readonly CircularContainer expandingRing; @@ -102,6 +105,17 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default }); } + [BackgroundDependencyLoader] + private void load(DrawableHitObject hitObject, OsuColour colours) + { + drawableSwell = (DrawableSwell)hitObject; + drawableSwell.UpdateHitProgress += animateSwellProgress; + drawableSwell.ApplyCustomUpdateState += updateStateTransforms; + + expandingRing.Colour = colours.YellowLight; + targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); + } + protected virtual Drawable CreateCentreCircle() { return new SwellCirclePiece() @@ -111,18 +125,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default }; } - [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void animateSwellProgress(int numHits, int requiredHits) { - expandingRing.Colour = colours.YellowLight; - targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); - } + float completion = (float)numHits / requiredHits; - public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits) - { - float completion = (float)numHits / swell.HitObject.RequiredHits; - - centreCircle.RotateTo((float)(completion * swell.HitObject.Duration / 8), 4000, Easing.OutQuint); + centreCircle.RotateTo((float)(completion * drawableSwell.HitObject.Duration / 8), 4000, Easing.OutQuint); expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); @@ -132,23 +139,42 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default .FadeTo(completion / 8, 2000, Easing.OutQuint); } - public void AnimateSwellCompletion(ArmedState state) + private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { - const double transition_duration = 300; + if (!(drawableHitObject is DrawableSwell drawableSwell)) + return; - bodyContainer.FadeOut(transition_duration, Easing.OutQuad); - bodyContainer.ScaleTo(1.4f, transition_duration); - centreCircle.FadeOut(transition_duration, Easing.OutQuad); + Swell swell = drawableSwell.HitObject; + + using (BeginAbsoluteSequence(swell.StartTime)) + { + if (state == ArmedState.Idle) + expandingRing.FadeTo(0); + + const double ring_appear_offset = 100; + + targetRing.Delay(ring_appear_offset).ScaleTo(target_ring_scale, 400, Easing.OutQuint); + } + + using (BeginAbsoluteSequence(drawableSwell.HitStateUpdateTime)) + { + const double transition_duration = 300; + + bodyContainer.FadeOut(transition_duration, Easing.OutQuad); + bodyContainer.ScaleTo(1.4f, transition_duration); + centreCircle.FadeOut(transition_duration, Easing.OutQuad); + } } - public void AnimateSwellStart(DrawableTaikoHitObject swell) + protected override void Dispose(bool isDisposing) { - if (swell.IsHit == false) - expandingRing.FadeTo(0); + base.Dispose(isDisposing); - const double ring_appear_offset = 100; - - targetRing.Delay(ring_appear_offset).ScaleTo(target_ring_scale, 400, Easing.OutQuint); + if (drawableSwell.IsNotNull()) + { + drawableSwell.UpdateHitProgress -= animateSwellProgress; + drawableSwell.ApplyCustomUpdateState -= updateStateTransforms; + } } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index e487c5e051..60a0b1d951 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -12,11 +12,14 @@ using osu.Framework.Audio.Sample; using osu.Game.Audio; using osuTK; using osu.Game.Rulesets.Objects.Drawables; +using osu.Framework.Extensions.ObjectExtensions; namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { - public partial class LegacySwell : Container, ISkinnableSwell + public partial class LegacySwell : Container { + private DrawableSwell drawableSwell = null!; + private Container bodyContainer = null!; private Sprite warning = null!; private Sprite spinnerCircle = null!; @@ -35,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } [BackgroundDependencyLoader] - private void load(ISkinSource skin, SkinManager skinManager) + private void load(DrawableHitObject hitObject, ISkinSource skin, SkinManager skinManager) { Child = new Container { @@ -96,49 +99,71 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } }; + drawableSwell = (DrawableSwell)hitObject; + drawableSwell.UpdateHitProgress += animateSwellProgress; + drawableSwell.ApplyCustomUpdateState += updateStateTransforms; clearSample = skin.GetSample(new SampleInfo("spinner-osu")); } - public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits) + private void animateSwellProgress(int numHits, int requiredHits) { - remainingHitsCountdown.Text = $"{swell.HitObject.RequiredHits - numHits}"; + remainingHitsCountdown.Text = $"{requiredHits - numHits}"; spinnerCircle.RotateTo(180f * numHits, 1000, Easing.OutQuint); } - public void AnimateSwellCompletion(ArmedState state) + private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { - const double clear_transition_duration = 300; + if (!(drawableHitObject is DrawableSwell drawableSwell)) + return; - bodyContainer.FadeOut(clear_transition_duration, Easing.OutQuad); + Swell swell = drawableSwell.HitObject; - if (state == ArmedState.Hit) + using (BeginAbsoluteSequence(swell.StartTime)) { - if (!samplePlayed) + if (state == ArmedState.Idle) { - clearSample?.Play(); - samplePlayed = true; + remainingHitsCountdown.Text = $"{swell.RequiredHits}"; + samplePlayed = false; } - clearAnimation - .FadeIn(clear_transition_duration, Easing.InQuad) - .ScaleTo(0.8f, clear_transition_duration, Easing.InQuad) - .Delay(700).FadeOut(200, Easing.OutQuad); + const double body_transition_duration = 100; + + warning.FadeOut(body_transition_duration); + bodyContainer.FadeIn(body_transition_duration); + shrinkingRing.ResizeTo(0.1f, swell.Duration); + } + + using (BeginAbsoluteSequence(drawableSwell.HitStateUpdateTime)) + { + const double clear_transition_duration = 300; + + bodyContainer.FadeOut(clear_transition_duration, Easing.OutQuad); + + if (state == ArmedState.Hit) + { + if (!samplePlayed) + { + clearSample?.Play(); + samplePlayed = true; + } + + clearAnimation + .FadeIn(clear_transition_duration, Easing.InQuad) + .ScaleTo(0.8f, clear_transition_duration, Easing.InQuad) + .Delay(700).FadeOut(200, Easing.OutQuad); + } } } - public void AnimateSwellStart(DrawableTaikoHitObject swell) + protected override void Dispose(bool isDisposing) { - if (swell.IsHit == false) + base.Dispose(isDisposing); + + if (drawableSwell.IsNotNull()) { - remainingHitsCountdown.Text = $"{swell.HitObject.RequiredHits}"; - samplePlayed = false; + drawableSwell.UpdateHitProgress -= animateSwellProgress; + drawableSwell.ApplyCustomUpdateState -= updateStateTransforms; } - - const double body_transition_duration = 100; - - warning.FadeOut(body_transition_duration); - bodyContainer.FadeIn(body_transition_duration); - shrinkingRing.ResizeTo(0.1f, swell.HitObject.Duration); } } } From ad2b469b143d74da7843a42563fe3e170a53d35c Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Fri, 31 Jan 2025 18:52:19 +0800 Subject: [PATCH 260/801] remove spinner-osu.png workaround https://github.com/ppy/osu/issues/22084 --- osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 60a0b1d951..405b0b7692 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -86,8 +86,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy }, clearAnimation = new Sprite { - // File extension is included here because of a GetTexture limitation, see #21543 - Texture = skin.GetTexture("spinner-osu.png"), + Texture = skin.GetTexture("spinner-osu"), Anchor = Anchor.Centre, Origin = Anchor.Centre, Position = new Vector2(0f, -165f), From c3981f1097f1d7d3a29422261ad39d43819cf1dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 31 Jan 2025 12:05:30 +0100 Subject: [PATCH 261/801] Do not reset online info on beatmap save --- .../Editing/TestSceneLocallyModifyingOnlineBeatmaps.cs | 6 +++++- osu.Game/Beatmaps/BeatmapManager.cs | 3 --- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneLocallyModifyingOnlineBeatmaps.cs b/osu.Game.Tests/Visual/Editing/TestSceneLocallyModifyingOnlineBeatmaps.cs index 7f9a69833c..636b3f54d8 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneLocallyModifyingOnlineBeatmaps.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneLocallyModifyingOnlineBeatmaps.cs @@ -4,6 +4,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Extensions; +using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Tests.Resources; @@ -25,13 +26,16 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestLocallyModifyingOnlineBeatmap() { + string initialHash = string.Empty; AddAssert("editor beatmap has online ID", () => EditorBeatmap.BeatmapInfo.OnlineID, () => Is.GreaterThan(0)); + AddStep("store hash for later", () => initialHash = EditorBeatmap.BeatmapInfo.MD5Hash); AddStep("delete first hitobject", () => EditorBeatmap.RemoveAt(0)); SaveEditor(); ReloadEditorToSameBeatmap(); - AddAssert("editor beatmap online ID reset", () => EditorBeatmap.BeatmapInfo.OnlineID, () => Is.EqualTo(-1)); + AddAssert("beatmap marked as locally modified", () => EditorBeatmap.BeatmapInfo.Status, () => Is.EqualTo(BeatmapOnlineStatus.LocallyModified)); + AddAssert("beatmap hash changed", () => EditorBeatmap.BeatmapInfo.MD5Hash, () => Is.Not.EqualTo(initialHash)); } } } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index aa67d3c548..1e66b28b15 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -475,11 +475,8 @@ namespace osu.Game.Beatmaps beatmapContent.BeatmapInfo = beatmapInfo; // Since now this is a locally-modified beatmap, we also set all relevant flags to indicate this. - // Importantly, the `ResetOnlineInfo()` call must happen before encoding, as online ID is encoded into the `.osu` file, - // which influences the beatmap checksums. beatmapInfo.LastLocalUpdate = DateTimeOffset.Now; beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified; - beatmapInfo.ResetOnlineInfo(); Realm.Write(r => { From 7ef861670379b42ce17ba648c5e5d016fa4a995e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 31 Jan 2025 12:22:05 +0100 Subject: [PATCH 262/801] Fix broken user-facing messaging when beatmap hash mismatch is detected --- osu.Game/Screens/Play/SubmittingPlayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 24c5b2c3d4..0a230ea00b 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -152,7 +152,7 @@ namespace osu.Game.Screens.Play Logger.Log($"Please ensure that you are using the latest version of the official game releases.\n\n{whatWillHappen}", level: LogLevel.Important); break; - case @"invalid beatmap_hash": + case @"invalid or missing beatmap_hash": Logger.Log($"This beatmap does not match the online version. Please update or redownload it.\n\n{whatWillHappen}", level: LogLevel.Important); break; From ac17b4065f06571cc3bf30cc7536e4746a78e9d3 Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Fri, 31 Jan 2025 19:55:29 +0800 Subject: [PATCH 263/801] change legacy spinner animations to match stable Also removed a few fallbacks pointed out in code review that I don't understand. --- .../Skinning/Legacy/LegacySwell.cs | 46 +++++++++++++------ 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 405b0b7692..9ed21b1bb0 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -13,6 +13,7 @@ using osu.Game.Audio; using osuTK; using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Extensions.ObjectExtensions; +using System; namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { @@ -23,10 +24,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private Container bodyContainer = null!; private Sprite warning = null!; private Sprite spinnerCircle = null!; - private Sprite shrinkingRing = null!; + private Sprite approachCircle = null!; private Sprite clearAnimation = null!; private ISample? clearSample; - private LegacySpriteText remainingHitsCountdown = null!; + private LegacySpriteText remainingHitsText = null!; private bool samplePlayed; @@ -40,6 +41,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy [BackgroundDependencyLoader] private void load(DrawableHitObject hitObject, ISkinSource skin, SkinManager skinManager) { + var spinnerCircleProvider = skin.FindProvider(s => s.GetTexture("spinner-circle") != null); + Child = new Container { Anchor = Anchor.Centre, @@ -49,7 +52,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { warning = new Sprite { - Texture = skin.GetTexture("spinner-warning") ?? skinManager.DefaultClassicSkin.GetTexture("spinner-circle"), + Texture = skin.GetTexture("spinner-warning"), Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = skin.GetTexture("spinner-warning") != null ? Vector2.One : new Vector2(0.18f), @@ -70,14 +73,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy Origin = Anchor.Centre, Scale = new Vector2(0.8f), }, - shrinkingRing = new Sprite + approachCircle = new Sprite { - Texture = skin.GetTexture("spinner-approachcircle") ?? skinManager.DefaultClassicSkin.GetTexture("spinner-approachcircle"), + Texture = skin.GetTexture("spinner-approachcircle"), Anchor = Anchor.Centre, Origin = Anchor.Centre, - Scale = Vector2.One, + Scale = new Vector2(1.86f * 0.8f), }, - remainingHitsCountdown = new LegacySpriteText(LegacyFont.Combo) + remainingHitsText = new LegacySpriteText(LegacyFont.Combo) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -106,8 +109,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private void animateSwellProgress(int numHits, int requiredHits) { - remainingHitsCountdown.Text = $"{requiredHits - numHits}"; - spinnerCircle.RotateTo(180f * numHits, 1000, Easing.OutQuint); + remainingHitsText.Text = $"{requiredHits - numHits}"; + remainingHitsText.ScaleTo(1.6f - 0.6f * ((float)numHits / requiredHits), 60, Easing.OutQuad); + + spinnerCircle.ClearTransforms(); + spinnerCircle + .RotateTo(180f * numHits, 1000, Easing.OutQuint) + .ScaleTo(Math.Min(0.94f, spinnerCircle.Scale.X + 0.02f)) + .ScaleTo(0.8f, 400, Easing.OutQuad); } private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) @@ -121,7 +130,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { if (state == ArmedState.Idle) { - remainingHitsCountdown.Text = $"{swell.RequiredHits}"; + remainingHitsText.Text = $"{swell.RequiredHits}"; samplePlayed = false; } @@ -129,14 +138,17 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy warning.FadeOut(body_transition_duration); bodyContainer.FadeIn(body_transition_duration); - shrinkingRing.ResizeTo(0.1f, swell.Duration); + approachCircle.ResizeTo(0.1f * 0.8f, swell.Duration); } using (BeginAbsoluteSequence(drawableSwell.HitStateUpdateTime)) { const double clear_transition_duration = 300; + const double clear_fade_in = 120; - bodyContainer.FadeOut(clear_transition_duration, Easing.OutQuad); + bodyContainer + .FadeOut(clear_transition_duration, Easing.OutQuad) + .ScaleTo(1.05f, clear_transition_duration, Easing.OutQuad); if (state == ArmedState.Hit) { @@ -147,9 +159,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } clearAnimation - .FadeIn(clear_transition_duration, Easing.InQuad) - .ScaleTo(0.8f, clear_transition_duration, Easing.InQuad) - .Delay(700).FadeOut(200, Easing.OutQuad); + .FadeIn(clear_fade_in) + .MoveTo(new Vector2(320, 240)) + .ScaleTo(0.4f) + .MoveTo(new Vector2(320, 150), clear_fade_in * 2, Easing.OutQuad) + .ScaleTo(1f, clear_fade_in * 2, Easing.Out) + .Delay(clear_fade_in * 3) + .FadeOut(clear_fade_in * 2.5); } } } From a62a84a30f7e92b9a855dfba7ddeb5c42a2bb442 Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Fri, 31 Jan 2025 20:48:29 +0800 Subject: [PATCH 264/801] fix code style --- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs | 6 ------ osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs | 2 +- osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs | 6 +++--- osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs | 2 +- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index e0276db911..363a6bf8e1 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -156,12 +156,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } } - protected override void UpdateStartTimeStateTransforms() - { - base.UpdateStartTimeStateTransforms(); - - } - protected override void UpdateHitStateTransforms(ArmedState state) { switch (state) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs index 65cd936e38..3b3684d219 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon { protected override Drawable CreateCentreCircle() { - return new ArgonSwellCirclePiece() + return new ArgonSwellCirclePiece { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs index 852116cbfe..a588f866c6 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Depth = 1, - Children = new Drawable[] + Children = new[] { expandingRing = new CircularContainer { @@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default protected virtual Drawable CreateCentreCircle() { - return new SwellCirclePiece() + return new SwellCirclePiece { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -141,7 +141,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { - if (!(drawableHitObject is DrawableSwell drawableSwell)) + if (!(drawableHitObject is DrawableSwell)) return; Swell swell = drawableSwell.HitObject; diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 9ed21b1bb0..43b2d5c435 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { - if (!(drawableHitObject is DrawableSwell drawableSwell)) + if (!(drawableHitObject is DrawableSwell)) return; Swell swell = drawableSwell.HitObject; From e794389fe83644323a563a343338e282783b53b1 Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Sat, 1 Feb 2025 13:34:52 +0800 Subject: [PATCH 265/801] further adjust swell behavior The outstanding visual issues of the clear animation is fixed. The HandleUserInput state management is removed as it no longer seems necessary. --- .../Objects/Drawables/DrawableSwell.cs | 14 +-- .../Skinning/Legacy/LegacySwell.cs | 109 +++++++++--------- 2 files changed, 60 insertions(+), 63 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 363a6bf8e1..d75fdbc40a 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -158,21 +158,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override void UpdateHitStateTransforms(ArmedState state) { + base.UpdateHitStateTransforms(state); + switch (state) { case ArmedState.Idle: - // Only for rewind support. Reallows user inputs if swell is rewound from being hit/missed to being idle. - HandleUserInput = true; break; case ArmedState.Miss: + this.Delay(300).FadeOut(); + break; + case ArmedState.Hit: - const int clear_animation_duration = 1200; - - // Postpone drawable hitobject expiration until it has animated/faded out. Inputs on the object are disallowed during this delay. - LifetimeEnd = Time.Current + clear_animation_duration; - HandleUserInput = false; - + this.Delay(660).FadeOut(); break; } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 43b2d5c435..0eb80d333f 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -41,64 +41,63 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy [BackgroundDependencyLoader] private void load(DrawableHitObject hitObject, ISkinSource skin, SkinManager skinManager) { - var spinnerCircleProvider = skin.FindProvider(s => s.GetTexture("spinner-circle") != null); - - Child = new Container + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - - Children = new Drawable[] + warning = new Sprite { - warning = new Sprite - { - Texture = skin.GetTexture("spinner-warning"), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = skin.GetTexture("spinner-warning") != null ? Vector2.One : new Vector2(0.18f), - }, - bodyContainer = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Position = new Vector2(200f, 100f), - Alpha = 0, + Texture = skin.GetTexture("spinner-warning"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = skin.GetTexture("spinner-warning") != null ? Vector2.One : new Vector2(0.18f), + }, + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(200f, 100f), - Children = new Drawable[] + Children = new Drawable[] + { + bodyContainer = new Container { - spinnerCircle = new Sprite + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + + Children = new Drawable[] { - Texture = skin.GetTexture("spinner-circle"), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(0.8f), - }, - approachCircle = new Sprite - { - Texture = skin.GetTexture("spinner-approachcircle"), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(1.86f * 0.8f), - }, - remainingHitsText = new LegacySpriteText(LegacyFont.Combo) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Position = new Vector2(0f, 165f), - Scale = Vector2.One, - }, - clearAnimation = new Sprite - { - Texture = skin.GetTexture("spinner-osu"), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Position = new Vector2(0f, -165f), - Scale = new Vector2(0.3f), - Alpha = 0, - }, - } + spinnerCircle = new Sprite + { + Texture = skin.GetTexture("spinner-circle"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(0.8f), + }, + approachCircle = new Sprite + { + Texture = skin.GetTexture("spinner-approachcircle"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1.86f * 0.8f), + }, + remainingHitsText = new LegacySpriteText(LegacyFont.Combo) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(0f, 165f), + Scale = Vector2.One, + }, + } + }, + clearAnimation = new Sprite + { + Texture = skin.GetTexture("spinner-osu"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + }, }, - } + }, }; drawableSwell = (DrawableSwell)hitObject; @@ -110,7 +109,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private void animateSwellProgress(int numHits, int requiredHits) { remainingHitsText.Text = $"{requiredHits - numHits}"; - remainingHitsText.ScaleTo(1.6f - 0.6f * ((float)numHits / requiredHits), 60, Easing.OutQuad); + remainingHitsText.ScaleTo(1.6f - (0.6f * ((float)numHits / requiredHits)), 60, Easing.OutQuad); spinnerCircle.ClearTransforms(); spinnerCircle @@ -160,9 +159,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy clearAnimation .FadeIn(clear_fade_in) - .MoveTo(new Vector2(320, 240)) + .MoveTo(new Vector2(0, 0)) .ScaleTo(0.4f) - .MoveTo(new Vector2(320, 150), clear_fade_in * 2, Easing.OutQuad) + .MoveTo(new Vector2(0, -90), clear_fade_in * 2, Easing.OutQuad) .ScaleTo(1f, clear_fade_in * 2, Easing.Out) .Delay(clear_fade_in * 3) .FadeOut(clear_fade_in * 2.5); From cc3bb590c97b1d818229d06d614003c20370163c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 1 Feb 2025 14:48:13 +0900 Subject: [PATCH 266/801] Remove pointless comment --- osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index a1dabd66bc..75f56bffa4 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -59,8 +59,6 @@ namespace osu.Game.Rulesets.Mania.UI this.Delay(50) .ScaleTo(0.75f, 250) .FadeOut(200); - - // osu!mania uses a custom fade length, so the base call is intentionally omitted. break; } } From 3cde11ab773f705e4132d7f837150e1b1232c11b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 19:28:39 +0900 Subject: [PATCH 267/801] Re-enable masking by default --- .../Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs | 7 +++++++ osu.Game/Screens/SelectV2/Carousel.cs | 1 - 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs index 3a516ea762..0e72ee4f8c 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs @@ -32,6 +32,13 @@ namespace osu.Game.Tests.Visual.SongSelect RemoveAllBeatmaps(); } + [Test] + public void TestOffScreenLoading() + { + AddStep("disable masking", () => Scroll.Masking = false); + AddStep("enable masking", () => Scroll.Masking = true); + } + [Test] public void TestAddRemoveOneByOne() { diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 648c2d090a..811bb120e1 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -205,7 +205,6 @@ namespace osu.Game.Screens.SelectV2 InternalChild = scroll = new CarouselScrollContainer { RelativeSizeAxes = Axes.Both, - Masking = false, }; Items.BindCollectionChanged((_, _) => FilterAsync()); From d5dc55149d93cd534e3106a5997be2262d18be17 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 19:29:14 +0900 Subject: [PATCH 268/801] Add initial difficulty grouping support --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 59 ++++++--- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 36 +++++- osu.Game/Screens/SelectV2/GroupPanel.cs | 113 ++++++++++++++++++ 3 files changed, 191 insertions(+), 17 deletions(-) create mode 100644 osu.Game/Screens/SelectV2/GroupPanel.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index bb13c7449d..9a87fba140 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -92,34 +92,56 @@ namespace osu.Game.Screens.SelectV2 #region Selection handling + private GroupDefinition? lastSelectedGroup; + private BeatmapInfo? lastSelectedBeatmap; + protected override void HandleItemSelected(object? model) { base.HandleItemSelected(model); - // Selecting a set isn't valid – let's re-select the first difficulty. - if (model is BeatmapSetInfo setInfo) + switch (model) { - CurrentSelection = setInfo.Beatmaps.First(); - return; - } + case GroupDefinition group: + if (lastSelectedGroup != null) + setVisibilityOfGroupItems(lastSelectedGroup, false); + lastSelectedGroup = group; - if (model is BeatmapInfo beatmapInfo) - setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, true); + setVisibilityOfGroupItems(group, true); + + // In stable, you can kinda select a group (expand without changing selection) + // For simplicity, let's not do that for now and handle similar to a beatmap set header. + CurrentSelection = grouping.GroupItems[group].First().Model; + return; + + case BeatmapSetInfo setInfo: + // Selecting a set isn't valid – let's re-select the first difficulty. + CurrentSelection = setInfo.Beatmaps.First(); + return; + + case BeatmapInfo beatmapInfo: + if (lastSelectedBeatmap != null) + setVisibilityOfSetItems(lastSelectedBeatmap.BeatmapSet!, false); + lastSelectedBeatmap = beatmapInfo; + + setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, true); + break; + } } - protected override void HandleItemDeselected(object? model) + private void setVisibilityOfGroupItems(GroupDefinition group, bool visible) { - base.HandleItemDeselected(model); - - if (model is BeatmapInfo beatmapInfo) - setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, false); + if (grouping.GroupItems.TryGetValue(group, out var items)) + { + foreach (var i in items) + i.IsVisible = visible; + } } private void setVisibilityOfSetItems(BeatmapSetInfo set, bool visible) { - if (grouping.SetItems.TryGetValue(set, out var group)) + if (grouping.SetItems.TryGetValue(set, out var items)) { - foreach (var i in group) + foreach (var i in items) i.IsVisible = visible; } } @@ -143,9 +165,11 @@ namespace osu.Game.Screens.SelectV2 private readonly DrawablePool beatmapPanelPool = new DrawablePool(100); private readonly DrawablePool setPanelPool = new DrawablePool(100); + private readonly DrawablePool groupPanelPool = new DrawablePool(100); private void setupPools() { + AddInternal(groupPanelPool); AddInternal(beatmapPanelPool); AddInternal(setPanelPool); } @@ -154,7 +178,12 @@ namespace osu.Game.Screens.SelectV2 { switch (item.Model) { + case GroupDefinition: + return groupPanelPool.Get(); + case BeatmapInfo: + // TODO: if beatmap is a group selection target, it needs to be a different drawable + // with more information attached. return beatmapPanelPool.Get(); case BeatmapSetInfo: @@ -166,4 +195,6 @@ namespace osu.Game.Screens.SelectV2 #endregion } + + public record GroupDefinition(string Title); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 0658263a8c..e8384a8a2d 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -18,7 +18,13 @@ namespace osu.Game.Screens.SelectV2 /// public IDictionary> SetItems => setItems; + /// + /// Groups contain children which are group-selectable. This dictionary holds the relationships between groups-panels to allow expanding them on selection. + /// + public IDictionary> GroupItems => groupItems; + private readonly Dictionary> setItems = new Dictionary>(); + private readonly Dictionary> groupItems = new Dictionary>(); private readonly Func getCriteria; @@ -31,15 +37,40 @@ namespace osu.Game.Screens.SelectV2 { var criteria = getCriteria(); + int starGroup = int.MinValue; + if (criteria.SplitOutDifficulties) { + var diffItems = new List(items.Count()); + + GroupDefinition? group = null; + foreach (var item in items) { - item.IsVisible = true; + var b = (BeatmapInfo)item.Model; + + if (b.StarRating > starGroup) + { + starGroup = (int)Math.Floor(b.StarRating); + group = new GroupDefinition($"{starGroup} - {++starGroup} *"); + diffItems.Add(new CarouselItem(group) + { + DrawHeight = GroupPanel.HEIGHT, + IsGroupSelectionTarget = true + }); + } + + if (!groupItems.TryGetValue(group!, out var related)) + groupItems[group!] = related = new HashSet(); + related.Add(item); + + diffItems.Add(item); + + item.IsVisible = false; item.IsGroupSelectionTarget = true; } - return items; + return diffItems; } CarouselItem? lastItem = null; @@ -64,7 +95,6 @@ namespace osu.Game.Screens.SelectV2 if (!setItems.TryGetValue(b.BeatmapSet!, out var related)) setItems[b.BeatmapSet!] = related = new HashSet(); - related.Add(item); } diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs new file mode 100644 index 0000000000..e837d8a32f --- /dev/null +++ b/osu.Game/Screens/SelectV2/GroupPanel.cs @@ -0,0 +1,113 @@ +// 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.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Graphics.Sprites; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class GroupPanel : PoolableDrawable, ICarouselPanel + { + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 2; + + [Resolved] + private BeatmapCarousel carousel { get; set; } = null!; + + private Box activationFlash = null!; + private OsuSpriteText text = null!; + + [BackgroundDependencyLoader] + private void load() + { + Size = new Vector2(500, HEIGHT); + Masking = true; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = Color4.DarkBlue.Darken(5), + Alpha = 0.8f, + RelativeSizeAxes = Axes.Both, + }, + activationFlash = new Box + { + Colour = Color4.White, + Blending = BlendingParameters.Additive, + Alpha = 0, + RelativeSizeAxes = Axes.Both, + }, + text = new OsuSpriteText + { + Padding = new MarginPadding(5), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + }; + + Selected.BindValueChanged(value => + { + activationFlash.FadeTo(value.NewValue ? 0.2f : 0, 500, Easing.OutQuint); + }); + + KeyboardSelected.BindValueChanged(value => + { + if (value.NewValue) + { + BorderThickness = 5; + BorderColour = Color4.Pink; + } + else + { + BorderThickness = 0; + } + }); + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + Debug.Assert(Item != null); + Debug.Assert(Item.IsGroupSelectionTarget); + + GroupDefinition group = (GroupDefinition)Item.Model; + + text.Text = group.Title; + + this.FadeInFromZero(500, Easing.OutQuint); + } + + protected override bool OnClick(ClickEvent e) + { + carousel.CurrentSelection = Item!.Model; + return true; + } + + #region ICarouselPanel + + public CarouselItem? Item { get; set; } + public BindableBool Selected { get; } = new BindableBool(); + public BindableBool KeyboardSelected { get; } = new BindableBool(); + + public double DrawYPosition { get; set; } + + public void Activated() + { + // sets should never be activated. + throw new InvalidOperationException(); + } + + #endregion + } +} From 764f799dcb3aeb33cb905888d811a91e5a37640f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Jan 2025 22:53:17 +0900 Subject: [PATCH 269/801] Improve selection flow using early exit and invalidation --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 31 +++++++++++--- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 3 ++ osu.Game/Screens/SelectV2/Carousel.cs | 41 ++++++++++--------- 3 files changed, 50 insertions(+), 25 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 9a87fba140..0a7ca5a6bb 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -95,7 +95,7 @@ namespace osu.Game.Screens.SelectV2 private GroupDefinition? lastSelectedGroup; private BeatmapInfo? lastSelectedBeatmap; - protected override void HandleItemSelected(object? model) + protected override bool HandleItemSelected(object? model) { base.HandleItemSelected(model); @@ -104,6 +104,14 @@ namespace osu.Game.Screens.SelectV2 case GroupDefinition group: if (lastSelectedGroup != null) setVisibilityOfGroupItems(lastSelectedGroup, false); + + // Collapsing an open group. + if (lastSelectedGroup == group) + { + lastSelectedGroup = null; + return false; + } + lastSelectedGroup = group; setVisibilityOfGroupItems(group, true); @@ -111,21 +119,34 @@ namespace osu.Game.Screens.SelectV2 // In stable, you can kinda select a group (expand without changing selection) // For simplicity, let's not do that for now and handle similar to a beatmap set header. CurrentSelection = grouping.GroupItems[group].First().Model; - return; + return false; case BeatmapSetInfo setInfo: // Selecting a set isn't valid – let's re-select the first difficulty. CurrentSelection = setInfo.Beatmaps.First(); - return; + return false; case BeatmapInfo beatmapInfo: if (lastSelectedBeatmap != null) setVisibilityOfSetItems(lastSelectedBeatmap.BeatmapSet!, false); lastSelectedBeatmap = beatmapInfo; - setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, true); - break; + // If we have groups, we need to account for them. + if (grouping.GroupItems.Count > 0) + { + // Find the containing group. There should never be too many groups so iterating is efficient enough. + var group = grouping.GroupItems.Single(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key; + setVisibilityOfGroupItems(group, true); + } + else + setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, true); + + // Ensure the group containing this beatmap is also visible. + // TODO: need to update visibility of correct group? + return true; } + + return true; } private void setVisibilityOfGroupItems(GroupDefinition group, bool visible) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index e8384a8a2d..9ecf735980 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -35,6 +35,9 @@ namespace osu.Game.Screens.SelectV2 public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => { + setItems.Clear(); + groupItems.Clear(); + var criteria = getCriteria(); int starGroup = int.MinValue; diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 811bb120e1..7184aaa866 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Framework.Bindables; +using osu.Framework.Caching; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -170,9 +171,8 @@ namespace osu.Game.Screens.SelectV2 /// /// Called when an item is "selected". /// - protected virtual void HandleItemSelected(object? model) - { - } + /// Whether the item should be selected. + protected virtual bool HandleItemSelected(object? model) => true; /// /// Called when an item is "deselected". @@ -410,6 +410,8 @@ namespace osu.Game.Screens.SelectV2 #region Selection handling + private readonly Cached selectionValid = new Cached(); + private Selection currentKeyboardSelection = new Selection(); private Selection currentSelection = new Selection(); @@ -418,29 +420,21 @@ namespace osu.Game.Screens.SelectV2 if (currentSelection.Model == model) return; - var previousSelection = currentSelection; + if (HandleItemSelected(model)) + { + if (currentSelection.Model != null) + HandleItemDeselected(currentSelection.Model); - if (previousSelection.Model != null) - HandleItemDeselected(previousSelection.Model); - - currentSelection = currentKeyboardSelection = new Selection(model); - HandleItemSelected(currentSelection.Model); - - // `HandleItemSelected` can alter `CurrentSelection`, which will recursively call `setSelection()` again. - // if that happens, the rest of this method should be a no-op. - if (currentSelection.Model != model) - return; - - refreshAfterSelection(); - scrollToSelection(); + currentKeyboardSelection = new Selection(model); + currentSelection = currentKeyboardSelection; + selectionValid.Invalidate(); + } } private void setKeyboardSelection(object? model) { currentKeyboardSelection = new Selection(model); - - refreshAfterSelection(); - scrollToSelection(); + selectionValid.Invalidate(); } /// @@ -525,6 +519,13 @@ namespace osu.Game.Screens.SelectV2 if (carouselItems == null) return; + if (!selectionValid.IsValid) + { + refreshAfterSelection(); + scrollToSelection(); + selectionValid.Validate(); + } + var range = getDisplayRange(); if (range != displayedRange) From d74939e6e983267a5bc8be37d94108d46581b02f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 31 Jan 2025 20:58:32 +0900 Subject: [PATCH 270/801] Fix backwards traversal of groupings and allow toggling groups without updating selection --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 64 +++++++++++++------ .../SelectV2/BeatmapCarouselFilterGrouping.cs | 9 +-- osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 1 - osu.Game/Screens/SelectV2/Carousel.cs | 18 +++++- osu.Game/Screens/SelectV2/CarouselItem.cs | 5 -- osu.Game/Screens/SelectV2/GroupPanel.cs | 1 - 6 files changed, 60 insertions(+), 38 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 0a7ca5a6bb..10bc069cfc 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -102,23 +102,15 @@ namespace osu.Game.Screens.SelectV2 switch (model) { case GroupDefinition group: - if (lastSelectedGroup != null) - setVisibilityOfGroupItems(lastSelectedGroup, false); - - // Collapsing an open group. + // Special case – collapsing an open group. if (lastSelectedGroup == group) { + setVisibilityOfGroupItems(lastSelectedGroup, false); lastSelectedGroup = null; return false; } - lastSelectedGroup = group; - - setVisibilityOfGroupItems(group, true); - - // In stable, you can kinda select a group (expand without changing selection) - // For simplicity, let's not do that for now and handle similar to a beatmap set header. - CurrentSelection = grouping.GroupItems[group].First().Model; + setVisibleGroup(group); return false; case BeatmapSetInfo setInfo: @@ -127,28 +119,52 @@ namespace osu.Game.Screens.SelectV2 return false; case BeatmapInfo beatmapInfo: - if (lastSelectedBeatmap != null) - setVisibilityOfSetItems(lastSelectedBeatmap.BeatmapSet!, false); - lastSelectedBeatmap = beatmapInfo; // If we have groups, we need to account for them. - if (grouping.GroupItems.Count > 0) + if (Criteria.SplitOutDifficulties) { // Find the containing group. There should never be too many groups so iterating is efficient enough. - var group = grouping.GroupItems.Single(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key; - setVisibilityOfGroupItems(group, true); + GroupDefinition group = grouping.GroupItems.Single(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key; + + setVisibleGroup(group); } else - setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, true); + { + setVisibleSet(beatmapInfo); + } - // Ensure the group containing this beatmap is also visible. - // TODO: need to update visibility of correct group? return true; } return true; } + protected override bool CheckValidForGroupSelection(CarouselItem item) + { + switch (item.Model) + { + case BeatmapSetInfo: + return true; + + case BeatmapInfo: + return Criteria.SplitOutDifficulties; + + case GroupDefinition: + return false; + + default: + throw new ArgumentException($"Unsupported model type {item.Model}"); + } + } + + private void setVisibleGroup(GroupDefinition group) + { + if (lastSelectedGroup != null) + setVisibilityOfGroupItems(lastSelectedGroup, false); + lastSelectedGroup = group; + setVisibilityOfGroupItems(group, true); + } + private void setVisibilityOfGroupItems(GroupDefinition group, bool visible) { if (grouping.GroupItems.TryGetValue(group, out var items)) @@ -158,6 +174,14 @@ namespace osu.Game.Screens.SelectV2 } } + private void setVisibleSet(BeatmapInfo beatmapInfo) + { + if (lastSelectedBeatmap != null) + setVisibilityOfSetItems(lastSelectedBeatmap.BeatmapSet!, false); + lastSelectedBeatmap = beatmapInfo; + setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, true); + } + private void setVisibilityOfSetItems(BeatmapSetInfo set, bool visible) { if (grouping.SetItems.TryGetValue(set, out var items)) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 9ecf735980..951b010564 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -56,11 +56,7 @@ namespace osu.Game.Screens.SelectV2 { starGroup = (int)Math.Floor(b.StarRating); group = new GroupDefinition($"{starGroup} - {++starGroup} *"); - diffItems.Add(new CarouselItem(group) - { - DrawHeight = GroupPanel.HEIGHT, - IsGroupSelectionTarget = true - }); + diffItems.Add(new CarouselItem(group) { DrawHeight = GroupPanel.HEIGHT }); } if (!groupItems.TryGetValue(group!, out var related)) @@ -70,7 +66,6 @@ namespace osu.Game.Screens.SelectV2 diffItems.Add(item); item.IsVisible = false; - item.IsGroupSelectionTarget = true; } return diffItems; @@ -92,7 +87,6 @@ namespace osu.Game.Screens.SelectV2 newItems.Add(new CarouselItem(b.BeatmapSet!) { DrawHeight = BeatmapSetPanel.HEIGHT, - IsGroupSelectionTarget = true }); } @@ -104,7 +98,6 @@ namespace osu.Game.Screens.SelectV2 newItems.Add(item); lastItem = item; - item.IsGroupSelectionTarget = false; item.IsVisible = false; } diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index 37e8b88f71..06e3ad3426 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -67,7 +67,6 @@ namespace osu.Game.Screens.SelectV2 base.PrepareForUse(); Debug.Assert(Item != null); - Debug.Assert(Item.IsGroupSelectionTarget); var beatmapSetInfo = (BeatmapSetInfo)Item.Model; diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 7184aaa866..a76b6efee9 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -168,6 +168,13 @@ namespace osu.Game.Screens.SelectV2 protected Drawable? GetMaterialisedDrawableForItem(CarouselItem item) => scroll.Panels.SingleOrDefault(p => ((ICarouselPanel)p).Item == item); + /// + /// When a user is traversing the carousel via group selection keys, assert whether the item provided is a valid target. + /// + /// The candidate item. + /// Whether the provided item is a valid group target. If false, more panels will be checked in the user's requested direction until a valid target is found. + protected virtual bool CheckValidForGroupSelection(CarouselItem item) => true; + /// /// Called when an item is "selected". /// @@ -373,7 +380,7 @@ namespace osu.Game.Screens.SelectV2 // make sure to go back to the group header this item belongs to, so that the block below doesn't find it and stop too early. if (isGroupSelection && direction < 0) { - while (!carouselItems[selectionIndex].IsGroupSelectionTarget) + while (!CheckValidForGroupSelection(carouselItems[selectionIndex])) selectionIndex--; } @@ -394,7 +401,11 @@ namespace osu.Game.Screens.SelectV2 bool attemptSelection(CarouselItem item) { - if (!item.IsVisible || (isGroupSelection && !item.IsGroupSelectionTarget)) + // Keyboard (non-group) selection should only consider visible items. + if (!isGroupSelection && !item.IsVisible) + return false; + + if (isGroupSelection && !CheckValidForGroupSelection(item)) return false; if (isGroupSelection) @@ -427,8 +438,9 @@ namespace osu.Game.Screens.SelectV2 currentKeyboardSelection = new Selection(model); currentSelection = currentKeyboardSelection; - selectionValid.Invalidate(); } + + selectionValid.Invalidate(); } private void setKeyboardSelection(object? model) diff --git a/osu.Game/Screens/SelectV2/CarouselItem.cs b/osu.Game/Screens/SelectV2/CarouselItem.cs index 2cb96a3d7f..13d5c840cf 100644 --- a/osu.Game/Screens/SelectV2/CarouselItem.cs +++ b/osu.Game/Screens/SelectV2/CarouselItem.cs @@ -29,11 +29,6 @@ namespace osu.Game.Screens.SelectV2 /// public float DrawHeight { get; set; } = DEFAULT_HEIGHT; - /// - /// Whether this item should be a valid target for user group selection hotkeys. - /// - public bool IsGroupSelectionTarget { get; set; } - /// /// Whether this item is visible or collapsed (hidden). /// diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs index e837d8a32f..882d77cb8d 100644 --- a/osu.Game/Screens/SelectV2/GroupPanel.cs +++ b/osu.Game/Screens/SelectV2/GroupPanel.cs @@ -79,7 +79,6 @@ namespace osu.Game.Screens.SelectV2 base.PrepareForUse(); Debug.Assert(Item != null); - Debug.Assert(Item.IsGroupSelectionTarget); GroupDefinition group = (GroupDefinition)Item.Model; From 645c26ca19a16e9c5b33fb66125011c806ca2d78 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 1 Feb 2025 11:18:45 +0900 Subject: [PATCH 271/801] Simplify keyboard traversal logic --- osu.Game/Screens/SelectV2/Carousel.cs | 149 +++++++++++++------------- 1 file changed, 73 insertions(+), 76 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index a76b6efee9..312dbc1bd9 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -309,19 +309,19 @@ namespace osu.Game.Screens.SelectV2 return true; case GlobalAction.SelectNext: - selectNext(1, isGroupSelection: false); - return true; - - case GlobalAction.SelectNextGroup: - selectNext(1, isGroupSelection: true); + traverseKeyboardSelection(1); return true; case GlobalAction.SelectPrevious: - selectNext(-1, isGroupSelection: false); + traverseKeyboardSelection(-1); + return true; + + case GlobalAction.SelectNextGroup: + traverseGroupSelection(1); return true; case GlobalAction.SelectPreviousGroup: - selectNext(-1, isGroupSelection: true); + traverseGroupSelection(-1); return true; } @@ -332,89 +332,86 @@ namespace osu.Game.Screens.SelectV2 { } - /// - /// Select the next valid selection relative to a current selection. - /// This is generally for keyboard based traversal. - /// - /// Positive for downwards, negative for upwards. - /// Whether the selection should traverse groups. Group selection updates the actual selection immediately, while non-group selection will only prepare a future keyboard selection. - /// Whether selection was possible. - private bool selectNext(int direction, bool isGroupSelection) + private void traverseKeyboardSelection(int direction) { - // Ensure sanity - Debug.Assert(direction != 0); - direction = direction > 0 ? 1 : -1; + if (carouselItems == null || carouselItems.Count == 0) return; - if (carouselItems == null || carouselItems.Count == 0) - return false; + int originalIndex; - // If the user has a different keyboard selection and requests - // group selection, first transfer the keyboard selection to actual selection. - if (isGroupSelection && currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) - { - TryActivateSelection(); - return true; - } + if (currentKeyboardSelection.Index != null) + originalIndex = currentKeyboardSelection.Index.Value; + else if (direction > 0) + originalIndex = carouselItems.Count - 1; + else + originalIndex = 0; - CarouselItem? selectionItem = currentKeyboardSelection.CarouselItem; - int selectionIndex = currentKeyboardSelection.Index ?? -1; - - // To keep things simple, let's first handle the cases where there's no selection yet. - if (selectionItem == null || selectionIndex < 0) - { - // Start by selecting the first item. - selectionItem = carouselItems.First(); - selectionIndex = 0; - - // In the forwards case, immediately attempt selection of this panel. - // If selection fails, continue with standard logic to find the next valid selection. - if (direction > 0 && attemptSelection(selectionItem)) - return true; - - // In the backwards direction we can just allow the selection logic to go ahead and loop around to the last valid. - } - - Debug.Assert(selectionItem != null); - - // As a second special case, if we're group selecting backwards and the current selection isn't a group, - // make sure to go back to the group header this item belongs to, so that the block below doesn't find it and stop too early. - if (isGroupSelection && direction < 0) - { - while (!CheckValidForGroupSelection(carouselItems[selectionIndex])) - selectionIndex--; - } - - CarouselItem? newItem; + int newIndex = originalIndex; // Iterate over every item back to the current selection, finding the first valid item. // The fail condition is when we reach the selection after a cyclic loop over every item. do { - selectionIndex += direction; - newItem = carouselItems[(selectionIndex + carouselItems.Count) % carouselItems.Count]; + newIndex = (newIndex + direction + carouselItems.Count) % carouselItems.Count; + var newItem = carouselItems[newIndex]; - if (attemptSelection(newItem)) - return true; - } while (newItem != selectionItem); + if (newItem.IsVisible) + { + setKeyboardSelection(newItem.Model); + return; + } + } while (newIndex != originalIndex); + } - return false; + /// + /// Select the next valid selection relative to a current selection. + /// This is generally for keyboard based traversal. + /// + /// Positive for downwards, negative for upwards. + /// Whether selection was possible. + private void traverseGroupSelection(int direction) + { + if (carouselItems == null || carouselItems.Count == 0) return; - bool attemptSelection(CarouselItem item) + // If the user has a different keyboard selection and requests + // group selection, first transfer the keyboard selection to actual selection. + if (currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) { - // Keyboard (non-group) selection should only consider visible items. - if (!isGroupSelection && !item.IsVisible) - return false; - - if (isGroupSelection && !CheckValidForGroupSelection(item)) - return false; - - if (isGroupSelection) - setSelection(item.Model); - else - setKeyboardSelection(item.Model); - - return true; + TryActivateSelection(); + return; } + + int originalIndex; + + if (currentKeyboardSelection.Index != null) + originalIndex = currentKeyboardSelection.Index.Value; + else if (direction > 0) + originalIndex = carouselItems.Count - 1; + else + originalIndex = 0; + + int newIndex = originalIndex; + + // As a second special case, if we're group selecting backwards and the current selection isn't a group, + // make sure to go back to the group header this item belongs to, so that the block below doesn't find it and stop too early. + if (direction < 0) + { + while (!CheckValidForGroupSelection(carouselItems[newIndex])) + newIndex--; + } + + // Iterate over every item back to the current selection, finding the first valid item. + // The fail condition is when we reach the selection after a cyclic loop over every item. + do + { + newIndex = (newIndex + direction + carouselItems.Count) % carouselItems.Count; + var newItem = carouselItems[newIndex]; + + if (CheckValidForGroupSelection(newItem)) + { + setSelection(newItem.Model); + return; + } + } while (newIndex != originalIndex); } #endregion From 9c34819ff4a533f8a39879dd8a5053676bff415a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 1 Feb 2025 14:55:48 +0900 Subject: [PATCH 272/801] Add test coverage for grouped selection --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 50 +++++++- ...estSceneBeatmapCarouselV2GroupSelection.cs | 121 ++++++++++++++++++ .../TestSceneBeatmapCarouselV2Selection.cs | 112 +++++++--------- osu.Game/Screens/SelectV2/Carousel.cs | 2 +- 4 files changed, 217 insertions(+), 68 deletions(-) create mode 100644 osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 281be924a1..5143d681a6 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -21,6 +21,7 @@ using osu.Game.Screens.SelectV2; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; using osuTK.Graphics; +using osuTK.Input; using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel; namespace osu.Game.Tests.Visual.SongSelect @@ -53,7 +54,7 @@ namespace osu.Game.Tests.Visual.SongSelect } [SetUpSteps] - public void SetUpSteps() + public virtual void SetUpSteps() { RemoveAllBeatmaps(); @@ -135,6 +136,53 @@ namespace osu.Game.Tests.Visual.SongSelect protected void WaitForSorting() => AddUntilStep("sorting finished", () => Carousel.IsFiltering, () => Is.False); protected void WaitForScrolling() => AddUntilStep("scroll finished", () => Scroll.Current, () => Is.EqualTo(Scroll.Target)); + protected void SelectNextPanel() => AddStep("select next panel", () => InputManager.Key(Key.Down)); + protected void SelectPrevPanel() => AddStep("select prev panel", () => InputManager.Key(Key.Up)); + protected void SelectNextGroup() => AddStep("select next group", () => InputManager.Key(Key.Right)); + protected void SelectPrevGroup() => AddStep("select prev group", () => InputManager.Key(Key.Left)); + + protected void Select() => AddStep("select", () => InputManager.Key(Key.Enter)); + + protected void CheckNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null); + protected void CheckHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null); + + protected void WaitForGroupSelection(int group, int panel) + { + AddUntilStep($"selected is group{group} panel{panel}", () => + { + var groupingFilter = Carousel.Filters.OfType().Single(); + + GroupDefinition g = groupingFilter.GroupItems.Keys.ElementAt(group); + CarouselItem item = groupingFilter.GroupItems[g].ElementAt(panel); + + return ReferenceEquals(Carousel.CurrentSelection, item.Model); + }); + } + + protected void WaitForSelection(int set, int? diff = null) + { + AddUntilStep($"selected is set{set}{(diff.HasValue ? $" diff{diff.Value}" : "")}", () => + { + if (diff != null) + return ReferenceEquals(Carousel.CurrentSelection, BeatmapSets[set].Beatmaps[diff.Value]); + + return BeatmapSets[set].Beatmaps.Contains(Carousel.CurrentSelection); + }); + } + + protected void ClickVisiblePanel(int index) + where T : Drawable + { + AddStep($"click panel at index {index}", () => + { + Carousel.ChildrenOfType() + .Where(p => ((ICarouselPanel)p).Item?.IsVisible == true) + .Reverse() + .ElementAt(index) + .TriggerClick(); + }); + } + /// /// Add requested beatmap sets count to list. /// diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs new file mode 100644 index 0000000000..bcb609500f --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs @@ -0,0 +1,121 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; +using osu.Game.Screens.SelectV2; + +namespace osu.Game.Tests.Visual.SongSelect +{ + [TestFixture] + public partial class TestSceneBeatmapCarouselV2GroupSelection : BeatmapCarouselV2TestScene + { + public override void SetUpSteps() + { + RemoveAllBeatmaps(); + + CreateCarousel(); + + SortBy(new FilterCriteria { Sort = SortMode.Difficulty }); + } + + [Test] + public void TestOpenCloseGroupWithNoSelection() + { + AddBeatmaps(10, 5); + WaitForDrawablePanels(); + + AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + CheckNoSelection(); + + ClickVisiblePanel(0); + AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + CheckNoSelection(); + + ClickVisiblePanel(0); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + CheckNoSelection(); + } + + [Test] + public void TestCarouselRemembersSelection() + { + AddBeatmaps(10); + WaitForDrawablePanels(); + + SelectNextGroup(); + + object? selection = null; + + AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model); + + CheckHasSelection(); + AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null); + AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); + + RemoveAllBeatmaps(); + AddUntilStep("no drawable selection", getSelectedPanel, () => Is.Null); + + AddBeatmaps(10); + WaitForDrawablePanels(); + + CheckHasSelection(); + AddAssert("no drawable selection", getSelectedPanel, () => Is.Null); + + AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!)); + + AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); + AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); + AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); + + ClickVisiblePanel(0); + AddUntilStep("carousel item not visible", getSelectedPanel, () => Is.Null); + + ClickVisiblePanel(0); + AddUntilStep("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); + + BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); + } + + [Test] + public void TestKeyboardSelection() + { + AddBeatmaps(10, 3); + WaitForDrawablePanels(); + + SelectNextPanel(); + SelectNextPanel(); + SelectNextPanel(); + SelectNextPanel(); + CheckNoSelection(); + + // open first group + Select(); + CheckNoSelection(); + AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + + SelectNextPanel(); + Select(); + WaitForGroupSelection(0, 0); + + SelectNextGroup(); + WaitForGroupSelection(0, 1); + + SelectNextGroup(); + WaitForGroupSelection(0, 2); + + SelectPrevGroup(); + WaitForGroupSelection(0, 1); + + SelectPrevGroup(); + WaitForGroupSelection(0, 0); + + SelectPrevGroup(); + WaitForGroupSelection(2, 9); + } + } +} diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs index 3c42969d8c..50395cf1ff 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs @@ -22,10 +22,10 @@ namespace osu.Game.Tests.Visual.SongSelect { AddBeatmaps(10); WaitForDrawablePanels(); - checkNoSelection(); + CheckNoSelection(); - select(); - checkNoSelection(); + Select(); + CheckNoSelection(); AddStep("press down arrow", () => InputManager.PressKey(Key.Down)); checkSelectionIterating(false); @@ -39,8 +39,8 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("release up arrow", () => InputManager.ReleaseKey(Key.Up)); checkSelectionIterating(false); - select(); - checkHasSelection(); + Select(); + CheckHasSelection(); } /// @@ -52,7 +52,7 @@ namespace osu.Game.Tests.Visual.SongSelect { AddBeatmaps(10); WaitForDrawablePanels(); - checkNoSelection(); + CheckNoSelection(); AddStep("press right arrow", () => InputManager.PressKey(Key.Right)); checkSelectionIterating(true); @@ -73,13 +73,13 @@ namespace osu.Game.Tests.Visual.SongSelect AddBeatmaps(10); WaitForDrawablePanels(); - selectNextGroup(); + SelectNextGroup(); object? selection = null; AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model); - checkHasSelection(); + CheckHasSelection(); AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null); AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); @@ -89,13 +89,14 @@ namespace osu.Game.Tests.Visual.SongSelect AddBeatmaps(10); WaitForDrawablePanels(); - checkHasSelection(); + CheckHasSelection(); AddAssert("no drawable selection", getSelectedPanel, () => Is.Null); AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!)); + AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); - AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); + AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); } @@ -108,10 +109,10 @@ namespace osu.Game.Tests.Visual.SongSelect AddBeatmaps(total_set_count); WaitForDrawablePanels(); - selectNextGroup(); - waitForSelection(0, 0); - selectPrevGroup(); - waitForSelection(total_set_count - 1, 0); + SelectNextGroup(); + WaitForSelection(0, 0); + SelectPrevGroup(); + WaitForSelection(total_set_count - 1, 0); } [Test] @@ -122,10 +123,10 @@ namespace osu.Game.Tests.Visual.SongSelect AddBeatmaps(total_set_count); WaitForDrawablePanels(); - selectPrevGroup(); - waitForSelection(total_set_count - 1, 0); - selectNextGroup(); - waitForSelection(0, 0); + SelectPrevGroup(); + WaitForSelection(total_set_count - 1, 0); + SelectNextGroup(); + WaitForSelection(0, 0); } [Test] @@ -134,71 +135,50 @@ namespace osu.Game.Tests.Visual.SongSelect AddBeatmaps(10, 3); WaitForDrawablePanels(); - selectNextPanel(); - selectNextPanel(); - selectNextPanel(); - selectNextPanel(); - checkNoSelection(); + SelectNextPanel(); + SelectNextPanel(); + SelectNextPanel(); + SelectNextPanel(); + CheckNoSelection(); - select(); - waitForSelection(3, 0); + Select(); + WaitForSelection(3, 0); - selectNextPanel(); - waitForSelection(3, 0); + SelectNextPanel(); + WaitForSelection(3, 0); - select(); - waitForSelection(3, 1); + Select(); + WaitForSelection(3, 1); - selectNextPanel(); - waitForSelection(3, 1); + SelectNextPanel(); + WaitForSelection(3, 1); - select(); - waitForSelection(3, 2); + Select(); + WaitForSelection(3, 2); - selectNextPanel(); - waitForSelection(3, 2); + SelectNextPanel(); + WaitForSelection(3, 2); - select(); - waitForSelection(4, 0); + Select(); + WaitForSelection(4, 0); } [Test] public void TestEmptyTraversal() { - selectNextPanel(); - checkNoSelection(); + SelectNextPanel(); + CheckNoSelection(); - selectNextGroup(); - checkNoSelection(); + SelectNextGroup(); + CheckNoSelection(); - selectPrevPanel(); - checkNoSelection(); + SelectPrevPanel(); + CheckNoSelection(); - selectPrevGroup(); - checkNoSelection(); + SelectPrevGroup(); + CheckNoSelection(); } - private void waitForSelection(int set, int? diff = null) - { - AddUntilStep($"selected is set{set}{(diff.HasValue ? $" diff{diff.Value}" : "")}", () => - { - if (diff != null) - return ReferenceEquals(Carousel.CurrentSelection, BeatmapSets[set].Beatmaps[diff.Value]); - - return BeatmapSets[set].Beatmaps.Contains(Carousel.CurrentSelection); - }); - } - - private void selectNextPanel() => AddStep("select next panel", () => InputManager.Key(Key.Down)); - private void selectPrevPanel() => AddStep("select prev panel", () => InputManager.Key(Key.Up)); - private void selectNextGroup() => AddStep("select next group", () => InputManager.Key(Key.Right)); - private void selectPrevGroup() => AddStep("select prev group", () => InputManager.Key(Key.Left)); - - private void select() => AddStep("select", () => InputManager.Key(Key.Enter)); - - private void checkNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null); - private void checkHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null); - private void checkSelectionIterating(bool isIterating) { object? selection = null; diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 312dbc1bd9..0da9cb5c19 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -131,7 +131,7 @@ namespace osu.Game.Screens.SelectV2 /// /// A filter may add, mutate or remove items. /// - protected IEnumerable Filters { get; init; } = Enumerable.Empty(); + public IEnumerable Filters { get; init; } = Enumerable.Empty(); /// /// All items which are to be considered for display in this carousel. From 6a18d18feb0ada227cb85fdb9144439196b3cef7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 2 Feb 2025 13:28:31 +0900 Subject: [PATCH 273/801] Fix null handling when no items are populated but a selection is made --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 10bc069cfc..858888c517 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -124,9 +124,10 @@ namespace osu.Game.Screens.SelectV2 if (Criteria.SplitOutDifficulties) { // Find the containing group. There should never be too many groups so iterating is efficient enough. - GroupDefinition group = grouping.GroupItems.Single(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key; + GroupDefinition? group = grouping.GroupItems.SingleOrDefault(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key; - setVisibleGroup(group); + if (group != null) + setVisibleGroup(group); } else { From 48e30f4ee80af5fd9c0e6e39bfd28d48a5df6ccf Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Mon, 3 Feb 2025 09:49:37 +0800 Subject: [PATCH 274/801] remove skinning section swell delay test Replaced by TestHitSwellThenHitHit in TestSceneSwellJudgements. --- .../TestSceneDrawableSwellExpireDelay.cs | 41 ------------------- 1 file changed, 41 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs deleted file mode 100644 index ad78ed3b20..0000000000 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs +++ /dev/null @@ -1,41 +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 NUnit.Framework; -using osu.Game.Rulesets.Replays; -using osu.Game.Rulesets.Scoring; -using osu.Game.Rulesets.Taiko.Objects; -using osu.Game.Rulesets.Taiko.Replays; -using osu.Game.Rulesets.Taiko.Tests.Judgements; - -namespace osu.Game.Rulesets.Taiko.Tests.Skinning -{ - public partial class TestSceneDrawableSwellExpireDelay : JudgementTest - { - [Test] - public void TestExpireDelay() - { - const double swell_start = 1000; - const double swell_duration = 1000; - - Swell swell = new Swell - { - StartTime = swell_start, - Duration = swell_duration, - }; - - Hit hit = new Hit { StartTime = swell_start + swell_duration + 50 }; - - List frames = new List - { - new TaikoReplayFrame(0), - new TaikoReplayFrame(2100, TaikoAction.LeftCentre), - }; - - PerformTest(frames, CreateBeatmap(swell, hit)); - - AssertResult(0, HitResult.Ok); - } - } -} From 210fa14759313b8b8f0b1aadc7c5e0c84394a4ee Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 3 Feb 2025 14:15:43 +0900 Subject: [PATCH 275/801] Play sound via results screen instead --- .../Expanded/Accuracy/AccuracyCircle.cs | 49 +----- osu.Game/Screens/Ranking/ResultsScreen.cs | 166 ++++++++++++------ 2 files changed, 116 insertions(+), 99 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index 319a87fdfc..4b960b05fb 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -2,8 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; @@ -91,6 +91,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy private readonly ScoreInfo score; + [Resolved] + private ResultsScreen? resultsScreen { get; set; } + private CircularProgress accuracyCircle = null!; private GradedCircles gradedCircles = null!; private Container badges = null!; @@ -101,7 +104,6 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy private PoolableSkinnableSample? badgeMaxSound; private PoolableSkinnableSample? swooshUpSound; private PoolableSkinnableSample? rankImpactSound; - private PoolableSkinnableSample? rankApplauseSound; private readonly Bindable tickPlaybackRate = new Bindable(); @@ -197,15 +199,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy if (withFlair) { - var applauseSamples = new List { applauseSampleName }; - if (score.Rank >= ScoreRank.B) - // when rank is B or higher, play legacy applause sample on legacy skins. - applauseSamples.Insert(0, @"applause"); - AddRangeInternal(new Drawable[] { rankImpactSound = new PoolableSkinnableSample(new SampleInfo(impactSampleName)), - rankApplauseSound = new PoolableSkinnableSample(new SampleInfo(applauseSamples.ToArray())), scoreTickSound = new PoolableSkinnableSample(new SampleInfo(@"Results/score-tick")), badgeTickSound = new PoolableSkinnableSample(new SampleInfo(@"Results/badge-dink")), badgeMaxSound = new PoolableSkinnableSample(new SampleInfo(@"Results/badge-dink-max")), @@ -333,16 +329,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy }); const double applause_pre_delay = 545f; - const double applause_volume = 0.8f; using (BeginDelayedSequence(applause_pre_delay)) - { - Schedule(() => - { - rankApplauseSound!.VolumeTo(applause_volume); - rankApplauseSound!.Play(); - }); - } + Schedule(() => resultsScreen?.PlayApplause(score.Rank)); } } @@ -384,34 +373,6 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy } } - private string applauseSampleName - { - get - { - switch (score.Rank) - { - default: - case ScoreRank.D: - return @"Results/applause-d"; - - case ScoreRank.C: - return @"Results/applause-c"; - - case ScoreRank.B: - return @"Results/applause-b"; - - case ScoreRank.A: - return @"Results/applause-a"; - - case ScoreRank.S: - case ScoreRank.SH: - case ScoreRank.X: - case ScoreRank.XH: - return @"Results/applause-s"; - } - } - } - private string impactSampleName { get diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 95dbfb2712..b10684b22e 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -17,6 +17,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Screens; +using osu.Game.Audio; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -29,10 +30,12 @@ using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking.Expanded.Accuracy; using osu.Game.Screens.Ranking.Statistics; +using osu.Game.Skinning; using osuTK; namespace osu.Game.Screens.Ranking { + [Cached] public abstract partial class ResultsScreen : ScreenWithBeatmapBackground, IKeyBindingHandler { protected const float BACKGROUND_BLUR = 20; @@ -64,7 +67,6 @@ namespace osu.Game.Screens.Ranking private Drawable bottomPanel = null!; private Container detachedPanelContainer = null!; - private AudioContainer audioContainer = null!; private bool lastFetchCompleted; @@ -101,80 +103,76 @@ namespace osu.Game.Screens.Ranking popInSample = audio.Samples.Get(@"UI/overlay-pop-in"); - InternalChild = audioContainer = new AudioContainer + InternalChild = new PopoverContainer { RelativeSizeAxes = Axes.Both, - Child = new PopoverContainer + Child = new GridContainer { RelativeSizeAxes = Axes.Both, - Child = new GridContainer + Content = new[] { - RelativeSizeAxes = Axes.Both, - Content = new[] + new Drawable[] { - new Drawable[] + VerticalScrollContent = new VerticalScrollContainer { - VerticalScrollContent = new VerticalScrollContainer + RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, + Child = new Container { RelativeSizeAxes = Axes.Both, - ScrollbarVisible = false, - Child = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - StatisticsPanel = createStatisticsPanel().With(panel => - { - panel.RelativeSizeAxes = Axes.Both; - panel.Score.BindTarget = SelectedScore; - }), - ScorePanelList = new ScorePanelList - { - RelativeSizeAxes = Axes.Both, - SelectedScore = { BindTarget = SelectedScore }, - PostExpandAction = () => StatisticsPanel.ToggleVisibility() - }, - detachedPanelContainer = new Container - { - RelativeSizeAxes = Axes.Both - }, - } - } - }, - }, - new[] - { - bottomPanel = new Container - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - Height = TwoLayerButton.SIZE_EXTENDED.Y, - Alpha = 0, Children = new Drawable[] { - new Box + StatisticsPanel = createStatisticsPanel().With(panel => + { + panel.RelativeSizeAxes = Axes.Both; + panel.Score.BindTarget = SelectedScore; + }), + ScorePanelList = new ScorePanelList { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#333") + SelectedScore = { BindTarget = SelectedScore }, + PostExpandAction = () => StatisticsPanel.ToggleVisibility() }, - buttons = new FillFlowContainer + detachedPanelContainer = new Container { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(5), - Direction = FillDirection.Horizontal + RelativeSizeAxes = Axes.Both }, } } - } + }, }, - RowDimensions = new[] + new[] { - new Dimension(), - new Dimension(GridSizeMode.AutoSize) + bottomPanel = new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = TwoLayerButton.SIZE_EXTENDED.Y, + Alpha = 0, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#333") + }, + buttons = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5), + Direction = FillDirection.Horizontal + }, + } + } } + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize) } } }; @@ -268,6 +266,64 @@ namespace osu.Game.Screens.Ranking } } + #region Applause + + private PoolableSkinnableSample? rankApplauseSound; + + public void PlayApplause(ScoreRank rank) + { + const double applause_volume = 0.8f; + + if (!this.IsCurrentScreen()) + return; + + rankApplauseSound?.Dispose(); + + var applauseSamples = new List(); + + if (rank >= ScoreRank.B) + // when rank is B or higher, play legacy applause sample on legacy skins. + applauseSamples.Insert(0, @"applause"); + + switch (rank) + { + default: + case ScoreRank.D: + applauseSamples.Add(@"Results/applause-d"); + break; + + case ScoreRank.C: + applauseSamples.Add(@"Results/applause-c"); + break; + + case ScoreRank.B: + applauseSamples.Add(@"Results/applause-b"); + break; + + case ScoreRank.A: + applauseSamples.Add(@"Results/applause-a"); + break; + + case ScoreRank.S: + case ScoreRank.SH: + case ScoreRank.X: + case ScoreRank.XH: + applauseSamples.Add(@"Results/applause-s"); + break; + } + + LoadComponentAsync(rankApplauseSound = new PoolableSkinnableSample(new SampleInfo(applauseSamples.ToArray())), s => + { + if (!this.IsCurrentScreen() || s != rankApplauseSound) + return; + + rankApplauseSound.VolumeTo(applause_volume); + rankApplauseSound.Play(); + }); + } + + #endregion + /// /// Performs a fetch/refresh of scores to be displayed. /// @@ -336,7 +392,7 @@ namespace osu.Game.Screens.Ranking if (!skipExitTransition) this.FadeOut(100); - audioContainer.Volume.Value = 0; + rankApplauseSound?.Stop(); return false; } From 9033a4d480ed78a69c5c57c10c31789b15b688fd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 3 Feb 2025 14:20:56 +0900 Subject: [PATCH 276/801] Remove unused using --- osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index 4b960b05fb..f6cf71d8a6 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -3,7 +3,6 @@ using System; using System.Linq; -using System.Threading; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; From a23de0b1885a3c5f62e4b9971b094167d8c5b1a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Feb 2025 16:29:39 +0900 Subject: [PATCH 277/801] Avoid accessing `WorkingBeatmap.Beatmap` every update call Notice in passing. Comes with overheads that can be easily avoided. Left a note for a future (slightly more involved) optimisation. --- osu.Game/Beatmaps/WorkingBeatmap.cs | 2 ++ .../Play/MasterGameplayClockContainer.cs | 32 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 890a969415..fd40097c4e 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -203,6 +203,8 @@ namespace osu.Game.Beatmaps { try { + // TODO: This is a touch expensive and can become an issue if being accessed every Update call. + // Optimally we would not involve the async flow if things are already loaded. return loadBeatmapAsync().GetResultSafely(); } catch (AggregateException ae) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index c20d461526..747ea3090c 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -53,7 +53,7 @@ namespace osu.Game.Screens.Play private readonly Bindable playbackRateValid = new Bindable(true); - private readonly WorkingBeatmap beatmap; + private readonly IBeatmap beatmap; private Track track; @@ -63,20 +63,19 @@ namespace osu.Game.Screens.Play /// /// Create a new master gameplay clock container. /// - /// The beatmap to be used for time and metadata references. + /// The beatmap to be used for time and metadata references. /// The latest time which should be used when introducing gameplay. Will be used when skipping forward. - public MasterGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime) - : base(beatmap.Track, applyOffsets: true, requireDecoupling: true) + public MasterGameplayClockContainer(WorkingBeatmap working, double gameplayStartTime) + : base(working.Track, applyOffsets: true, requireDecoupling: true) { - this.beatmap = beatmap; - - track = beatmap.Track; + beatmap = working.Beatmap; + track = working.Track; GameplayStartTime = gameplayStartTime; - StartTime = findEarliestStartTime(gameplayStartTime, beatmap); + StartTime = findEarliestStartTime(gameplayStartTime, working); } - private static double findEarliestStartTime(double gameplayStartTime, WorkingBeatmap beatmap) + private static double findEarliestStartTime(double gameplayStartTime, WorkingBeatmap working) { // here we are trying to find the time to start playback from the "zero" point. // generally this is either zero, or some point earlier than zero in the case of storyboards, lead-ins etc. @@ -86,15 +85,15 @@ namespace osu.Game.Screens.Play // if a storyboard is present, it may dictate the appropriate start time by having events in negative time space. // this is commonly used to display an intro before the audio track start. - double? firstStoryboardEvent = beatmap.Storyboard.EarliestEventTime; + double? firstStoryboardEvent = working.Storyboard.EarliestEventTime; if (firstStoryboardEvent != null) time = Math.Min(time, firstStoryboardEvent.Value); // some beatmaps specify a current lead-in time which should be used instead of the ruleset-provided value when available. // this is not available as an option in the live editor but can still be applied via .osu editing. - double firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime; - if (beatmap.Beatmap.AudioLeadIn > 0) - time = Math.Min(time, firstHitObjectTime - beatmap.Beatmap.AudioLeadIn); + double firstHitObjectTime = working.Beatmap.HitObjects.First().StartTime; + if (working.Beatmap.AudioLeadIn > 0) + time = Math.Min(time, firstHitObjectTime - working.Beatmap.AudioLeadIn); return time; } @@ -136,7 +135,7 @@ namespace osu.Game.Screens.Play { removeAdjustmentsFromTrack(); - track = new TrackVirtual(beatmap.Track.Length); + track = new TrackVirtual(track.Length); track.Seek(CurrentTime); if (IsRunning) track.Start(); @@ -228,9 +227,8 @@ namespace osu.Game.Screens.Play removeAdjustmentsFromTrack(); } - ControlPointInfo IBeatSyncProvider.ControlPoints => beatmap.Beatmap.ControlPointInfo; + ControlPointInfo IBeatSyncProvider.ControlPoints => beatmap.ControlPointInfo; + ChannelAmplitudes IHasAmplitudes.CurrentAmplitudes => track.CurrentAmplitudes; IClock IBeatSyncProvider.Clock => this; - - ChannelAmplitudes IHasAmplitudes.CurrentAmplitudes => beatmap.TrackLoaded ? beatmap.Track.CurrentAmplitudes : ChannelAmplitudes.Empty; } } From c587958f387db1287218801292a1ed9480d8edef Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 1 Feb 2025 03:01:47 -0500 Subject: [PATCH 278/801] Apply depth ordering relative to selected item --- osu.Game/Screens/SelectV2/Carousel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 648c2d090a..f41154b878 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -544,8 +544,8 @@ namespace osu.Game.Screens.SelectV2 if (c.Item == null) continue; - if (panel.Depth != c.DrawYPosition) - scroll.Panels.ChangeChildDepth(panel, (float)c.DrawYPosition); + double selectedYPos = currentSelection?.CarouselItem?.CarouselYPosition ?? 0; + scroll.Panels.ChangeChildDepth(panel, (float)Math.Abs(c.DrawYPosition - selectedYPos)); if (c.DrawYPosition != c.Item.CarouselYPosition) c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed); From 26a8fb6984e66ef3d992db23beec6f86ca0b682d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Feb 2025 17:34:55 +0900 Subject: [PATCH 279/801] Make distance snap settings mutually exclusive --- osu.Game/Screens/Edit/Editor.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index d5ed54db81..6b18b05174 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -330,6 +330,18 @@ namespace osu.Game.Screens.Edit editorTimelineShowTicks = config.GetBindable(OsuSetting.EditorTimelineShowTicks); editorContractSidebars = config.GetBindable(OsuSetting.EditorContractSidebars); + // These two settings don't work together. Make them mutually exclusive to let the user know. + editorAutoSeekOnPlacement.BindValueChanged(enabled => + { + if (enabled.NewValue) + editorLimitedDistanceSnap.Value = false; + }); + editorLimitedDistanceSnap.BindValueChanged(enabled => + { + if (enabled.NewValue) + editorAutoSeekOnPlacement.Value = false; + }); + AddInternal(new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, From df51d345c5e1e49b98c43b898f38ccd0403b5abf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Feb 2025 17:36:47 +0900 Subject: [PATCH 280/801] Change menus to fade out with a slight delay so settings changes are visible Useful for cases like https://github.com/ppy/osu/pull/31778, where a change to one setting can affect another. --- osu.Game/Graphics/UserInterface/OsuMenu.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuMenu.cs b/osu.Game/Graphics/UserInterface/OsuMenu.cs index 7cc1bab25f..9b099c0884 100644 --- a/osu.Game/Graphics/UserInterface/OsuMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuMenu.cs @@ -68,7 +68,9 @@ namespace osu.Game.Graphics.UserInterface if (!TopLevelMenu && wasOpened) menuSamples?.PlayCloseSample(); - this.FadeOut(300, Easing.OutQuint); + this.Delay(50) + .FadeOut(300, Easing.OutQuint); + wasOpened = false; } @@ -77,12 +79,21 @@ namespace osu.Game.Graphics.UserInterface if (Direction == Direction.Vertical) { Width = newSize.X; - this.ResizeHeightTo(newSize.Y, 300, Easing.OutQuint); + + if (newSize.Y > 0) + this.ResizeHeightTo(newSize.Y, 300, Easing.OutQuint); + else + // Delay until the fade out finishes from AnimateClose. + this.Delay(350).ResizeHeightTo(0); } else { Height = newSize.Y; - this.ResizeWidthTo(newSize.X, 300, Easing.OutQuint); + if (newSize.X > 0) + this.ResizeWidthTo(newSize.X, 300, Easing.OutQuint); + else + // Delay until the fade out finishes from AnimateClose. + this.Delay(350).ResizeWidthTo(0); } } From 55f46e3b668fbc16856f872044cc011f739e05b8 Mon Sep 17 00:00:00 2001 From: NecoDev <120387312+necocat0918@users.noreply.github.com> Date: Mon, 3 Feb 2025 16:06:27 +0800 Subject: [PATCH 281/801] Added warning --- osu.Game/Screens/Edit/BookmarkResetDialog.cs | 26 ++++++++++++++++++++ osu.Game/Screens/Edit/Editor.cs | 3 ++- 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Screens/Edit/BookmarkResetDialog.cs diff --git a/osu.Game/Screens/Edit/BookmarkResetDialog.cs b/osu.Game/Screens/Edit/BookmarkResetDialog.cs new file mode 100644 index 0000000000..48a0202c86 --- /dev/null +++ b/osu.Game/Screens/Edit/BookmarkResetDialog.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Game.Overlays.Dialog; + +namespace osu.Game.Screens.Edit +{ + public partial class BookmarkResetDialog : DeletionDialog + { + private readonly EditorBeatmap editor; + + public BookmarkResetDialog(EditorBeatmap editorBeatmap) + { + editor = editorBeatmap; + BodyText = "All Bookmarks"; + } + + [BackgroundDependencyLoader] + private void load() + { + DangerousAction = () => editor.Bookmarks.Clear(); + } + } +} + diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index d5ed54db81..8cffab87ea 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using DiffPlex.Model; using JetBrains.Annotations; using osu.Framework; using osu.Framework.Allocation; @@ -450,7 +451,7 @@ namespace osu.Game.Screens.Edit { Hotkey = new Hotkey(GlobalAction.EditorSeekToNextBookmark) }, - new EditorMenuItem(EditorStrings.ResetBookmarks, MenuItemType.Destructive, () => editorBeatmap.Bookmarks.Clear()) + new EditorMenuItem(EditorStrings.ResetBookmarks, MenuItemType.Destructive, () => dialogOverlay?.Push(new BookmarkResetDialog(editorBeatmap))) } } } From 444e0970d600d90e087e47af1816b63d7487a796 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Feb 2025 18:54:18 +0900 Subject: [PATCH 282/801] Standardise naming to use "Freestyle" not "FreeStyle" --- osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs | 2 +- osu.Game/Online/Rooms/PlaylistItem.cs | 8 ++++---- ...ButtonFreeStyle.cs => FooterButtonFreestyle.cs} | 4 ++-- .../OnlinePlay/Lounge/Components/DrawableRoom.cs | 2 +- ...eeStyleStatusPill.cs => FreestyleStatusPill.cs} | 12 ++++++------ osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 8 ++++---- .../Multiplayer/MultiplayerMatchSongSelect.cs | 2 +- .../Screens/OnlinePlay/OnlinePlaySongSelect.cs | 14 +++++++------- .../OnlinePlay/Playlists/PlaylistsSongSelect.cs | 2 +- 9 files changed, 27 insertions(+), 27 deletions(-) rename osu.Game/Screens/OnlinePlay/{FooterButtonFreeStyle.cs => FooterButtonFreestyle.cs} (96%) rename osu.Game/Screens/OnlinePlay/Lounge/Components/{FreeStyleStatusPill.cs => FreestyleStatusPill.cs} (84%) diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index 4dfb3b389d..b737cda4ba 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -60,7 +60,7 @@ namespace osu.Game.Online.Rooms /// Indicates whether participants in the room are able to pick their own choice of beatmap difficulty and ruleset. /// [Key(11)] - public bool FreeStyle { get; set; } + public bool Freestyle { get; set; } [SerializationConstructor] public MultiplayerPlaylistItem() diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index e8725b6792..817b42f503 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -71,7 +71,7 @@ namespace osu.Game.Online.Rooms /// Indicates whether participants in the room are able to pick their own choice of beatmap difficulty and ruleset. /// [JsonProperty("freestyle")] - public bool FreeStyle { get; set; } + public bool Freestyle { get; set; } /// /// A beatmap representing this playlist item. @@ -107,7 +107,7 @@ namespace osu.Game.Online.Rooms PlayedAt = item.PlayedAt; RequiredMods = item.RequiredMods.ToArray(); AllowedMods = item.AllowedMods.ToArray(); - FreeStyle = item.FreeStyle; + Freestyle = item.Freestyle; } public void MarkInvalid() => valid.Value = false; @@ -139,7 +139,7 @@ namespace osu.Game.Online.Rooms PlayedAt = PlayedAt, AllowedMods = AllowedMods, RequiredMods = RequiredMods, - FreeStyle = FreeStyle, + Freestyle = Freestyle, valid = { Value = Valid.Value }, }; } @@ -152,6 +152,6 @@ namespace osu.Game.Online.Rooms && PlaylistOrder == other.PlaylistOrder && AllowedMods.SequenceEqual(other.AllowedMods) && RequiredMods.SequenceEqual(other.RequiredMods) - && FreeStyle == other.FreeStyle; + && Freestyle == other.Freestyle; } } diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs similarity index 96% rename from osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs rename to osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs index 0e22b3d3fb..157f90d078 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs @@ -16,7 +16,7 @@ using osu.Game.Localisation; namespace osu.Game.Screens.OnlinePlay { - public partial class FooterButtonFreeStyle : FooterButton, IHasCurrentValue + public partial class FooterButtonFreestyle : FooterButton, IHasCurrentValue { private readonly BindableWithCurrent current = new BindableWithCurrent(); @@ -34,7 +34,7 @@ namespace osu.Game.Screens.OnlinePlay [Resolved] private OsuColour colours { get; set; } = null!; - public FooterButtonFreeStyle() + public FooterButtonFreestyle() { // Overwrite any external behaviour as we delegate the main toggle action to a sub-button. base.Action = () => current.Value = !current.Value; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 7bc0b612f1..a16267aa10 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -169,7 +169,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft }, - new FreeStyleStatusPill(Room) + new FreestyleStatusPill(Room) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/FreestyleStatusPill.cs similarity index 84% rename from osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs rename to osu.Game/Screens/OnlinePlay/Lounge/Components/FreestyleStatusPill.cs index 1c0135fb89..b306e27f84 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/FreestyleStatusPill.cs @@ -10,7 +10,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Lounge.Components { - public partial class FreeStyleStatusPill : OnlinePlayPill + public partial class FreestyleStatusPill : OnlinePlayPill { private readonly Room room; @@ -19,7 +19,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components protected override FontUsage Font => base.Font.With(weight: FontWeight.SemiBold); - public FreeStyleStatusPill(Room room) + public FreestyleStatusPill(Room room) { this.room = room; } @@ -35,7 +35,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components TextFlow.Colour = Color4.Black; room.PropertyChanged += onRoomPropertyChanged; - updateFreeStyleStatus(); + updateFreestyleStatus(); } private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e) @@ -44,15 +44,15 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { case nameof(Room.CurrentPlaylistItem): case nameof(Room.Playlist): - updateFreeStyleStatus(); + updateFreestyleStatus(); break; } } - private void updateFreeStyleStatus() + private void updateFreestyleStatus() { PlaylistItem? currentItem = room.Playlist.GetCurrentItem() ?? room.CurrentPlaylistItem; - Alpha = currentItem?.FreeStyle == true ? 1 : 0; + Alpha = currentItem?.Freestyle == true ? 1 : 0; } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index c9c9c3eca7..9f7e193131 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -450,11 +450,11 @@ namespace osu.Game.Screens.OnlinePlay.Match Ruleset.Value = GetGameplayRuleset(); bool freeMod = item.AllowedMods.Any(); - bool freeStyle = item.FreeStyle; + bool freestyle = item.Freestyle; // For now, the game can never be in a state where freemod and freestyle are on at the same time. // This will change, but due to the current implementation if this was to occur drawables will overlap so let's assert. - Debug.Assert(!freeMod || !freeStyle); + Debug.Assert(!freeMod || !freestyle); if (freeMod) { @@ -468,7 +468,7 @@ namespace osu.Game.Screens.OnlinePlay.Match UserModsSelectOverlay.IsValidMod = _ => false; } - if (freeStyle) + if (freestyle) { UserStyleSection.Show(); @@ -481,7 +481,7 @@ namespace osu.Game.Screens.OnlinePlay.Match UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true) { AllowReordering = false, - AllowEditing = freeStyle, + AllowEditing = freestyle, RequestEdit = _ => OpenStyleSelection() }; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 5754bcb963..b42a58787d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -87,7 +87,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer RulesetID = item.RulesetID, RequiredMods = item.RequiredMods.ToArray(), AllowedMods = item.AllowedMods.ToArray(), - FreeStyle = item.FreeStyle + Freestyle = item.Freestyle }; Task task = itemToEdit != null ? client.EditPlaylistItem(multiplayerItem) : client.AddPlaylistItem(multiplayerItem); diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index f6403c010e..8d1e3c3cb1 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -42,7 +42,7 @@ namespace osu.Game.Screens.OnlinePlay protected override UserActivity InitialActivity => new UserActivity.InLobby(room); protected readonly Bindable> FreeMods = new Bindable>(Array.Empty()); - protected readonly Bindable FreeStyle = new Bindable(); + protected readonly Bindable Freestyle = new Bindable(); private readonly Room room; private readonly PlaylistItem? initialItem; @@ -112,17 +112,17 @@ namespace osu.Game.Screens.OnlinePlay FreeMods.Value = initialItem.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); } - FreeStyle.Value = initialItem.FreeStyle; + Freestyle.Value = initialItem.Freestyle; } Mods.BindValueChanged(onModsChanged); Ruleset.BindValueChanged(onRulesetChanged); - FreeStyle.BindValueChanged(onFreeStyleChanged, true); + Freestyle.BindValueChanged(onFreestyleChanged, true); freeModSelectOverlayRegistration = OverlayManager?.RegisterBlockingOverlay(freeModSelect); } - private void onFreeStyleChanged(ValueChangedEvent enabled) + private void onFreestyleChanged(ValueChangedEvent enabled) { if (enabled.NewValue) { @@ -162,7 +162,7 @@ namespace osu.Game.Screens.OnlinePlay RulesetID = Ruleset.Value.OnlineID, RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(), - FreeStyle = FreeStyle.Value + Freestyle = Freestyle.Value }; return SelectItem(item); @@ -204,12 +204,12 @@ namespace osu.Game.Screens.OnlinePlay baseButtons.Single(i => i.button is FooterButtonMods).button.TooltipText = MultiplayerMatchStrings.RequiredModsButtonTooltip; freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) { Current = FreeMods }; - var freeStyleButton = new FooterButtonFreeStyle { Current = FreeStyle }; + var freestyleButton = new FooterButtonFreestyle { Current = Freestyle }; baseButtons.InsertRange(baseButtons.FindIndex(b => b.button is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[] { (freeModsFooterButton, null), - (freeStyleButton, null) + (freestyleButton, null) }); return baseButtons; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs index abf80c0d44..84446ed0cf 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists RulesetID = Ruleset.Value.OnlineID, RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(), - FreeStyle = FreeStyle.Value + Freestyle = Freestyle.Value }; } } From 37abb1a21bc24185b8d554fc38f2f0cef09284e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Feb 2025 19:09:58 +0900 Subject: [PATCH 283/801] Tidy up button construction code --- osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 8d1e3c3cb1..4ca6abbf7d 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -203,13 +203,10 @@ namespace osu.Game.Screens.OnlinePlay baseButtons.Single(i => i.button is FooterButtonMods).button.TooltipText = MultiplayerMatchStrings.RequiredModsButtonTooltip; - freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) { Current = FreeMods }; - var freestyleButton = new FooterButtonFreestyle { Current = Freestyle }; - baseButtons.InsertRange(baseButtons.FindIndex(b => b.button is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[] { - (freeModsFooterButton, null), - (freestyleButton, null) + (freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) { Current = FreeMods }, null), + (new FooterButtonFreestyle { Current = Freestyle }, null) }); return baseButtons; From 8bb7bea04e56fab9247baa59ae879e16c8b4bd9b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Feb 2025 19:21:21 +0900 Subject: [PATCH 284/801] Rename freestyle select screen classes for better discoverability --- ...MatchStyleSelect.cs => MultiplayerMatchFreestyleSelect.cs} | 4 ++-- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- ...{OnlinePlayStyleSelect.cs => OnlinePlayFreestyleSelect.cs} | 4 ++-- ...istsRoomStyleSelect.cs => PlaylistsRoomFreestyleSelect.cs} | 4 ++-- .../Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) rename osu.Game/Screens/OnlinePlay/Multiplayer/{MultiplayerMatchStyleSelect.cs => MultiplayerMatchFreestyleSelect.cs} (94%) rename osu.Game/Screens/OnlinePlay/{OnlinePlayStyleSelect.cs => OnlinePlayFreestyleSelect.cs} (94%) rename osu.Game/Screens/OnlinePlay/Playlists/{PlaylistsRoomStyleSelect.cs => PlaylistsRoomFreestyleSelect.cs} (87%) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchFreestyleSelect.cs similarity index 94% rename from osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchFreestyleSelect.cs index 3fe4926052..0c04c2712c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchFreestyleSelect.cs @@ -12,7 +12,7 @@ using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Multiplayer { - public partial class MultiplayerMatchStyleSelect : OnlinePlayStyleSelect + public partial class MultiplayerMatchFreestyleSelect : OnlinePlayFreestyleSelect { [Resolved] private MultiplayerClient client { get; set; } = null!; @@ -25,7 +25,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private LoadingLayer loadingLayer = null!; private IDisposable? selectionOperation; - public MultiplayerMatchStyleSelect(Room room, PlaylistItem item) + public MultiplayerMatchFreestyleSelect(Room room, PlaylistItem item) : base(room, item) { } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index f882fb7f89..b803c5f28b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -258,7 +258,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) return; - this.Push(new MultiplayerMatchStyleSelect(Room, item)); + this.Push(new MultiplayerMatchFreestyleSelect(Room, item)); } protected override Drawable CreateFooter() => new MultiplayerMatchFooter diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs similarity index 94% rename from osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs rename to osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs index 4d34000d3c..4844d096ce 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs @@ -16,7 +16,7 @@ using osu.Game.Users; namespace osu.Game.Screens.OnlinePlay { - public abstract partial class OnlinePlayStyleSelect : SongSelect, IOnlinePlaySubScreen, IHandlePresentBeatmap + public abstract partial class OnlinePlayFreestyleSelect : SongSelect, IOnlinePlaySubScreen, IHandlePresentBeatmap { public string ShortTitle => "style selection"; @@ -29,7 +29,7 @@ namespace osu.Game.Screens.OnlinePlay private readonly Room room; private readonly PlaylistItem item; - protected OnlinePlayStyleSelect(Room room, PlaylistItem item) + protected OnlinePlayFreestyleSelect(Room room, PlaylistItem item) { this.room = room; this.item = item; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFreestyleSelect.cs similarity index 87% rename from osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs rename to osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFreestyleSelect.cs index 912496ba34..9c85088cc9 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFreestyleSelect.cs @@ -9,12 +9,12 @@ using osu.Game.Rulesets; namespace osu.Game.Screens.OnlinePlay.Playlists { - public partial class PlaylistsRoomStyleSelect : OnlinePlayStyleSelect + public partial class PlaylistsRoomFreestyleSelect : OnlinePlayFreestyleSelect { public new readonly Bindable Beatmap = new Bindable(); public new readonly Bindable Ruleset = new Bindable(); - public PlaylistsRoomStyleSelect(Room room, PlaylistItem item) + public PlaylistsRoomFreestyleSelect(Room room, PlaylistItem item) : base(room, item) { } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 2c74767f42..2195ed4722 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -319,7 +319,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) return; - this.Push(new PlaylistsRoomStyleSelect(Room, item) + this.Push(new PlaylistsRoomFreestyleSelect(Room, item) { Beatmap = { BindTarget = userBeatmap }, Ruleset = { BindTarget = userRuleset } From 99192404f125b3f5f380b4a167f7a6be1d6646ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Feb 2025 19:26:14 +0900 Subject: [PATCH 285/801] Tidy up `WorkingBeatmap` passing in `ctor` --- .../Screens/Play/MasterGameplayClockContainer.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 747ea3090c..07ecb5a5fb 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -12,6 +12,7 @@ using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Overlays; +using osu.Game.Storyboards; namespace osu.Game.Screens.Play { @@ -72,10 +73,10 @@ namespace osu.Game.Screens.Play track = working.Track; GameplayStartTime = gameplayStartTime; - StartTime = findEarliestStartTime(gameplayStartTime, working); + StartTime = findEarliestStartTime(gameplayStartTime, beatmap, working.Storyboard); } - private static double findEarliestStartTime(double gameplayStartTime, WorkingBeatmap working) + private static double findEarliestStartTime(double gameplayStartTime, IBeatmap beatmap, Storyboard storyboard) { // here we are trying to find the time to start playback from the "zero" point. // generally this is either zero, or some point earlier than zero in the case of storyboards, lead-ins etc. @@ -85,15 +86,15 @@ namespace osu.Game.Screens.Play // if a storyboard is present, it may dictate the appropriate start time by having events in negative time space. // this is commonly used to display an intro before the audio track start. - double? firstStoryboardEvent = working.Storyboard.EarliestEventTime; + double? firstStoryboardEvent = storyboard.EarliestEventTime; if (firstStoryboardEvent != null) time = Math.Min(time, firstStoryboardEvent.Value); // some beatmaps specify a current lead-in time which should be used instead of the ruleset-provided value when available. // this is not available as an option in the live editor but can still be applied via .osu editing. - double firstHitObjectTime = working.Beatmap.HitObjects.First().StartTime; - if (working.Beatmap.AudioLeadIn > 0) - time = Math.Min(time, firstHitObjectTime - working.Beatmap.AudioLeadIn); + double firstHitObjectTime = beatmap.HitObjects.First().StartTime; + if (beatmap.AudioLeadIn > 0) + time = Math.Min(time, firstHitObjectTime - beatmap.AudioLeadIn); return time; } From c7780c9fdca97525d2f20920bc44951b652e4854 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Feb 2025 19:53:46 +0900 Subject: [PATCH 286/801] Refactor how grouping is performed --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 2 +- .../TestSceneBeatmapCarouselV2Basics.cs | 2 +- ...estSceneBeatmapCarouselV2GroupSelection.cs | 2 +- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 133 +++++++++++------- 4 files changed, 85 insertions(+), 54 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 5143d681a6..0a9719423c 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.SongSelect }); } - protected void SortBy(FilterCriteria criteria) => AddStep($"sort by {criteria.Sort}", () => Carousel.Filter(criteria)); + protected void SortBy(FilterCriteria criteria) => AddStep($"sort {criteria.Sort} group {criteria.Group}", () => Carousel.Filter(criteria)); protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); protected void WaitForSorting() => AddUntilStep("sorting finished", () => Carousel.IsFiltering, () => Is.False); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs index 0e72ee4f8c..8ffb51b995 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.SongSelect public void TestSorting() { AddBeatmaps(10); - SortBy(new FilterCriteria { Sort = SortMode.Difficulty }); + SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty }); SortBy(new FilterCriteria { Sort = SortMode.Artist }); } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs index bcb609500f..5728583507 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.SongSelect CreateCarousel(); - SortBy(new FilterCriteria { Sort = SortMode.Difficulty }); + SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty }); } [Test] diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 951b010564..34fbfdbaa6 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using osu.Game.Beatmaps; using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; namespace osu.Game.Screens.SelectV2 { @@ -35,70 +36,100 @@ namespace osu.Game.Screens.SelectV2 public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => { + bool groupSetsTogether; + setItems.Clear(); groupItems.Clear(); var criteria = getCriteria(); - - int starGroup = int.MinValue; - - if (criteria.SplitOutDifficulties) - { - var diffItems = new List(items.Count()); - - GroupDefinition? group = null; - - foreach (var item in items) - { - var b = (BeatmapInfo)item.Model; - - if (b.StarRating > starGroup) - { - starGroup = (int)Math.Floor(b.StarRating); - group = new GroupDefinition($"{starGroup} - {++starGroup} *"); - diffItems.Add(new CarouselItem(group) { DrawHeight = GroupPanel.HEIGHT }); - } - - if (!groupItems.TryGetValue(group!, out var related)) - groupItems[group!] = related = new HashSet(); - related.Add(item); - - diffItems.Add(item); - - item.IsVisible = false; - } - - return diffItems; - } - - CarouselItem? lastItem = null; - var newItems = new List(items.Count()); - foreach (var item in items) + // Add criteria groups. + switch (criteria.Group) + { + default: + groupSetsTogether = true; + newItems.AddRange(items); + break; + + case GroupMode.Difficulty: + groupSetsTogether = false; + int starGroup = int.MinValue; + + foreach (var item in items) + { + cancellationToken.ThrowIfCancellationRequested(); + + var b = (BeatmapInfo)item.Model; + + if (b.StarRating > starGroup) + { + starGroup = (int)Math.Floor(b.StarRating); + newItems.Add(new CarouselItem(new GroupDefinition($"{starGroup} - {++starGroup} *")) { DrawHeight = GroupPanel.HEIGHT }); + } + + newItems.Add(item); + } + + break; + } + + // Add set headers wherever required. + CarouselItem? lastItem = null; + + if (groupSetsTogether) + { + for (int i = 0; i < newItems.Count; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + + var item = newItems[i]; + + if (item.Model is BeatmapInfo beatmap) + { + if (groupSetsTogether) + { + bool newBeatmapSet = lastItem == null || (lastItem.Model is BeatmapInfo lastBeatmap && lastBeatmap.BeatmapSet!.ID != beatmap.BeatmapSet!.ID); + + if (newBeatmapSet) + { + newItems.Insert(i, new CarouselItem(beatmap.BeatmapSet!) { DrawHeight = BeatmapSetPanel.HEIGHT }); + i++; + } + + if (!setItems.TryGetValue(beatmap.BeatmapSet!, out var related)) + setItems[beatmap.BeatmapSet!] = related = new HashSet(); + + related.Add(item); + item.IsVisible = false; + } + } + + lastItem = item; + } + } + + // Link group items to their headers. + GroupDefinition? lastGroup = null; + + foreach (var item in newItems) { cancellationToken.ThrowIfCancellationRequested(); - if (item.Model is BeatmapInfo b) + if (item.Model is GroupDefinition group) { - // Add set header - if (lastItem == null || (lastItem.Model is BeatmapInfo b2 && b2.BeatmapSet!.OnlineID != b.BeatmapSet!.OnlineID)) - { - newItems.Add(new CarouselItem(b.BeatmapSet!) - { - DrawHeight = BeatmapSetPanel.HEIGHT, - }); - } - - if (!setItems.TryGetValue(b.BeatmapSet!, out var related)) - setItems[b.BeatmapSet!] = related = new HashSet(); - related.Add(item); + lastGroup = group; + continue; } - newItems.Add(item); - lastItem = item; + if (lastGroup != null) + { + if (!groupItems.TryGetValue(lastGroup, out var groupRelated)) + groupItems[lastGroup] = groupRelated = new HashSet(); + groupRelated.Add(item); - item.IsVisible = false; + item.IsVisible = false; + } } return newItems; From a1185df2ebb833c0cfb9a4a93987a2a97e547453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 22 Jan 2025 14:33:14 +0100 Subject: [PATCH 287/801] Refactor `IDistanceSnapProvider` to accept slider velocity objects as a reference Method signatures are also changed to be a lot more explicit as to what inputs they expect. --- .../Edit/CatchDistanceSnapProvider.cs | 2 +- .../Components/PathControlPointVisualiser.cs | 2 +- .../Sliders/SliderPlacementBlueprint.cs | 2 +- .../Sliders/SliderSelectionBlueprint.cs | 4 +- .../Edit/OsuDistanceSnapGrid.cs | 5 +- .../Edit/OsuDistanceSnapProvider.cs | 2 +- .../Edit/OsuHitObjectComposer.cs | 17 ++-- ...tSceneHitObjectComposerDistanceSnapping.cs | 31 +++---- .../Editing/TestSceneDistanceSnapGrid.cs | 14 +-- .../Edit/ComposerDistanceSnapProvider.cs | 46 +++------ .../Rulesets/Edit/IDistanceSnapProvider.cs | 93 ++++++++++++------- .../Rulesets/Objects/SliderPathExtensions.cs | 4 +- .../Components/CircularDistanceSnapGrid.cs | 30 +++--- .../Compose/Components/DistanceSnapGrid.cs | 20 ++-- 14 files changed, 138 insertions(+), 134 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs b/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs index ae4025aa2f..420a0eb34f 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Edit // // The implementation below is probably correct but should be checked if/when exposed via controls. - float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime()); + float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime); float previousEndX = (before as JuiceStream)?.EndX ?? ((CatchHitObject)before).EffectiveX; float actualDistance = Math.Abs(previousEndX - ((CatchHitObject)after).EffectiveX); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index b9938209ae..bc3d27fd68 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -34,7 +34,7 @@ using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { public partial class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler, IHasContextMenu - where T : OsuHitObject, IHasPath + where T : OsuHitObject, IHasPath, IHasSliderVelocity { public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside the playfield. diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 21817045c4..a747d4fce8 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -434,7 +434,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders if (state == SliderPlacementState.Drawing) HitObject.Path.ExpectedDistance.Value = (float)HitObject.Path.CalculatedDistance; else - HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? (float)HitObject.Path.CalculatedDistance; + HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance((float)HitObject.Path.CalculatedDistance, HitObject.StartTime, HitObject) ?? (float)HitObject.Path.CalculatedDistance; bodyPiece.UpdateFrom(HitObject); headCirclePiece.UpdateFrom(HitObject.HeadCircle); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 740862c9fd..f7c25b43dd 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -274,9 +274,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } else { - double minDistance = distanceSnapProvider?.GetBeatSnapDistanceAt(HitObject, false) * oldVelocityMultiplier ?? 1; + double minDistance = distanceSnapProvider?.GetBeatSnapDistance() * oldVelocityMultiplier ?? 1; // Add a small amount to the proposed distance to make it easier to snap to the full length of the slider. - proposedDistance = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)proposedDistance + 1, DistanceSnapTarget.Start) ?? proposedDistance; + proposedDistance = distanceSnapProvider?.FindSnappedDistance((float)proposedDistance + 1, HitObject.StartTime, HitObject) ?? proposedDistance; proposedDistance = MathHelper.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance); } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs index 848c994974..3323acce15 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs @@ -5,6 +5,7 @@ using JetBrains.Annotations; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit.Compose.Components; @@ -12,8 +13,8 @@ namespace osu.Game.Rulesets.Osu.Edit { public partial class OsuDistanceSnapGrid : CircularDistanceSnapGrid { - public OsuDistanceSnapGrid(OsuHitObject hitObject, [CanBeNull] OsuHitObject nextHitObject = null) - : base(hitObject, hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime - 1) + public OsuDistanceSnapGrid(OsuHitObject hitObject, [CanBeNull] OsuHitObject nextHitObject = null, [CanBeNull] IHasSliderVelocity sliderVelocitySource = null) + : base(hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime - 1, sliderVelocitySource) { Masking = true; } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs index 4042cfa0e2..3c0889d027 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Edit { public override double ReadCurrentDistanceSnap(HitObject before, HitObject after) { - float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime()); + float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime); float actualDistance = Vector2.Distance(((OsuHitObject)before).EndPosition, ((OsuHitObject)after).Position); return actualDistance / expectedDistance; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 563d0b1e3e..60c37cd4a4 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -23,6 +23,7 @@ using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; @@ -406,22 +407,26 @@ namespace osu.Game.Rulesets.Osu.Edit { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(targetOffset); - int sourceIndex = -1; + int positionSourceObjectIndex = -1; + IHasSliderVelocity? sliderVelocitySource = null; for (int i = 0; i < EditorBeatmap.HitObjects.Count; i++) { if (!sourceSelector(EditorBeatmap.HitObjects[i])) break; - sourceIndex = i; + positionSourceObjectIndex = i; + + if (EditorBeatmap.HitObjects[i] is IHasSliderVelocity hasSliderVelocity) + sliderVelocitySource = hasSliderVelocity; } - if (sourceIndex == -1) + if (positionSourceObjectIndex == -1) return null; - HitObject sourceObject = EditorBeatmap.HitObjects[sourceIndex]; + HitObject sourceObject = EditorBeatmap.HitObjects[positionSourceObjectIndex]; - int targetIndex = sourceIndex + targetOffset; + int targetIndex = positionSourceObjectIndex + targetOffset; HitObject targetObject = null; // Keep advancing the target object while its start time falls before the end time of the source object @@ -442,7 +447,7 @@ namespace osu.Game.Rulesets.Osu.Edit if (sourceObject is Spinner) return null; - return new OsuDistanceSnapGrid((OsuHitObject)sourceObject, (OsuHitObject)targetObject); + return new OsuDistanceSnapGrid((OsuHitObject)sourceObject, (OsuHitObject)targetObject, sliderVelocitySource); } } } diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index 0f8583253b..af116ad334 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs @@ -12,6 +12,7 @@ using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Edit; @@ -67,17 +68,7 @@ namespace osu.Game.Tests.Editing { AddStep($"set slider multiplier = {multiplier}", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = multiplier); - assertSnapDistance(100 * multiplier, null, true); - } - - [TestCase(1)] - [TestCase(2)] - public void TestSpeedMultiplierDoesNotChangeDistanceSnap(float multiplier) - { - assertSnapDistance(100, new Slider - { - SliderVelocityMultiplier = multiplier - }, false); + assertSnapDistance(100 * multiplier); } [TestCase(1)] @@ -87,7 +78,7 @@ namespace osu.Game.Tests.Editing assertSnapDistance(100 * multiplier, new Slider { SliderVelocityMultiplier = multiplier - }, true); + }); } [TestCase(1)] @@ -96,7 +87,7 @@ namespace osu.Game.Tests.Editing { AddStep($"set divisor = {divisor}", () => BeatDivisor.Value = divisor); - assertSnapDistance(100f / divisor, null, true); + assertSnapDistance(100f / divisor); } /// @@ -114,7 +105,7 @@ namespace osu.Game.Tests.Editing }; AddStep("add to beatmap", () => composer.EditorBeatmap.Add(referenceObject)); - assertSnapDistance(base_distance * slider_velocity, referenceObject, true); + assertSnapDistance(base_distance * slider_velocity, referenceObject); assertSnappedDistance(base_distance * slider_velocity + 10, base_distance * slider_velocity, referenceObject); assertSnappedDuration(base_distance * slider_velocity + 10, 1000, referenceObject); @@ -289,20 +280,20 @@ namespace osu.Game.Tests.Editing AddUntilStep("use current snap not available", () => getCurrentSnapButton().Enabled.Value, () => Is.False); } - private void assertSnapDistance(float expectedDistance, HitObject? referenceObject, bool includeSliderVelocity) - => AddAssert($"distance is {expectedDistance}", () => composer.DistanceSnapProvider.GetBeatSnapDistanceAt(referenceObject ?? new HitObject(), includeSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); + private void assertSnapDistance(float expectedDistance, IHasSliderVelocity? hasSliderVelocity = null) + => AddAssert($"distance is {expectedDistance}", () => composer.DistanceSnapProvider.GetBeatSnapDistance(hasSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); private void assertDurationToDistance(double duration, float expectedDistance, HitObject? referenceObject = null) - => AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DistanceSnapProvider.DurationToDistance(referenceObject ?? new HitObject(), duration), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); + => AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DistanceSnapProvider.DurationToDistance(duration, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); private void assertDistanceToDuration(float distance, double expectedDuration, HitObject? referenceObject = null) - => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceSnapProvider.DistanceToDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON)); + => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceSnapProvider.DistanceToDuration(distance, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON)); private void assertSnappedDuration(float distance, double expectedDuration, HitObject? referenceObject = null) - => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON)); + => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDuration(distance, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON)); private void assertSnappedDistance(float distance, float expectedDistance, HitObject? referenceObject = null) - => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDistance(referenceObject ?? new HitObject(), distance, DistanceSnapTarget.End), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); + => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDistance(distance, referenceObject?.GetEndTime() ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); private partial class TestHitObjectComposer : OsuHitObjectComposer { diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs index 818862d958..51e4f526a1 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs @@ -10,7 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Screens.Edit; @@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.Editing public new int MaxIntervals => base.MaxIntervals; public TestDistanceSnapGrid(double? endTime = null) - : base(new HitObject(), grid_position, 0, endTime) + : base(grid_position, 0, endTime) { } @@ -191,15 +191,15 @@ namespace osu.Game.Tests.Visual.Editing Bindable IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier; - public float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true) => beat_snap_distance; + public float GetBeatSnapDistance(IHasSliderVelocity withVelocity = null) => beat_snap_distance; - public float DurationToDistance(HitObject referenceObject, double duration) => (float)duration; + public float DurationToDistance(double duration, double timingReference, IHasSliderVelocity withVelocity = null) => (float)duration; - public double DistanceToDuration(HitObject referenceObject, float distance) => distance; + public double DistanceToDuration(float distance, double timingReference, IHasSliderVelocity withVelocity = null) => distance; - public double FindSnappedDuration(HitObject referenceObject, float distance) => 0; + public double FindSnappedDuration(float distance, double snapReferenceTime, IHasSliderVelocity withVelocity = null) => 0; - public float FindSnappedDistance(HitObject referenceObject, float distance, DistanceSnapTarget target) => 0; + public float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null) => 0; } } } diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs index 0ca01ccee6..997d1f927b 100644 --- a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs @@ -265,57 +265,41 @@ namespace osu.Game.Rulesets.Edit #region IDistanceSnapProvider - public virtual float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true) + public virtual float GetBeatSnapDistance(IHasSliderVelocity? withVelocity = null) { - return (float)(100 * (useReferenceSliderVelocity && referenceObject is IHasSliderVelocity hasSliderVelocity ? hasSliderVelocity.SliderVelocityMultiplier : 1) * editorBeatmap.Difficulty.SliderMultiplier * 1 + return (float)(100 * (withVelocity?.SliderVelocityMultiplier ?? 1) * editorBeatmap.Difficulty.SliderMultiplier * 1 / beatSnapProvider.BeatDivisor); } - public virtual float DurationToDistance(HitObject referenceObject, double duration) + public virtual float DurationToDistance(double duration, double timingReference, IHasSliderVelocity? withVelocity = null) { - double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime); - return (float)(duration / beatLength * GetBeatSnapDistanceAt(referenceObject)); + double beatLength = beatSnapProvider.GetBeatLengthAtTime(timingReference); + return (float)(duration / beatLength * GetBeatSnapDistance(withVelocity)); } - public virtual double DistanceToDuration(HitObject referenceObject, float distance) + public virtual double DistanceToDuration(float distance, double timingReference, IHasSliderVelocity? withVelocity = null) { - double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime); - return distance / GetBeatSnapDistanceAt(referenceObject) * beatLength; + double beatLength = beatSnapProvider.GetBeatLengthAtTime(timingReference); + return distance / GetBeatSnapDistance(withVelocity) * beatLength; } - public virtual double FindSnappedDuration(HitObject referenceObject, float distance) - => beatSnapProvider.SnapTime(referenceObject.StartTime + DistanceToDuration(referenceObject, distance), referenceObject.StartTime) - referenceObject.StartTime; + public virtual double FindSnappedDuration(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null) + => beatSnapProvider.SnapTime(snapReferenceTime + DistanceToDuration(distance, snapReferenceTime, withVelocity), snapReferenceTime) - snapReferenceTime; - public virtual float FindSnappedDistance(HitObject referenceObject, float distance, DistanceSnapTarget target) + public virtual float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null) { - double referenceTime; + double actualDuration = snapReferenceTime + DistanceToDuration(distance, snapReferenceTime, withVelocity); - switch (target) - { - case DistanceSnapTarget.Start: - referenceTime = referenceObject.StartTime; - break; + double snappedTime = beatSnapProvider.SnapTime(actualDuration, snapReferenceTime); - case DistanceSnapTarget.End: - referenceTime = referenceObject.GetEndTime(); - break; - - default: - throw new ArgumentOutOfRangeException(nameof(target), target, $"Unknown {nameof(DistanceSnapTarget)} value"); - } - - double actualDuration = referenceTime + DistanceToDuration(referenceObject, distance); - - double snappedTime = beatSnapProvider.SnapTime(actualDuration, referenceTime); - - double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceTime); + double beatLength = beatSnapProvider.GetBeatLengthAtTime(snapReferenceTime); // we don't want to exceed the actual duration and snap to a point in the future. // as we are snapping to beat length via SnapTime (which will round-to-nearest), check for snapping in the forward direction and reverse it. if (snappedTime > actualDuration + 1) snappedTime -= beatLength; - return DurationToDistance(referenceObject, snappedTime - referenceTime); + return DurationToDistance(snappedTime - snapReferenceTime, snapReferenceTime, withVelocity); } #endregion diff --git a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs index 612e09d3ea..99a9083273 100644 --- a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs @@ -4,7 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Beatmaps; -using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Edit { @@ -22,53 +22,74 @@ namespace osu.Game.Rulesets.Edit Bindable DistanceSpacingMultiplier { get; } /// - /// Retrieves the distance between two points within a timing point that are one beat length apart. + /// Returns the spatial distance between objects which are temporally one beat apart. + /// Depends on: + /// + /// the slider velocity taken from , + /// the beatmap's ,, + /// the current beat divisor. + /// + /// Note that the returned value does NOT depend on ; + /// consumers are expected to include that multiplier as they see fit. /// - /// An object to be used as a reference point for this operation. - /// Whether the 's slider velocity should be factored into the returned distance. - /// The distance between two points residing in the timing point that are one beat length apart. - float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true); + float GetBeatSnapDistance(IHasSliderVelocity? withVelocity = null); /// - /// Converts a duration to a distance without applying any snapping. + /// Converts a temporal duration into a spatial distance. + /// Does not perform any snapping. + /// Depends on: + /// + /// the provided, + /// a used to retrieve the beat length of the beatmap at that time, + /// the slider velocity taken from , + /// the beatmap's ,, + /// the current beat divisor. + /// + /// Note that the returned value does NOT depend on ; + /// consumers are expected to include that multiplier as they see fit. /// - /// An object to be used as a reference point for this operation. - /// The duration to convert. - /// A value that represents as a distance in the timing point. - float DurationToDistance(HitObject referenceObject, double duration); + float DurationToDistance(double duration, double timingReference, IHasSliderVelocity? withVelocity = null); /// - /// Converts a distance to a duration without applying any snapping. + /// Converts a spatial distance into a temporal duration. + /// Does not perform any snapping. + /// Depends on: + /// + /// the provided, + /// a used to retrieve the beat length of the beatmap at that time, + /// the slider velocity taken from , + /// the beatmap's ,, + /// the current beat divisor. + /// + /// Note that the returned value does NOT depend on ; + /// consumers are expected to include that multiplier as they see fit. /// - /// An object to be used as a reference point for this operation. - /// The distance to convert. - /// A value that represents as a duration in the timing point. - double DistanceToDuration(HitObject referenceObject, float distance); + double DistanceToDuration(float distance, double timingReference, IHasSliderVelocity? withVelocity = null); /// - /// Given a distance from the provided hit object, find the valid snapped duration. + /// Converts a spatial distance into a temporal duration and then snaps said duration to the beat, relative to . + /// Depends on: + /// + /// the provided, + /// a used to retrieve the beat length of the beatmap at that time, + /// the slider velocity taken from , + /// the beatmap's ,, + /// the current beat divisor. + /// /// - /// An object to be used as a reference point for this operation. - /// The distance to convert. - /// A value that represents as a duration snapped to the closest beat of the timing point. - double FindSnappedDuration(HitObject referenceObject, float distance); + double FindSnappedDuration(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null); /// - /// Given a distance from the provided hit object, find the valid snapped distance. + /// Snaps a spatial distance to the beat, relative to . + /// Depends on: + /// + /// the provided, + /// a used to retrieve the beat length of the beatmap at that time, + /// the slider velocity taken from , + /// the beatmap's ,, + /// the current beat divisor. + /// /// - /// An object to be used as a reference point for this operation. - /// The distance to convert. - /// Whether the distance measured should be from the start or the end of . - /// - /// A value that represents snapped to the closest beat of the timing point. - /// The distance will always be less than or equal to the provided . - /// - float FindSnappedDistance(HitObject referenceObject, float distance, DistanceSnapTarget target); - } - - public enum DistanceSnapTarget - { - Start, - End, + float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null); } } diff --git a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs index a631274f74..4ce8166421 100644 --- a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs +++ b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs @@ -15,9 +15,9 @@ namespace osu.Game.Rulesets.Objects /// Snaps the provided 's duration using the . /// public static void SnapTo(this THitObject hitObject, IDistanceSnapProvider? snapProvider) - where THitObject : HitObject, IHasPath + where THitObject : HitObject, IHasPath, IHasSliderVelocity { - hitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance(hitObject, (float)hitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? hitObject.Path.CalculatedDistance; + hitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance((float)hitObject.Path.CalculatedDistance, hitObject.StartTime, hitObject) ?? hitObject.Path.CalculatedDistance; } /// diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs index e84c2ebc35..9ddf54b779 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osuTK; using osuTK.Graphics; @@ -16,8 +16,8 @@ namespace osu.Game.Screens.Edit.Compose.Components { public abstract partial class CircularDistanceSnapGrid : DistanceSnapGrid { - protected CircularDistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null) - : base(referenceObject, startPosition, startTime, endTime) + protected CircularDistanceSnapGrid(Vector2 startPosition, double startTime, double? endTime = null, IHasSliderVelocity? sliderVelocitySource = null) + : base(startPosition, startTime, endTime, sliderVelocitySource) { } @@ -56,14 +56,14 @@ namespace osu.Game.Screens.Edit.Compose.Components // Picture the scenario where the user has just placed an object on a 1/2 snap, then changes to // 1/3 snap and expects to be able to place the next object on a valid 1/3 snap, regardless of the // fact that the 1/2 snap reference object is not valid for 1/3 snapping. - float offset = SnapProvider.FindSnappedDistance(ReferenceObject, 0, DistanceSnapTarget.End); + float offset = SnapProvider.FindSnappedDistance(0, StartTime, SliderVelocitySource); for (int i = 0; i < requiredCircles; i++) { const float thickness = 4; float diameter = (offset + (i + 1) * DistanceBetweenTicks + thickness / 2) * 2; - AddInternal(new Ring(ReferenceObject, GetColourForIndexFromPlacement(i)) + AddInternal(new Ring(StartTime, GetColourForIndexFromPlacement(i)) { Position = StartPosition, Origin = Anchor.Centre, @@ -98,19 +98,19 @@ namespace osu.Game.Screens.Edit.Compose.Components travelLength = DistanceBetweenTicks; float snappedDistance = fixedTime != null - ? SnapProvider.DurationToDistance(ReferenceObject, fixedTime.Value - ReferenceObject.GetEndTime()) + ? SnapProvider.DurationToDistance(fixedTime.Value - StartTime, StartTime, SliderVelocitySource) // When interacting with the resolved snap provider, the distance spacing multiplier should first be removed // to allow for snapping at a non-multiplied ratio. - : SnapProvider.FindSnappedDistance(ReferenceObject, travelLength / distanceSpacingMultiplier, DistanceSnapTarget.End); + : SnapProvider.FindSnappedDistance(travelLength / distanceSpacingMultiplier, StartTime, SliderVelocitySource); - double snappedTime = StartTime + SnapProvider.DistanceToDuration(ReferenceObject, snappedDistance); + double snappedTime = StartTime + SnapProvider.DistanceToDuration(snappedDistance, StartTime, SliderVelocitySource); if (snappedTime > LatestEndTime) { double tickLength = Beatmap.GetBeatLengthAtTime(StartTime); - snappedDistance = SnapProvider.DurationToDistance(ReferenceObject, MaxIntervals * tickLength); - snappedTime = StartTime + SnapProvider.DistanceToDuration(ReferenceObject, snappedDistance); + snappedDistance = SnapProvider.DurationToDistance(MaxIntervals * tickLength, StartTime, SliderVelocitySource); + snappedTime = StartTime + SnapProvider.DistanceToDuration(snappedDistance, StartTime, SliderVelocitySource); } // The multiplier can then be reapplied to the final position. @@ -127,13 +127,13 @@ namespace osu.Game.Screens.Edit.Compose.Components [Resolved] private EditorClock? editorClock { get; set; } - private readonly HitObject referenceObject; + private readonly double startTime; private readonly Color4 baseColour; - public Ring(HitObject referenceObject, Color4 baseColour) + public Ring(double startTime, Color4 baseColour) { - this.referenceObject = referenceObject; + this.startTime = startTime; Colour = this.baseColour = baseColour; @@ -148,9 +148,9 @@ namespace osu.Game.Screens.Edit.Compose.Components return; float distanceSpacingMultiplier = (float)snapProvider.DistanceSpacingMultiplier.Value; - double timeFromReferencePoint = editorClock.CurrentTime - referenceObject.GetEndTime(); + double timeFromReferencePoint = editorClock.CurrentTime - startTime; - float distanceForCurrentTime = snapProvider.DurationToDistance(referenceObject, timeFromReferencePoint) + float distanceForCurrentTime = snapProvider.DurationToDistance(timeFromReferencePoint, startTime) * distanceSpacingMultiplier; float timeBasedAlpha = 1 - Math.Clamp(Math.Abs(distanceForCurrentTime - Size.X / 2) / 30, 0, 1); diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs index aaf58e0f7a..dd1671cfdd 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -12,7 +13,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Layout; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osuTK; using osuTK.Graphics; @@ -48,6 +49,9 @@ namespace osu.Game.Screens.Edit.Compose.Components protected readonly double? LatestEndTime; + [CanBeNull] + protected readonly IHasSliderVelocity SliderVelocitySource; + [Resolved] protected OsuColour Colours { get; private set; } @@ -62,19 +66,17 @@ namespace osu.Game.Screens.Edit.Compose.Components private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); - protected readonly HitObject ReferenceObject; - /// /// Creates a new . /// - /// A reference object to gather relevant difficulty values from. /// The position at which the grid should start. The first tick is located one distance spacing length away from this point. /// The snapping time at . /// The time at which the snapping grid should end. If null, the grid will continue until the bounds of the screen are exceeded. - protected DistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null) + /// The reference object with slider velocity to include in the calculations for distance snapping. + protected DistanceSnapGrid(Vector2 startPosition, double startTime, double? endTime = null, [CanBeNull] IHasSliderVelocity sliderVelocitySource = null) { - ReferenceObject = referenceObject; LatestEndTime = endTime; + SliderVelocitySource = sliderVelocitySource; StartPosition = startPosition; StartTime = startTime; @@ -97,14 +99,14 @@ namespace osu.Game.Screens.Edit.Compose.Components private void updateSpacing() { float distanceSpacingMultiplier = (float)DistanceSpacingMultiplier.Value; - float beatSnapDistance = SnapProvider.GetBeatSnapDistanceAt(ReferenceObject, false); + float beatSnapDistance = SnapProvider.GetBeatSnapDistance(SliderVelocitySource); DistanceBetweenTicks = beatSnapDistance * distanceSpacingMultiplier; if (LatestEndTime == null) MaxIntervals = int.MaxValue; else - MaxIntervals = (int)((LatestEndTime.Value - StartTime) / SnapProvider.DistanceToDuration(ReferenceObject, beatSnapDistance)); + MaxIntervals = (int)((LatestEndTime.Value - StartTime) / SnapProvider.DistanceToDuration(beatSnapDistance, StartTime, SliderVelocitySource)); gridCache.Invalidate(); } @@ -132,7 +134,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The original position in coordinate space local to this . /// /// Whether the snap operation should be temporally constrained to a particular time instant, - /// thus fixing the possible positions to a set distance from the . + /// thus fixing the possible positions to a set distance relative from the . /// /// A tuple containing the snapped position in coordinate space local to this and the respective time value. public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position, double? fixedTime = null); From df37768ff4075ca4de2c4afee377967d745d5e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Feb 2025 13:55:04 +0100 Subject: [PATCH 288/801] Remove unused method Only used in test code. --- ...tSceneHitObjectComposerDistanceSnapping.cs | 37 ------------------- .../Editing/TestSceneDistanceSnapGrid.cs | 2 - .../Edit/ComposerDistanceSnapProvider.cs | 3 -- .../Rulesets/Edit/IDistanceSnapProvider.cs | 13 ------- 4 files changed, 55 deletions(-) diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index af116ad334..408db39d54 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs @@ -107,7 +107,6 @@ namespace osu.Game.Tests.Editing assertSnapDistance(base_distance * slider_velocity, referenceObject); assertSnappedDistance(base_distance * slider_velocity + 10, base_distance * slider_velocity, referenceObject); - assertSnappedDuration(base_distance * slider_velocity + 10, 1000, referenceObject); assertDistanceToDuration(base_distance * slider_velocity, 1000, referenceObject); assertDurationToDistance(1000, base_distance * slider_velocity, referenceObject); @@ -155,39 +154,6 @@ namespace osu.Game.Tests.Editing assertDistanceToDuration(400, 1000); } - [Test] - public void TestGetSnappedDurationFromDistance() - { - assertSnappedDuration(0, 0); - assertSnappedDuration(50, 1000); - assertSnappedDuration(100, 1000); - assertSnappedDuration(150, 2000); - assertSnappedDuration(200, 2000); - assertSnappedDuration(250, 3000); - - AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = 2); - - assertSnappedDuration(0, 0); - assertSnappedDuration(50, 0); - assertSnappedDuration(100, 1000); - assertSnappedDuration(150, 1000); - assertSnappedDuration(200, 1000); - assertSnappedDuration(250, 1000); - - AddStep("set beat length = 500", () => - { - composer.EditorBeatmap.ControlPointInfo.Clear(); - composer.EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 }); - }); - - assertSnappedDuration(50, 0); - assertSnappedDuration(100, 500); - assertSnappedDuration(150, 500); - assertSnappedDuration(200, 500); - assertSnappedDuration(250, 500); - assertSnappedDuration(400, 1000); - } - [Test] public void GetSnappedDistanceFromDistance() { @@ -289,9 +255,6 @@ namespace osu.Game.Tests.Editing private void assertDistanceToDuration(float distance, double expectedDuration, HitObject? referenceObject = null) => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceSnapProvider.DistanceToDuration(distance, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON)); - private void assertSnappedDuration(float distance, double expectedDuration, HitObject? referenceObject = null) - => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDuration(distance, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON)); - private void assertSnappedDistance(float distance, float expectedDistance, HitObject? referenceObject = null) => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDistance(distance, referenceObject?.GetEndTime() ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs index 51e4f526a1..af02333468 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs @@ -197,8 +197,6 @@ namespace osu.Game.Tests.Visual.Editing public double DistanceToDuration(float distance, double timingReference, IHasSliderVelocity withVelocity = null) => distance; - public double FindSnappedDuration(float distance, double snapReferenceTime, IHasSliderVelocity withVelocity = null) => 0; - public float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null) => 0; } } diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs index 997d1f927b..d0b279f201 100644 --- a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs @@ -283,9 +283,6 @@ namespace osu.Game.Rulesets.Edit return distance / GetBeatSnapDistance(withVelocity) * beatLength; } - public virtual double FindSnappedDuration(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null) - => beatSnapProvider.SnapTime(snapReferenceTime + DistanceToDuration(distance, snapReferenceTime, withVelocity), snapReferenceTime) - snapReferenceTime; - public virtual float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null) { double actualDuration = snapReferenceTime + DistanceToDuration(distance, snapReferenceTime, withVelocity); diff --git a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs index 99a9083273..8006db14a3 100644 --- a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs @@ -66,19 +66,6 @@ namespace osu.Game.Rulesets.Edit /// double DistanceToDuration(float distance, double timingReference, IHasSliderVelocity? withVelocity = null); - /// - /// Converts a spatial distance into a temporal duration and then snaps said duration to the beat, relative to . - /// Depends on: - /// - /// the provided, - /// a used to retrieve the beat length of the beatmap at that time, - /// the slider velocity taken from , - /// the beatmap's ,, - /// the current beat divisor. - /// - /// - double FindSnappedDuration(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null); - /// /// Snaps a spatial distance to the beat, relative to . /// Depends on: From 2d6f64e89185e71d755201c52c951236116a0fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Feb 2025 15:17:32 +0100 Subject: [PATCH 289/801] Fix code quality --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 2 +- osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs | 2 +- osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 60c37cd4a4..b3e23daa99 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -408,7 +408,7 @@ namespace osu.Game.Rulesets.Osu.Edit ArgumentOutOfRangeException.ThrowIfNegativeOrZero(targetOffset); int positionSourceObjectIndex = -1; - IHasSliderVelocity? sliderVelocitySource = null; + IHasSliderVelocity sliderVelocitySource = null; for (int i = 0; i < EditorBeatmap.HitObjects.Count; i++) { diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs index af02333468..fb57422e66 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs @@ -197,7 +197,7 @@ namespace osu.Game.Tests.Visual.Editing public double DistanceToDuration(float distance, double timingReference, IHasSliderVelocity withVelocity = null) => distance; - public float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null) => 0; + public float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity withVelocity = null) => 0; } } } diff --git a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs index 8006db14a3..195dbf0d46 100644 --- a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs @@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Edit double DistanceToDuration(float distance, double timingReference, IHasSliderVelocity? withVelocity = null); /// - /// Snaps a spatial distance to the beat, relative to . + /// Snaps a spatial distance to the beat, relative to . /// Depends on: /// /// the provided, From b433eef1389ae8a07627ee6a9597bebe336d61c6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 01:51:43 +0900 Subject: [PATCH 290/801] Remove redundant conditional check --- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 34fbfdbaa6..ea737d8b7f 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -87,22 +87,19 @@ namespace osu.Game.Screens.SelectV2 if (item.Model is BeatmapInfo beatmap) { - if (groupSetsTogether) + bool newBeatmapSet = lastItem == null || (lastItem.Model is BeatmapInfo lastBeatmap && lastBeatmap.BeatmapSet!.ID != beatmap.BeatmapSet!.ID); + + if (newBeatmapSet) { - bool newBeatmapSet = lastItem == null || (lastItem.Model is BeatmapInfo lastBeatmap && lastBeatmap.BeatmapSet!.ID != beatmap.BeatmapSet!.ID); - - if (newBeatmapSet) - { - newItems.Insert(i, new CarouselItem(beatmap.BeatmapSet!) { DrawHeight = BeatmapSetPanel.HEIGHT }); - i++; - } - - if (!setItems.TryGetValue(beatmap.BeatmapSet!, out var related)) - setItems[beatmap.BeatmapSet!] = related = new HashSet(); - - related.Add(item); - item.IsVisible = false; + newItems.Insert(i, new CarouselItem(beatmap.BeatmapSet!) { DrawHeight = BeatmapSetPanel.HEIGHT }); + i++; } + + if (!setItems.TryGetValue(beatmap.BeatmapSet!, out var related)) + setItems[beatmap.BeatmapSet!] = related = new HashSet(); + + related.Add(item); + item.IsVisible = false; } lastItem = item; From b5c4e3bc147e0c4f085de754ed8019dc18ead270 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 02:41:56 +0900 Subject: [PATCH 291/801] Add failing tests for traversal on group headers --- .../TestSceneBeatmapCarouselV2GroupSelection.cs | 14 ++++++++++++++ .../TestSceneBeatmapCarouselV2Selection.cs | 15 +++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs index 5728583507..04ca0a9085 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs @@ -81,6 +81,20 @@ namespace osu.Game.Tests.Visual.SongSelect BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); } + [Test] + public void TestGroupSelectionOnHeader() + { + AddBeatmaps(10, 3); + WaitForDrawablePanels(); + + SelectNextGroup(); + WaitForGroupSelection(0, 0); + + SelectPrevPanel(); + SelectPrevGroup(); + WaitForGroupSelection(2, 9); + } + [Test] public void TestKeyboardSelection() { diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs index 50395cf1ff..b087c252e4 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs @@ -129,6 +129,21 @@ namespace osu.Game.Tests.Visual.SongSelect WaitForSelection(0, 0); } + [Test] + public void TestGroupSelectionOnHeader() + { + AddBeatmaps(10, 3); + WaitForDrawablePanels(); + + SelectNextGroup(); + SelectNextGroup(); + WaitForSelection(1, 0); + + SelectPrevPanel(); + SelectPrevGroup(); + WaitForSelection(0, 0); + } + [Test] public void TestKeyboardSelection() { From e454fa558cb5891ac6614dd9c626fa21834c168f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 02:55:57 +0900 Subject: [PATCH 292/801] Adjust group traversal logic to handle cases where keyboard selection redirects --- osu.Game/Screens/SelectV2/Carousel.cs | 35 +++++++++++++++------------ 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 0da9cb5c19..a13de0e26d 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -377,26 +377,31 @@ namespace osu.Game.Screens.SelectV2 if (currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) { TryActivateSelection(); - return; + + // There's a chance this couldn't resolve, at which point continue with standard traversal. + if (currentSelection.CarouselItem == currentKeyboardSelection.CarouselItem) + return; } int originalIndex; + int newIndex; - if (currentKeyboardSelection.Index != null) - originalIndex = currentKeyboardSelection.Index.Value; - else if (direction > 0) - originalIndex = carouselItems.Count - 1; - else - originalIndex = 0; - - int newIndex = originalIndex; - - // As a second special case, if we're group selecting backwards and the current selection isn't a group, - // make sure to go back to the group header this item belongs to, so that the block below doesn't find it and stop too early. - if (direction < 0) + if (currentSelection.Index == null) { - while (!CheckValidForGroupSelection(carouselItems[newIndex])) - newIndex--; + // If there's no current selection, start from either end of the full list. + newIndex = originalIndex = direction > 0 ? carouselItems.Count - 1 : 0; + } + else + { + newIndex = originalIndex = currentSelection.Index.Value; + + // As a second special case, if we're group selecting backwards and the current selection isn't a group, + // make sure to go back to the group header this item belongs to, so that the block below doesn't find it and stop too early. + if (direction < 0) + { + while (!CheckValidForGroupSelection(carouselItems[newIndex])) + newIndex--; + } } // Iterate over every item back to the current selection, finding the first valid item. From 38933039880b3b50eaef5557290a9c806dd79f51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 17 Oct 2024 11:59:27 +0200 Subject: [PATCH 293/801] Implement "form button" control --- .../UserInterface/TestSceneFormControls.cs | 166 ++++++++------- .../Graphics/UserInterfaceV2/FormButton.cs | 189 ++++++++++++++++++ 2 files changed, 280 insertions(+), 75 deletions(-) create mode 100644 osu.Game/Graphics/UserInterfaceV2/FormButton.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs index 118fbca97b..2003f5de83 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Game.Beatmaps; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; @@ -27,87 +28,102 @@ namespace osu.Game.Tests.Visual.UserInterface Child = new PopoverContainer { RelativeSizeAxes = Axes.Both, - Child = new FillFlowContainer + Child = new OsuScrollContainer { - RelativeSizeAxes = Axes.Y, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 400, - Direction = FillDirection.Vertical, - Spacing = new Vector2(5), - Padding = new MarginPadding(10), - Children = new Drawable[] + RelativeSizeAxes = Axes.Both, + Child = new FillFlowContainer { - new FormTextBox + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 400, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5), + Padding = new MarginPadding(10), + Children = new Drawable[] { - Caption = "Artist", - HintText = "Poot artist here!", - PlaceholderText = "Here is an artist", - TabbableContentContainer = this, - }, - new FormTextBox - { - Caption = "Artist", - HintText = "Poot artist here!", - PlaceholderText = "Here is an artist", - Current = { Disabled = true }, - TabbableContentContainer = this, - }, - new FormNumberBox(allowDecimals: true) - { - Caption = "Number", - HintText = "Insert your favourite number", - PlaceholderText = "Mine is 42!", - TabbableContentContainer = this, - }, - new FormCheckBox - { - Caption = EditorSetupStrings.LetterboxDuringBreaks, - HintText = EditorSetupStrings.LetterboxDuringBreaksDescription, - }, - new FormCheckBox - { - Caption = EditorSetupStrings.LetterboxDuringBreaks, - HintText = EditorSetupStrings.LetterboxDuringBreaksDescription, - Current = { Disabled = true }, - }, - new FormSliderBar - { - Caption = "Slider", - Current = new BindableFloat + new FormTextBox { - MinValue = 0, - MaxValue = 10, - Value = 5, - Precision = 0.1f, + Caption = "Artist", + HintText = "Poot artist here!", + PlaceholderText = "Here is an artist", + TabbableContentContainer = this, }, - TabbableContentContainer = this, - }, - new FormEnumDropdown - { - Caption = EditorSetupStrings.EnableCountdown, - HintText = EditorSetupStrings.CountdownDescription, - }, - new FormFileSelector - { - Caption = "File selector", - PlaceholderText = "Select a file", - }, - new FormBeatmapFileSelector(true) - { - Caption = "File selector with intermediate choice dialog", - PlaceholderText = "Select a file", - }, - new FormColourPalette - { - Caption = "Combo colours", - Colours = + new FormTextBox { - Colour4.Red, - Colour4.Green, - Colour4.Blue, - Colour4.Yellow, - } + Caption = "Artist", + HintText = "Poot artist here!", + PlaceholderText = "Here is an artist", + Current = { Disabled = true }, + TabbableContentContainer = this, + }, + new FormNumberBox(allowDecimals: true) + { + Caption = "Number", + HintText = "Insert your favourite number", + PlaceholderText = "Mine is 42!", + TabbableContentContainer = this, + }, + new FormCheckBox + { + Caption = EditorSetupStrings.LetterboxDuringBreaks, + HintText = EditorSetupStrings.LetterboxDuringBreaksDescription, + }, + new FormCheckBox + { + Caption = EditorSetupStrings.LetterboxDuringBreaks, + HintText = EditorSetupStrings.LetterboxDuringBreaksDescription, + Current = { Disabled = true }, + }, + new FormSliderBar + { + Caption = "Slider", + Current = new BindableFloat + { + MinValue = 0, + MaxValue = 10, + Value = 5, + Precision = 0.1f, + }, + TabbableContentContainer = this, + }, + new FormEnumDropdown + { + Caption = EditorSetupStrings.EnableCountdown, + HintText = EditorSetupStrings.CountdownDescription, + }, + new FormFileSelector + { + Caption = "File selector", + PlaceholderText = "Select a file", + }, + new FormBeatmapFileSelector(true) + { + Caption = "File selector with intermediate choice dialog", + PlaceholderText = "Select a file", + }, + new FormColourPalette + { + Caption = "Combo colours", + Colours = + { + Colour4.Red, + Colour4.Green, + Colour4.Blue, + Colour4.Yellow, + } + }, + new FormButton + { + Caption = "No text in button", + Action = () => { }, + }, + new FormButton + { + Caption = "Text in button which is pretty long and is very likely to wrap", + ButtonText = "Foo the bar", + Action = () => { }, + }, }, }, }, diff --git a/osu.Game/Graphics/UserInterfaceV2/FormButton.cs b/osu.Game/Graphics/UserInterfaceV2/FormButton.cs new file mode 100644 index 0000000000..fec855153b --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/FormButton.cs @@ -0,0 +1,189 @@ +// 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.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public partial class FormButton : CompositeDrawable + { + /// + /// Caption describing this button, displayed on the left of it. + /// + public LocalisableString Caption { get; init; } + + public LocalisableString ButtonText { get; init; } + + public Action? Action { get; init; } + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + Height = 50; + + Masking = true; + CornerRadius = 5; + CornerExponent = 2.5f; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Left = 9, + Right = 5, + Vertical = 5, + }, + Children = new Drawable[] + { + new OsuTextFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Width = 0.45f, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = Caption, + }, + new Button + { + Action = Action, + Text = ButtonText, + RelativeSizeAxes = ButtonText == default ? Axes.None : Axes.X, + Width = ButtonText == default ? 90 : 0.45f, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + } + }, + }, + }; + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + updateState(); + } + + private void updateState() + { + BorderThickness = IsHovered ? 2 : 0; + + if (IsHovered) + BorderColour = colourProvider.Light4; + } + + public partial class Button : OsuButton + { + private TrianglesV2? triangles { get; set; } + + protected override float HoverLayerFinalAlpha => 0; + + private Color4? triangleGradientSecondColour; + + public override Color4 BackgroundColour + { + get => base.BackgroundColour; + set + { + base.BackgroundColour = value; + triangleGradientSecondColour = BackgroundColour.Lighten(0.2f); + updateColours(); + } + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider overlayColourProvider) + { + DefaultBackgroundColour = overlayColourProvider.Colour3; + triangleGradientSecondColour ??= overlayColourProvider.Colour1; + + if (Text == default) + { + Add(new SpriteIcon + { + Icon = FontAwesome.Solid.ChevronRight, + Size = new Vector2(16), + Shadow = true, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Content.CornerRadius = 2; + + Add(triangles = new TrianglesV2 + { + Thickness = 0.02f, + SpawnRatio = 0.6f, + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue, + }); + + updateColours(); + } + + private void updateColours() + { + if (triangles == null) + return; + + Debug.Assert(triangleGradientSecondColour != null); + + triangles.Colour = ColourInfo.GradientVertical(triangleGradientSecondColour.Value, BackgroundColour); + } + + protected override bool OnHover(HoverEvent e) + { + Debug.Assert(triangleGradientSecondColour != null); + + Background.FadeColour(triangleGradientSecondColour.Value, 300, Easing.OutQuint); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + Background.FadeColour(BackgroundColour, 300, Easing.OutQuint); + base.OnHoverLost(e); + } + } + } +} From 4dd4e52e6dc43c1a4f4fe55c4262650b3e4e6919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Feb 2025 08:58:37 +0100 Subject: [PATCH 294/801] Implement visual appearance of beatmap submission wizard --- .../TestSceneBeatmapSubmissionOverlay.cs | 42 ++++++ osu.Game/Configuration/OsuConfigManager.cs | 5 + .../Localisation/BeatmapSubmissionStrings.cs | 124 ++++++++++++++++++ .../Overlays/FirstRunSetup/ScreenWelcome.cs | 2 + osu.Game/Overlays/WizardOverlay.cs | 13 +- osu.Game/Overlays/WizardScreen.cs | 3 + .../Submission/BeatmapSubmissionOverlay.cs | 28 ++++ .../Submission/ScreenContentPermissions.cs | 44 +++++++ .../ScreenFrequentlyAskedQuestions.cs | 62 +++++++++ .../Submission/ScreenSubmissionSettings.cs | 73 +++++++++++ 10 files changed, 389 insertions(+), 7 deletions(-) create mode 100644 osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs create mode 100644 osu.Game/Localisation/BeatmapSubmissionStrings.cs create mode 100644 osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs create mode 100644 osu.Game/Screens/Edit/Submission/ScreenContentPermissions.cs create mode 100644 osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs create mode 100644 osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs new file mode 100644 index 0000000000..07a794b7eb --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs @@ -0,0 +1,42 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Screens.Edit.Submission; +using osu.Game.Screens.Footer; + +namespace osu.Game.Tests.Visual.Editing +{ + public partial class TestSceneBeatmapSubmissionOverlay : OsuTestScene + { + private ScreenFooter footer = null!; + private BeatmapSubmissionOverlay overlay = null!; + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("add overlay", () => + { + var receptor = new ScreenFooter.BackReceptor(); + footer = new ScreenFooter(receptor); + + Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new[] { (typeof(ScreenFooter), (object)footer) }, + Children = new Drawable[] + { + receptor, + overlay = new BeatmapSubmissionOverlay() + { + State = { Value = Visibility.Visible, }, + }, + footer, + } + }; + }); + } + } +} diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 908f434655..1244dd8cfc 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -220,6 +220,9 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.AlwaysShowHoldForMenuButton, false); SetDefault(OsuSetting.AlwaysRequireHoldingForPause, false); SetDefault(OsuSetting.EditorShowStoryboard, true); + + SetDefault(OsuSetting.EditorSubmissionNotifyOnDiscussionReplies, true); + SetDefault(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission, true); } protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup) @@ -461,5 +464,7 @@ namespace osu.Game.Configuration BeatmapListingFeaturedArtistFilter, ShowMobileDisclaimer, EditorShowStoryboard, + EditorSubmissionNotifyOnDiscussionReplies, + EditorSubmissionLoadInBrowserAfterSubmission, } } diff --git a/osu.Game/Localisation/BeatmapSubmissionStrings.cs b/osu.Game/Localisation/BeatmapSubmissionStrings.cs new file mode 100644 index 0000000000..85fe922703 --- /dev/null +++ b/osu.Game/Localisation/BeatmapSubmissionStrings.cs @@ -0,0 +1,124 @@ +// 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.Localisation; + +namespace osu.Game.Localisation +{ + public static class BeatmapSubmissionStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.BeatmapSubmission"; + + /// + /// "Beatmap submission" + /// + public static LocalisableString BeatmapSubmissionTitle => new TranslatableString(getKey(@"beatmap_submission_title"), @"Beatmap submission"); + + /// + /// "Share your beatmap with the world!" + /// + public static LocalisableString BeatmapSubmissionDescription => new TranslatableString(getKey(@"beatmap_submission_description"), @"Share your beatmap with the world!"); + + /// + /// "Content permissions" + /// + public static LocalisableString ContentPermissions => new TranslatableString(getKey(@"content_permissions"), @"Content permissions"); + + /// + /// "I understand" + /// + public static LocalisableString ContentPermissionsAcknowledgement => new TranslatableString(getKey(@"content_permissions_acknowledgement"), @"I understand"); + + /// + /// "Frequently asked questions" + /// + public static LocalisableString FrequentlyAskedQuestions => new TranslatableString(getKey(@"frequently_asked_questions"), @"Frequently asked questions"); + + /// + /// "Submission settings" + /// + public static LocalisableString SubmissionSettings => new TranslatableString(getKey(@"submission_settings"), @"Submission settings"); + + /// + /// "Before you continue, we ask you to check whether the content you are uploading has been cleared for upload. Please understand that you are responsible for the content you upload to the platform and if in doubt, should ask permission from the creators before uploading!" + /// + public static LocalisableString ContentPermissionsDisclaimer => new TranslatableString(getKey(@"content_permissions_disclaimer"), @"Before you continue, we ask you to check whether the content you are uploading has been cleared for upload. Please understand that you are responsible for the content you upload to the platform and if in doubt, should ask permission from the creators before uploading!"); + + /// + /// "Check the content usage guidelines for more information" + /// + public static LocalisableString CheckContentUsageGuidelines => new TranslatableString(getKey(@"check_content_usage_guidelines"), @"Check the content usage guidelines for more information"); + + /// + /// "Beatmap ranking criteria" + /// + public static LocalisableString BeatmapRankingCriteria => new TranslatableString(getKey(@"beatmap_ranking_criteria"), @"Beatmap ranking criteria"); + + /// + /// "Not sure you meet the guidelines? Check the list and speed up the ranking process!" + /// + public static LocalisableString BeatmapRankingCriteriaDescription => new TranslatableString(getKey(@"beatmap_ranking_criteria_description"), @"Not sure you meet the guidelines? Check the list and speed up the ranking process!"); + + /// + /// "Submission process" + /// + public static LocalisableString SubmissionProcess => new TranslatableString(getKey(@"submission_process"), @"Submission process"); + + /// + /// "Unsure about the submission process? Check out the wiki entry!" + /// + public static LocalisableString SubmissionProcessDescription => new TranslatableString(getKey(@"submission_process_description"), @"Unsure about the submission process? Check out the wiki entry!"); + + /// + /// "Mapping help forum" + /// + public static LocalisableString MappingHelpForum => new TranslatableString(getKey(@"mapping_help_forum"), @"Mapping help forum"); + + /// + /// "Got some questions about mapping and submission? Ask them in the forums!" + /// + public static LocalisableString MappingHelpForumDescription => new TranslatableString(getKey(@"mapping_help_forum_description"), @"Got some questions about mapping and submission? Ask them in the forums!"); + + /// + /// "Modding queues forum" + /// + public static LocalisableString ModdingQueuesForum => new TranslatableString(getKey(@"modding_queues_forum"), @"Modding queues forum"); + + /// + /// "Having trouble getting feedback? Why not ask in a mod queue!" + /// + public static LocalisableString ModdingQueuesForumDescription => new TranslatableString(getKey(@"modding_queues_forum_description"), @"Having trouble getting feedback? Why not ask in a mod queue!"); + + /// + /// "Where would you like to post your map?" + /// + public static LocalisableString BeatmapSubmissionTargetCaption => new TranslatableString(getKey(@"beatmap_submission_target_caption"), @"Where would you like to post your map?"); + + /// + /// "Works in Progress / Help (incomplete, not ready for ranking)" + /// + public static LocalisableString BeatmapSubmissionTargetWIP => new TranslatableString(getKey(@"beatmap_submission_target_wip"), @"Works in Progress / Help (incomplete, not ready for ranking)"); + + /// + /// "Pending (complete, ready for ranking)" + /// + public static LocalisableString BeatmapSubmissionTargetPending => new TranslatableString(getKey(@"beatmap_submission_target_pending"), @"Pending (complete, ready for ranking)"); + + /// + /// "Receive notifications for discussion replies" + /// + public static LocalisableString NotifyOnDiscussionReplies => new TranslatableString(getKey(@"notify_for_discussion_replies"), @"Receive notifications for discussion replies"); + + /// + /// "Load in browser after submission" + /// + public static LocalisableString LoadInBrowserAfterSubmission => new TranslatableString(getKey(@"load_in_browser_after_submission"), @"Load in browser after submission"); + + /// + /// "Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost." + /// + public static LocalisableString LegacyExportDisclaimer => new TranslatableString(getKey(@"legacy_export_disclaimer"), @"Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost."); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs b/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs index 93cf555bc9..e03a08dd46 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs @@ -65,6 +65,8 @@ namespace osu.Game.Overlays.FirstRunSetup }; } + public override LocalisableString? NextStepText => FirstRunSetupOverlayStrings.GetStarted; + private partial class LanguageSelectionFlow : FillFlowContainer { private Bindable language = null!; diff --git a/osu.Game/Overlays/WizardOverlay.cs b/osu.Game/Overlays/WizardOverlay.cs index 38701efc96..34ffa7bd77 100644 --- a/osu.Game/Overlays/WizardOverlay.cs +++ b/osu.Game/Overlays/WizardOverlay.cs @@ -227,7 +227,7 @@ namespace osu.Game.Overlays updateButtons(); } - private void updateButtons() => DisplayedFooterContent?.UpdateButtons(CurrentStepIndex, steps); + private void updateButtons() => DisplayedFooterContent?.UpdateButtons(CurrentStepIndex, CurrentScreen, steps); public partial class WizardFooterContent : VisibilityContainer { @@ -248,24 +248,23 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.X, Width = 1, Text = FirstRunSetupOverlayStrings.GetStarted, - DarkerColour = colourProvider.Colour2, - LighterColour = colourProvider.Colour1, + DarkerColour = colourProvider.Colour3, + LighterColour = colourProvider.Colour2, Action = () => ShowNextStep?.Invoke(), }; } - public void UpdateButtons(int? currentStep, IReadOnlyList steps) + public void UpdateButtons(int? currentStep, WizardScreen? currentScreen, IReadOnlyList steps) { NextButton.Enabled.Value = currentStep != null; if (currentStep == null) return; - bool isFirstStep = currentStep == 0; bool isLastStep = currentStep == steps.Count - 1; - if (isFirstStep) - NextButton.Text = FirstRunSetupOverlayStrings.GetStarted; + if (currentScreen?.NextStepText != null) + NextButton.Text = currentScreen.NextStepText.Value; else { NextButton.Text = isLastStep diff --git a/osu.Game/Overlays/WizardScreen.cs b/osu.Game/Overlays/WizardScreen.cs index 7f3b1fe7f4..5112efaa61 100644 --- a/osu.Game/Overlays/WizardScreen.cs +++ b/osu.Game/Overlays/WizardScreen.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -102,5 +103,7 @@ namespace osu.Game.Overlays base.OnSuspending(e); } + + public virtual LocalisableString? NextStepText => null; } } diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs new file mode 100644 index 0000000000..da2abd8c23 --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs @@ -0,0 +1,28 @@ +// 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.Overlays; +using osu.Game.Localisation; + +namespace osu.Game.Screens.Edit.Submission +{ + public partial class BeatmapSubmissionOverlay : WizardOverlay + { + public BeatmapSubmissionOverlay() + : base(OverlayColourScheme.Aquamarine) + { + } + + [BackgroundDependencyLoader] + private void load() + { + AddStep(); + AddStep(); + AddStep(); + + Header.Title = BeatmapSubmissionStrings.BeatmapSubmissionTitle; + Header.Description = BeatmapSubmissionStrings.BeatmapSubmissionDescription; + } + } +} diff --git a/osu.Game/Screens/Edit/Submission/ScreenContentPermissions.cs b/osu.Game/Screens/Edit/Submission/ScreenContentPermissions.cs new file mode 100644 index 0000000000..92a4ac4e4e --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/ScreenContentPermissions.cs @@ -0,0 +1,44 @@ +// 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.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; +using osu.Game.Overlays; + +namespace osu.Game.Screens.Edit.Submission +{ + [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.ContentPermissions))] + public partial class ScreenContentPermissions : WizardScreen + { + [BackgroundDependencyLoader] + private void load(OsuGame? game) + { + Content.AddRange(new Drawable[] + { + new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = BeatmapSubmissionStrings.ContentPermissionsDisclaimer, + }, + new RoundedButton + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Width = 450, + Text = BeatmapSubmissionStrings.CheckContentUsageGuidelines, + Action = () => game?.ShowWiki(@"Rules/Content_usage_permissions"), + }, + }); + } + + public override LocalisableString? NextStepText => BeatmapSubmissionStrings.ContentPermissionsAcknowledgement; + } +} diff --git a/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs b/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs new file mode 100644 index 0000000000..c8d226bbcb --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs @@ -0,0 +1,62 @@ +// 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.Localisation; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; +using osu.Game.Online.API; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.Edit.Submission +{ + [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.FrequentlyAskedQuestions))] + public partial class ScreenFrequentlyAskedQuestions : WizardScreen + { + [BackgroundDependencyLoader] + private void load(OsuGame? game, IAPIProvider api) + { + Content.Add(new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new FormButton + { + RelativeSizeAxes = Axes.X, + Caption = BeatmapSubmissionStrings.BeatmapRankingCriteriaDescription, + ButtonText = BeatmapSubmissionStrings.BeatmapRankingCriteria, + Action = () => game?.ShowWiki(@"Ranking_Criteria"), + }, + new FormButton + { + RelativeSizeAxes = Axes.X, + Caption = BeatmapSubmissionStrings.SubmissionProcessDescription, + ButtonText = BeatmapSubmissionStrings.SubmissionProcess, + Action = () => game?.ShowWiki(@"Beatmap_ranking_procedure"), + }, + new FormButton + { + RelativeSizeAxes = Axes.X, + Caption = BeatmapSubmissionStrings.MappingHelpForumDescription, + ButtonText = BeatmapSubmissionStrings.MappingHelpForum, + Action = () => game?.OpenUrlExternally($@"{api.WebsiteRootUrl}/community/forums/56"), + }, + new FormButton + { + RelativeSizeAxes = Axes.X, + Caption = BeatmapSubmissionStrings.ModdingQueuesForumDescription, + ButtonText = BeatmapSubmissionStrings.ModdingQueuesForum, + Action = () => game?.OpenUrlExternally($@"{api.WebsiteRootUrl}/community/forums/60"), + }, + }, + }); + } + } +} diff --git a/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs b/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs new file mode 100644 index 0000000000..72da94afa1 --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs @@ -0,0 +1,73 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.Edit.Submission +{ + [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.SubmissionSettings))] + public partial class ScreenSubmissionSettings : WizardScreen + { + private readonly BindableBool notifyOnDiscussionReplies = new BindableBool(); + private readonly BindableBool loadInBrowserAfterSubmission = new BindableBool(); + + [BackgroundDependencyLoader] + private void load(OsuConfigManager configManager, OsuColour colours) + { + configManager.BindWith(OsuSetting.EditorSubmissionNotifyOnDiscussionReplies, notifyOnDiscussionReplies); + configManager.BindWith(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission, loadInBrowserAfterSubmission); + + Content.Add(new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new FormEnumDropdown + { + RelativeSizeAxes = Axes.X, + Caption = BeatmapSubmissionStrings.BeatmapSubmissionTargetCaption, + }, + new FormCheckBox + { + Caption = BeatmapSubmissionStrings.NotifyOnDiscussionReplies, + Current = notifyOnDiscussionReplies, + }, + new FormCheckBox + { + Caption = BeatmapSubmissionStrings.LoadInBrowserAfterSubmission, + Current = loadInBrowserAfterSubmission, + }, + new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE, weight: FontWeight.Bold)) + { + RelativeSizeAxes = Axes.X, + Colour = colours.Orange1, + Text = BeatmapSubmissionStrings.LegacyExportDisclaimer, + Padding = new MarginPadding { Top = 20 } + }, + } + }); + } + + private enum BeatmapSubmissionTarget + { + [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetWIP))] + WIP, + + [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetPending))] + Pending, + } + } +} From 2f2dc158e0353aa5ba27108980a1bed1466a2f36 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 17:44:59 +0900 Subject: [PATCH 295/801] Ensure test step doesn't consider pooled instances of drawables --- osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 0a9719423c..2e67e625f9 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -175,7 +175,8 @@ namespace osu.Game.Tests.Visual.SongSelect { AddStep($"click panel at index {index}", () => { - Carousel.ChildrenOfType() + Carousel.ChildrenOfType().Single() + .ChildrenOfType() .Where(p => ((ICarouselPanel)p).Item?.IsVisible == true) .Reverse() .ElementAt(index) From ccdb6e4c4870ef64b3a2e549716c4bf7b412b646 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 17:50:14 +0900 Subject: [PATCH 296/801] Fix carousel tests failing due to dependency on depth ordering --- osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 2e67e625f9..f7be5f12e8 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -178,7 +178,7 @@ namespace osu.Game.Tests.Visual.SongSelect Carousel.ChildrenOfType().Single() .ChildrenOfType() .Where(p => ((ICarouselPanel)p).Item?.IsVisible == true) - .Reverse() + .OrderBy(p => p.Y) .ElementAt(index) .TriggerClick(); }); From 58560f8acfe0259795358e969ddee6ca0600d2ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 17:11:09 +0900 Subject: [PATCH 297/801] Add tracking of expansion states for groups and sets --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 3 +- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 38 ++++++++++++------- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 20 +++++----- osu.Game/Screens/SelectV2/CarouselItem.cs | 7 +++- 4 files changed, 42 insertions(+), 26 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index f7be5f12e8..72c9611fdb 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -153,7 +153,8 @@ namespace osu.Game.Tests.Visual.SongSelect var groupingFilter = Carousel.Filters.OfType().Single(); GroupDefinition g = groupingFilter.GroupItems.Keys.ElementAt(group); - CarouselItem item = groupingFilter.GroupItems[g].ElementAt(panel); + // offset by one because the group itself is included in the items list. + CarouselItem item = groupingFilter.GroupItems[g].ElementAt(panel + 1); return ReferenceEquals(Carousel.CurrentSelection, item.Model); }); diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 858888c517..9f62780dda 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -105,12 +105,12 @@ namespace osu.Game.Screens.SelectV2 // Special case – collapsing an open group. if (lastSelectedGroup == group) { - setVisibilityOfGroupItems(lastSelectedGroup, false); + setExpansionStateOfGroup(lastSelectedGroup, false); lastSelectedGroup = null; return false; } - setVisibleGroup(group); + setExpandedGroup(group); return false; case BeatmapSetInfo setInfo: @@ -127,11 +127,11 @@ namespace osu.Game.Screens.SelectV2 GroupDefinition? group = grouping.GroupItems.SingleOrDefault(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key; if (group != null) - setVisibleGroup(group); + setExpandedGroup(group); } else { - setVisibleSet(beatmapInfo); + setExpandedSet(beatmapInfo); } return true; @@ -158,37 +158,47 @@ namespace osu.Game.Screens.SelectV2 } } - private void setVisibleGroup(GroupDefinition group) + private void setExpandedGroup(GroupDefinition group) { if (lastSelectedGroup != null) - setVisibilityOfGroupItems(lastSelectedGroup, false); + setExpansionStateOfGroup(lastSelectedGroup, false); lastSelectedGroup = group; - setVisibilityOfGroupItems(group, true); + setExpansionStateOfGroup(group, true); } - private void setVisibilityOfGroupItems(GroupDefinition group, bool visible) + private void setExpansionStateOfGroup(GroupDefinition group, bool expanded) { if (grouping.GroupItems.TryGetValue(group, out var items)) { foreach (var i in items) - i.IsVisible = visible; + { + if (i.Model is GroupDefinition) + i.IsExpanded = expanded; + else + i.IsVisible = expanded; + } } } - private void setVisibleSet(BeatmapInfo beatmapInfo) + private void setExpandedSet(BeatmapInfo beatmapInfo) { if (lastSelectedBeatmap != null) - setVisibilityOfSetItems(lastSelectedBeatmap.BeatmapSet!, false); + setExpansionStateOfSetItems(lastSelectedBeatmap.BeatmapSet!, false); lastSelectedBeatmap = beatmapInfo; - setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, true); + setExpansionStateOfSetItems(beatmapInfo.BeatmapSet!, true); } - private void setVisibilityOfSetItems(BeatmapSetInfo set, bool visible) + private void setExpansionStateOfSetItems(BeatmapSetInfo set, bool expanded) { if (grouping.SetItems.TryGetValue(set, out var items)) { foreach (var i in items) - i.IsVisible = visible; + { + if (i.Model is BeatmapSetInfo) + i.IsExpanded = expanded; + else + i.IsVisible = expanded; + } } } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index ea737d8b7f..e4160cc0fa 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -65,7 +65,11 @@ namespace osu.Game.Screens.SelectV2 if (b.StarRating > starGroup) { starGroup = (int)Math.Floor(b.StarRating); - newItems.Add(new CarouselItem(new GroupDefinition($"{starGroup} - {++starGroup} *")) { DrawHeight = GroupPanel.HEIGHT }); + var groupDefinition = new GroupDefinition($"{starGroup} - {++starGroup} *"); + var groupItem = new CarouselItem(groupDefinition) { DrawHeight = GroupPanel.HEIGHT }; + + newItems.Add(groupItem); + groupItems[groupDefinition] = new HashSet { groupItem }; } newItems.Add(item); @@ -91,14 +95,13 @@ namespace osu.Game.Screens.SelectV2 if (newBeatmapSet) { - newItems.Insert(i, new CarouselItem(beatmap.BeatmapSet!) { DrawHeight = BeatmapSetPanel.HEIGHT }); + var setItem = new CarouselItem(beatmap.BeatmapSet!) { DrawHeight = BeatmapSetPanel.HEIGHT }; + setItems[beatmap.BeatmapSet!] = new HashSet { setItem }; + newItems.Insert(i, setItem); i++; } - if (!setItems.TryGetValue(beatmap.BeatmapSet!, out var related)) - setItems[beatmap.BeatmapSet!] = related = new HashSet(); - - related.Add(item); + setItems[beatmap.BeatmapSet!].Add(item); item.IsVisible = false; } @@ -121,10 +124,7 @@ namespace osu.Game.Screens.SelectV2 if (lastGroup != null) { - if (!groupItems.TryGetValue(lastGroup, out var groupRelated)) - groupItems[lastGroup] = groupRelated = new HashSet(); - groupRelated.Add(item); - + groupItems[lastGroup].Add(item); item.IsVisible = false; } } diff --git a/osu.Game/Screens/SelectV2/CarouselItem.cs b/osu.Game/Screens/SelectV2/CarouselItem.cs index 13d5c840cf..32be33e99a 100644 --- a/osu.Game/Screens/SelectV2/CarouselItem.cs +++ b/osu.Game/Screens/SelectV2/CarouselItem.cs @@ -30,10 +30,15 @@ namespace osu.Game.Screens.SelectV2 public float DrawHeight { get; set; } = DEFAULT_HEIGHT; /// - /// Whether this item is visible or collapsed (hidden). + /// Whether this item is visible or hidden. /// public bool IsVisible { get; set; } = true; + /// + /// Whether this item is expanded or not. Should only be used for headers of groups. + /// + public bool IsExpanded { get; set; } + public CarouselItem(object model) { Model = model; From 61419ec9c840fe55886c338f0eb53a8dd919be89 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Feb 2025 17:54:03 +0900 Subject: [PATCH 298/801] Refactor user presence watching to be tokenised --- .../Online/TestSceneMetadataClient.cs | 52 +++++++++++++++ .../Online/TestSceneCurrentlyOnlineDisplay.cs | 12 ++-- osu.Game/Online/Metadata/MetadataClient.cs | 59 ++++++++++++++--- .../Online/Metadata/OnlineMetadataClient.cs | 63 +++++++++---------- osu.Game/Overlays/DashboardOverlay.cs | 9 ++- .../Visual/Metadata/TestMetadataClient.cs | 20 +++--- 6 files changed, 156 insertions(+), 59 deletions(-) create mode 100644 osu.Game.Tests/Online/TestSceneMetadataClient.cs diff --git a/osu.Game.Tests/Online/TestSceneMetadataClient.cs b/osu.Game.Tests/Online/TestSceneMetadataClient.cs new file mode 100644 index 0000000000..8c738eeca6 --- /dev/null +++ b/osu.Game.Tests/Online/TestSceneMetadataClient.cs @@ -0,0 +1,52 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Tests.Visual; +using osu.Game.Tests.Visual.Metadata; + +namespace osu.Game.Tests.Online +{ + [TestFixture] + [HeadlessTest] + public class TestSceneMetadataClient : OsuTestScene + { + private TestMetadataClient client = null!; + + [SetUp] + public void Setup() => Schedule(() => + { + Child = client = new TestMetadataClient(); + }); + + [Test] + public void TestWatchingMultipleTimesInvokesServerMethodsOnce() + { + int countBegin = 0; + int countEnd = 0; + + IDisposable token1 = null!; + IDisposable token2 = null!; + + AddStep("setup", () => + { + client.OnBeginWatchingUserPresence += () => countBegin++; + client.OnEndWatchingUserPresence += () => countEnd++; + }); + + AddStep("begin watching presence (1)", () => token1 = client.BeginWatchingUserPresence()); + AddAssert("server method invoked once", () => countBegin, () => Is.EqualTo(1)); + + AddStep("begin watching presence (2)", () => token2 = client.BeginWatchingUserPresence()); + AddAssert("server method not invoked a second time", () => countBegin, () => Is.EqualTo(1)); + + AddStep("end watching presence (1)", () => token1.Dispose()); + AddAssert("server method not invoked", () => countEnd, () => Is.EqualTo(0)); + + AddStep("end watching presence (2)", () => token2.Dispose()); + AddAssert("server method invoked once", () => countEnd, () => Is.EqualTo(1)); + } + } +} diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs index b696c5d8ca..2e53ec2ba4 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs @@ -65,7 +65,9 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestBasicDisplay() { - AddStep("Begin watching user presence", () => metadataClient.BeginWatchingUserPresence()); + IDisposable token = null!; + + AddStep("Begin watching user presence", () => token = metadataClient.BeginWatchingUserPresence()); AddStep("Add online user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.ChoosingBeatmap() })); AddUntilStep("Panel loaded", () => currentlyOnline.ChildrenOfType().FirstOrDefault()?.User.Id == 2); AddAssert("Spectate button disabled", () => currentlyOnline.ChildrenOfType().First().Enabled.Value, () => Is.False); @@ -78,14 +80,16 @@ namespace osu.Game.Tests.Visual.Online AddStep("Remove playing user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, null)); AddUntilStep("Panel no longer present", () => !currentlyOnline.ChildrenOfType().Any()); - AddStep("End watching user presence", () => metadataClient.EndWatchingUserPresence()); + AddStep("End watching user presence", () => token.Dispose()); } [Test] public void TestUserWasPlayingBeforeWatchingUserPresence() { + IDisposable token = null!; + AddStep("User began playing", () => spectatorClient.SendStartPlay(streamingUser.Id, 0)); - AddStep("Begin watching user presence", () => metadataClient.BeginWatchingUserPresence()); + AddStep("Begin watching user presence", () => token = metadataClient.BeginWatchingUserPresence()); AddStep("Add online user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.ChoosingBeatmap() })); AddUntilStep("Panel loaded", () => currentlyOnline.ChildrenOfType().FirstOrDefault()?.User.Id == 2); AddAssert("Spectate button enabled", () => currentlyOnline.ChildrenOfType().First().Enabled.Value, () => Is.True); @@ -93,7 +97,7 @@ namespace osu.Game.Tests.Visual.Online AddStep("User finished playing", () => spectatorClient.SendEndPlay(streamingUser.Id)); AddAssert("Spectate button disabled", () => currentlyOnline.ChildrenOfType().First().Enabled.Value, () => Is.False); AddStep("Remove playing user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, null)); - AddStep("End watching user presence", () => metadataClient.EndWatchingUserPresence()); + AddStep("End watching user presence", () => token.Dispose()); } internal partial class TestUserLookupCache : UserLookupCache diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs index 356c50bcc0..1da245e80d 100644 --- a/osu.Game/Online/Metadata/MetadataClient.cs +++ b/osu.Game/Online/Metadata/MetadataClient.cs @@ -3,11 +3,13 @@ using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; using osu.Game.Users; namespace osu.Game.Online.Metadata @@ -37,11 +39,6 @@ namespace osu.Game.Online.Metadata #region User presence updates - /// - /// Whether the client is currently receiving user presence updates from the server. - /// - public abstract IBindable IsWatchingUserPresence { get; } - /// /// The information about the current user. /// @@ -82,11 +79,36 @@ namespace osu.Game.Online.Metadata /// public abstract Task UpdateStatus(UserStatus? status); - /// - public abstract Task BeginWatchingUserPresence(); + private int userPresenceWatchCount; + + protected bool IsWatchingUserPresence + => Interlocked.CompareExchange(ref userPresenceWatchCount, userPresenceWatchCount, userPresenceWatchCount) > 0; + + /// + public IDisposable BeginWatchingUserPresence() + => new UserPresenceWatchToken(this); /// - public abstract Task EndWatchingUserPresence(); + Task IMetadataServer.BeginWatchingUserPresence() + { + if (Interlocked.Increment(ref userPresenceWatchCount) == 1) + return BeginWatchingUserPresenceInternal(); + + return Task.CompletedTask; + } + + /// + Task IMetadataServer.EndWatchingUserPresence() + { + if (Interlocked.Decrement(ref userPresenceWatchCount) == 0) + return EndWatchingUserPresenceInternal(); + + return Task.CompletedTask; + } + + protected abstract Task BeginWatchingUserPresenceInternal(); + + protected abstract Task EndWatchingUserPresenceInternal(); /// public abstract Task UserPresenceUpdated(int userId, UserPresence? presence); @@ -94,6 +116,27 @@ namespace osu.Game.Online.Metadata /// public abstract Task FriendPresenceUpdated(int userId, UserPresence? presence); + private class UserPresenceWatchToken : IDisposable + { + private readonly IMetadataServer server; + private bool isDisposed; + + public UserPresenceWatchToken(IMetadataServer server) + { + this.server = server; + server.BeginWatchingUserPresence().FireAndForget(); + } + + public void Dispose() + { + if (isDisposed) + return; + + server.EndWatchingUserPresence().FireAndForget(); + isDisposed = true; + } + } + #endregion #region Daily Challenge diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index 5aeeb04d11..c7c7dfc58b 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -20,9 +20,6 @@ namespace osu.Game.Online.Metadata { public override IBindable IsConnected { get; } = new Bindable(); - public override IBindable IsWatchingUserPresence => isWatchingUserPresence; - private readonly BindableBool isWatchingUserPresence = new BindableBool(); - public override UserPresence LocalUserPresence => localUserPresence; private UserPresence localUserPresence; @@ -109,15 +106,18 @@ namespace osu.Game.Online.Metadata { Schedule(() => { - isWatchingUserPresence.Value = false; userPresences.Clear(); friendPresences.Clear(); dailyChallengeInfo.Value = null; localUserPresence = default; }); + return; } + if (IsWatchingUserPresence) + BeginWatchingUserPresenceInternal(); + if (localUser.Value is not GuestUser) { UpdateActivity(userActivity.Value); @@ -201,6 +201,31 @@ namespace osu.Game.Online.Metadata return connection.InvokeAsync(nameof(IMetadataServer.UpdateStatus), status); } + protected override Task BeginWatchingUserPresenceInternal() + { + if (connector?.IsConnected.Value != true) + return Task.CompletedTask; + + Logger.Log($@"{nameof(OnlineMetadataClient)} began watching user presence", LoggingTarget.Network); + + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMetadataServer.BeginWatchingUserPresence)); + } + + protected override Task EndWatchingUserPresenceInternal() + { + if (connector?.IsConnected.Value != true) + return Task.CompletedTask; + + Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching user presence", LoggingTarget.Network); + + // must be scheduled before any remote calls to avoid mis-ordering. + Schedule(() => userPresences.Clear()); + + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence)); + } + public override Task UserPresenceUpdated(int userId, UserPresence? presence) { Schedule(() => @@ -237,36 +262,6 @@ namespace osu.Game.Online.Metadata return Task.CompletedTask; } - public override async Task BeginWatchingUserPresence() - { - if (connector?.IsConnected.Value != true) - throw new OperationCanceledException(); - - Debug.Assert(connection != null); - await connection.InvokeAsync(nameof(IMetadataServer.BeginWatchingUserPresence)).ConfigureAwait(false); - Schedule(() => isWatchingUserPresence.Value = true); - Logger.Log($@"{nameof(OnlineMetadataClient)} began watching user presence", LoggingTarget.Network); - } - - public override async Task EndWatchingUserPresence() - { - try - { - if (connector?.IsConnected.Value != true) - throw new OperationCanceledException(); - - // must be scheduled before any remote calls to avoid mis-ordering. - Schedule(() => userPresences.Clear()); - Debug.Assert(connection != null); - await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence)).ConfigureAwait(false); - Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching user presence", LoggingTarget.Network); - } - finally - { - Schedule(() => isWatchingUserPresence.Value = false); - } - } - public override Task DailyChallengeUpdated(DailyChallengeInfo? info) { Schedule(() => dailyChallengeInfo.Value = info); diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs index 1861f892bd..1912736135 100644 --- a/osu.Game/Overlays/DashboardOverlay.cs +++ b/osu.Game/Overlays/DashboardOverlay.cs @@ -6,7 +6,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Game.Online.Metadata; -using osu.Game.Online.Multiplayer; using osu.Game.Overlays.Dashboard; using osu.Game.Overlays.Dashboard.Friends; @@ -18,6 +17,7 @@ namespace osu.Game.Overlays private MetadataClient metadataClient { get; set; } = null!; private IBindable metadataConnected = null!; + private IDisposable? userPresenceWatchToken; public DashboardOverlay() : base(OverlayColourScheme.Purple) @@ -61,9 +61,12 @@ namespace osu.Game.Overlays return; if (State.Value == Visibility.Visible) - metadataClient.BeginWatchingUserPresence().FireAndForget(); + userPresenceWatchToken ??= metadataClient.BeginWatchingUserPresence(); else - metadataClient.EndWatchingUserPresence().FireAndForget(); + { + userPresenceWatchToken?.Dispose(); + userPresenceWatchToken = null; + } } } } diff --git a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs index d14cbd7743..dca1b0e468 100644 --- a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs +++ b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs @@ -16,9 +16,6 @@ namespace osu.Game.Tests.Visual.Metadata public override IBindable IsConnected => isConnected; private readonly BindableBool isConnected = new BindableBool(true); - public override IBindable IsWatchingUserPresence => isWatchingUserPresence; - private readonly BindableBool isWatchingUserPresence = new BindableBool(); - public override UserPresence LocalUserPresence => localUserPresence; private UserPresence localUserPresence; @@ -34,15 +31,18 @@ namespace osu.Game.Tests.Visual.Metadata [Resolved] private IAPIProvider api { get; set; } = null!; - public override Task BeginWatchingUserPresence() + public event Action? OnBeginWatchingUserPresence; + public event Action? OnEndWatchingUserPresence; + + protected override Task BeginWatchingUserPresenceInternal() { - isWatchingUserPresence.Value = true; + OnBeginWatchingUserPresence?.Invoke(); return Task.CompletedTask; } - public override Task EndWatchingUserPresence() + protected override Task EndWatchingUserPresenceInternal() { - isWatchingUserPresence.Value = false; + OnEndWatchingUserPresence?.Invoke(); return Task.CompletedTask; } @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Metadata { localUserPresence = localUserPresence with { Activity = activity }; - if (isWatchingUserPresence.Value) + if (IsWatchingUserPresence) { if (userPresences.ContainsKey(api.LocalUser.Value.Id)) userPresences[api.LocalUser.Value.Id] = localUserPresence; @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.Metadata { localUserPresence = localUserPresence with { Status = status }; - if (isWatchingUserPresence.Value) + if (IsWatchingUserPresence) { if (userPresences.ContainsKey(api.LocalUser.Value.Id)) userPresences[api.LocalUser.Value.Id] = localUserPresence; @@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual.Metadata public override Task UserPresenceUpdated(int userId, UserPresence? presence) { - if (isWatchingUserPresence.Value) + if (IsWatchingUserPresence) { if (presence?.Status != null) { From 2f90bb4d6793475835d1d51bef92b2c40f69112c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Feb 2025 17:55:50 +0900 Subject: [PATCH 299/801] Watch global user presence while in spectator screen --- osu.Game/Screens/Spectate/SpectatorScreen.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index ddc638b7c5..84b5889751 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -12,6 +12,7 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Metadata; using osu.Game.Online.Spectator; using osu.Game.Replays; using osu.Game.Rulesets; @@ -38,6 +39,9 @@ namespace osu.Game.Screens.Spectate [Resolved] private SpectatorClient spectatorClient { get; set; } = null!; + [Resolved] + private MetadataClient metadataClient { get; set; } = null!; + [Resolved] private UserLookupCache userLookupCache { get; set; } = null!; @@ -50,6 +54,7 @@ namespace osu.Game.Screens.Spectate private readonly Dictionary gameplayStates = new Dictionary(); private IDisposable? realmSubscription; + private IDisposable? userWatchToken; /// /// Creates a new . @@ -64,6 +69,8 @@ namespace osu.Game.Screens.Spectate { base.LoadComplete(); + userWatchToken = metadataClient.BeginWatchingUserPresence(); + userLookupCache.GetUsersAsync(users.ToArray()).ContinueWith(task => Schedule(() => { var foundUsers = task.GetResultSafely(); @@ -282,6 +289,7 @@ namespace osu.Game.Screens.Spectate } realmSubscription?.Dispose(); + userWatchToken?.Dispose(); } } } From 599b59cb1447467048bda41105956bd0c532863e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 17:16:36 +0900 Subject: [PATCH 300/801] Add expanded state to sample drawable representations --- ...estSceneBeatmapCarouselV2GroupSelection.cs | 25 ++++++++++++++++++- osu.Game/Screens/SelectV2/BeatmapPanel.cs | 1 + osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 9 ++++++- osu.Game/Screens/SelectV2/Carousel.cs | 2 ++ osu.Game/Screens/SelectV2/GroupPanel.cs | 10 +++++++- osu.Game/Screens/SelectV2/ICarouselPanel.cs | 7 +++++- 6 files changed, 50 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs index 04ca0a9085..f4d97be5a5 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] - public void TestOpenCloseGroupWithNoSelection() + public void TestOpenCloseGroupWithNoSelectionMouse() { AddBeatmaps(10, 5); WaitForDrawablePanels(); @@ -41,6 +41,29 @@ namespace osu.Game.Tests.Visual.SongSelect CheckNoSelection(); } + [Test] + public void TestOpenCloseGroupWithNoSelectionKeyboard() + { + AddBeatmaps(10, 5); + WaitForDrawablePanels(); + + AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + CheckNoSelection(); + + SelectNextPanel(); + Select(); + AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + AddAssert("keyboard selected is expanded", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); + CheckNoSelection(); + + Select(); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddAssert("keyboard selected is collapsed", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); + CheckNoSelection(); + + GroupPanel? getKeyboardSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.KeyboardSelected.Value); + } + [Test] public void TestCarouselRemembersSelection() { diff --git a/osu.Game/Screens/SelectV2/BeatmapPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs index 4a9e406def..3edfd4203b 100644 --- a/osu.Game/Screens/SelectV2/BeatmapPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapPanel.cs @@ -100,6 +100,7 @@ namespace osu.Game.Screens.SelectV2 public CarouselItem? Item { get; set; } public BindableBool Selected { get; } = new BindableBool(); + public BindableBool Expanded { get; } = new BindableBool(); public BindableBool KeyboardSelected { get; } = new BindableBool(); public double DrawYPosition { get; set; } diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index 06e3ad3426..79ffe0f68a 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -25,6 +25,7 @@ namespace osu.Game.Screens.SelectV2 private BeatmapCarousel carousel { get; set; } = null!; private OsuSpriteText text = null!; + private Box box = null!; [BackgroundDependencyLoader] private void load() @@ -34,7 +35,7 @@ namespace osu.Game.Screens.SelectV2 InternalChildren = new Drawable[] { - new Box + box = new Box { Colour = Color4.Yellow.Darken(5), Alpha = 0.8f, @@ -48,6 +49,11 @@ namespace osu.Game.Screens.SelectV2 } }; + Expanded.BindValueChanged(value => + { + box.FadeColour(value.NewValue ? Color4.Yellow.Darken(2) : Color4.Yellow.Darken(5), 500, Easing.OutQuint); + }); + KeyboardSelected.BindValueChanged(value => { if (value.NewValue) @@ -85,6 +91,7 @@ namespace osu.Game.Screens.SelectV2 public CarouselItem? Item { get; set; } public BindableBool Selected { get; } = new BindableBool(); + public BindableBool Expanded { get; } = new BindableBool(); public BindableBool KeyboardSelected { get; } = new BindableBool(); public double DrawYPosition { get; set; } diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index a1bafac620..608ef207d9 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -571,6 +571,7 @@ namespace osu.Game.Screens.SelectV2 c.Selected.Value = c.Item == currentSelection?.CarouselItem; c.KeyboardSelected.Value = c.Item == currentKeyboardSelection?.CarouselItem; + c.Expanded.Value = c.Item.IsExpanded; } } @@ -674,6 +675,7 @@ namespace osu.Game.Screens.SelectV2 carouselPanel.Item = null; carouselPanel.Selected.Value = false; carouselPanel.KeyboardSelected.Value = false; + carouselPanel.Expanded.Value = false; } #endregion diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs index 882d77cb8d..7ed256ca6a 100644 --- a/osu.Game/Screens/SelectV2/GroupPanel.cs +++ b/osu.Game/Screens/SelectV2/GroupPanel.cs @@ -26,6 +26,8 @@ namespace osu.Game.Screens.SelectV2 private Box activationFlash = null!; private OsuSpriteText text = null!; + private Box box = null!; + [BackgroundDependencyLoader] private void load() { @@ -34,7 +36,7 @@ namespace osu.Game.Screens.SelectV2 InternalChildren = new Drawable[] { - new Box + box = new Box { Colour = Color4.DarkBlue.Darken(5), Alpha = 0.8f, @@ -60,6 +62,11 @@ namespace osu.Game.Screens.SelectV2 activationFlash.FadeTo(value.NewValue ? 0.2f : 0, 500, Easing.OutQuint); }); + Expanded.BindValueChanged(value => + { + box.FadeColour(value.NewValue ? Color4.SkyBlue : Color4.DarkBlue.Darken(5), 500, Easing.OutQuint); + }); + KeyboardSelected.BindValueChanged(value => { if (value.NewValue) @@ -97,6 +104,7 @@ namespace osu.Game.Screens.SelectV2 public CarouselItem? Item { get; set; } public BindableBool Selected { get; } = new BindableBool(); + public BindableBool Expanded { get; } = new BindableBool(); public BindableBool KeyboardSelected { get; } = new BindableBool(); public double DrawYPosition { get; set; } diff --git a/osu.Game/Screens/SelectV2/ICarouselPanel.cs b/osu.Game/Screens/SelectV2/ICarouselPanel.cs index a956bb22a3..4fba0d2827 100644 --- a/osu.Game/Screens/SelectV2/ICarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/ICarouselPanel.cs @@ -14,10 +14,15 @@ namespace osu.Game.Screens.SelectV2 public interface ICarouselPanel { /// - /// Whether this item has selection. Should be read from to update the visual state. + /// Whether this item has selection (see ). Should be read from to update the visual state. /// BindableBool Selected { get; } + /// + /// Whether this item is expanded (see ). Should be read from to update the visual state. + /// + BindableBool Expanded { get; } + /// /// Whether this item has keyboard selection. Should be read from to update the visual state. /// From 0ad97c1fad6c85b2e95864ba007ba670de00b3ba Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Feb 2025 18:24:57 +0900 Subject: [PATCH 301/801] Fix inspection --- osu.Game.Tests/Online/TestSceneMetadataClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Online/TestSceneMetadataClient.cs b/osu.Game.Tests/Online/TestSceneMetadataClient.cs index 8c738eeca6..04e1d91edf 100644 --- a/osu.Game.Tests/Online/TestSceneMetadataClient.cs +++ b/osu.Game.Tests/Online/TestSceneMetadataClient.cs @@ -11,7 +11,7 @@ namespace osu.Game.Tests.Online { [TestFixture] [HeadlessTest] - public class TestSceneMetadataClient : OsuTestScene + public partial class TestSceneMetadataClient : OsuTestScene { private TestMetadataClient client = null!; From 6c6063464aed10ca52237ac764386fd1877a64a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 18:41:26 +0900 Subject: [PATCH 302/801] Remove `Scheduler.AddOnce` from `updateSpecifics` To keep things simple, let's not bother debouncing this. The debouncing was causing spectating handling to fail because of two interdependent components binding to `BeatmapAvailability`: Binding to update the screen's `Beatmap` after a download completes: https://github.com/ppy/osu/blob/58747061171c4ebe70201dfe4d3329ed7f4343f5/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs#L266-L267 Binding to attempt a load request: https://github.com/ppy/osu/blob/8bb7bea04e56fab9247baa59ae879e16c8b4bd9b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs#L67 The first must update the beatmap before the second runs, else gameplay will not load due to `Beatmap.IsDefault`. --- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 9f7e193131..f4d50b5170 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -427,7 +427,7 @@ namespace osu.Game.Screens.OnlinePlay.Match /// The screen to enter. protected abstract Screen CreateGameplayScreen(PlaylistItem selectedItem); - private void updateSpecifics() => Scheduler.AddOnce(() => + private void updateSpecifics() { if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) return; @@ -487,7 +487,7 @@ namespace osu.Game.Screens.OnlinePlay.Match } else UserStyleSection.Hide(); - }); + } protected virtual APIMod[] GetGameplayMods() => UserMods.Value.Select(m => new APIMod(m)).Concat(SelectedItem.Value!.RequiredMods).ToArray(); From ccc446a8ca8d004ff74cba2b11bb0d438861f3ed Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Tue, 4 Feb 2025 17:48:44 +0800 Subject: [PATCH 303/801] code cleanup --- .../Objects/Drawables/DrawableSwell.cs | 2 +- .../Argon/TaikoArgonSkinTransformer.cs | 2 +- .../Skinning/Legacy/LegacySwellCirclePiece.cs | 23 ------------------- .../Legacy/TaikoLegacySkinTransformer.cs | 2 +- .../TaikoSkinComponents.cs | 2 +- 5 files changed, 4 insertions(+), 27 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index d75fdbc40a..1dde4b6f9c 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables AddInternal(ticks = new Container { RelativeSizeAxes = Axes.Both }); } - protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.SwellBody), + protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Swell), _ => new DefaultSwell { Anchor = Anchor.Centre, diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index b588a22d12..26bb1900b9 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon case TaikoSkinComponents.TaikoExplosionOk: return new ArgonHitExplosion(taikoComponent.Component); - case TaikoSkinComponents.SwellBody: + case TaikoSkinComponents.Swell: return new ArgonSwell(); } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs deleted file mode 100644 index 40501d1d40..0000000000 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs +++ /dev/null @@ -1,23 +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.Sprites; -using osu.Game.Skinning; -using osuTK; - -namespace osu.Game.Rulesets.Taiko.Skinning.Legacy -{ - internal partial class LegacySwellCirclePiece : Sprite - { - [BackgroundDependencyLoader] - private void load(ISkinSource skin, SkinManager skinManager) - { - Texture = skin.GetTexture("spinner-warning") ?? skinManager.DefaultClassicSkin.GetTexture("spinner-circle"); - Anchor = Anchor.Centre; - Origin = Anchor.Centre; - Scale = skin.GetTexture("spinner-warning") != null ? Vector2.One : new Vector2(0.18f); - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index 8fa4551fd4..c6221e0589 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy case TaikoSkinComponents.DrumRollTick: return this.GetAnimation("sliderscorepoint", false, false); - case TaikoSkinComponents.SwellBody: + case TaikoSkinComponents.Swell: if (GetTexture("spinner-circle") != null) return new LegacySwell(); diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index 05c6316a05..28133ffcb2 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Taiko RimHit, DrumRollBody, DrumRollTick, - SwellBody, + Swell, HitTarget, PlayfieldBackgroundLeft, PlayfieldBackgroundRight, From 731f100aaf656ae8273412dc8bdb3134415d4889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Feb 2025 11:45:15 +0100 Subject: [PATCH 304/801] Fix incorrect snapping behaviour when previous object is not snapped to beat --- osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs | 2 ++ .../Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs index 195dbf0d46..bb0a0dbd7f 100644 --- a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs @@ -76,6 +76,8 @@ namespace osu.Game.Rulesets.Edit /// the beatmap's ,, /// the current beat divisor. /// + /// Note that the returned value does NOT depend on ; + /// consumers are expected to include that multiplier as they see fit. /// float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null); } diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs index 9ddf54b779..164a209958 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs @@ -56,7 +56,7 @@ namespace osu.Game.Screens.Edit.Compose.Components // Picture the scenario where the user has just placed an object on a 1/2 snap, then changes to // 1/3 snap and expects to be able to place the next object on a valid 1/3 snap, regardless of the // fact that the 1/2 snap reference object is not valid for 1/3 snapping. - float offset = SnapProvider.FindSnappedDistance(0, StartTime, SliderVelocitySource); + float offset = (float)(SnapProvider.FindSnappedDistance(0, StartTime, SliderVelocitySource) * DistanceSpacingMultiplier.Value); for (int i = 0; i < requiredCircles; i++) { From d28ea7bfbf5a81e2d4a97966c3b04fc1e37729bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Feb 2025 12:30:36 +0100 Subject: [PATCH 305/801] Fix code quality --- .../Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs index 07a794b7eb..e3e8c0de39 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs @@ -12,7 +12,6 @@ namespace osu.Game.Tests.Visual.Editing public partial class TestSceneBeatmapSubmissionOverlay : OsuTestScene { private ScreenFooter footer = null!; - private BeatmapSubmissionOverlay overlay = null!; [SetUpSteps] public void SetUpSteps() @@ -29,7 +28,7 @@ namespace osu.Game.Tests.Visual.Editing Children = new Drawable[] { receptor, - overlay = new BeatmapSubmissionOverlay() + new BeatmapSubmissionOverlay { State = { Value = Visibility.Visible, }, }, From a0b6610054d3385cf39ea43e6e4051e64b52eb3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 Jan 2025 15:05:22 +0100 Subject: [PATCH 306/801] Always select the closest control point group regardless of whether it has a timing point --- osu.Game/Screens/Edit/Timing/ControlPointList.cs | 15 +++------------ osu.Game/Screens/Edit/Timing/TimingScreen.cs | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointList.cs b/osu.Game/Screens/Edit/Timing/ControlPointList.cs index 12c6390812..86d8ac681f 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointList.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointList.cs @@ -20,6 +20,8 @@ namespace osu.Game.Screens.Edit.Timing { public partial class ControlPointList : CompositeDrawable { + public Action? SelectClosestTimingPoint { get; init; } + private ControlPointTable table = null!; private Container controls = null!; private OsuButton deleteButton = null!; @@ -75,7 +77,7 @@ namespace osu.Game.Screens.Edit.Timing new RoundedButton { Text = "Select closest to current time", - Action = goToCurrentGroup, + Action = SelectClosestTimingPoint, Size = new Vector2(220, 30), Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -146,17 +148,6 @@ namespace osu.Game.Screens.Edit.Timing table.Padding = new MarginPadding { Bottom = controls.DrawHeight }; } - private void goToCurrentGroup() - { - double accurateTime = clock.CurrentTimeAccurate; - - var activeTimingPoint = Beatmap.ControlPointInfo.TimingPointAt(accurateTime); - var activeEffectPoint = Beatmap.ControlPointInfo.EffectPointAt(accurateTime); - - double latestActiveTime = Math.Max(activeTimingPoint.Time, activeEffectPoint.Time); - selectedGroup.Value = Beatmap.ControlPointInfo.GroupAt(latestActiveTime); - } - private void delete() { if (selectedGroup.Value == null) diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index cddde34aca..e2ef356808 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -37,7 +38,10 @@ namespace osu.Game.Screens.Edit.Timing { new Drawable[] { - new ControlPointList(), + new ControlPointList + { + SelectClosestTimingPoint = selectClosestTimingPoint, + }, new ControlPointSettings(), }, } @@ -70,8 +74,13 @@ namespace osu.Game.Screens.Edit.Timing if (editorClock == null) return; - var nearestTimingPoint = EditorBeatmap.ControlPointInfo.TimingPointAt(editorClock.CurrentTime); - SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(nearestTimingPoint.Time); + double accurateTime = editorClock.CurrentTimeAccurate; + + var activeTimingPoint = EditorBeatmap.ControlPointInfo.TimingPointAt(accurateTime); + var activeEffectPoint = EditorBeatmap.ControlPointInfo.EffectPointAt(accurateTime); + + double latestActiveTime = Math.Max(activeTimingPoint.Time, activeEffectPoint.Time); + SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(latestActiveTime); } protected override void ConfigureTimeline(TimelineArea timelineArea) From 2dbf30a0965767f0c8be93d918abe59322910a6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Feb 2025 12:44:05 +0100 Subject: [PATCH 307/801] Select timing point on enter if no effect point is active at the time Noticed during testing. --- osu.Game/Screens/Edit/Timing/TimingScreen.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index e2ef356808..e7bf798298 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -79,8 +79,13 @@ namespace osu.Game.Screens.Edit.Timing var activeTimingPoint = EditorBeatmap.ControlPointInfo.TimingPointAt(accurateTime); var activeEffectPoint = EditorBeatmap.ControlPointInfo.EffectPointAt(accurateTime); - double latestActiveTime = Math.Max(activeTimingPoint.Time, activeEffectPoint.Time); - SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(latestActiveTime); + if (activeEffectPoint.Equals(EffectControlPoint.DEFAULT)) + SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(activeTimingPoint.Time); + else + { + double latestActiveTime = Math.Max(activeTimingPoint.Time, activeEffectPoint.Time); + SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(latestActiveTime); + } } protected override void ConfigureTimeline(TimelineArea timelineArea) From 386fb553923f37976bd2e8f53ce169cabfa0e170 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 21:48:45 +0900 Subject: [PATCH 308/801] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index d2682fc024..6bbd432ee7 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 309a9dcc87..ca2604858c 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 099ce3953127e075f73ee5b11d0f1307e012fe07 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 23:21:41 +0900 Subject: [PATCH 309/801] Use same delay in context menus --- osu.Game/Graphics/UserInterface/OsuContextMenu.cs | 7 +++---- osu.Game/Graphics/UserInterface/OsuMenu.cs | 7 +++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs index 433d37834f..e81d77ce43 100644 --- a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs @@ -12,8 +12,6 @@ namespace osu.Game.Graphics.UserInterface { public partial class OsuContextMenu : OsuMenu { - private const int fade_duration = 250; - [Resolved] private OsuMenuSamples menuSamples { get; set; } = null!; @@ -48,7 +46,7 @@ namespace osu.Game.Graphics.UserInterface protected override void AnimateOpen() { wasOpened = true; - this.FadeIn(fade_duration, Easing.OutQuint); + this.FadeIn(FADE_DURATION, Easing.OutQuint); if (!playClickSample) return; @@ -59,7 +57,8 @@ namespace osu.Game.Graphics.UserInterface protected override void AnimateClose() { - this.FadeOut(fade_duration, Easing.OutQuint); + this.Delay(DELAY_BEFORE_FADE_OUT) + .FadeOut(FADE_DURATION, Easing.OutQuint); if (wasOpened) menuSamples.PlayCloseSample(); diff --git a/osu.Game/Graphics/UserInterface/OsuMenu.cs b/osu.Game/Graphics/UserInterface/OsuMenu.cs index 9b099c0884..a75769b16b 100644 --- a/osu.Game/Graphics/UserInterface/OsuMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuMenu.cs @@ -18,6 +18,9 @@ namespace osu.Game.Graphics.UserInterface { public partial class OsuMenu : Menu { + protected const double DELAY_BEFORE_FADE_OUT = 50; + protected const double FADE_DURATION = 280; + // todo: this shouldn't be required after https://github.com/ppy/osu-framework/issues/4519 is fixed. private bool wasOpened; @@ -68,8 +71,8 @@ namespace osu.Game.Graphics.UserInterface if (!TopLevelMenu && wasOpened) menuSamples?.PlayCloseSample(); - this.Delay(50) - .FadeOut(300, Easing.OutQuint); + this.Delay(DELAY_BEFORE_FADE_OUT) + .FadeOut(FADE_DURATION, Easing.OutQuint); wasOpened = false; } From db7b665f4dc73d6250183285078734007f728e49 Mon Sep 17 00:00:00 2001 From: NecoDev <120387312+necocat0918@users.noreply.github.com> Date: Wed, 5 Feb 2025 13:31:57 +0800 Subject: [PATCH 310/801] Removed unused using For https://github.com/ppy/osu/pull/31780 --- osu.Game/Screens/Edit/Editor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 8cffab87ea..1914aae13c 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -8,7 +8,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using DiffPlex.Model; using JetBrains.Annotations; using osu.Framework; using osu.Framework.Allocation; From fa844b0ebc783222beadd1e6889dada450823219 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 15:01:59 +0900 Subject: [PATCH 311/801] Rename `Colour` / `Rhythm` related fields and classes --- .../Difficulty/Evaluators/ColourEvaluator.cs | 18 +++++----- .../Difficulty/Evaluators/RhythmEvaluator.cs | 12 +++---- .../Difficulty/Evaluators/StaminaEvaluator.cs | 4 +-- ...yHitObjectColour.cs => TaikoColourData.cs} | 2 +- .../TaikoColourDifficultyPreprocessor.cs | 10 +++--- ...yHitObjectRhythm.cs => TaikoRhythmData.cs} | 35 +++++++++---------- .../TaikoRhythmDifficultyPreprocessor.cs | 4 +-- .../Preprocessing/TaikoDifficultyHitObject.cs | 8 ++--- .../Difficulty/Skills/Reading.cs | 2 +- .../Difficulty/Skills/Stamina.cs | 2 +- 10 files changed, 48 insertions(+), 49 deletions(-) rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/{TaikoDifficultyHitObjectColour.cs => TaikoColourData.cs} (96%) rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/{TaikoDifficultyHitObjectRhythm.cs => TaikoRhythmData.cs} (75%) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index 166c01f507..b715dfc37a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -34,8 +34,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators var previousHitObject = (TaikoDifficultyHitObject)current.Previous(1); - double currentRatio = current.Rhythm.Ratio; - double previousRatio = previousHitObject.Rhythm.Ratio; + double currentRatio = current.RhythmData.Ratio; + double previousRatio = previousHitObject.RhythmData.Ratio; // A consistent interval is defined as the percentage difference between the two rhythmic ratios with the margin of error. if (Math.Abs(1 - currentRatio / previousRatio) <= threshold) @@ -61,17 +61,17 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators public static double EvaluateDifficultyOf(DifficultyHitObject hitObject) { var taikoObject = (TaikoDifficultyHitObject)hitObject; - TaikoDifficultyHitObjectColour colour = taikoObject.Colour; + TaikoColourData colourData = taikoObject.ColourData; double difficulty = 0.0d; - if (colour.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak - difficulty += evaluateMonoStreakDifficulty(colour.MonoStreak); + if (colourData.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak + difficulty += evaluateMonoStreakDifficulty(colourData.MonoStreak); - if (colour.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern - difficulty += evaluateAlternatingMonoPatternDifficulty(colour.AlternatingMonoPattern); + if (colourData.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern + difficulty += evaluateAlternatingMonoPatternDifficulty(colourData.AlternatingMonoPattern); - if (colour.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern - difficulty += evaluateRepeatingHitPatternsDifficulty(colour.RepeatingHitPattern); + if (colourData.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern + difficulty += evaluateRepeatingHitPatternsDifficulty(colourData.RepeatingHitPattern); double consistencyPenalty = consistentRatioPenalty(taikoObject); difficulty *= consistencyPenalty; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs index f4686f2fe3..3b3aea07f3 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs @@ -18,21 +18,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// public static double EvaluateDifficultyOf(DifficultyHitObject hitObject, double hitWindow) { - TaikoDifficultyHitObjectRhythm rhythm = ((TaikoDifficultyHitObject)hitObject).Rhythm; + TaikoRhythmData rhythmData = ((TaikoDifficultyHitObject)hitObject).RhythmData; double difficulty = 0.0d; double sameRhythm = 0; double samePattern = 0; double intervalPenalty = 0; - if (rhythm.SameRhythmGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmGroupedHitObjects + if (rhythmData.SameRhythmGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmGroupedHitObjects { - sameRhythm += 10.0 * evaluateDifficultyOf(rhythm.SameRhythmGroupedHitObjects, hitWindow); - intervalPenalty = repeatedIntervalPenalty(rhythm.SameRhythmGroupedHitObjects, hitWindow); + sameRhythm += 10.0 * evaluateDifficultyOf(rhythmData.SameRhythmGroupedHitObjects, hitWindow); + intervalPenalty = repeatedIntervalPenalty(rhythmData.SameRhythmGroupedHitObjects, hitWindow); } - if (rhythm.SamePatternsGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SamePatternsGroupedHitObjects - samePattern += 1.15 * ratioDifficulty(rhythm.SamePatternsGroupedHitObjects.IntervalRatio); + if (rhythmData.SamePatternsGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SamePatternsGroupedHitObjects + samePattern += 1.15 * ratioDifficulty(rhythmData.SamePatternsGroupedHitObjects.IntervalRatio); difficulty += Math.Max(sameRhythm, samePattern) * intervalPenalty; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs index a9884b2328..32ed8ec189 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs @@ -55,8 +55,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// private static int availableFingersFor(TaikoDifficultyHitObject hitObject) { - DifficultyHitObject? previousColourChange = hitObject.Colour.PreviousColourChange; - DifficultyHitObject? nextColourChange = hitObject.Colour.NextColourChange; + DifficultyHitObject? previousColourChange = hitObject.ColourData.PreviousColourChange; + DifficultyHitObject? nextColourChange = hitObject.ColourData.NextColourChange; if (previousColourChange != null && hitObject.StartTime - previousColourChange.StartTime < 300) { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourData.cs similarity index 96% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourData.cs index abf6fb3672..81201b6584 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourData.cs @@ -8,7 +8,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour /// /// Stores colour compression information for a . /// - public class TaikoDifficultyHitObjectColour + public class TaikoColourData { /// /// The that encodes this note. diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs index 18a299ae92..3c6ef7c53c 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs @@ -14,8 +14,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour public static class TaikoColourDifficultyPreprocessor { /// - /// Processes and encodes a list of s into a list of s, - /// assigning the appropriate s to each . + /// Processes and encodes a list of s into a list of s, + /// assigning the appropriate s to each . /// public static void ProcessAndAssign(List hitObjects) { @@ -41,9 +41,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour foreach (var hitObject in monoStreak.HitObjects) { - hitObject.Colour.RepeatingHitPattern = repeatingHitPattern; - hitObject.Colour.AlternatingMonoPattern = monoPattern; - hitObject.Colour.MonoStreak = monoStreak; + hitObject.ColourData.RepeatingHitPattern = repeatingHitPattern; + hitObject.ColourData.AlternatingMonoPattern = monoPattern; + hitObject.ColourData.MonoStreak = monoStreak; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs similarity index 75% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs index 3503a836fa..d895dcfc55 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm /// /// Stores rhythm data for a . /// - public class TaikoDifficultyHitObjectRhythm + public class TaikoRhythmData { /// /// The group of hit objects with consistent rhythm that this object belongs to. @@ -39,25 +39,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm /// speeding up is generally harder than slowing down (with exceptions of rhythm changes requiring a hand switch). /// /// - private static readonly TaikoDifficultyHitObjectRhythm[] common_rhythms = + private static readonly TaikoRhythmData[] common_rhythms = { - new TaikoDifficultyHitObjectRhythm(1, 1), - new TaikoDifficultyHitObjectRhythm(2, 1), - new TaikoDifficultyHitObjectRhythm(1, 2), - new TaikoDifficultyHitObjectRhythm(3, 1), - new TaikoDifficultyHitObjectRhythm(1, 3), - new TaikoDifficultyHitObjectRhythm(3, 2), - new TaikoDifficultyHitObjectRhythm(2, 3), - new TaikoDifficultyHitObjectRhythm(5, 4), - new TaikoDifficultyHitObjectRhythm(4, 5) + new TaikoRhythmData(1, 1), + new TaikoRhythmData(2, 1), + new TaikoRhythmData(1, 2), + new TaikoRhythmData(3, 1), + new TaikoRhythmData(1, 3), + new TaikoRhythmData(3, 2), + new TaikoRhythmData(2, 3), + new TaikoRhythmData(5, 4), + new TaikoRhythmData(4, 5) }; /// - /// Initialises a new instance of s, + /// Initialises a new instance of s, /// calculating the closest rhythm change and its associated difficulty for the current hit object. /// /// The current being processed. - public TaikoDifficultyHitObjectRhythm(TaikoDifficultyHitObject current) + public TaikoRhythmData(TaikoDifficultyHitObject current) { var previous = current.Previous(0); @@ -67,8 +67,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm return; } - TaikoDifficultyHitObjectRhythm closestRhythm = getClosestRhythm(current.DeltaTime, previous.DeltaTime); - Ratio = closestRhythm.Ratio; + TaikoRhythmData closestRhythmData = getClosestRhythm(current.DeltaTime, previous.DeltaTime); + Ratio = closestRhythmData.Ratio; } /// @@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm /// /// The numerator for . /// The denominator for - private TaikoDifficultyHitObjectRhythm(int numerator, int denominator) + private TaikoRhythmData(int numerator, int denominator) { Ratio = numerator / (double)denominator; } @@ -88,11 +88,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm /// The time difference between the current hit object and the previous one. /// The time difference between the previous hit object and the one before it. /// The closest matching rhythm from . - private TaikoDifficultyHitObjectRhythm getClosestRhythm(double currentDeltaTime, double previousDeltaTime) + private TaikoRhythmData getClosestRhythm(double currentDeltaTime, double previousDeltaTime) { double ratio = currentDeltaTime / previousDeltaTime; return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First(); } } } - diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs index 3ebc0c25b7..45cc29c99e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm { foreach (var hitObject in rhythmGroup.HitObjects) { - hitObject.Rhythm.SameRhythmGroupedHitObjects = rhythmGroup; + hitObject.RhythmData.SameRhythmGroupedHitObjects = rhythmGroup; hitObject.HitObjectInterval = rhythmGroup.HitObjectInterval; } } @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm { foreach (var hitObject in patternGroup.AllHitObjects) { - hitObject.Rhythm.SamePatternsGroupedHitObjects = patternGroup; + hitObject.RhythmData.SamePatternsGroupedHitObjects = patternGroup; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index d6a2d5874e..5c5503c25d 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// /// The rhythm required to hit this hit object. /// - public readonly TaikoDifficultyHitObjectRhythm Rhythm; + public readonly TaikoRhythmData RhythmData; /// /// The interval between this hit object and the surrounding hit objects in its rhythm group. @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// Colour data for this hit object. This is used by colour evaluator to calculate colour difficulty, but can be used /// by other skills in the future. /// - public readonly TaikoDifficultyHitObjectColour Colour; + public readonly TaikoColourData ColourData; /// /// The adjusted BPM of this hit object, based on its slider velocity and scroll speed. @@ -92,10 +92,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing noteDifficultyHitObjects = noteObjects; // Create the Colour object, its properties should be filled in by TaikoDifficultyPreprocessor - Colour = new TaikoDifficultyHitObjectColour(); + ColourData = new TaikoColourData(); // Create a Rhythm object, its properties are filled in by TaikoDifficultyHitObjectRhythm - Rhythm = new TaikoDifficultyHitObjectRhythm(this); + RhythmData = new TaikoRhythmData(this); switch ((hitObject as Hit)?.Type) { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs index 885131404a..7be1107b70 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills } var taikoObject = (TaikoDifficultyHitObject)current; - int index = taikoObject.Colour.MonoStreak?.HitObjects.IndexOf(taikoObject) ?? 0; + int index = taikoObject.ColourData.MonoStreak?.HitObjects.IndexOf(taikoObject) ?? 0; currentStrain *= DifficultyCalculationUtils.Logistic(index, 4, -1 / 25.0, 0.5) + 0.5; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 12e1396dd7..0e1f3d41cf 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills // Safely prevents previous strains from shifting as new notes are added. var currentObject = current as TaikoDifficultyHitObject; - int index = currentObject?.Colour.MonoStreak?.HitObjects.IndexOf(currentObject) ?? 0; + int index = currentObject?.ColourData.MonoStreak?.HitObjects.IndexOf(currentObject) ?? 0; double monolengthBonus = isConvert ? 1 : 1 + Math.Min(Math.Max((index - 5) / 50.0, 0), 0.30); From 709ad02a517606b07b6a4aaf3f55e611a94219c9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 15:09:51 +0900 Subject: [PATCH 312/801] Simplify `TaikoRhythmData`'s ratio computation --- .../Preprocessing/Rhythm/TaikoRhythmData.cs | 68 +++++++------------ 1 file changed, 25 insertions(+), 43 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs index d895dcfc55..6c4a332624 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs @@ -27,30 +27,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm /// to previous for the rhythm change. /// A above 1 indicates a slow-down; a below 1 indicates a speed-up. /// - public readonly double Ratio; - - /// - /// List of most common rhythm changes in taiko maps. Based on how each object's interval compares to the previous object. - /// /// - /// The general guidelines for the values are: - /// - /// rhythm changes with ratio closer to 1 (that are not 1) are harder to play, - /// speeding up is generally harder than slowing down (with exceptions of rhythm changes requiring a hand switch). - /// + /// This is snapped to the closest matching . /// - private static readonly TaikoRhythmData[] common_rhythms = - { - new TaikoRhythmData(1, 1), - new TaikoRhythmData(2, 1), - new TaikoRhythmData(1, 2), - new TaikoRhythmData(3, 1), - new TaikoRhythmData(1, 3), - new TaikoRhythmData(3, 2), - new TaikoRhythmData(2, 3), - new TaikoRhythmData(5, 4), - new TaikoRhythmData(4, 5) - }; + public readonly double Ratio; /// /// Initialises a new instance of s, @@ -67,31 +47,33 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm return; } - TaikoRhythmData closestRhythmData = getClosestRhythm(current.DeltaTime, previous.DeltaTime); - Ratio = closestRhythmData.Ratio; + double actualRatio = current.DeltaTime / previous.DeltaTime; + double closestRatio = common_ratios.OrderBy(r => Math.Abs(r - actualRatio)).First(); + + Ratio = closestRatio; } /// - /// Creates an object representing a rhythm change. + /// List of most common rhythm changes in taiko maps. Based on how each object's interval compares to the previous object. /// - /// The numerator for . - /// The denominator for - private TaikoRhythmData(int numerator, int denominator) + /// + /// The general guidelines for the values are: + /// + /// rhythm changes with ratio closer to 1 (that are not 1) are harder to play, + /// speeding up is generally harder than slowing down (with exceptions of rhythm changes requiring a hand switch). + /// + /// + private static readonly double[] common_ratios = new[] { - Ratio = numerator / (double)denominator; - } - - /// - /// Determines the closest rhythm change from that matches the timing ratio - /// between the current and previous intervals. - /// - /// The time difference between the current hit object and the previous one. - /// The time difference between the previous hit object and the one before it. - /// The closest matching rhythm from . - private TaikoRhythmData getClosestRhythm(double currentDeltaTime, double previousDeltaTime) - { - double ratio = currentDeltaTime / previousDeltaTime; - return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First(); - } + 1.0 / 1, + 2.0 / 1, + 1.0 / 2, + 3.0 / 1, + 1.0 / 3, + 3.0 / 2, + 2.0 / 3, + 5.0 / 4, + 4.0 / 5 + }; } } From fc933902844ce21ffa6961920dd96bbe47d94fa1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 15:10:15 +0900 Subject: [PATCH 313/801] Remove unused `HitObjectInterval` --- .../Rhythm/TaikoRhythmDifficultyPreprocessor.cs | 5 ----- .../Difficulty/Preprocessing/TaikoDifficultyHitObject.cs | 5 ----- 2 files changed, 10 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs index 45cc29c99e..8b126f85ce 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs @@ -16,10 +16,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm foreach (var rhythmGroup in rhythmGroups) { foreach (var hitObject in rhythmGroup.HitObjects) - { hitObject.RhythmData.SameRhythmGroupedHitObjects = rhythmGroup; - hitObject.HitObjectInterval = rhythmGroup.HitObjectInterval; - } } var patternGroups = createSamePatternGroupedHitObjects(rhythmGroups); @@ -27,9 +24,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm foreach (var patternGroup in patternGroups) { foreach (var hitObject in patternGroup.AllHitObjects) - { hitObject.RhythmData.SamePatternsGroupedHitObjects = patternGroup; - } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 5c5503c25d..489b36b259 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -43,11 +43,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public readonly TaikoRhythmData RhythmData; - /// - /// The interval between this hit object and the surrounding hit objects in its rhythm group. - /// - public double? HitObjectInterval { get; set; } - /// /// Colour data for this hit object. This is used by colour evaluator to calculate colour difficulty, but can be used /// by other skills in the future. From 325483192a26f41d7019c4cf28c22fe91da1f1e3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 15:13:04 +0900 Subject: [PATCH 314/801] Tidy up xmldoc and remove another unused field --- .../Preprocessing/TaikoDifficultyHitObject.cs | 52 ++++++++----------- .../Difficulty/TaikoDifficultyCalculator.cs | 1 - .../Difficulty/Utils/IHasInterval.cs | 2 +- 3 files changed, 23 insertions(+), 32 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 489b36b259..f407e13ff1 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm; @@ -39,13 +40,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public readonly int NoteIndex; /// - /// The rhythm required to hit this hit object. + /// Rhythm data used by . + /// This is populated via . /// public readonly TaikoRhythmData RhythmData; /// - /// Colour data for this hit object. This is used by colour evaluator to calculate colour difficulty, but can be used - /// by other skills in the future. + /// Colour data used by and . + /// This is populated via . /// public readonly TaikoColourData ColourData; @@ -54,19 +56,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public double EffectiveBPM; - /// - /// The current slider velocity of this hit object. - /// - public double CurrentSliderVelocity; - - public double Interval => DeltaTime; - /// /// Creates a new difficulty hit object. /// /// The gameplay associated with this difficulty object. /// The gameplay preceding . - /// The gameplay preceding . /// The rate of the gameplay clock. Modified by speed-changing mods. /// The list of all s in the current beatmap. /// The list of centre (don) s in the current beatmap. @@ -75,7 +69,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// The position of this in the list. /// The control point info of the beatmap. /// The global slider velocity of the beatmap. - public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, + public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, List objects, List centreHitObjects, List rimHitObjects, @@ -86,29 +80,26 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { noteDifficultyHitObjects = noteObjects; - // Create the Colour object, its properties should be filled in by TaikoDifficultyPreprocessor ColourData = new TaikoColourData(); - - // Create a Rhythm object, its properties are filled in by TaikoDifficultyHitObjectRhythm RhythmData = new TaikoRhythmData(this); - switch ((hitObject as Hit)?.Type) + if (hitObject is Hit hit) { - case HitType.Centre: - MonoIndex = centreHitObjects.Count; - centreHitObjects.Add(this); - monoDifficultyHitObjects = centreHitObjects; - break; + switch (hit.Type) + { + case HitType.Centre: + MonoIndex = centreHitObjects.Count; + centreHitObjects.Add(this); + monoDifficultyHitObjects = centreHitObjects; + break; - case HitType.Rim: - MonoIndex = rimHitObjects.Count; - rimHitObjects.Add(this); - monoDifficultyHitObjects = rimHitObjects; - break; - } + case HitType.Rim: + MonoIndex = rimHitObjects.Count; + rimHitObjects.Add(this); + monoDifficultyHitObjects = rimHitObjects; + break; + } - if (hitObject is Hit) - { NoteIndex = noteObjects.Count; noteObjects.Add(this); } @@ -121,7 +112,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing // Calculate the slider velocity at the note's start time. double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, normalisedStartTime, clockRate); - CurrentSliderVelocity = currentSliderVelocity; EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity; } @@ -142,5 +132,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public TaikoDifficultyHitObject? PreviousNote(int backwardsIndex) => noteDifficultyHitObjects.ElementAtOrDefault(NoteIndex - (backwardsIndex + 1)); public TaikoDifficultyHitObject? NextNote(int forwardsIndex) => noteDifficultyHitObjects.ElementAtOrDefault(NoteIndex + (forwardsIndex + 1)); + + public double Interval => DeltaTime; } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index acd654f9b8..6b9986bd68 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -78,7 +78,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty difficultyHitObjects.Add(new TaikoDifficultyHitObject( beatmap.HitObjects[i], beatmap.HitObjects[i - 1], - beatmap.HitObjects[i - 2], clockRate, difficultyHitObjects, centreObjects, diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs index 8f80bb6079..a42940180c 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Utils public interface IHasInterval { /// - /// The interval between 2 objects start times. + /// The interval – ie delta time – between this object and a known previous object. /// double Interval { get; } } From 8447679db9f038b5ddfefbe7337d87ea38000c22 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 15:41:31 +0900 Subject: [PATCH 315/801] Initial tidy-up pass on `IntervalGroupingUtils` --- .../TaikoRhythmDifficultyPreprocessor.cs | 17 +++----- .../Difficulty/Utils/IntervalGroupingUtils.cs | 41 ++++++++----------- 2 files changed, 23 insertions(+), 35 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs index 8b126f85ce..5bc0fdbc03 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data; using osu.Game.Rulesets.Taiko.Difficulty.Utils; @@ -31,13 +32,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm private static List createSameRhythmGroupedHitObjects(List hitObjects) { var rhythmGroups = new List(); - var groups = IntervalGroupingUtils.GroupByInterval(hitObjects); - foreach (var group in groups) - { - var previous = rhythmGroups.Count > 0 ? rhythmGroups[^1] : null; - rhythmGroups.Add(new SameRhythmHitObjectGrouping(previous, group)); - } + foreach (var grouped in IntervalGroupingUtils.GroupByInterval(hitObjects)) + rhythmGroups.Add(new SameRhythmHitObjectGrouping(rhythmGroups.LastOrDefault(), grouped)); return rhythmGroups; } @@ -45,13 +42,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm private static List createSamePatternGroupedHitObjects(List rhythmGroups) { var patternGroups = new List(); - var groups = IntervalGroupingUtils.GroupByInterval(rhythmGroups); - foreach (var group in groups) - { - var previous = patternGroups.Count > 0 ? patternGroups[^1] : null; - patternGroups.Add(new SamePatternsGroupedHitObjects(previous, group)); - } + foreach (var grouped in IntervalGroupingUtils.GroupByInterval(rhythmGroups)) + patternGroups.Add(new SamePatternsGroupedHitObjects(patternGroups.LastOrDefault(), grouped)); return patternGroups; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs index 3b6f5406b4..f04dec1c08 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs @@ -8,56 +8,51 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Utils { public static class IntervalGroupingUtils { - public static List> GroupByInterval(IReadOnlyList data, double marginOfError = 5) where T : IHasInterval + public static List> GroupByInterval(IReadOnlyList objects) where T : IHasInterval { var groups = new List>(); - if (data.Count == 0) - return groups; int i = 0; - - while (i < data.Count) - { - var group = createGroup(data, ref i, marginOfError); - groups.Add(group); - } + while (i < objects.Count) + groups.Add(createNextGroup(objects, ref i)); return groups; } - private static List createGroup(IReadOnlyList data, ref int i, double marginOfError) where T : IHasInterval + private static List createNextGroup(IReadOnlyList objects, ref int i) where T : IHasInterval { - var children = new List { data[i] }; + const double margin_of_error = 5; + + var groupedObjects = new List { objects[i] }; i++; - for (; i < data.Count - 1; i++) + for (; i < objects.Count - 1; i++) { - // An interval change occured, add the current data if the next interval is larger. - if (!Precision.AlmostEquals(data[i].Interval, data[i + 1].Interval, marginOfError)) + // An interval change occured, add the current object if the next interval is larger. + if (!Precision.AlmostEquals(objects[i].Interval, objects[i + 1].Interval, margin_of_error)) { - if (data[i + 1].Interval > data[i].Interval + marginOfError) + if (objects[i + 1].Interval > objects[i].Interval + margin_of_error) { - children.Add(data[i]); + groupedObjects.Add(objects[i]); i++; } - return children; + return groupedObjects; } // No interval change occurred - children.Add(data[i]); + groupedObjects.Add(objects[i]); } - // Check if the last two objects in the data form a "flat" rhythm pattern within the specified margin of error. + // Check if the last two objects in the object form a "flat" rhythm pattern within the specified margin of error. // If true, add the current object to the group and increment the index to process the next object. - if (data.Count > 2 && i < data.Count && - Precision.AlmostEquals(data[^1].Interval, data[^2].Interval, marginOfError)) + if (objects.Count > 2 && i < objects.Count && Precision.AlmostEquals(objects[^1].Interval, objects[^2].Interval, margin_of_error)) { - children.Add(data[i]); + groupedObjects.Add(objects[i]); i++; } - return children; + return groupedObjects; } } } From 09d26fbf5ed006339da279ca449d9f87dd5ba961 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 15:58:34 +0900 Subject: [PATCH 316/801] Minor adjustments --- osu.Game/Graphics/UserInterfaceV2/FormButton.cs | 2 +- osu.Game/Localisation/BeatmapSubmissionStrings.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormButton.cs b/osu.Game/Graphics/UserInterfaceV2/FormButton.cs index fec855153b..1c5d4b5d80 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormButton.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormButton.cs @@ -148,7 +148,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 { base.LoadComplete(); - Content.CornerRadius = 2; + Content.CornerRadius = 4; Add(triangles = new TrianglesV2 { diff --git a/osu.Game/Localisation/BeatmapSubmissionStrings.cs b/osu.Game/Localisation/BeatmapSubmissionStrings.cs index 85fe922703..a4c2b36894 100644 --- a/osu.Game/Localisation/BeatmapSubmissionStrings.cs +++ b/osu.Game/Localisation/BeatmapSubmissionStrings.cs @@ -90,9 +90,9 @@ namespace osu.Game.Localisation public static LocalisableString ModdingQueuesForumDescription => new TranslatableString(getKey(@"modding_queues_forum_description"), @"Having trouble getting feedback? Why not ask in a mod queue!"); /// - /// "Where would you like to post your map?" + /// "Where would you like to post your beatmap?" /// - public static LocalisableString BeatmapSubmissionTargetCaption => new TranslatableString(getKey(@"beatmap_submission_target_caption"), @"Where would you like to post your map?"); + public static LocalisableString BeatmapSubmissionTargetCaption => new TranslatableString(getKey(@"beatmap_submission_target_caption"), @"Where would you like to post your beatmap?"); /// /// "Works in Progress / Help (incomplete, not ready for ranking)" From 2356d3e2d0c02c8e12fb27b8ef1b3b5766d9a5e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 16:24:13 +0900 Subject: [PATCH 317/801] Refactor `OsuContextMenu` to avoid code duplication --- .../Graphics/UserInterface/OsuContextMenu.cs | 33 ++++--------------- osu.Game/Graphics/UserInterface/OsuMenu.cs | 26 ++++++++++----- 2 files changed, 23 insertions(+), 36 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs index e81d77ce43..72ffde3574 100644 --- a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs @@ -15,12 +15,8 @@ namespace osu.Game.Graphics.UserInterface [Resolved] private OsuMenuSamples menuSamples { get; set; } = null!; - // todo: this shouldn't be required after https://github.com/ppy/osu-framework/issues/4519 is fixed. - private bool wasOpened; - private readonly bool playClickSample; - - public OsuContextMenu(bool playClickSample = false) - : base(Direction.Vertical) + public OsuContextMenu(bool playSamples) + : base(Direction.Vertical, topLevelMenu: false, playSamples) { MaskingContainer.CornerRadius = 5; MaskingContainer.EdgeEffect = new EdgeEffectParameters @@ -33,8 +29,6 @@ namespace osu.Game.Graphics.UserInterface ItemsContainer.Padding = new MarginPadding { Vertical = DrawableOsuMenuItem.MARGIN_VERTICAL }; MaxHeight = 250; - - this.playClickSample = playClickSample; } [BackgroundDependencyLoader] @@ -45,27 +39,12 @@ namespace osu.Game.Graphics.UserInterface protected override void AnimateOpen() { - wasOpened = true; - this.FadeIn(FADE_DURATION, Easing.OutQuint); + if (PlaySamples && !WasOpened) + menuSamples.PlayClickSample(); - if (!playClickSample) - return; - - menuSamples.PlayClickSample(); - menuSamples.PlayOpenSample(); + base.AnimateOpen(); } - protected override void AnimateClose() - { - this.Delay(DELAY_BEFORE_FADE_OUT) - .FadeOut(FADE_DURATION, Easing.OutQuint); - - if (wasOpened) - menuSamples.PlayCloseSample(); - - wasOpened = false; - } - - protected override Menu CreateSubMenu() => new OsuContextMenu(); + protected override Menu CreateSubMenu() => new OsuContextMenu(false); // sub menu samples are handled by OsuMenu.OnSubmenuOpen. } } diff --git a/osu.Game/Graphics/UserInterface/OsuMenu.cs b/osu.Game/Graphics/UserInterface/OsuMenu.cs index a75769b16b..11d9000dfa 100644 --- a/osu.Game/Graphics/UserInterface/OsuMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuMenu.cs @@ -22,20 +22,28 @@ namespace osu.Game.Graphics.UserInterface protected const double FADE_DURATION = 280; // todo: this shouldn't be required after https://github.com/ppy/osu-framework/issues/4519 is fixed. - private bool wasOpened; + protected bool WasOpened { get; private set; } + + public bool PlaySamples { get; } [Resolved] private OsuMenuSamples menuSamples { get; set; } = null!; public OsuMenu(Direction direction, bool topLevelMenu = false) + : this(direction, topLevelMenu, playSamples: !topLevelMenu) + { + } + + protected OsuMenu(Direction direction, bool topLevelMenu, bool playSamples) : base(direction, topLevelMenu) { + PlaySamples = playSamples; BackgroundColour = Color4.Black.Opacity(0.5f); MaskingContainer.CornerRadius = 4; ItemsContainer.Padding = new MarginPadding(5); - OnSubmenuOpen += _ => { menuSamples?.PlaySubOpenSample(); }; + OnSubmenuOpen += _ => menuSamples?.PlaySubOpenSample(); } protected override void Update() @@ -59,22 +67,22 @@ namespace osu.Game.Graphics.UserInterface protected override void AnimateOpen() { - if (!TopLevelMenu && !wasOpened) + if (PlaySamples && !WasOpened) menuSamples?.PlayOpenSample(); - this.FadeIn(300, Easing.OutQuint); - wasOpened = true; + WasOpened = true; + this.FadeIn(FADE_DURATION, Easing.OutQuint); } protected override void AnimateClose() { - if (!TopLevelMenu && wasOpened) + if (PlaySamples && WasOpened) menuSamples?.PlayCloseSample(); this.Delay(DELAY_BEFORE_FADE_OUT) .FadeOut(FADE_DURATION, Easing.OutQuint); - wasOpened = false; + WasOpened = false; } protected override void UpdateSize(Vector2 newSize) @@ -87,7 +95,7 @@ namespace osu.Game.Graphics.UserInterface this.ResizeHeightTo(newSize.Y, 300, Easing.OutQuint); else // Delay until the fade out finishes from AnimateClose. - this.Delay(350).ResizeHeightTo(0); + this.Delay(DELAY_BEFORE_FADE_OUT + FADE_DURATION).ResizeHeightTo(0); } else { @@ -96,7 +104,7 @@ namespace osu.Game.Graphics.UserInterface this.ResizeWidthTo(newSize.X, 300, Easing.OutQuint); else // Delay until the fade out finishes from AnimateClose. - this.Delay(350).ResizeWidthTo(0); + this.Delay(DELAY_BEFORE_FADE_OUT + FADE_DURATION).ResizeWidthTo(0); } } From 14273824dcce96bf5c0e59a344a10305fc2bf253 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 19:37:00 +0900 Subject: [PATCH 318/801] Fix `Carousel.FilterAsync` not working when called from a non-update thread I was trying to be smart about things and make use of our `SynchronisationContext` setup, but it turns out to not work in all cases due to the context being missing depending on where you are calling the method from. For now let's prefer the "works everywhere" method of scheduling the final work back to update. --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 2 +- osu.Game/Screens/SelectV2/Carousel.cs | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 72c9611fdb..b29394c55d 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.SongSelect }); } - protected void SortBy(FilterCriteria criteria) => AddStep($"sort {criteria.Sort} group {criteria.Group}", () => Carousel.Filter(criteria)); + protected void SortBy(FilterCriteria criteria) => AddStep($"sort:{criteria.Sort} group:{criteria.Group}", () => Carousel.Filter(criteria)); protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); protected void WaitForSorting() => AddUntilStep("sorting finished", () => Carousel.IsFiltering, () => Is.False); diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 608ef207d9..78c2c99d99 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -228,8 +228,6 @@ namespace osu.Game.Screens.SelectV2 private async Task performFilter() { - Debug.Assert(SynchronizationContext.Current != null); - Stopwatch stopwatch = Stopwatch.StartNew(); var cts = new CancellationTokenSource(); @@ -266,19 +264,22 @@ namespace osu.Game.Screens.SelectV2 { log("Cancelled due to newer request arriving"); } - }, cts.Token).ConfigureAwait(true); + }, cts.Token).ConfigureAwait(false); if (cts.Token.IsCancellationRequested) return; - log("Items ready for display"); - carouselItems = items.ToList(); - displayedRange = null; + Schedule(() => + { + log("Items ready for display"); + carouselItems = items.ToList(); + displayedRange = null; - // Need to call this to ensure correct post-selection logic is handled on the new items list. - HandleItemSelected(currentSelection.Model); + // Need to call this to ensure correct post-selection logic is handled on the new items list. + HandleItemSelected(currentSelection.Model); - refreshAfterSelection(); + refreshAfterSelection(); + }); void log(string text) => Logger.Log($"Carousel[op {cts.GetHashCode().ToString()}] {stopwatch.ElapsedMilliseconds} ms: {text}"); } From 7f8f528ae20da7ac8e0a0cb9a91e64e633b80c87 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 5 Feb 2025 16:26:21 +0900 Subject: [PATCH 319/801] Add helper for testing mod/freemod validity --- osu.Game.Tests/Mods/ModUtilsTest.cs | 35 ++++++++++++++++ .../Multiplayer/MultiplayerMatchSongSelect.cs | 5 --- .../OnlinePlay/OnlinePlaySongSelect.cs | 20 ++++----- osu.Game/Utils/ModUtils.cs | 41 +++++++++++++++++++ 4 files changed, 86 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index decb0a31ac..2964ca9396 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -6,6 +6,7 @@ using System.Linq; using Moq; using NUnit.Framework; using osu.Framework.Localisation; +using osu.Game.Online.Rooms; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; @@ -342,6 +343,40 @@ namespace osu.Game.Tests.Mods Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.055).ToString(), "1.06x"); } + [Test] + public void TestRoomModValidity() + { + Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModHardRock(), MatchType.Playlists)); + Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModDoubleTime(), MatchType.Playlists)); + Assert.IsTrue(ModUtils.IsValidModForMatchType(new ModAdaptiveSpeed(), MatchType.Playlists)); + Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModAutoplay(), MatchType.Playlists)); + Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModTouchDevice(), MatchType.Playlists)); + + Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModHardRock(), MatchType.HeadToHead)); + Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModDoubleTime(), MatchType.HeadToHead)); + // For now, adaptive speed isn't allowed in multiplayer because it's a per-user rate adjustment. + Assert.IsFalse(ModUtils.IsValidModForMatchType(new ModAdaptiveSpeed(), MatchType.HeadToHead)); + Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModAutoplay(), MatchType.HeadToHead)); + Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModTouchDevice(), MatchType.HeadToHead)); + } + + [Test] + public void TestRoomFreeModValidity() + { + Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new OsuModHardRock(), MatchType.Playlists)); + Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new OsuModDoubleTime(), MatchType.Playlists)); + Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new ModAdaptiveSpeed(), MatchType.Playlists)); + Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModAutoplay(), MatchType.Playlists)); + Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModTouchDevice(), MatchType.Playlists)); + + Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new OsuModHardRock(), MatchType.HeadToHead)); + // For now, all rate adjustment mods aren't allowed as free mods in multiplayer. + Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModDoubleTime(), MatchType.HeadToHead)); + Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new ModAdaptiveSpeed(), MatchType.HeadToHead)); + Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModAutoplay(), MatchType.HeadToHead)); + Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModTouchDevice(), MatchType.HeadToHead)); + } + public abstract class CustomMod1 : Mod, IModCompatibilitySpecification { } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index b42a58787d..7328e01026 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -11,7 +11,6 @@ using osu.Framework.Screens; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; -using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select; namespace osu.Game.Screens.OnlinePlay.Multiplayer @@ -122,9 +121,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); - - protected override bool IsValidMod(Mod mod) => base.IsValidMod(mod) && mod.ValidForMultiplayer; - - protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && mod.ValidForMultiplayerAsFreeMod; } } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 4ca6abbf7d..1164c4c0fc 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay freeModSelect = new FreeModSelectOverlay { SelectedMods = { BindTarget = FreeMods }, - IsValidMod = IsValidFreeMod, + IsValidMod = isValidFreeMod, }; } @@ -144,10 +144,10 @@ namespace osu.Game.Screens.OnlinePlay private void onModsChanged(ValueChangedEvent> mods) { - FreeMods.Value = FreeMods.Value.Where(checkCompatibleFreeMod).ToList(); + FreeMods.Value = FreeMods.Value.Where(isValidFreeMod).ToList(); // Reset the validity delegate to update the overlay's display. - freeModSelect.IsValidMod = IsValidFreeMod; + freeModSelect.IsValidMod = isValidFreeMod; } private void onRulesetChanged(ValueChangedEvent ruleset) @@ -194,7 +194,7 @@ namespace osu.Game.Screens.OnlinePlay protected override ModSelectOverlay CreateModSelectOverlay() => new UserModSelectOverlay(OverlayColourScheme.Plum) { - IsValidMod = IsValidMod + IsValidMod = isValidMod }; protected override IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons() @@ -217,18 +217,18 @@ namespace osu.Game.Screens.OnlinePlay /// /// The to check. /// Whether is a valid mod for online play. - protected virtual bool IsValidMod(Mod mod) => mod.HasImplementation && ModUtils.FlattenMod(mod).All(m => m.UserPlayable); + private bool isValidMod(Mod mod) => ModUtils.IsValidModForMatchType(mod, room.Type); /// /// Checks whether a given is valid for per-player free-mod selection. /// /// The to check. /// Whether is a selectable free-mod. - protected virtual bool IsValidFreeMod(Mod mod) => IsValidMod(mod) && checkCompatibleFreeMod(mod); - - private bool checkCompatibleFreeMod(Mod mod) - => Mods.Value.All(m => m.Acronym != mod.Acronym) // Mod must not be contained in the required mods. - && ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray()); // Mod must be compatible with all the required mods. + private bool isValidFreeMod(Mod mod) => ModUtils.IsValidFreeModForMatchType(mod, room.Type) + // Mod must not be contained in the required mods. + && Mods.Value.All(m => m.Acronym != mod.Acronym) + // Mod must be compatible with all the required mods. + && ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray()); protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 15fc34b468..ac24bf2130 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -8,6 +8,7 @@ using System.Linq; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Localisation; using osu.Game.Online.API; +using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -292,5 +293,45 @@ namespace osu.Game.Utils return rate; } + + /// + /// Determines whether a mod can be applied to playlist items in the given match type. + /// + /// The mod to test. + /// The match type. + public static bool IsValidModForMatchType(Mod mod, MatchType type) + { + if (mod.Type == ModType.System || !mod.UserPlayable || !mod.HasImplementation) + return false; + + switch (type) + { + case MatchType.Playlists: + return true; + + default: + return mod.ValidForMultiplayer; + } + } + + /// + /// Determines whether a mod can be applied as a free mod to playlist items in the given match type. + /// + /// The mod to test. + /// The match type. + public static bool IsValidFreeModForMatchType(Mod mod, MatchType type) + { + if (mod.Type == ModType.System || !mod.UserPlayable || !mod.HasImplementation) + return false; + + switch (type) + { + case MatchType.Playlists: + return true; + + default: + return mod.ValidForMultiplayerAsFreeMod; + } + } } } From 5c9e84caf0350760c1f7d78cbe80024aed7661de Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 17:31:54 +0900 Subject: [PATCH 320/801] Add lock object --- osu.Game/Screens/SelectV2/Carousel.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 78c2c99d99..681da84390 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -226,12 +226,14 @@ namespace osu.Game.Screens.SelectV2 private Task filterTask = Task.CompletedTask; private CancellationTokenSource cancellationSource = new CancellationTokenSource(); + private readonly object cancellationLock = new object(); + private async Task performFilter() { Stopwatch stopwatch = Stopwatch.StartNew(); var cts = new CancellationTokenSource(); - lock (this) + lock (cancellationLock) { cancellationSource.Cancel(); cancellationSource = cts; From b7aa71c9759dc7d69249948591ebb60de34e2750 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 18:24:07 +0900 Subject: [PATCH 321/801] Adjust xmldoc slightly to convey the disposal pattern --- osu.Game/Online/Metadata/MetadataClient.cs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs index 1da245e80d..9885419b65 100644 --- a/osu.Game/Online/Metadata/MetadataClient.cs +++ b/osu.Game/Online/Metadata/MetadataClient.cs @@ -73,10 +73,8 @@ namespace osu.Game.Online.Metadata return null; } - /// public abstract Task UpdateActivity(UserActivity? activity); - /// public abstract Task UpdateStatus(UserStatus? status); private int userPresenceWatchCount; @@ -84,11 +82,12 @@ namespace osu.Game.Online.Metadata protected bool IsWatchingUserPresence => Interlocked.CompareExchange(ref userPresenceWatchCount, userPresenceWatchCount, userPresenceWatchCount) > 0; - /// - public IDisposable BeginWatchingUserPresence() - => new UserPresenceWatchToken(this); + /// + /// Signals to the server that we want to begin receiving status updates for all users. + /// + /// An which will end the session when disposed. + public IDisposable BeginWatchingUserPresence() => new UserPresenceWatchToken(this); - /// Task IMetadataServer.BeginWatchingUserPresence() { if (Interlocked.Increment(ref userPresenceWatchCount) == 1) @@ -97,7 +96,6 @@ namespace osu.Game.Online.Metadata return Task.CompletedTask; } - /// Task IMetadataServer.EndWatchingUserPresence() { if (Interlocked.Decrement(ref userPresenceWatchCount) == 0) @@ -110,10 +108,8 @@ namespace osu.Game.Online.Metadata protected abstract Task EndWatchingUserPresenceInternal(); - /// public abstract Task UserPresenceUpdated(int userId, UserPresence? presence); - /// public abstract Task FriendPresenceUpdated(int userId, UserPresence? presence); private class UserPresenceWatchToken : IDisposable @@ -143,7 +139,6 @@ namespace osu.Game.Online.Metadata public abstract IBindable DailyChallengeInfo { get; } - /// public abstract Task DailyChallengeUpdated(DailyChallengeInfo? info); #endregion From c5deb9f36b067f03bd9d597967ac17f8502ade27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 10:28:09 +0100 Subject: [PATCH 322/801] Use alternative lockless solution for atomic cancellation token recreation --- osu.Game/Screens/SelectV2/Carousel.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 681da84390..0b706b4bb8 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -226,18 +226,13 @@ namespace osu.Game.Screens.SelectV2 private Task filterTask = Task.CompletedTask; private CancellationTokenSource cancellationSource = new CancellationTokenSource(); - private readonly object cancellationLock = new object(); - private async Task performFilter() { Stopwatch stopwatch = Stopwatch.StartNew(); var cts = new CancellationTokenSource(); - lock (cancellationLock) - { - cancellationSource.Cancel(); - cancellationSource = cts; - } + var previousCancellationSource = Interlocked.Exchange(ref cancellationSource, cts); + await previousCancellationSource.CancelAsync().ConfigureAwait(false); if (DebounceDelay > 0) { From e5943e460d657cb12545078d10b89ca58f6456f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 10:28:23 +0100 Subject: [PATCH 323/801] Unify `ConfigureAwait()` calls across method --- osu.Game/Screens/SelectV2/Carousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 0b706b4bb8..3371e45453 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -237,7 +237,7 @@ namespace osu.Game.Screens.SelectV2 if (DebounceDelay > 0) { log($"Filter operation queued, waiting for {DebounceDelay} ms debounce"); - await Task.Delay(DebounceDelay, cts.Token).ConfigureAwait(true); + await Task.Delay(DebounceDelay, cts.Token).ConfigureAwait(false); } // Copy must be performed on update thread for now (see ConfigureAwait above). From 40ea7ff2383248c4e3cdbd2c042cf692792f7bd8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 18:48:48 +0900 Subject: [PATCH 324/801] Add better documentation for interval change code --- .../Difficulty/Utils/IntervalGroupingUtils.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs index f04dec1c08..7bd7aa7677 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs @@ -28,9 +28,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Utils for (; i < objects.Count - 1; i++) { - // An interval change occured, add the current object if the next interval is larger. if (!Precision.AlmostEquals(objects[i].Interval, objects[i + 1].Interval, margin_of_error)) { + // When an interval change occurs, include the object with the differing interval in the case it increased + // See https://github.com/ppy/osu/pull/31636#discussion_r1942368372 for rationale. if (objects[i + 1].Interval > objects[i].Interval + margin_of_error) { groupedObjects.Add(objects[i]); From fc5832ce67d7af1b32f88109b705788c4bf07e07 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 04:44:06 -0500 Subject: [PATCH 325/801] Support variable spacing between carousel items --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 11 +++++++ osu.Game/Screens/SelectV2/Carousel.cs | 33 +++++++++++++------- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 9f62780dda..12660d8642 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -20,12 +20,23 @@ namespace osu.Game.Screens.SelectV2 [Cached] public partial class BeatmapCarousel : Carousel { + public const float SPACING = 5f; + private IBindableList detachedBeatmaps = null!; private readonly LoadingLayer loading; private readonly BeatmapCarouselFilterGrouping grouping; + protected override float GetSpacingBetweenPanels(CarouselItem top, CarouselItem bottom) + { + if (top.Model is BeatmapInfo || bottom.Model is BeatmapInfo) + // Beatmap difficulty panels do not overlap with themselves or any other panel. + return SPACING; + + return -SPACING; + } + public BeatmapCarousel() { DebounceDelay = 100; diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 608ef207d9..d7b6f251c3 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -50,11 +50,6 @@ namespace osu.Game.Screens.SelectV2 /// public float DistanceOffscreenToPreload { get; set; } - /// - /// Vertical space between panel layout. Negative value can be used to create an overlapping effect. - /// - protected float SpacingBetweenPanels { get; set; } = -5; - /// /// When a new request arrives to change filtering, the number of milliseconds to wait before performing the filter. /// Regardless of any external debouncing, this is a safety measure to avoid triggering too many threaded operations. @@ -116,6 +111,11 @@ namespace osu.Game.Screens.SelectV2 } } + /// + /// Returns the vertical spacing between two given carousel items. Negative value can be used to create an overlapping effect. + /// + protected virtual float GetSpacingBetweenPanels(CarouselItem top, CarouselItem bottom) => 0f; + #endregion #region Properties and methods concerning implementations @@ -260,7 +260,7 @@ namespace osu.Game.Screens.SelectV2 } log("Updating Y positions"); - updateYPositions(items, visibleHalfHeight, SpacingBetweenPanels); + updateYPositions(items, visibleHalfHeight); } catch (OperationCanceledException) { @@ -283,17 +283,26 @@ namespace osu.Game.Screens.SelectV2 void log(string text) => Logger.Log($"Carousel[op {cts.GetHashCode().ToString()}] {stopwatch.ElapsedMilliseconds} ms: {text}"); } - private static void updateYPositions(IEnumerable carouselItems, float offset, float spacing) + private void updateYPositions(IEnumerable carouselItems, float offset) { + CarouselItem? previousVisible = null; + foreach (var item in carouselItems) - updateItemYPosition(item, ref offset, spacing); + updateItemYPosition(item, ref previousVisible, ref offset); } - private static void updateItemYPosition(CarouselItem item, ref float offset, float spacing) + private void updateItemYPosition(CarouselItem item, ref CarouselItem? previousVisible, ref float offset) { + float spacing = previousVisible == null || !item.IsVisible ? 0 : GetSpacingBetweenPanels(previousVisible, item); + + offset += spacing; item.CarouselYPosition = offset; + if (item.IsVisible) - offset += item.DrawHeight + spacing; + { + offset += item.DrawHeight; + previousVisible = item; + } } #endregion @@ -470,7 +479,7 @@ namespace osu.Game.Screens.SelectV2 return; } - float spacing = SpacingBetweenPanels; + CarouselItem? lastVisible = null; int count = carouselItems.Count; Selection prevKeyboard = currentKeyboardSelection; @@ -482,7 +491,7 @@ namespace osu.Game.Screens.SelectV2 { var item = carouselItems[i]; - updateItemYPosition(item, ref yPos, spacing); + updateItemYPosition(item, ref lastVisible, ref yPos); if (ReferenceEquals(item.Model, currentKeyboardSelection.Model)) currentKeyboardSelection = new Selection(item.Model, item, item.CarouselYPosition, i); From c389dbc711cc90aa2bd7c942d479f9c5336b377f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 04:45:32 -0500 Subject: [PATCH 326/801] Extend panel input area to cover gaps --- osu.Game/Screens/SelectV2/BeatmapPanel.cs | 10 ++++++++++ osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 10 ++++++++++ osu.Game/Screens/SelectV2/GroupPanel.cs | 10 ++++++++++ 3 files changed, 30 insertions(+) diff --git a/osu.Game/Screens/SelectV2/BeatmapPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs index 3edfd4203b..2fe509402b 100644 --- a/osu.Game/Screens/SelectV2/BeatmapPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapPanel.cs @@ -24,6 +24,16 @@ namespace osu.Game.Screens.SelectV2 private Box activationFlash = null!; private OsuSpriteText text = null!; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + var inputRectangle = DrawRectangle; + + // Cover the gaps introduced by the spacing between BeatmapPanels. + inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); + + return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); + } + [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index 79ffe0f68a..85d5cc097d 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -27,6 +27,16 @@ namespace osu.Game.Screens.SelectV2 private OsuSpriteText text = null!; private Box box = null!; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + var inputRectangle = DrawRectangle; + + // Cover a gap introduced by the spacing between a BeatmapSetPanel and a BeatmapPanel either below/above it. + inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); + + return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); + } + [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs index 7ed256ca6a..df930a3111 100644 --- a/osu.Game/Screens/SelectV2/GroupPanel.cs +++ b/osu.Game/Screens/SelectV2/GroupPanel.cs @@ -28,6 +28,16 @@ namespace osu.Game.Screens.SelectV2 private Box box = null!; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + var inputRectangle = DrawRectangle; + + // Cover a gap introduced by the spacing between a GroupPanel and a BeatmapPanel either below/above it. + inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); + + return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); + } + [BackgroundDependencyLoader] private void load() { From 6037d5d8ce256fe70d6a7b22a723d1e26fb6ea42 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 04:46:05 -0500 Subject: [PATCH 327/801] Add test coverage --- ...estSceneBeatmapCarouselV2GroupSelection.cs | 62 ++++++++++++++++ .../TestSceneBeatmapCarouselV2Selection.cs | 70 ++++++++++++++----- 2 files changed, 115 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs index f4d97be5a5..ebdc54864e 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Testing; @@ -8,6 +9,8 @@ using osu.Game.Beatmaps; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; +using osuTK; +using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelect { @@ -154,5 +157,64 @@ namespace osu.Game.Tests.Visual.SongSelect SelectPrevGroup(); WaitForGroupSelection(2, 9); } + + [Test] + public void TestInputHandlingWithinGaps() + { + AddBeatmaps(5, 2); + WaitForDrawablePanels(); + SelectNextGroup(); + + clickOnPanel(0, 1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); + WaitForGroupSelection(0, 1); + + clickOnPanel(0, 0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + WaitForGroupSelection(0, 0); + + SelectNextPanel(); + Select(); + WaitForGroupSelection(0, 1); + + clickOnGroup(0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + AddAssert("group 0 collapsed", () => this.ChildrenOfType().OrderBy(g => g.Y).ElementAt(0).Expanded.Value, () => Is.False); + clickOnGroup(0, p => p.LayoutRectangle.Centre); + AddAssert("group 0 expanded", () => this.ChildrenOfType().OrderBy(g => g.Y).ElementAt(0).Expanded.Value, () => Is.True); + + AddStep("scroll to end", () => Scroll.ScrollToEnd(false)); + clickOnPanel(0, 4, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + WaitForGroupSelection(0, 4); + + clickOnGroup(1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); + AddAssert("group 1 expanded", () => this.ChildrenOfType().OrderBy(g => g.Y).ElementAt(1).Expanded.Value, () => Is.True); + } + + private void clickOnGroup(int group, Func pos) + { + AddStep($"click on group{group}", () => + { + var groupingFilter = Carousel.Filters.OfType().Single(); + var model = groupingFilter.GroupItems.Keys.ElementAt(group); + + var panel = this.ChildrenOfType().Single(b => ReferenceEquals(b.Item!.Model, model)); + InputManager.MoveMouseTo(panel.ToScreenSpace(pos(panel))); + InputManager.Click(MouseButton.Left); + }); + } + + private void clickOnPanel(int group, int panel, Func pos) + { + AddStep($"click on group{group} panel{panel}", () => + { + var groupingFilter = Carousel.Filters.OfType().Single(); + + var g = groupingFilter.GroupItems.Keys.ElementAt(group); + // offset by one because the group itself is included in the items list. + object model = groupingFilter.GroupItems[g].ElementAt(panel + 1).Model; + + var p = this.ChildrenOfType().Single(b => ReferenceEquals(b.Item!.Model, model)); + InputManager.MoveMouseTo(p.ToScreenSpace(pos(p))); + InputManager.Click(MouseButton.Left); + }); + } } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs index b087c252e4..5541e217cf 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs @@ -1,11 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Screens.SelectV2; +using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelect @@ -94,9 +96,8 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!)); - AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); - AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); + AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); } @@ -129,21 +130,6 @@ namespace osu.Game.Tests.Visual.SongSelect WaitForSelection(0, 0); } - [Test] - public void TestGroupSelectionOnHeader() - { - AddBeatmaps(10, 3); - WaitForDrawablePanels(); - - SelectNextGroup(); - SelectNextGroup(); - WaitForSelection(1, 0); - - SelectPrevPanel(); - SelectPrevGroup(); - WaitForSelection(0, 0); - } - [Test] public void TestKeyboardSelection() { @@ -194,6 +180,34 @@ namespace osu.Game.Tests.Visual.SongSelect CheckNoSelection(); } + [Test] + public void TestInputHandlingWithinGaps() + { + AddBeatmaps(2, 5); + WaitForDrawablePanels(); + SelectNextGroup(); + + clickOnDifficulty(0, 1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); + WaitForSelection(0, 1); + + clickOnDifficulty(0, 0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + WaitForSelection(0, 0); + + SelectNextPanel(); + Select(); + WaitForSelection(0, 1); + + clickOnSet(0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + WaitForSelection(0, 0); + + AddStep("scroll to end", () => Scroll.ScrollToEnd(false)); + clickOnDifficulty(0, 4, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + WaitForSelection(0, 4); + + clickOnSet(1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); + WaitForSelection(1, 0); + } + private void checkSelectionIterating(bool isIterating) { object? selection = null; @@ -207,5 +221,27 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("selection not changed", () => Carousel.CurrentSelection == selection); } } + + private void clickOnSet(int set, Func pos) + { + AddStep($"click on set{set}", () => + { + var model = BeatmapSets[set]; + var panel = this.ChildrenOfType().Single(b => ReferenceEquals(b.Item!.Model, model)); + InputManager.MoveMouseTo(panel.ToScreenSpace(pos(panel))); + InputManager.Click(MouseButton.Left); + }); + } + + private void clickOnDifficulty(int set, int diff, Func pos) + { + AddStep($"click on set{set} diff{diff}", () => + { + var model = BeatmapSets[set].Beatmaps[diff]; + var panel = this.ChildrenOfType().Single(b => ReferenceEquals(b.Item!.Model, model)); + InputManager.MoveMouseTo(panel.ToScreenSpace(pos(panel))); + InputManager.Click(MouseButton.Left); + }); + } } } From c370c75fe2793dd379b4f4b8983fd3a35da17511 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 05:47:34 -0500 Subject: [PATCH 328/801] Allow ordering certain carousel panels behind others --- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 14 ++++++++++++-- osu.Game/Screens/SelectV2/Carousel.cs | 7 +++++-- osu.Game/Screens/SelectV2/CarouselItem.cs | 6 ++++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index e4160cc0fa..55cb5fa5f9 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -66,7 +66,12 @@ namespace osu.Game.Screens.SelectV2 { starGroup = (int)Math.Floor(b.StarRating); var groupDefinition = new GroupDefinition($"{starGroup} - {++starGroup} *"); - var groupItem = new CarouselItem(groupDefinition) { DrawHeight = GroupPanel.HEIGHT }; + + var groupItem = new CarouselItem(groupDefinition) + { + DrawHeight = GroupPanel.HEIGHT, + DepthLayer = -2 + }; newItems.Add(groupItem); groupItems[groupDefinition] = new HashSet { groupItem }; @@ -95,7 +100,12 @@ namespace osu.Game.Screens.SelectV2 if (newBeatmapSet) { - var setItem = new CarouselItem(beatmap.BeatmapSet!) { DrawHeight = BeatmapSetPanel.HEIGHT }; + var setItem = new CarouselItem(beatmap.BeatmapSet!) + { + DrawHeight = BeatmapSetPanel.HEIGHT, + DepthLayer = -1 + }; + setItems[beatmap.BeatmapSet!] = new HashSet { setItem }; newItems.Insert(i, setItem); i++; diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 608ef207d9..5dc8d80476 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -550,6 +550,9 @@ namespace osu.Game.Screens.SelectV2 updateDisplayedRange(range); } + double selectedYPos = currentSelection?.CarouselItem?.CarouselYPosition ?? 0; + double maximumDistanceFromSelection = scroll.Panels.Select(p => Math.Abs(((ICarouselPanel)p).DrawYPosition - selectedYPos)).DefaultIfEmpty().Max(); + foreach (var panel in scroll.Panels) { var c = (ICarouselPanel)panel; @@ -558,8 +561,8 @@ namespace osu.Game.Screens.SelectV2 if (c.Item == null) continue; - double selectedYPos = currentSelection?.CarouselItem?.CarouselYPosition ?? 0; - scroll.Panels.ChangeChildDepth(panel, (float)Math.Abs(c.DrawYPosition - selectedYPos)); + float normalisedDepth = (float)(Math.Abs(selectedYPos - c.DrawYPosition) / maximumDistanceFromSelection); + scroll.Panels.ChangeChildDepth(panel, normalisedDepth + c.Item.DepthLayer); if (c.DrawYPosition != c.Item.CarouselYPosition) c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed); diff --git a/osu.Game/Screens/SelectV2/CarouselItem.cs b/osu.Game/Screens/SelectV2/CarouselItem.cs index 32be33e99a..e497c3890c 100644 --- a/osu.Game/Screens/SelectV2/CarouselItem.cs +++ b/osu.Game/Screens/SelectV2/CarouselItem.cs @@ -29,6 +29,12 @@ namespace osu.Game.Screens.SelectV2 /// public float DrawHeight { get; set; } = DEFAULT_HEIGHT; + /// + /// A number that defines the layer which this should be placed on depth-wise. + /// The higher the number, the farther the panel associated with this item is taken to the background. + /// + public int DepthLayer { get; set; } = 0; + /// /// Whether this item is visible or hidden. /// From 11de4296210a9727b8f8dbad928409611b669e93 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 18:59:29 +0900 Subject: [PATCH 329/801] Add support for grouping by artist --- osu.Game.Tests/Resources/TestResources.cs | 29 ++++++++++++++----- .../SongSelect/BeatmapCarouselV2TestScene.cs | 2 +- .../TestSceneBeatmapCarouselV2Basics.cs | 1 + osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 29 ++++++++++--------- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 29 ++++++++++++++++++- 5 files changed, 68 insertions(+), 22 deletions(-) diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index e0572e604c..bf08097ffd 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -85,7 +85,8 @@ namespace osu.Game.Tests.Resources /// /// Number of difficulties. If null, a random number between 1 and 20 will be used. /// Rulesets to cycle through when creating difficulties. If null, osu! ruleset will be used. - public static BeatmapSetInfo CreateTestBeatmapSetInfo(int? difficultyCount = null, RulesetInfo[] rulesets = null) + /// Whether to randomise metadata to create a better distribution. + public static BeatmapSetInfo CreateTestBeatmapSetInfo(int? difficultyCount = null, RulesetInfo[] rulesets = null, bool randomiseMetadata = false) { int j = 0; @@ -95,13 +96,27 @@ namespace osu.Game.Tests.Resources int setId = GetNextTestID(); - var metadata = new BeatmapMetadata + char getRandomCharacter() { - // Create random metadata, then we can check if sorting works based on these - Artist = "Some Artist " + RNG.Next(0, 9), - Title = $"Some Song (set id {setId:000}) {Guid.NewGuid()}", - Author = { Username = "Some Guy " + RNG.Next(0, 9) }, - }; + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz*"; + return chars[RNG.Next(chars.Length)]; + } + + var metadata = randomiseMetadata + ? new BeatmapMetadata + { + // Create random metadata, then we can check if sorting works based on these + Artist = $"{getRandomCharacter()}ome Artist " + RNG.Next(0, 9), + Title = $"{getRandomCharacter()}ome Song (set id {setId:000}) {Guid.NewGuid()}", + Author = { Username = $"{getRandomCharacter()}ome Guy " + RNG.Next(0, 9) }, + } + : new BeatmapMetadata + { + // Create random metadata, then we can check if sorting works based on these + Artist = "Some Artist " + RNG.Next(0, 9), + Title = $"Some Song (set id {setId:000}) {Guid.NewGuid()}", + Author = { Username = "Some Guy " + RNG.Next(0, 9) }, + }; Logger.Log($"🛠️ Generating beatmap set \"{metadata}\" for test consumption."); diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 72c9611fdb..f5ea959c51 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -193,7 +193,7 @@ namespace osu.Game.Tests.Visual.SongSelect protected void AddBeatmaps(int count, int? fixedDifficultiesPerSet = null) => AddStep($"add {count} beatmaps", () => { for (int i = 0; i < count; i++) - BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4))); + BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4), randomiseMetadata: true)); }); protected void RemoveAllBeatmaps() => AddStep("clear all beatmaps", () => BeatmapSets.Clear()); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs index 8ffb51b995..a173920dc6 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs @@ -51,6 +51,7 @@ namespace osu.Game.Tests.Visual.SongSelect { AddBeatmaps(10); SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty }); + SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist }); SortBy(new FilterCriteria { Sort = SortMode.Artist }); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 9f62780dda..e7311fbfbc 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -119,20 +119,12 @@ namespace osu.Game.Screens.SelectV2 return false; case BeatmapInfo beatmapInfo: + // Find any containing group. There should never be too many groups so iterating is efficient enough. + GroupDefinition? containingGroup = grouping.GroupItems.SingleOrDefault(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key; - // If we have groups, we need to account for them. - if (Criteria.SplitOutDifficulties) - { - // Find the containing group. There should never be too many groups so iterating is efficient enough. - GroupDefinition? group = grouping.GroupItems.SingleOrDefault(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key; - - if (group != null) - setExpandedGroup(group); - } - else - { - setExpandedSet(beatmapInfo); - } + if (containingGroup != null) + setExpandedGroup(containingGroup); + setExpandedSet(beatmapInfo); return true; } @@ -170,6 +162,7 @@ namespace osu.Game.Screens.SelectV2 { if (grouping.GroupItems.TryGetValue(group, out var items)) { + // First pass ignoring set groupings. foreach (var i in items) { if (i.Model is GroupDefinition) @@ -177,6 +170,16 @@ namespace osu.Game.Screens.SelectV2 else i.IsVisible = expanded; } + + // Second pass to hide set children when not meant to be displayed. + if (expanded) + { + foreach (var i in items) + { + if (i.Model is BeatmapSetInfo set) + setExpansionStateOfSetItems(set, i.IsExpanded); + } + } } } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index e4160cc0fa..d4e0a166ab 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -52,6 +52,33 @@ namespace osu.Game.Screens.SelectV2 newItems.AddRange(items); break; + case GroupMode.Artist: + groupSetsTogether = true; + char groupChar = (char)0; + + foreach (var item in items) + { + cancellationToken.ThrowIfCancellationRequested(); + + var b = (BeatmapInfo)item.Model; + + char beatmapFirstChar = char.ToUpperInvariant(b.Metadata.Artist[0]); + + if (beatmapFirstChar > groupChar) + { + groupChar = beatmapFirstChar; + var groupDefinition = new GroupDefinition($"{groupChar}"); + var groupItem = new CarouselItem(groupDefinition) { DrawHeight = GroupPanel.HEIGHT }; + + newItems.Add(groupItem); + groupItems[groupDefinition] = new HashSet { groupItem }; + } + + newItems.Add(item); + } + + break; + case GroupMode.Difficulty: groupSetsTogether = false; int starGroup = int.MinValue; @@ -91,7 +118,7 @@ namespace osu.Game.Screens.SelectV2 if (item.Model is BeatmapInfo beatmap) { - bool newBeatmapSet = lastItem == null || (lastItem.Model is BeatmapInfo lastBeatmap && lastBeatmap.BeatmapSet!.ID != beatmap.BeatmapSet!.ID); + bool newBeatmapSet = lastItem?.Model is not BeatmapInfo lastBeatmap || lastBeatmap.BeatmapSet!.ID != beatmap.BeatmapSet!.ID; if (newBeatmapSet) { From 2d75030e36c2304d86a8f617d320cc468c31a73d Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 07:17:25 -0500 Subject: [PATCH 330/801] Change default carousel item header to 50px --- osu.Game/Screens/SelectV2/CarouselItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/CarouselItem.cs b/osu.Game/Screens/SelectV2/CarouselItem.cs index 32be33e99a..65b62be6ba 100644 --- a/osu.Game/Screens/SelectV2/CarouselItem.cs +++ b/osu.Game/Screens/SelectV2/CarouselItem.cs @@ -11,7 +11,7 @@ namespace osu.Game.Screens.SelectV2 /// public sealed class CarouselItem : IComparable { - public const float DEFAULT_HEIGHT = 40; + public const float DEFAULT_HEIGHT = 50; /// /// The model this item is representing. From f2d259cd95f405cdf835fd228c18b4eebc11fbf3 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 07:17:49 -0500 Subject: [PATCH 331/801] Cache overlay colour provider to carousel tests --- osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 72c9611fdb..3a83ff68c6 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -37,6 +37,9 @@ namespace osu.Game.Tests.Visual.SongSelect [Cached(typeof(BeatmapStore))] private BeatmapStore store; + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + private OsuTextFlowContainer stats = null!; private int beatmapCount; From a5fa04e4d6b8cd4852c5c172488d571ebc121809 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 07:18:55 -0500 Subject: [PATCH 332/801] Extend beatmap carousel width in tests --- osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 3a83ff68c6..a3f6eaf152 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -15,6 +15,7 @@ using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Overlays; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; @@ -105,7 +106,7 @@ namespace osu.Game.Tests.Visual.SongSelect { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Width = 500, + Width = 800, RelativeSizeAxes = Axes.Y, }, }, From 092b953dca56991df3e3c69cafd6a430aac5a115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 11:31:18 +0100 Subject: [PATCH 333/801] Implement visual component for displaying submission progress --- .../TestSceneSubmissionStageProgress.cs | 47 ++++ .../Submission/SubmissionStageProgress.cs | 212 ++++++++++++++++++ 2 files changed, 259 insertions(+) create mode 100644 osu.Game.Tests/Visual/Editing/TestSceneSubmissionStageProgress.cs create mode 100644 osu.Game/Screens/Edit/Submission/SubmissionStageProgress.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneSubmissionStageProgress.cs b/osu.Game.Tests/Visual/Editing/TestSceneSubmissionStageProgress.cs new file mode 100644 index 0000000000..47414bb24e --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneSubmissionStageProgress.cs @@ -0,0 +1,47 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Overlays; +using osu.Game.Screens.Edit.Submission; +using osuTK; + +namespace osu.Game.Tests.Visual.Editing +{ + public partial class TestSceneSubmissionStageProgress : OsuTestScene + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + [Test] + public void TestAppearance() + { + SubmissionStageProgress progress = null!; + + AddStep("create content", () => Child = new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.8f), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = progress = new SubmissionStageProgress + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + StageDescription = "Frobnicating the foobarator...", + } + }); + AddStep("not started", () => progress.SetNotStarted()); + AddStep("indeterminate progress", () => progress.SetInProgress()); + AddStep("30% progress", () => progress.SetInProgress(0.3f)); + AddStep("70% progress", () => progress.SetInProgress(0.7f)); + AddStep("completed", () => progress.SetCompleted()); + AddStep("failed", () => progress.SetFailed("the foobarator has defrobnicated")); + AddStep("failed with long message", () => progress.SetFailed("this is a very very very very VERY VEEEEEEEEEEEEEEEEEEEEEEEEERY long error message like you would never believe")); + AddStep("canceled", () => progress.SetCanceled()); + } + } +} diff --git a/osu.Game/Screens/Edit/Submission/SubmissionStageProgress.cs b/osu.Game/Screens/Edit/Submission/SubmissionStageProgress.cs new file mode 100644 index 0000000000..101313c627 --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/SubmissionStageProgress.cs @@ -0,0 +1,212 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.Edit.Submission +{ + public partial class SubmissionStageProgress : CompositeDrawable + { + public LocalisableString StageDescription { get; init; } + + private Bindable status { get; } = new Bindable(); + + private Bindable progress { get; } = new Bindable(); + + private Container progressBarContainer = null!; + private Box progressBar = null!; + private Container iconContainer = null!; + private OsuTextFlowContainer errorMessage = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChildren = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = StageDescription, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new Drawable[] + { + iconContainer = new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + }, + new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Children = + [ + progressBarContainer = new Container + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Width = 150, + Height = 10, + CornerRadius = 5, + Masking = true, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6, + }, + progressBar = new Box + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 0, + Colour = colourProvider.Highlight1, + } + } + }, + errorMessage = new OsuTextFlowContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + // should really be `CentreRight` too, but that's broken due to a framework bug + // (https://github.com/ppy/osu-framework/issues/5084) + TextAnchor = Anchor.BottomRight, + Width = 450, + AutoSizeAxes = Axes.Y, + Alpha = 0, + Colour = colours.Red1, + } + ] + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + status.BindValueChanged(_ => Scheduler.AddOnce(updateStatus), true); + progress.BindValueChanged(_ => Scheduler.AddOnce(updateProgress), true); + } + + public void SetNotStarted() => status.Value = StageStatusType.NotStarted; + + public void SetInProgress(float? progress = null) + { + this.progress.Value = progress; + status.Value = StageStatusType.InProgress; + } + + public void SetCompleted() => status.Value = StageStatusType.Completed; + + public void SetFailed(string reason) + { + status.Value = StageStatusType.Failed; + errorMessage.Text = reason; + } + + public void SetCanceled() => status.Value = StageStatusType.Canceled; + + private const float transition_duration = 200; + + private void updateProgress() + { + if (progress.Value != null) + progressBar.ResizeWidthTo(progress.Value.Value, transition_duration, Easing.OutQuint); + + progressBarContainer.FadeTo(status.Value == StageStatusType.InProgress && progress.Value != null ? 1 : 0, transition_duration, Easing.OutQuint); + } + + private void updateStatus() + { + progressBarContainer.FadeTo(status.Value == StageStatusType.InProgress && progress.Value != null ? 1 : 0, transition_duration, Easing.OutQuint); + errorMessage.FadeTo(status.Value == StageStatusType.Failed ? 1 : 0, transition_duration, Easing.OutQuint); + + iconContainer.Clear(); + iconContainer.ClearTransforms(); + + switch (status.Value) + { + case StageStatusType.InProgress: + iconContainer.Child = new LoadingSpinner + { + Size = new Vector2(16), + State = { Value = Visibility.Visible, }, + }; + iconContainer.Colour = colours.Orange1; + break; + + case StageStatusType.Completed: + iconContainer.Child = new SpriteIcon + { + Icon = FontAwesome.Solid.CheckCircle, + Size = new Vector2(16), + }; + iconContainer.Colour = colours.Green1; + iconContainer.FlashColour(Colour4.White, 1000, Easing.OutQuint); + break; + + case StageStatusType.Failed: + iconContainer.Child = new SpriteIcon + { + Icon = FontAwesome.Solid.ExclamationCircle, + Size = new Vector2(16), + }; + iconContainer.Colour = colours.Red1; + iconContainer.FlashColour(Colour4.White, 1000, Easing.OutQuint); + break; + + case StageStatusType.Canceled: + iconContainer.Child = new SpriteIcon + { + Icon = FontAwesome.Solid.Ban, + Size = new Vector2(16), + }; + iconContainer.Colour = colours.Gray8; + iconContainer.FlashColour(Colour4.White, 1000, Easing.OutQuint); + break; + } + } + + public enum StageStatusType + { + NotStarted, + InProgress, + Completed, + Failed, + Canceled, + } + } +} From 7d299bb2ad5221df7a81f8aa80c644b70af447b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 11:46:33 +0100 Subject: [PATCH 334/801] Expose `EndpointConfiguration` directly in `IAPIAccess` --- osu.Desktop/DiscordRichPresence.cs | 2 +- osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs | 2 +- .../Visual/Online/TestSceneWikiMarkdownContainer.cs | 10 +++++----- osu.Game/Beatmaps/BeatmapInfoExtensions.cs | 2 +- osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs | 4 ++-- osu.Game/Online/API/APIAccess.cs | 13 +++++-------- osu.Game/Online/API/APIRequest.cs | 2 +- osu.Game/Online/API/DummyAPIAccess.cs | 8 +++++--- osu.Game/Online/API/IAPIProvider.cs | 9 ++------- osu.Game/Online/Chat/ExternalLinkOpener.cs | 4 ++-- osu.Game/Online/Chat/NowPlayingCommand.cs | 2 +- osu.Game/Online/EndpointConfiguration.cs | 4 ++-- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 2 +- osu.Game/Overlays/Comments/DrawableComment.cs | 2 +- osu.Game/Overlays/Login/LoginForm.cs | 2 +- osu.Game/Overlays/Login/SecondFactorAuthForm.cs | 2 +- .../Profile/Header/BottomHeaderContainer.cs | 4 ++-- .../Header/Components/DrawableTournamentBanner.cs | 2 +- .../Overlays/Profile/Header/TopHeaderContainer.cs | 2 +- .../Sections/Recent/DrawableRecentActivity.cs | 2 +- osu.Game/Overlays/Wiki/WikiPanelContainer.cs | 2 +- osu.Game/Overlays/WikiOverlay.cs | 4 ++-- .../Submission/ScreenFrequentlyAskedQuestions.cs | 4 ++-- .../OnlinePlay/Lounge/Components/DrawableRoom.cs | 2 +- .../SelectV2/Leaderboards/LeaderboardScoreV2.cs | 2 +- 25 files changed, 44 insertions(+), 50 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 6afb3e319d..cf56fe6115 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -173,7 +173,7 @@ namespace osu.Desktop new Button { Label = "View beatmap", - Url = $@"{api.WebsiteRootUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}" + Url = $@"{api.EndpointConfiguration.WebsiteRootUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}" } }; } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs index f3ea20c1aa..e2d5bc2917 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs @@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Menus new APIMenuImage { Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png", - Url = $@"{API.WebsiteRootUrl}/home/news/2023-12-21-project-loved-december-2023", + Url = $@"{API.EndpointConfiguration.WebsiteRootUrl}/home/news/2023-12-21-project-loved-december-2023", } } }); diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs index 8909305602..cee3f37aea 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs @@ -67,19 +67,19 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestLink() { - AddStep("set current path", () => markdownContainer.CurrentPath = $"{API.WebsiteRootUrl}/wiki/Article_styling_criteria/"); + AddStep("set current path", () => markdownContainer.CurrentPath = $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/Article_styling_criteria/"); AddStep("set '/wiki/Main_page''", () => markdownContainer.Text = "[wiki main page](/wiki/Main_page)"); - AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Main_page"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/Main_page"); AddStep("set '../FAQ''", () => markdownContainer.Text = "[FAQ](../FAQ)"); - AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/FAQ"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/FAQ"); AddStep("set './Writing''", () => markdownContainer.Text = "[wiki writing guidline](./Writing)"); - AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Article_styling_criteria/Writing"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/Article_styling_criteria/Writing"); AddStep("set 'Formatting''", () => markdownContainer.Text = "[wiki formatting guidline](Formatting)"); - AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Article_styling_criteria/Formatting"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/Article_styling_criteria/Formatting"); } [Test] diff --git a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs index a82a288239..d0625c64e3 100644 --- a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs @@ -59,7 +59,7 @@ namespace osu.Game.Beatmaps if (beatmapInfo.OnlineID <= 0 || beatmapInfo.BeatmapSet == null) return null; - return $@"{api.WebsiteRootUrl}/beatmapsets/{beatmapInfo.BeatmapSet.OnlineID}#{ruleset?.ShortName ?? beatmapInfo.Ruleset.ShortName}/{beatmapInfo.OnlineID}"; + return $@"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapInfo.BeatmapSet.OnlineID}#{ruleset?.ShortName ?? beatmapInfo.Ruleset.ShortName}/{beatmapInfo.OnlineID}"; } } } diff --git a/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs index 8a107ed486..ac191d36a9 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs @@ -41,9 +41,9 @@ namespace osu.Game.Beatmaps return null; if (ruleset != null) - return $@"{api.WebsiteRootUrl}/beatmapsets/{beatmapSetInfo.OnlineID}#{ruleset.ShortName}"; + return $@"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapSetInfo.OnlineID}#{ruleset.ShortName}"; - return $@"{api.WebsiteRootUrl}/beatmapsets/{beatmapSetInfo.OnlineID}"; + return $@"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapSetInfo.OnlineID}"; } } } diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index f7fbacf76c..ef7b49868c 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -40,9 +40,7 @@ namespace osu.Game.Online.API private readonly Queue queue = new Queue(); - public string APIEndpointUrl { get; } - - public string WebsiteRootUrl { get; } + public EndpointConfiguration EndpointConfiguration { get; } /// /// The API response version. @@ -89,14 +87,13 @@ namespace osu.Game.Online.API APIVersion = now.Year * 10000 + now.Month * 100 + now.Day; } - APIEndpointUrl = endpointConfiguration.APIEndpointUrl; - WebsiteRootUrl = endpointConfiguration.WebsiteRootUrl; + EndpointConfiguration = endpointConfiguration; NotificationsClient = setUpNotificationsClient(); - authentication = new OAuth(endpointConfiguration.APIClientID, endpointConfiguration.APIClientSecret, APIEndpointUrl); + authentication = new OAuth(endpointConfiguration.APIClientID, endpointConfiguration.APIClientSecret, EndpointConfiguration.APIEndpointUrl); log = Logger.GetLogger(LoggingTarget.Network); - log.Add($@"API endpoint root: {APIEndpointUrl}"); + log.Add($@"API endpoint root: {EndpointConfiguration.APIEndpointUrl}"); log.Add($@"API request version: {APIVersion}"); ProvidedUsername = config.Get(OsuSetting.Username); @@ -408,7 +405,7 @@ namespace osu.Game.Online.API var req = new RegistrationRequest { - Url = $@"{APIEndpointUrl}/users", + Url = $@"{EndpointConfiguration.APIEndpointUrl}/users", Method = HttpMethod.Post, Username = username, Email = email, diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 5cbe9040ba..575e6f8a10 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -71,7 +71,7 @@ namespace osu.Game.Online.API protected virtual WebRequest CreateWebRequest() => new OsuWebRequest(Uri); - protected virtual string Uri => $@"{API!.APIEndpointUrl}/api/v2/{Target}"; + protected virtual string Uri => $@"{API!.EndpointConfiguration.APIEndpointUrl}/api/v2/{Target}"; protected IAPIProvider? API; diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 48c08afb8c..7b3a8f357b 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -41,9 +41,11 @@ namespace osu.Game.Online.API public string ProvidedUsername => LocalUser.Value.Username; - public string APIEndpointUrl => "http://localhost"; - - public string WebsiteRootUrl => "http://localhost"; + public EndpointConfiguration EndpointConfiguration { get; } = new EndpointConfiguration + { + APIEndpointUrl = "http://localhost", + WebsiteRootUrl = "http://localhost", + }; public int APIVersion => int.Parse(DateTime.Now.ToString("yyyyMMdd")); diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 3b6763d736..048193def7 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -51,14 +51,9 @@ namespace osu.Game.Online.API string ProvidedUsername { get; } /// - /// The URL endpoint for this API. Does not include a trailing slash. + /// Holds configuration for online endpoints. /// - string APIEndpointUrl { get; } - - /// - /// The root URL of the website, excluding the trailing slash. - /// - string WebsiteRootUrl { get; } + EndpointConfiguration EndpointConfiguration { get; } /// /// The version of the API. diff --git a/osu.Game/Online/Chat/ExternalLinkOpener.cs b/osu.Game/Online/Chat/ExternalLinkOpener.cs index f76d42c96d..1615b72033 100644 --- a/osu.Game/Online/Chat/ExternalLinkOpener.cs +++ b/osu.Game/Online/Chat/ExternalLinkOpener.cs @@ -49,12 +49,12 @@ namespace osu.Game.Online.Chat if (url.StartsWith('/')) { - url = $"{api.WebsiteRootUrl}{url}"; + url = $"{api.EndpointConfiguration.WebsiteRootUrl}{url}"; isTrustedDomain = true; } else { - isTrustedDomain = url.StartsWith(api.WebsiteRootUrl, StringComparison.Ordinal); + isTrustedDomain = url.StartsWith(api.EndpointConfiguration.WebsiteRootUrl, StringComparison.Ordinal); } if (!url.CheckIsValidUrl()) diff --git a/osu.Game/Online/Chat/NowPlayingCommand.cs b/osu.Game/Online/Chat/NowPlayingCommand.cs index db44017a1b..5e71980a55 100644 --- a/osu.Game/Online/Chat/NowPlayingCommand.cs +++ b/osu.Game/Online/Chat/NowPlayingCommand.cs @@ -95,7 +95,7 @@ namespace osu.Game.Online.Chat string getBeatmapPart() { - return beatmapOnlineID > 0 ? $"[{api.WebsiteRootUrl}/b/{beatmapOnlineID} {beatmapDisplayTitle}]" : beatmapDisplayTitle; + return beatmapOnlineID > 0 ? $"[{api.EndpointConfiguration.WebsiteRootUrl}/b/{beatmapOnlineID} {beatmapDisplayTitle}]" : beatmapDisplayTitle; } string getRulesetPart() diff --git a/osu.Game/Online/EndpointConfiguration.cs b/osu.Game/Online/EndpointConfiguration.cs index bd3c945124..8f76da41fd 100644 --- a/osu.Game/Online/EndpointConfiguration.cs +++ b/osu.Game/Online/EndpointConfiguration.cs @@ -9,12 +9,12 @@ namespace osu.Game.Online public class EndpointConfiguration { /// - /// The base URL for the website. + /// The base URL for the website. Does not include a trailing slash. /// public string WebsiteRootUrl { get; set; } = string.Empty; /// - /// The endpoint for the main (osu-web) API. + /// The endpoint for the main (osu-web) API. Does not include a trailing slash. /// public string APIEndpointUrl { get; set; } = string.Empty; diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 6acf236bf3..f7efa08969 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -436,7 +436,7 @@ namespace osu.Game.Online.Leaderboards items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods)); if (Score.OnlineID > 0) - items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.WebsiteRootUrl}/scores/{Score.OnlineID}"))); + items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.EndpointConfiguration.WebsiteRootUrl}/scores/{Score.OnlineID}"))); if (Score.Files.Count > 0) { diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index d664a44be9..b06be3e74a 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -419,7 +419,7 @@ namespace osu.Game.Overlays.Comments private void copyUrl() { - clipboard.SetText($@"{api.APIEndpointUrl}/comments/{Comment.Id}"); + clipboard.SetText($@"{api.EndpointConfiguration.APIEndpointUrl}/comments/{Comment.Id}"); onScreenDisplay?.Display(new CopyUrlToast()); } diff --git a/osu.Game/Overlays/Login/LoginForm.cs b/osu.Game/Overlays/Login/LoginForm.cs index 0ff30da2a1..2b6d523b95 100644 --- a/osu.Game/Overlays/Login/LoginForm.cs +++ b/osu.Game/Overlays/Login/LoginForm.cs @@ -129,7 +129,7 @@ namespace osu.Game.Overlays.Login } }; - forgottenPasswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.WebsiteRootUrl}/home/password-reset"); + forgottenPasswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.EndpointConfiguration.WebsiteRootUrl}/home/password-reset"); password.OnCommit += (_, _) => performLogin(); diff --git a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs index 506cb70d09..e36d62f827 100644 --- a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs +++ b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs @@ -98,7 +98,7 @@ namespace osu.Game.Overlays.Login explainText.AddParagraph(UserVerificationStrings.BoxInfoCheckSpam); // We can't support localisable strings with nested links yet. Not sure if we even can (probably need to allow markdown link formatting or something). explainText.AddParagraph("If you can't access your email or have forgotten what you used, please follow the "); - explainText.AddLink(UserVerificationStrings.BoxInfoRecoverLink, $"{api.WebsiteRootUrl}/home/password-reset"); + explainText.AddLink(UserVerificationStrings.BoxInfoRecoverLink, $"{api.EndpointConfiguration.WebsiteRootUrl}/home/password-reset"); explainText.AddText(". You can also "); explainText.AddLink(UserVerificationStrings.BoxInfoReissueLink, () => { diff --git a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs index d5b4d844b2..d9d23f16fd 100644 --- a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs @@ -124,12 +124,12 @@ namespace osu.Game.Overlays.Profile.Header } topLinkContainer.AddText("Contributed "); - topLinkContainer.AddLink("forum post".ToQuantity(user.PostCount, "#,##0"), $"{api.WebsiteRootUrl}/users/{user.Id}/posts", creationParameters: embolden); + topLinkContainer.AddLink("forum post".ToQuantity(user.PostCount, "#,##0"), $"{api.EndpointConfiguration.WebsiteRootUrl}/users/{user.Id}/posts", creationParameters: embolden); addSpacer(topLinkContainer); topLinkContainer.AddText("Posted "); - topLinkContainer.AddLink("comment".ToQuantity(user.CommentsCount, "#,##0"), $"{api.WebsiteRootUrl}/comments?user_id={user.Id}", creationParameters: embolden); + topLinkContainer.AddLink("comment".ToQuantity(user.CommentsCount, "#,##0"), $"{api.EndpointConfiguration.WebsiteRootUrl}/comments?user_id={user.Id}", creationParameters: embolden); string websiteWithoutProtocol = user.Website; diff --git a/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs b/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs index c099009ca4..a66a5c8fe9 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs @@ -33,7 +33,7 @@ namespace osu.Game.Overlays.Profile.Header.Components Texture = textures.Get(banner.Image), }; - Action = () => game?.OpenUrlExternally($@"{api.WebsiteRootUrl}/community/tournaments/{banner.TournamentId}"); + Action = () => game?.OpenUrlExternally($@"{api.EndpointConfiguration.WebsiteRootUrl}/community/tournaments/{banner.TournamentId}"); } protected override void LoadComplete() diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index 165a576c03..fb1bdca57c 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -213,7 +213,7 @@ namespace osu.Game.Overlays.Profile.Header cover.User = user; avatar.User = user; usernameText.Text = user?.Username ?? string.Empty; - openUserExternally.Link = $@"{api.WebsiteRootUrl}/users/{user?.Id ?? 0}"; + openUserExternally.Link = $@"{api.EndpointConfiguration.WebsiteRootUrl}/users/{user?.Id ?? 0}"; userFlag.CountryCode = user?.CountryCode ?? default; userCountryText.Text = (user?.CountryCode ?? default).GetDescription(); userCountryContainer.Action = () => rankingsOverlay?.ShowCountry(user?.CountryCode ?? default); diff --git a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs index 8a0003b4ea..a0bcf2dc47 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs @@ -223,7 +223,7 @@ namespace osu.Game.Overlays.Profile.Sections.Recent private void addBeatmapsetLink() => content.AddLink(activity.Beatmapset.AsNonNull().Title, LinkAction.OpenBeatmapSet, getLinkArgument(activity.Beatmapset.AsNonNull().Url), creationParameters: t => t.Font = getLinkFont()); - private object getLinkArgument(string url) => MessageFormatter.GetLinkDetails($"{api.WebsiteRootUrl}{url}").Argument.AsNonNull(); + private object getLinkArgument(string url) => MessageFormatter.GetLinkDetails($"{api.EndpointConfiguration.WebsiteRootUrl}{url}").Argument.AsNonNull(); private FontUsage getLinkFont(FontWeight fontWeight = FontWeight.Regular) => OsuFont.GetFont(size: font_size, weight: fontWeight, italics: true); diff --git a/osu.Game/Overlays/Wiki/WikiPanelContainer.cs b/osu.Game/Overlays/Wiki/WikiPanelContainer.cs index 555dab852e..773dde6436 100644 --- a/osu.Game/Overlays/Wiki/WikiPanelContainer.cs +++ b/osu.Game/Overlays/Wiki/WikiPanelContainer.cs @@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Wiki Padding = new MarginPadding(padding), Child = new WikiPanelMarkdownContainer(isFullWidth) { - CurrentPath = $@"{api.WebsiteRootUrl}/wiki/", + CurrentPath = $@"{api.EndpointConfiguration.WebsiteRootUrl}/wiki/", Text = text, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y diff --git a/osu.Game/Overlays/WikiOverlay.cs b/osu.Game/Overlays/WikiOverlay.cs index ef258da82b..c360d1eb9e 100644 --- a/osu.Game/Overlays/WikiOverlay.cs +++ b/osu.Game/Overlays/WikiOverlay.cs @@ -167,7 +167,7 @@ namespace osu.Game.Overlays } else { - LoadDisplay(articlePage = new WikiArticlePage($@"{api.WebsiteRootUrl}/wiki/{path.Value}/", response.Markdown)); + LoadDisplay(articlePage = new WikiArticlePage($@"{api.EndpointConfiguration.WebsiteRootUrl}/wiki/{path.Value}/", response.Markdown)); } } @@ -176,7 +176,7 @@ namespace osu.Game.Overlays wikiData.Value = null; path.Value = "error"; - LoadDisplay(articlePage = new WikiArticlePage($@"{api.WebsiteRootUrl}/wiki/", + LoadDisplay(articlePage = new WikiArticlePage($@"{api.EndpointConfiguration.WebsiteRootUrl}/wiki/", $"Something went wrong when trying to fetch page \"{originalPath}\".\n\n[Return to the main page]({INDEX_PATH}).")); } diff --git a/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs b/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs index c8d226bbcb..ff9cb07e2d 100644 --- a/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs +++ b/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs @@ -46,14 +46,14 @@ namespace osu.Game.Screens.Edit.Submission RelativeSizeAxes = Axes.X, Caption = BeatmapSubmissionStrings.MappingHelpForumDescription, ButtonText = BeatmapSubmissionStrings.MappingHelpForum, - Action = () => game?.OpenUrlExternally($@"{api.WebsiteRootUrl}/community/forums/56"), + Action = () => game?.OpenUrlExternally($@"{api.EndpointConfiguration.WebsiteRootUrl}/community/forums/56"), }, new FormButton { RelativeSizeAxes = Axes.X, Caption = BeatmapSubmissionStrings.ModdingQueuesForumDescription, ButtonText = BeatmapSubmissionStrings.ModdingQueuesForum, - Action = () => game?.OpenUrlExternally($@"{api.WebsiteRootUrl}/community/forums/60"), + Action = () => game?.OpenUrlExternally($@"{api.EndpointConfiguration.WebsiteRootUrl}/community/forums/60"), }, }, }); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 377c840d25..7b2e2c02f7 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -361,7 +361,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components return items.ToArray(); - string formatRoomUrl(long id) => $@"{api.WebsiteRootUrl}/multiplayer/rooms/{id}"; + string formatRoomUrl(long id) => $@"{api.EndpointConfiguration.WebsiteRootUrl}/multiplayer/rooms/{id}"; } } diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs index 732fb2cd8c..2460fbe6f8 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs @@ -778,7 +778,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => SelectedMods.Value = score.Mods.Where(m => IsValidMod.Invoke(m)).ToArray())); if (score.OnlineID > 0) - items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.WebsiteRootUrl}/scores/{score.OnlineID}"))); + items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.EndpointConfiguration.WebsiteRootUrl}/scores/{score.OnlineID}"))); if (score.Files.Count <= 0) return items.ToArray(); From aaffd72032042834bb5b982fd9524a7427aa0f7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 11:48:07 +0100 Subject: [PATCH 335/801] Add beatmap submission service URL to endpoint configuration --- osu.Game/Online/EndpointConfiguration.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Online/EndpointConfiguration.cs b/osu.Game/Online/EndpointConfiguration.cs index 8f76da41fd..39dd72d41a 100644 --- a/osu.Game/Online/EndpointConfiguration.cs +++ b/osu.Game/Online/EndpointConfiguration.cs @@ -42,5 +42,10 @@ namespace osu.Game.Online /// The endpoint for the SignalR metadata server. /// public string MetadataEndpointUrl { get; set; } = string.Empty; + + /// + /// The root URL for the service handling beatmap submission. Does not include a trailing slash. + /// + public string? BeatmapSubmissionServiceUrl { get; set; } } } From 8940ee5d9cb3a41a974d30ba3d3efa0dea74c751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 11:52:43 +0100 Subject: [PATCH 336/801] Add API request & response structures for beatmap submission --- .../Online/API/Requests/APIUploadRequest.cs | 26 ++++++ .../Requests/PatchBeatmapPackageRequest.cs | 51 ++++++++++++ .../API/Requests/PutBeatmapSetRequest.cs | 82 +++++++++++++++++++ .../Requests/ReplaceBeatmapPackageRequest.cs | 45 ++++++++++ .../Responses/PutBeatmapSetResponse.cs | 30 +++++++ 5 files changed, 234 insertions(+) create mode 100644 osu.Game/Online/API/Requests/APIUploadRequest.cs create mode 100644 osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs create mode 100644 osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs create mode 100644 osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs create mode 100644 osu.Game/Online/API/Requests/Responses/PutBeatmapSetResponse.cs diff --git a/osu.Game/Online/API/Requests/APIUploadRequest.cs b/osu.Game/Online/API/Requests/APIUploadRequest.cs new file mode 100644 index 0000000000..3503b4cebb --- /dev/null +++ b/osu.Game/Online/API/Requests/APIUploadRequest.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using osu.Framework.IO.Network; + +namespace osu.Game.Online.API.Requests +{ + public abstract class APIUploadRequest : APIRequest + { + protected override WebRequest CreateWebRequest() + { + var request = base.CreateWebRequest(); + request.UploadProgress += onUploadProgress; + return request; + } + + private void onUploadProgress(long current, long total) + { + Debug.Assert(API != null); + API.Schedule(() => Progressed?.Invoke(current, total)); + } + + public event APIProgressHandler? Progressed; + } +} diff --git a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs new file mode 100644 index 0000000000..85981448da --- /dev/null +++ b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs @@ -0,0 +1,51 @@ +// 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.Net.Http; +using osu.Framework.IO.Network; + +namespace osu.Game.Online.API.Requests +{ + public class PatchBeatmapPackageRequest : APIUploadRequest + { + protected override string Uri + { + get + { + // can be removed once the service has been successfully deployed to production + if (API!.EndpointConfiguration.BeatmapSubmissionServiceUrl == null) + throw new NotSupportedException("Beatmap submission not supported in this configuration!"); + + return $@"{API!.EndpointConfiguration.BeatmapSubmissionServiceUrl!}/beatmapsets/{BeatmapSetID}"; + } + } + + protected override string Target => throw new NotSupportedException(); + + public uint BeatmapSetID { get; } + public Dictionary FilesChanged { get; } = new Dictionary(); + public HashSet FilesDeleted { get; } = new HashSet(); + + public PatchBeatmapPackageRequest(uint beatmapSetId) + { + BeatmapSetID = beatmapSetId; + } + + protected override WebRequest CreateWebRequest() + { + var request = base.CreateWebRequest(); + request.Method = HttpMethod.Patch; + + foreach ((string filename, byte[] content) in FilesChanged) + request.AddFile(@"filesChanged", content, filename); + + foreach (string filename in FilesDeleted) + request.AddParameter(@"filesDeleted", filename, RequestParameterType.Form); + + request.Timeout = 60_000; + return request; + } + } +} diff --git a/osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs b/osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs new file mode 100644 index 0000000000..03b8397681 --- /dev/null +++ b/osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs @@ -0,0 +1,82 @@ +// 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 System.Net.Http; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using osu.Framework.IO.Network; +using osu.Framework.Localisation; +using osu.Game.Localisation; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Online.API.Requests +{ + public class PutBeatmapSetRequest : APIRequest + { + protected override string Uri + { + get + { + // can be removed once the service has been successfully deployed to production + if (API!.EndpointConfiguration.BeatmapSubmissionServiceUrl == null) + throw new NotSupportedException("Beatmap submission not supported in this configuration!"); + + return $@"{API!.EndpointConfiguration.BeatmapSubmissionServiceUrl}/beatmapsets"; + } + } + + protected override string Target => throw new NotSupportedException(); + + [JsonProperty("beatmapset_id")] + public uint? BeatmapSetID { get; init; } + + [JsonProperty("beatmaps_to_create")] + public uint BeatmapsToCreate { get; init; } + + [JsonProperty("beatmaps_to_keep")] + public uint[] BeatmapsToKeep { get; init; } = []; + + [JsonProperty("target")] + public BeatmapSubmissionTarget SubmissionTarget { get; init; } + + private PutBeatmapSetRequest() + { + } + + public static PutBeatmapSetRequest CreateNew(uint beatmapCount, BeatmapSubmissionTarget target) => new PutBeatmapSetRequest + { + BeatmapsToCreate = beatmapCount, + SubmissionTarget = target, + }; + + public static PutBeatmapSetRequest UpdateExisting(uint beatmapSetId, IEnumerable beatmapsToKeep, uint beatmapsToCreate, BeatmapSubmissionTarget target) => new PutBeatmapSetRequest + { + BeatmapSetID = beatmapSetId, + BeatmapsToKeep = beatmapsToKeep.ToArray(), + BeatmapsToCreate = beatmapsToCreate, + SubmissionTarget = target, + }; + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + req.Method = HttpMethod.Put; + req.ContentType = @"application/json"; + req.AddRaw(JsonConvert.SerializeObject(this)); + return req; + } + } + + [JsonConverter(typeof(StringEnumConverter))] + public enum BeatmapSubmissionTarget + { + [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetWIP))] + WIP, + + [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetPending))] + Pending, + } +} diff --git a/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs new file mode 100644 index 0000000000..c9dd12d61e --- /dev/null +++ b/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs @@ -0,0 +1,45 @@ +// 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.Net.Http; +using osu.Framework.IO.Network; + +namespace osu.Game.Online.API.Requests +{ + public class ReplaceBeatmapPackageRequest : APIUploadRequest + { + protected override string Uri + { + get + { + // can be removed once the service has been successfully deployed to production + if (API!.EndpointConfiguration.BeatmapSubmissionServiceUrl == null) + throw new NotSupportedException("Beatmap submission not supported in this configuration!"); + + return $@"{API!.EndpointConfiguration.BeatmapSubmissionServiceUrl}/beatmapsets/{BeatmapSetID}"; + } + } + + protected override string Target => throw new NotSupportedException(); + + public uint BeatmapSetID { get; } + + private readonly byte[] oszPackage; + + public ReplaceBeatmapPackageRequest(uint beatmapSetID, byte[] oszPackage) + { + this.oszPackage = oszPackage; + BeatmapSetID = beatmapSetID; + } + + protected override WebRequest CreateWebRequest() + { + var request = base.CreateWebRequest(); + request.AddFile(@"beatmapArchive", oszPackage); + request.Method = HttpMethod.Put; + request.Timeout = 60_000; + return request; + } + } +} diff --git a/osu.Game/Online/API/Requests/Responses/PutBeatmapSetResponse.cs b/osu.Game/Online/API/Requests/Responses/PutBeatmapSetResponse.cs new file mode 100644 index 0000000000..e3ec617039 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/PutBeatmapSetResponse.cs @@ -0,0 +1,30 @@ +// 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 Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class PutBeatmapSetResponse + { + [JsonProperty("beatmapset_id")] + public uint BeatmapSetId { get; set; } + + [JsonProperty("beatmap_ids")] + public ICollection BeatmapIds { get; set; } = Array.Empty(); + + [JsonProperty("files")] + public ICollection Files { get; set; } = Array.Empty(); + } + + public struct BeatmapSetFile + { + [JsonProperty("filename")] + public string Filename { get; set; } + + [JsonProperty("sha2_hash")] + public string SHA2Hash { get; set; } + } +} From b6731ff7738ede0985297fd69d5b32a82c66bdfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 11:34:13 +0100 Subject: [PATCH 337/801] Add completion flag to `WizardOverlay` --- osu.Game/Overlays/WizardOverlay.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/WizardOverlay.cs b/osu.Game/Overlays/WizardOverlay.cs index 34ffa7bd77..2a881045fd 100644 --- a/osu.Game/Overlays/WizardOverlay.cs +++ b/osu.Game/Overlays/WizardOverlay.cs @@ -45,6 +45,8 @@ namespace osu.Game.Overlays private LoadingSpinner loading = null!; private ScheduledDelegate? loadingShowDelegate; + public bool Completed { get; private set; } + protected WizardOverlay(OverlayColourScheme scheme) : base(scheme) { @@ -221,6 +223,7 @@ namespace osu.Game.Overlays else { CurrentStepIndex = null; + Completed = true; Hide(); } From fff99a8b4008800ce5a870ac600618e84d8ffdc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 11:54:26 +0100 Subject: [PATCH 338/801] Implement special exporter intended specifically for submission flows --- osu.Game/Database/LegacyBeatmapExporter.cs | 23 +++++--- .../Submission/SubmissionBeatmapExporter.cs | 58 +++++++++++++++++++ 2 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 8f94fc9e63..e7e5ddb4d2 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -61,6 +61,20 @@ namespace osu.Game.Database Configuration = new LegacySkinDecoder().Decode(skinStreamReader) }; + MutateBeatmap(model, playableBeatmap); + + // Encode to legacy format + var stream = new MemoryStream(); + using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + new LegacyBeatmapEncoder(playableBeatmap, beatmapSkin).Encode(sw); + + stream.Seek(0, SeekOrigin.Begin); + + return stream; + } + + protected virtual void MutateBeatmap(BeatmapSetInfo beatmapSet, IBeatmap playableBeatmap) + { // Convert beatmap elements to be compatible with legacy format // So we truncate time and position values to integers, and convert paths with multiple segments to Bézier curves @@ -145,15 +159,6 @@ namespace osu.Game.Database hasPath.Path.ControlPoints.Add(new PathControlPoint(position)); } } - - // Encode to legacy format - var stream = new MemoryStream(); - using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - new LegacyBeatmapEncoder(playableBeatmap, beatmapSkin).Encode(sw); - - stream.Seek(0, SeekOrigin.Begin); - - return stream; } protected override string FileExtension => @".osz"; diff --git a/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs b/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs new file mode 100644 index 0000000000..3c50a1bf80 --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs @@ -0,0 +1,58 @@ +// 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 osu.Framework.Platform; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Screens.Edit.Submission +{ + public class SubmissionBeatmapExporter : LegacyBeatmapExporter + { + private readonly uint? beatmapSetId; + private readonly HashSet? beatmapIds; + + public SubmissionBeatmapExporter(Storage storage) + : base(storage) + { + } + + public SubmissionBeatmapExporter(Storage storage, PutBeatmapSetResponse putBeatmapSetResponse) + : base(storage) + { + beatmapSetId = putBeatmapSetResponse.BeatmapSetId; + beatmapIds = putBeatmapSetResponse.BeatmapIds.Select(id => (int)id).ToHashSet(); + } + + protected override void MutateBeatmap(BeatmapSetInfo beatmapSet, IBeatmap playableBeatmap) + { + base.MutateBeatmap(beatmapSet, playableBeatmap); + + if (beatmapSetId != null && beatmapIds != null) + { + playableBeatmap.BeatmapInfo.BeatmapSet = beatmapSet; + playableBeatmap.BeatmapInfo.BeatmapSet!.OnlineID = (int)beatmapSetId; + + if (beatmapIds.Contains(playableBeatmap.BeatmapInfo.OnlineID)) + { + beatmapIds.Remove(playableBeatmap.BeatmapInfo.OnlineID); + return; + } + + if (playableBeatmap.BeatmapInfo.OnlineID > 0) + throw new InvalidOperationException(@"Encountered beatmap with ID that has not been assigned to it by the server!"); + + if (beatmapIds.Count == 0) + throw new InvalidOperationException(@"Ran out of new beatmap IDs to assign to unsubmitted beatmaps!"); + + int newId = beatmapIds.First(); + beatmapIds.Remove(newId); + playableBeatmap.BeatmapInfo.OnlineID = newId; + } + } + } +} From 78e85dc2c7f773ac8cbde2b226ec6ba9b8791672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 12:22:33 +0100 Subject: [PATCH 339/801] Add beatmap submission support --- .../Localisation/BeatmapSubmissionStrings.cs | 40 ++ osu.Game/Localisation/EditorStrings.cs | 10 + osu.Game/Screens/Edit/Editor.cs | 55 ++- .../Submission/BeatmapSubmissionScreen.cs | 422 ++++++++++++++++++ .../Submission/BeatmapSubmissionSettings.cs | 13 + .../Submission/ScreenSubmissionSettings.cs | 15 +- 6 files changed, 544 insertions(+), 11 deletions(-) create mode 100644 osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs create mode 100644 osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs diff --git a/osu.Game/Localisation/BeatmapSubmissionStrings.cs b/osu.Game/Localisation/BeatmapSubmissionStrings.cs index a4c2b36894..50b65ab572 100644 --- a/osu.Game/Localisation/BeatmapSubmissionStrings.cs +++ b/osu.Game/Localisation/BeatmapSubmissionStrings.cs @@ -39,6 +39,31 @@ namespace osu.Game.Localisation /// public static LocalisableString SubmissionSettings => new TranslatableString(getKey(@"submission_settings"), @"Submission settings"); + /// + /// "Submit beatmap!" + /// + public static LocalisableString ConfirmSubmission => new TranslatableString(getKey(@"confirm_submission"), @"Submit beatmap!"); + + /// + /// "Exporting beatmap set in compatibility mode..." + /// + public static LocalisableString ExportingBeatmapSet => new TranslatableString(getKey(@"exporting_beatmap_set"), @"Exporting beatmap set in compatibility mode..."); + + /// + /// "Preparing beatmap set online..." + /// + public static LocalisableString PreparingBeatmapSet => new TranslatableString(getKey(@"preparing_beatmap_set"), @"Preparing beatmap set online..."); + + /// + /// "Uploading beatmap set contents..." + /// + public static LocalisableString UploadingBeatmapSetContents => new TranslatableString(getKey(@"uploading_beatmap_set_contents"), @"Uploading beatmap set contents..."); + + /// + /// "Updating local beatmap with relevant changes..." + /// + public static LocalisableString UpdatingLocalBeatmap => new TranslatableString(getKey(@"updating_local_beatmap"), @"Updating local beatmap with relevant changes..."); + /// /// "Before you continue, we ask you to check whether the content you are uploading has been cleared for upload. Please understand that you are responsible for the content you upload to the platform and if in doubt, should ask permission from the creators before uploading!" /// @@ -119,6 +144,21 @@ namespace osu.Game.Localisation /// public static LocalisableString LegacyExportDisclaimer => new TranslatableString(getKey(@"legacy_export_disclaimer"), @"Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost."); + /// + /// "Empty beatmaps cannot be submitted." + /// + public static LocalisableString EmptyBeatmapsCannotBeSubmitted => new TranslatableString(getKey(@"empty_beatmaps_cannot_be_submitted"), @"Empty beatmaps cannot be submitted."); + + /// + /// "Update beatmap!" + /// + public static LocalisableString UpdateBeatmap => new TranslatableString(getKey(@"update_beatmap"), @"Update beatmap!"); + + /// + /// "Upload NEW beatmap!" + /// + public static LocalisableString UploadNewBeatmap => new TranslatableString(getKey(@"upload_new_beatmap"), @"Upload NEW beatmap!"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 3b4026be11..2c834c38bb 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -69,6 +69,16 @@ namespace osu.Game.Localisation /// public static LocalisableString DeleteDifficulty => new TranslatableString(getKey(@"delete_difficulty"), @"Delete difficulty"); + /// + /// "Edit externally" + /// + public static LocalisableString EditExternally => new TranslatableString(getKey(@"edit_externally"), @"Edit externally"); + + /// + /// "Submit beatmap" + /// + public static LocalisableString SubmitBeatmap => new TranslatableString(getKey(@"submit_beatmap"), @"Submit beatmap"); + /// /// "setup" /// diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 3302fafbb8..c2a7264243 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -32,6 +32,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.Extensions; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; @@ -52,6 +53,7 @@ using osu.Game.Screens.Edit.Compose.Components.Timeline; using osu.Game.Screens.Edit.Design; using osu.Game.Screens.Edit.GameplayTest; using osu.Game.Screens.Edit.Setup; +using osu.Game.Screens.Edit.Submission; using osu.Game.Screens.Edit.Timing; using osu.Game.Screens.Edit.Verify; using osu.Game.Screens.OnlinePlay; @@ -111,6 +113,10 @@ namespace osu.Game.Screens.Edit [Resolved(canBeNull: true)] private INotificationOverlay notifications { get; set; } + [Resolved(canBeNull: true)] + [CanBeNull] + private LoginOverlay loginOverlay { get; set; } + [Resolved] private RealmAccess realm { get; set; } @@ -1309,11 +1315,22 @@ namespace osu.Game.Screens.Edit if (RuntimeInfo.IsDesktop) { - var externalEdit = new EditorMenuItem("Edit externally", MenuItemType.Standard, editExternally); + var externalEdit = new EditorMenuItem(EditorStrings.EditExternally, MenuItemType.Standard, editExternally); saveRelatedMenuItems.Add(externalEdit); yield return externalEdit; } + bool isSetMadeOfLegacyRulesetBeatmaps = (isNewBeatmap && Ruleset.Value.IsLegacyRuleset()) + || (!isNewBeatmap && Beatmap.Value.BeatmapSetInfo.Beatmaps.All(b => b.Ruleset.IsLegacyRuleset())); + bool submissionAvailable = api.EndpointConfiguration.BeatmapSubmissionServiceUrl != null; + + if (isSetMadeOfLegacyRulesetBeatmaps && submissionAvailable) + { + var upload = new EditorMenuItem(EditorStrings.SubmitBeatmap, MenuItemType.Standard, submitBeatmap); + saveRelatedMenuItems.Add(upload); + yield return upload; + } + yield return new OsuMenuItemSpacer(); yield return new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, this.Exit); } @@ -1353,6 +1370,42 @@ namespace osu.Game.Screens.Edit } } + private void submitBeatmap() + { + if (api.State.Value != APIState.Online) + { + loginOverlay?.Show(); + return; + } + + if (!editorBeatmap.HitObjects.Any()) + { + notifications?.Post(new SimpleNotification + { + Text = BeatmapSubmissionStrings.EmptyBeatmapsCannotBeSubmitted, + }); + return; + } + + if (HasUnsavedChanges) + { + dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptMutationOperation(() => + { + if (!Save()) + return false; + + startSubmission(); + return true; + }))); + } + else + { + startSubmission(); + } + + void startSubmission() => this.Push(new BeatmapSubmissionScreen()); + } + private void exportBeatmap(bool legacy) { if (HasUnsavedChanges) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs new file mode 100644 index 0000000000..796d975e4f --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -0,0 +1,422 @@ +// 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.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Development; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Configuration; +using osu.Game.Database; +using osu.Game.Extensions; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.IO.Archives; +using osu.Game.Localisation; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; +using osu.Game.Screens.Menu; +using osuTK; + +namespace osu.Game.Screens.Edit.Submission +{ + public partial class BeatmapSubmissionScreen : OsuScreen + { + private BeatmapSubmissionOverlay overlay = null!; + + public override bool AllowUserExit => false; + + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + [Resolved] + private RealmAccess realmAccess { get; set; } = null!; + + [Resolved] + private Storage storage { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private OsuConfigManager configManager { get; set; } = null!; + + [Resolved] + private OsuGame? game { get; set; } + + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; + + [Cached] + private BeatmapSubmissionSettings settings { get; } = new BeatmapSubmissionSettings(); + + private Container submissionProgress = null!; + private SubmissionStageProgress exportStep = null!; + private SubmissionStageProgress createSetStep = null!; + private SubmissionStageProgress uploadStep = null!; + private SubmissionStageProgress updateStep = null!; + private Container successContainer = null!; + private Container flashLayer = null!; + private RoundedButton backButton = null!; + + private uint? beatmapSetId; + + private SubmissionBeatmapExporter legacyBeatmapExporter = null!; + private ProgressNotification? exportProgressNotification; + private MemoryStream beatmapPackageStream = null!; + private ProgressNotification? updateProgressNotification; + + [BackgroundDependencyLoader] + private void load() + { + AddRangeInternal(new Drawable[] + { + overlay = new BeatmapSubmissionOverlay(), + submissionProgress = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.6f, + Masking = true, + CornerRadius = 10, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding(20), + Spacing = new Vector2(5), + Children = new Drawable[] + { + createSetStep = new SubmissionStageProgress + { + StageDescription = BeatmapSubmissionStrings.PreparingBeatmapSet, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + exportStep = new SubmissionStageProgress + { + StageDescription = BeatmapSubmissionStrings.ExportingBeatmapSet, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + uploadStep = new SubmissionStageProgress + { + StageDescription = BeatmapSubmissionStrings.UploadingBeatmapSetContents, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + updateStep = new SubmissionStageProgress + { + StageDescription = BeatmapSubmissionStrings.UpdatingLocalBeatmap, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + successContainer = new Container + { + Padding = new MarginPadding(20), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + AutoSizeDuration = 500, + AutoSizeEasing = Easing.OutQuint, + Masking = true, + CornerRadius = BeatmapCard.CORNER_RADIUS, + Child = flashLayer = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = BeatmapCard.CORNER_RADIUS, + Depth = float.MinValue, + Alpha = 0, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + } + } + }, + backButton = new RoundedButton + { + Text = CommonStrings.Back, + Width = 150, + Action = this.Exit, + Enabled = { Value = false }, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + } + } + } + } + } + }); + + overlay.State.BindValueChanged(_ => + { + if (overlay.State.Value == Visibility.Hidden) + { + if (!overlay.Completed) + this.Exit(); + else + { + submissionProgress.FadeIn(200, Easing.OutQuint); + createBeatmapSet(); + } + } + }); + beatmapPackageStream = new MemoryStream(); + } + + private void createBeatmapSet() + { + bool beatmapHasOnlineId = Beatmap.Value.BeatmapSetInfo.OnlineID > 0; + + var createRequest = beatmapHasOnlineId + ? PutBeatmapSetRequest.UpdateExisting( + (uint)Beatmap.Value.BeatmapSetInfo.OnlineID, + Beatmap.Value.BeatmapSetInfo.Beatmaps.Where(b => b.OnlineID > 0).Select(b => (uint)b.OnlineID).ToArray(), + (uint)Beatmap.Value.BeatmapSetInfo.Beatmaps.Count(b => b.OnlineID <= 0), + settings.Target.Value) + : PutBeatmapSetRequest.CreateNew((uint)Beatmap.Value.BeatmapSetInfo.Beatmaps.Count, settings.Target.Value); + + createRequest.Success += async response => + { + createSetStep.SetCompleted(); + beatmapSetId = response.BeatmapSetId; + + // at this point the set has an assigned online ID. + // it's important to proactively store it to the realm database, + // so that in the event in further failures in the process, the online ID is not lost. + // losing it can incur creation of redundant new sets server-side, or even cause online ID confusion. + if (!beatmapHasOnlineId) + { + await realmAccess.WriteAsync(r => + { + var refetchedSet = r.Find(Beatmap.Value.BeatmapSetInfo.ID); + refetchedSet!.OnlineID = (int)beatmapSetId.Value; + }).ConfigureAwait(true); + } + + legacyBeatmapExporter = new SubmissionBeatmapExporter(storage, response); + await createBeatmapPackage(response.Files).ConfigureAwait(true); + }; + createRequest.Failure += ex => + { + createSetStep.SetFailed(ex.Message); + backButton.Enabled.Value = true; + Logger.Log($"Beatmap set submission failed on creation: {ex}"); + }; + + createSetStep.SetInProgress(); + api.Queue(createRequest); + } + + private async Task createBeatmapPackage(ICollection onlineFiles) + { + Debug.Assert(ThreadSafety.IsUpdateThread); + exportStep.SetInProgress(); + + try + { + await legacyBeatmapExporter.ExportToStreamAsync(Beatmap.Value.BeatmapSetInfo.ToLive(realmAccess), beatmapPackageStream, exportProgressNotification = new ProgressNotification()) + .ConfigureAwait(true); + } + catch (Exception ex) + { + exportStep.SetFailed(ex.Message); + Logger.Log($"Beatmap set submission failed on export: {ex}"); + backButton.Enabled.Value = true; + exportProgressNotification = null; + } + + exportStep.SetCompleted(); + exportProgressNotification = null; + + if (onlineFiles.Count > 0) + await patchBeatmapSet(onlineFiles).ConfigureAwait(true); + else + replaceBeatmapSet(); + } + + private async Task patchBeatmapSet(ICollection onlineFiles) + { + Debug.Assert(beatmapSetId != null); + + var onlineFilesByFilename = onlineFiles.ToDictionary(f => f.Filename, f => f.SHA2Hash); + + // disposing the `ArchiveReader` makes the underlying stream no longer readable which we don't want. + // make a local copy to defend against it. + using var archiveReader = new ZipArchiveReader(new MemoryStream(beatmapPackageStream.ToArray())); + var filesToUpdate = new HashSet(); + + foreach (string filename in archiveReader.Filenames) + { + string localHash = archiveReader.GetStream(filename).ComputeSHA2Hash(); + + if (!onlineFilesByFilename.Remove(filename, out string? onlineHash)) + { + filesToUpdate.Add(filename); + continue; + } + + if (localHash != onlineHash) + filesToUpdate.Add(filename); + } + + var changedFiles = new Dictionary(); + + foreach (string file in filesToUpdate) + changedFiles.Add(file, await archiveReader.GetStream(file).ReadAllBytesToArrayAsync().ConfigureAwait(true)); + + var patchRequest = new PatchBeatmapPackageRequest(beatmapSetId.Value); + patchRequest.FilesChanged.AddRange(changedFiles); + patchRequest.FilesDeleted.AddRange(onlineFilesByFilename.Keys); + patchRequest.Success += async () => + { + uploadStep.SetCompleted(); + + if (configManager.Get(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission)) + game?.OpenUrlExternally($"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapSetId}"); + + await updateLocalBeatmap().ConfigureAwait(true); + }; + patchRequest.Failure += ex => + { + uploadStep.SetFailed(ex.Message); + Logger.Log($"Beatmap submission failed on upload: {ex}"); + backButton.Enabled.Value = true; + }; + patchRequest.Progressed += (current, total) => uploadStep.SetInProgress((float)current / total); + + api.Queue(patchRequest); + uploadStep.SetInProgress(); + } + + private void replaceBeatmapSet() + { + Debug.Assert(beatmapSetId != null); + + var uploadRequest = new ReplaceBeatmapPackageRequest(beatmapSetId.Value, beatmapPackageStream.ToArray()); + + uploadRequest.Success += async () => + { + uploadStep.SetCompleted(); + + if (configManager.Get(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission)) + game?.OpenUrlExternally($"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapSetId}"); + + await updateLocalBeatmap().ConfigureAwait(true); + }; + uploadRequest.Failure += ex => + { + uploadStep.SetFailed(ex.Message); + Logger.Log($"Beatmap submission failed on upload: {ex}"); + backButton.Enabled.Value = true; + }; + uploadRequest.Progressed += (current, total) => uploadStep.SetInProgress((float)current / Math.Max(total, 1)); + + api.Queue(uploadRequest); + uploadStep.SetInProgress(); + } + + private async Task updateLocalBeatmap() + { + Debug.Assert(beatmapSetId != null); + updateStep.SetInProgress(); + + Live? importedSet; + + try + { + importedSet = await beatmaps.ImportAsUpdate( + updateProgressNotification = new ProgressNotification(), + new ImportTask(beatmapPackageStream, $"{beatmapSetId}.osz"), + Beatmap.Value.BeatmapSetInfo).ConfigureAwait(true); + } + catch (Exception ex) + { + updateStep.SetFailed(ex.Message); + Logger.Log($"Beatmap submission failed on local update: {ex}"); + Schedule(() => backButton.Enabled.Value = true); + return; + } + + updateStep.SetCompleted(); + backButton.Enabled.Value = true; + backButton.Action = () => + { + game?.PerformFromScreen(s => + { + if (s is OsuScreen osuScreen) + { + Debug.Assert(importedSet != null); + var targetBeatmap = importedSet.Value.Beatmaps.FirstOrDefault(b => b.DifficultyName == Beatmap.Value.BeatmapInfo.DifficultyName) + ?? importedSet.Value.Beatmaps.First(); + osuScreen.Beatmap.Value = beatmaps.GetWorkingBeatmap(targetBeatmap); + } + + s.Push(new EditorLoader()); + }, [typeof(MainMenu)]); + }; + showBeatmapCard(); + } + + private void showBeatmapCard() + { + Debug.Assert(beatmapSetId != null); + + var getBeatmapSetRequest = new GetBeatmapSetRequest((int)beatmapSetId.Value); + getBeatmapSetRequest.Success += beatmapSet => + { + LoadComponentAsync(new BeatmapCardExtra(beatmapSet, false), loaded => + { + successContainer.Add(loaded); + flashLayer.FadeOutFromOne(2000, Easing.OutQuint); + }); + }; + + api.Queue(getBeatmapSetRequest); + } + + protected override void Update() + { + base.Update(); + + if (exportProgressNotification != null && exportProgressNotification.Ongoing) + exportStep.SetInProgress(exportProgressNotification.Progress); + + if (updateProgressNotification != null && updateProgressNotification.Ongoing) + updateStep.SetInProgress(updateProgressNotification.Progress); + } + + public override void OnEntering(ScreenTransitionEvent e) + { + base.OnEntering(e); + + overlay.Show(); + } + } +} diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs new file mode 100644 index 0000000000..359dc11f39 --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Game.Online.API.Requests; + +namespace osu.Game.Screens.Edit.Submission +{ + public class BeatmapSubmissionSettings + { + public Bindable Target { get; } = new Bindable(); + } +} diff --git a/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs b/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs index 72da94afa1..08b4d9f712 100644 --- a/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs +++ b/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs @@ -11,6 +11,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; +using osu.Game.Online.API.Requests; using osu.Game.Overlays; using osuTK; @@ -22,8 +23,10 @@ namespace osu.Game.Screens.Edit.Submission private readonly BindableBool notifyOnDiscussionReplies = new BindableBool(); private readonly BindableBool loadInBrowserAfterSubmission = new BindableBool(); + public override LocalisableString? NextStepText => BeatmapSubmissionStrings.ConfirmSubmission; + [BackgroundDependencyLoader] - private void load(OsuConfigManager configManager, OsuColour colours) + private void load(OsuConfigManager configManager, OsuColour colours, BeatmapSubmissionSettings settings) { configManager.BindWith(OsuSetting.EditorSubmissionNotifyOnDiscussionReplies, notifyOnDiscussionReplies); configManager.BindWith(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission, loadInBrowserAfterSubmission); @@ -39,6 +42,7 @@ namespace osu.Game.Screens.Edit.Submission { RelativeSizeAxes = Axes.X, Caption = BeatmapSubmissionStrings.BeatmapSubmissionTargetCaption, + Current = settings.Target, }, new FormCheckBox { @@ -60,14 +64,5 @@ namespace osu.Game.Screens.Edit.Submission } }); } - - private enum BeatmapSubmissionTarget - { - [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetWIP))] - WIP, - - [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetPending))] - Pending, - } } } From 206b5c93c0a8eb43c89d8fb8bc909f2e3aea9ab7 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 07:15:53 -0500 Subject: [PATCH 340/801] Implement beatmap set header design --- .../TestSceneBeatmapCarouselSetPanel.cs | 90 ++++++ .../TestSceneUpdateBeatmapSetButtonV2.cs | 62 ++++ .../Drawables/DifficultySpectrumDisplay.cs | 69 ++-- osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 297 +++++++++++++++--- .../SelectV2/BeatmapSetPanelBackground.cs | 108 +++++++ osu.Game/Screens/SelectV2/TopLocalRankV2.cs | 108 +++++++ .../SelectV2/UpdateBeatmapSetButtonV2.cs | 198 ++++++++++++ 7 files changed, 860 insertions(+), 72 deletions(-) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetPanel.cs create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs create mode 100644 osu.Game/Screens/SelectV2/TopLocalRankV2.cs create mode 100644 osu.Game/Screens/SelectV2/UpdateBeatmapSetButtonV2.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetPanel.cs new file mode 100644 index 0000000000..6b981d7b33 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetPanel.cs @@ -0,0 +1,90 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Overlays; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Resources; +using osu.Game.Tests.Visual.UserInterface; +using osuTK; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneBeatmapCarouselSetPanel : ThemeComparisonTestScene + { + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; + + private BeatmapSetInfo beatmapSet = null!; + + public TestSceneBeatmapCarouselSetPanel() + : base(false) + { + } + + [Test] + public void TestDisplay() + { + AddStep("set beatmap", () => + { + beatmapSet = beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => b.OnlineID == 241526) + ?? beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => !b.Protected) + ?? TestResources.CreateTestBeatmapSetInfo(); + CreateThemedContent(OverlayColourScheme.Aquamarine); + }); + } + + [Test] + public void TestRandomBeatmap() + { + AddStep("random beatmap", () => + { + beatmapSet = beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next()).First(); + CreateThemedContent(OverlayColourScheme.Aquamarine); + }); + } + + protected override Drawable CreateContent() + { + return new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 5f), + Children = new Drawable[] + { + new BeatmapSetPanel + { + Item = new CarouselItem(beatmapSet) + }, + new BeatmapSetPanel + { + Item = new CarouselItem(beatmapSet), + KeyboardSelected = { Value = true } + }, + new BeatmapSetPanel + { + Item = new CarouselItem(beatmapSet), + Expanded = { Value = true } + }, + new BeatmapSetPanel + { + Item = new CarouselItem(beatmapSet), + KeyboardSelected = { Value = true }, + Expanded = { Value = true } + }, + } + }; + } + } +} diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs new file mode 100644 index 0000000000..6e5d731453 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs @@ -0,0 +1,62 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Screens.SelectV2; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneUpdateBeatmapSetButtonV2 : OsuTestScene + { + private UpdateBeatmapSetButtonV2 button = null!; + + [SetUp] + public void SetUp() => Schedule(() => + { + Child = button = new UpdateBeatmapSetButtonV2 + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + }); + + [Test] + public void TestNullBeatmap() + { + AddStep("null beatmap", () => button.BeatmapSet = null); + AddAssert("button invisible", () => button.Alpha == 0f); + } + + [Test] + public void TestUpdatedBeatmap() + { + AddStep("updated beatmap", () => button.BeatmapSet = new BeatmapSetInfo + { + Beatmaps = { new BeatmapInfo() } + }); + AddAssert("button invisible", () => button.Alpha == 0f); + } + + [Test] + public void TestNonUpdatedBeatmap() + { + AddStep("non-updated beatmap", () => button.BeatmapSet = new BeatmapSetInfo + { + Beatmaps = + { + new BeatmapInfo + { + MD5Hash = "test", + OnlineMD5Hash = "online", + LastOnlineUpdate = DateTimeOffset.Now, + } + } + }); + + AddAssert("button visible", () => button.Alpha == 1f); + } + } +} diff --git a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs index 2fb3a8eee4..56f6c77ba8 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs @@ -28,7 +28,7 @@ namespace osu.Game.Beatmaps.Drawables dotSize = value; if (IsLoaded) - updateDotDimensions(); + updateDisplay(); } } @@ -42,13 +42,27 @@ namespace osu.Game.Beatmaps.Drawables dotSpacing = value; if (IsLoaded) - updateDotDimensions(); + updateDisplay(); + } + } + + private IBeatmapSetInfo? beatmapSet; + + public IBeatmapSetInfo? BeatmapSet + { + get => beatmapSet; + set + { + beatmapSet = value; + + if (IsLoaded) + updateDisplay(); } } private readonly FillFlowContainer flow; - public DifficultySpectrumDisplay(IBeatmapSetInfo beatmapSet) + public DifficultySpectrumDisplay(IBeatmapSetInfo? beatmapSet = null) { AutoSizeAxes = Axes.Both; @@ -59,25 +73,31 @@ namespace osu.Game.Beatmaps.Drawables Direction = FillDirection.Horizontal, }; - // matching web: https://github.com/ppy/osu-web/blob/d06d8c5e735eb1f48799b1654b528e9a7afb0a35/resources/assets/lib/beatmapset-panel.tsx#L127 - bool collapsed = beatmapSet.Beatmaps.Count() > 12; - - foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset).OrderBy(group => group.Key)) - flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key.OnlineID, rulesetGrouping, collapsed)); + BeatmapSet = beatmapSet; } protected override void LoadComplete() { base.LoadComplete(); - updateDotDimensions(); + updateDisplay(); } - private void updateDotDimensions() + private void updateDisplay() { - foreach (var group in flow) + flow.Clear(); + + if (beatmapSet == null) + return; + + // matching web: https://github.com/ppy/osu-web/blob/d06d8c5e735eb1f48799b1654b528e9a7afb0a35/resources/assets/lib/beatmapset-panel.tsx#L127 + bool collapsed = beatmapSet.Beatmaps.Count() > 12; + + foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset).OrderBy(group => group.Key)) { - group.DotSize = DotSize; - group.DotSpacing = DotSpacing; + flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key.OnlineID, rulesetGrouping, collapsed, dotSize) + { + Spacing = new Vector2(DotSpacing, 0f), + }); } } @@ -86,26 +106,14 @@ namespace osu.Game.Beatmaps.Drawables private readonly int rulesetId; private readonly IEnumerable beatmapInfos; private readonly bool collapsed; + private readonly Vector2 dotSize; - public RulesetDifficultyGroup(int rulesetId, IEnumerable beatmapInfos, bool collapsed) + public RulesetDifficultyGroup(int rulesetId, IEnumerable beatmapInfos, bool collapsed, Vector2 dotSize) { this.rulesetId = rulesetId; this.beatmapInfos = beatmapInfos; this.collapsed = collapsed; - } - - public Vector2 DotSize - { - set - { - foreach (var dot in Children.OfType()) - dot.Size = value; - } - } - - public float DotSpacing - { - set => Spacing = new Vector2(value, 0); + this.dotSize = dotSize; } [BackgroundDependencyLoader] @@ -125,7 +133,7 @@ namespace osu.Game.Beatmaps.Drawables if (!collapsed) { foreach (var beatmapInfo in beatmapInfos.OrderBy(bi => bi.StarRating)) - Add(new DifficultyDot(beatmapInfo.StarRating)); + Add(new DifficultyDot(beatmapInfo.StarRating, dotSize)); } else { @@ -145,9 +153,10 @@ namespace osu.Game.Beatmaps.Drawables { private readonly double starDifficulty; - public DifficultyDot(double starDifficulty) + public DifficultyDot(double starDifficulty, Vector2 dotSize) { this.starDifficulty = starDifficulty; + Size = dotSize; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index 85d5cc097d..4706ea487a 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -3,15 +3,24 @@ using System; using System.Diagnostics; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; using osuTK; using osuTK.Graphics; @@ -19,63 +28,182 @@ namespace osu.Game.Screens.SelectV2 { public partial class BeatmapSetPanel : PoolableDrawable, ICarouselPanel { - public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 2; + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; + + private const float arrow_container_width = 20; + private const float corner_radius = 10; + + private const float glow_offset = 10f; // extra space for the edge effect to not be cutoff by the right edge of the carousel. + private const float set_x_offset = 20f; // constant X offset for beatmap set panels specifically. + private const float preselected_x_offset = 25f; + private const float expanded_x_offset = 50f; + + private const float duration = 500; [Resolved] - private BeatmapCarousel carousel { get; set; } = null!; + private BeatmapCarousel? carousel { get; set; } - private OsuSpriteText text = null!; - private Box box = null!; + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) - { - var inputRectangle = DrawRectangle; + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; - // Cover a gap introduced by the spacing between a BeatmapSetPanel and a BeatmapPanel either below/above it. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); + [Resolved] + private OsuColour colours { get; set; } = null!; - return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); - } + private Container panel = null!; + private Box backgroundBorder = null!; + private BeatmapSetPanelBackground background = null!; + private Container backgroundContainer = null!; + private FillFlowContainer mainFlowContainer = null!; + private SpriteIcon chevronIcon = null!; + private Box hoverLayer = null!; + + private OsuSpriteText titleText = null!; + private OsuSpriteText artistText = null!; + private UpdateBeatmapSetButtonV2 updateButton = null!; + private BeatmapSetOnlineStatusPill statusPill = null!; + private DifficultySpectrumDisplay difficultiesDisplay = null!; [BackgroundDependencyLoader] private void load() { - Size = new Vector2(500, HEIGHT); - Masking = true; + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + RelativeSizeAxes = Axes.X; + Height = HEIGHT; - InternalChildren = new Drawable[] + InternalChild = panel = new Container { - box = new Box + Masking = true, + CornerRadius = corner_radius, + RelativeSizeAxes = Axes.Both, + EdgeEffect = new EdgeEffectParameters { - Colour = Color4.Yellow.Darken(5), - Alpha = 0.8f, - RelativeSizeAxes = Axes.Both, + Type = EdgeEffectType.Shadow, + Radius = 10, }, - text = new OsuSpriteText + Children = new Drawable[] { - Padding = new MarginPadding(5), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, + new BufferedContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + backgroundBorder = new Box + { + RelativeSizeAxes = Axes.Y, + Alpha = 0, + EdgeSmoothness = new Vector2(2, 0), + }, + backgroundContainer = new Container + { + Masking = true, + CornerRadius = corner_radius, + RelativeSizeAxes = Axes.X, + MaskingSmoothness = 2, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Children = new Drawable[] + { + background = new BeatmapSetPanelBackground + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + }, + }, + }, + } + }, + chevronIcon = new SpriteIcon + { + X = arrow_container_width / 2, + Origin = Anchor.Centre, + Anchor = Anchor.CentreLeft, + Icon = FontAwesome.Solid.ChevronRight, + Size = new Vector2(12), + Colour = colourProvider.Background5, + }, + mainFlowContainer = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Top = 7.5f, Left = 15, Bottom = 5 }, + Children = new Drawable[] + { + titleText = new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), + Shadow = true, + }, + artistText = new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), + Shadow = true, + }, + new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Top = 5f }, + Children = new Drawable[] + { + updateButton = new UpdateBeatmapSetButtonV2 + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Right = 5f, Top = -2f }, + }, + statusPill = new BeatmapSetOnlineStatusPill + { + AutoSizeAxes = Axes.Both, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + TextSize = 11, + TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, + Margin = new MarginPadding { Right = 5f }, + }, + difficultiesDisplay = new DifficultySpectrumDisplay + { + DotSize = new Vector2(5, 10), + DotSpacing = 2, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + }, + } + } + }, + hoverLayer = new Box + { + Colour = colours.Blue.Opacity(0.1f), + Alpha = 0, + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + }, + new HoverSounds(), } }; + } - Expanded.BindValueChanged(value => - { - box.FadeColour(value.NewValue ? Color4.Yellow.Darken(2) : Color4.Yellow.Darken(5), 500, Easing.OutQuint); - }); + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + var inputRectangle = panel.DrawRectangle; - KeyboardSelected.BindValueChanged(value => - { - if (value.NewValue) - { - BorderThickness = 5; - BorderColour = Color4.Pink; - } - else - { - BorderThickness = 0; - } - }); + // Cover a gap introduced by the spacing between a BeatmapSetPanel and a BeatmapPanel either above it or below it. + inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); + + return inputRectangle.Contains(panel.ToLocalSpace(screenSpacePos)); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Expanded.BindValueChanged(_ => updateExpandedDisplay(), true); + KeyboardSelected.BindValueChanged(_ => updateKeyboardSelectedDisplay(), true); } protected override void PrepareForUse() @@ -84,16 +212,101 @@ namespace osu.Game.Screens.SelectV2 Debug.Assert(Item != null); - var beatmapSetInfo = (BeatmapSetInfo)Item.Model; + var beatmapSet = (BeatmapSetInfo)Item.Model; - text.Text = $"{beatmapSetInfo.Metadata}"; + // Choice of background image matches BSS implementation (always uses the lowest `beatmap_id` from the set). + background.Beatmap = beatmaps.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID)); - this.FadeInFromZero(500, Easing.OutQuint); + titleText.Text = new RomanisableString(beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title); + artistText.Text = new RomanisableString(beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist); + updateButton.BeatmapSet = beatmapSet; + statusPill.Status = beatmapSet.Status; + difficultiesDisplay.BeatmapSet = beatmapSet; + + updateExpandedDisplay(); + FinishTransforms(true); + + this.FadeInFromZero(duration, Easing.OutQuint); + } + + protected override void FreeAfterUse() + { + base.FreeAfterUse(); + + background.Beatmap = null; + updateButton.BeatmapSet = null; + difficultiesDisplay.BeatmapSet = null; + } + + private void updateExpandedDisplay() + { + if (Item == null) + return; + + updatePanelPosition(); + + backgroundBorder.RelativeSizeAxes = Expanded.Value ? Axes.Both : Axes.Y; + backgroundBorder.Width = Expanded.Value ? 1 : arrow_container_width + corner_radius; + backgroundBorder.FadeTo(Expanded.Value ? 1 : 0, duration, Easing.OutQuint); + chevronIcon.FadeTo(Expanded.Value ? 1 : 0, duration, Easing.OutQuint); + + backgroundContainer.ResizeHeightTo(Expanded.Value ? HEIGHT - 4 : HEIGHT, duration, Easing.OutQuint); + backgroundContainer.MoveToX(Expanded.Value ? arrow_container_width : 0, duration, Easing.OutQuint); + mainFlowContainer.MoveToX(Expanded.Value ? arrow_container_width : 0, duration, Easing.OutQuint); + + panel.EdgeEffect = panel.EdgeEffect with { Radius = Expanded.Value ? 15 : 10 }; + + panel.FadeEdgeEffectTo(Expanded.Value + ? Color4Extensions.FromHex(@"4EBFFF").Opacity(0.5f) + : Color4.Black.Opacity(0.4f), duration, Easing.OutQuint); + } + + private void updateKeyboardSelectedDisplay() + { + updatePanelPosition(); + updateHover(); + } + + private void updatePanelPosition() + { + float x = glow_offset + set_x_offset + expanded_x_offset + preselected_x_offset; + + if (Expanded.Value) + x -= expanded_x_offset; + + if (KeyboardSelected.Value) + x -= preselected_x_offset; + + this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); + } + + private void updateHover() + { + bool hovered = IsHovered || KeyboardSelected.Value; + + if (hovered) + hoverLayer.FadeIn(100, Easing.OutQuint); + else + hoverLayer.FadeOut(1000, Easing.OutQuint); + } + + protected override bool OnHover(HoverEvent e) + { + updateHover(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateHover(); + base.OnHoverLost(e); } protected override bool OnClick(ClickEvent e) { - carousel.CurrentSelection = Item!.Model; + if (carousel != null) + carousel.CurrentSelection = Item!.Model; + return true; } diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs new file mode 100644 index 0000000000..435a0ad262 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs @@ -0,0 +1,108 @@ +// 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.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Beatmaps; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapSetPanelBackground : ModelBackedDrawable + { + protected override bool TransformImmediately => true; + + public WorkingBeatmap? Beatmap + { + get => Model; + set => Model = value; + } + + protected override Drawable CreateDrawable(WorkingBeatmap? model) => new BackgroundSprite(model); + + private partial class BackgroundSprite : CompositeDrawable + { + private readonly WorkingBeatmap? working; + + public BackgroundSprite(WorkingBeatmap? working) + { + this.working = working; + + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + var texture = working?.GetPanelBackground(); + + if (texture != null) + { + InternalChildren = new Drawable[] + { + new Sprite + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fill, + Texture = texture, + }, + new FillFlowContainer + { + Depth = -1, + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + // This makes the gradient not be perfectly horizontal, but diagonal at a ~40° angle + Shear = new Vector2(0.8f, 0), + Alpha = 0.5f, + Children = new[] + { + // The left half with no gradient applied + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Width = 0.4f, + }, + // Piecewise-linear gradient with 3 segments to make it appear smoother + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(Color4.Black, new Color4(0f, 0f, 0f, 0.9f)), + Width = 0.05f, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.9f), new Color4(0f, 0f, 0f, 0.1f)), + Width = 0.2f, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.1f), new Color4(0, 0, 0, 0)), + Width = 0.05f, + }, + } + }, + }; + } + else + { + InternalChild = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6, + }; + } + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/TopLocalRankV2.cs b/osu.Game/Screens/SelectV2/TopLocalRankV2.cs new file mode 100644 index 0000000000..241e92a67d --- /dev/null +++ b/osu.Game/Screens/SelectV2/TopLocalRankV2.cs @@ -0,0 +1,108 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Models; +using osu.Game.Online.API; +using osu.Game.Online.Leaderboards; +using osu.Game.Rulesets; +using osu.Game.Scoring; +using osuTK; +using Realms; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class TopLocalRankV2 : CompositeDrawable + { + private BeatmapInfo? beatmap; + + public BeatmapInfo? Beatmap + { + get => beatmap; + set + { + beatmap = value; + + if (IsLoaded) + updateSubscription(); + } + } + + [Resolved] + private IBindable ruleset { get; set; } = null!; + + [Resolved] + private RealmAccess realm { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private IDisposable? scoreSubscription; + + private readonly UpdateableRank updateable; + + public ScoreRank? DisplayedRank => updateable.Rank; + + public TopLocalRankV2(BeatmapInfo? beatmap = null) + { + AutoSizeAxes = Axes.Both; + + InternalChild = updateable = new UpdateableRank + { + Size = new Vector2(40, 20), + Alpha = 0, + }; + + Beatmap = beatmap; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ruleset.BindValueChanged(_ => updateSubscription(), true); + } + + private void updateSubscription() + { + scoreSubscription?.Dispose(); + + if (beatmap == null) + return; + + scoreSubscription = realm.RegisterForNotifications(r => + r.All() + .Filter($"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0" + + $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1" + + $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}" + + $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $2" + + $" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, beatmap.ID, ruleset.Value.ShortName), + localScoresChanged); + } + + private void localScoresChanged(IRealmCollection sender, ChangeSet? changes) + { + // This subscription may fire from changes to linked beatmaps, which we don't care about. + // It's currently not possible for a score to be modified after insertion, so we can safely ignore callbacks with only modifications. + if (changes?.HasCollectionChanges() == false) + return; + + ScoreInfo? topScore = sender.MaxBy(info => (info.TotalScore, -info.Date.UtcDateTime.Ticks)); + updateable.Rank = topScore?.Rank; + updateable.Alpha = topScore != null ? 1 : 0; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + scoreSubscription?.Dispose(); + } + } +} diff --git a/osu.Game/Screens/SelectV2/UpdateBeatmapSetButtonV2.cs b/osu.Game/Screens/SelectV2/UpdateBeatmapSetButtonV2.cs new file mode 100644 index 0000000000..2d1ce4ba48 --- /dev/null +++ b/osu.Game/Screens/SelectV2/UpdateBeatmapSetButtonV2.cs @@ -0,0 +1,198 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Overlays; +using osu.Game.Screens.Select.Carousel; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class UpdateBeatmapSetButtonV2 : OsuAnimatedButton + { + private BeatmapSetInfo? beatmapSet; + + public BeatmapSetInfo? BeatmapSet + { + get => beatmapSet; + set + { + beatmapSet = value; + + if (IsLoaded) + beatmapChanged(); + } + } + + private SpriteIcon icon = null!; + private Box progressFill = null!; + + [Resolved] + private BeatmapModelDownloader beatmapDownloader { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private LoginOverlay? loginOverlay { get; set; } + + [Resolved] + private IDialogOverlay? dialogOverlay { get; set; } + + public UpdateBeatmapSetButtonV2() + { + Size = new Vector2(75f, 22f); + } + + private Bindable preferNoVideo = null!; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + const float icon_size = 14; + + preferNoVideo = config.GetBindable(OsuSetting.PreferNoVideo); + + Content.Anchor = Anchor.Centre; + Content.Origin = Anchor.Centre; + Content.Shear = new Vector2(OsuGame.SHEAR, 0); + + Content.AddRange(new Drawable[] + { + progressFill = new Box + { + Colour = Color4.White, + Alpha = 0.2f, + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + Width = 0, + }, + new FillFlowContainer + { + Padding = new MarginPadding { Horizontal = 5, Vertical = 3 }, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4), + Shear = new Vector2(-OsuGame.SHEAR, 0), + Children = new Drawable[] + { + new Container + { + Size = new Vector2(icon_size), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Children = new Drawable[] + { + icon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.SyncAlt, + Size = new Vector2(icon_size), + }, + } + }, + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.Default.With(weight: FontWeight.Bold), + Text = "Update", + } + } + }, + }); + + Action = performUpdate; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + beatmapChanged(); + } + + private void beatmapChanged() + { + Alpha = beatmapSet?.AllBeatmapsUpToDate == false ? 1 : 0; + icon.Spin(4000, RotationDirection.Clockwise); + } + + protected override bool OnHover(HoverEvent e) + { + icon.Spin(400, RotationDirection.Clockwise); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + icon.Spin(4000, RotationDirection.Clockwise); + base.OnHoverLost(e); + } + + private bool updateConfirmed; + + private void performUpdate() + { + Debug.Assert(beatmapSet != null); + + if (!api.IsLoggedIn) + { + loginOverlay?.Show(); + return; + } + + if (dialogOverlay != null && beatmapSet.Status == BeatmapOnlineStatus.LocallyModified && !updateConfirmed) + { + dialogOverlay.Push(new UpdateLocalConfirmationDialog(() => + { + updateConfirmed = true; + performUpdate(); + })); + + return; + } + + updateConfirmed = false; + + beatmapDownloader.DownloadAsUpdate(beatmapSet, preferNoVideo.Value); + attachExistingDownload(); + } + + private void attachExistingDownload() + { + Debug.Assert(beatmapSet != null); + var download = beatmapDownloader.GetExistingDownload(beatmapSet); + + if (download != null) + { + Enabled.Value = false; + TooltipText = string.Empty; + + download.DownloadProgressed += progress => progressFill.ResizeWidthTo(progress, 100, Easing.OutQuint); + download.Failure += _ => attachExistingDownload(); + } + else + { + Enabled.Value = true; + TooltipText = "Update beatmap with online changes"; + + progressFill.ResizeWidthTo(0, 100, Easing.OutQuint); + } + } + } +} From 04d8bafdcee3c5b0a6a33e0046ced17f611da53f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 07:16:10 -0500 Subject: [PATCH 341/801] Implement beatmap difficulty panel design --- ...TestSceneBeatmapCarouselDifficultyPanel.cs | 101 +++++ osu.Game/Screens/SelectV2/BeatmapPanel.cs | 410 ++++++++++++++++-- 2 files changed, 463 insertions(+), 48 deletions(-) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyPanel.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyPanel.cs new file mode 100644 index 0000000000..c0ecb06085 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyPanel.cs @@ -0,0 +1,101 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Overlays; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Resources; +using osu.Game.Tests.Visual.UserInterface; +using osuTK; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneBeatmapCarouselDifficultyPanel : ThemeComparisonTestScene + { + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; + + private BeatmapInfo beatmap = null!; + + public TestSceneBeatmapCarouselDifficultyPanel() + : base(false) + { + } + + [Test] + public void TestDisplay() + { + AddStep("set beatmap", () => + { + var beatmapSet = beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => b.OnlineID == 241526) + ?? beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => !b.Protected) + ?? TestResources.CreateTestBeatmapSetInfo(); + + beatmap = beatmapSet.Beatmaps.First(); + CreateThemedContent(OverlayColourScheme.Aquamarine); + }); + } + + [Test] + public void TestRandomBeatmap() + { + AddStep("random beatmap", () => + { + beatmap = beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next()) + .First().Beatmaps.OrderBy(_ => RNG.Next()).First(); + CreateThemedContent(OverlayColourScheme.Aquamarine); + }); + } + + [Test] + public void TestManiaRuleset() + { + AddToggleStep("mania ruleset", v => Ruleset.Value = v ? new ManiaRuleset().RulesetInfo : new OsuRuleset().RulesetInfo); + } + + protected override Drawable CreateContent() + { + return new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 5f), + Children = new Drawable[] + { + new BeatmapPanel + { + Item = new CarouselItem(beatmap) + }, + new BeatmapPanel + { + Item = new CarouselItem(beatmap), + KeyboardSelected = { Value = true } + }, + new BeatmapPanel + { + Item = new CarouselItem(beatmap), + Selected = { Value = true } + }, + new BeatmapPanel + { + Item = new CarouselItem(beatmap), + KeyboardSelected = { Value = true }, + Selected = { Value = true } + }, + } + }; + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs index 2fe509402b..180acffe80 100644 --- a/osu.Game/Screens/SelectV2/BeatmapPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapPanel.cs @@ -1,16 +1,29 @@ // 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.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osuTK; using osuTK.Graphics; @@ -18,68 +31,234 @@ namespace osu.Game.Screens.SelectV2 { public partial class BeatmapPanel : PoolableDrawable, ICarouselPanel { - [Resolved] - private BeatmapCarousel carousel { get; set; } = null!; + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; + private const float colour_box_width = 30; + private const float corner_radius = 10; + + private const float glow_offset = 10f; // extra space for the edge effect to not be cutoff by the right edge of the carousel. + private const float difficulty_x_offset = 50f; // constant X offset for beatmap difficulty panels specifically. + private const float preselected_x_offset = 25f; + private const float selected_x_offset = 50f; + + private const float duration = 500; + + [Resolved] + private BeatmapCarousel? carousel { get; set; } + + [Resolved] + private IBindable ruleset { get; set; } = null!; + + [Resolved] + private IBindable> mods { get; set; } = null!; + + private Container panel = null!; + private StarCounter starCounter = null!; + private ConstrainedIconContainer iconContainer = null!; + private Box hoverLayer = null!; private Box activationFlash = null!; - private OsuSpriteText text = null!; + + private Box backgroundBorder = null!; + + private StarRatingDisplay starRatingDisplay = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private BeatmapDifficultyCache difficultyCache { get; set; } = null!; + + private OsuSpriteText keyCountText = null!; + + private IBindable? starDifficultyBindable; + private CancellationTokenSource? starDifficultyCancellationSource; + + private Container rightContainer = null!; + private Box starRatingGradient = null!; + private TopLocalRankV2 difficultyRank = null!; + private OsuSpriteText difficultyText = null!; + private OsuSpriteText authorText = null!; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + + RelativeSizeAxes = Axes.X; + Width = 0.9f; + Height = HEIGHT; + + InternalChild = panel = new Container + { + Masking = true, + CornerRadius = corner_radius, + RelativeSizeAxes = Axes.Both, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Offset = new Vector2(1f), + Radius = 10, + }, + Children = new Drawable[] + { + new BufferedContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + backgroundBorder = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.ForStarDifficulty(0), + EdgeSmoothness = new Vector2(2, 0), + }, + rightContainer = new Container + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Masking = true, + CornerRadius = corner_radius, + RelativeSizeAxes = Axes.X, + Height = HEIGHT, + X = colour_box_width, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(colourProvider.Background3, colourProvider.Background4), + }, + starRatingGradient = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + }, + }, + }, + } + }, + iconContainer = new ConstrainedIconContainer + { + X = colour_box_width / 2, + Origin = Anchor.Centre, + Anchor = Anchor.CentreLeft, + Size = new Vector2(20), + Colour = colourProvider.Background5, + }, + new FillFlowContainer + { + Padding = new MarginPadding { Top = 8, Left = colour_box_width + corner_radius }, + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new FillFlowContainer + { + Direction = FillDirection.Horizontal, + Spacing = new Vector2(3, 0), + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + difficultyRank = new TopLocalRankV2 + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Scale = new Vector2(0.75f) + }, + starCounter = new StarCounter + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Scale = new Vector2(0.4f) + } + } + }, + new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Children = new[] + { + keyCountText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Alpha = 0, + }, + difficultyText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding { Right = 8f }, + }, + authorText = new OsuSpriteText + { + Colour = colourProvider.Content2, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft + } + } + } + } + }, + hoverLayer = new Box + { + Colour = colours.Blue.Opacity(0.1f), + Alpha = 0, + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + }, + activationFlash = new Box + { + Blending = BlendingParameters.Additive, + Alpha = 0f, + RelativeSizeAxes = Axes.Both, + }, + new HoverSounds(), + } + }; + } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { - var inputRectangle = DrawRectangle; + var inputRectangle = panel.DrawRectangle; // Cover the gaps introduced by the spacing between BeatmapPanels. inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); + return inputRectangle.Contains(panel.ToLocalSpace(screenSpacePos)); } - [BackgroundDependencyLoader] - private void load() + protected override void LoadComplete() { - Size = new Vector2(500, CarouselItem.DEFAULT_HEIGHT); - Masking = true; + base.LoadComplete(); - InternalChildren = new Drawable[] + ruleset.BindValueChanged(_ => { - new Box - { - Colour = Color4.Aqua.Darken(5), - Alpha = 0.8f, - RelativeSizeAxes = Axes.Both, - }, - activationFlash = new Box - { - Colour = Color4.White, - Blending = BlendingParameters.Additive, - Alpha = 0, - RelativeSizeAxes = Axes.Both, - }, - text = new OsuSpriteText - { - Padding = new MarginPadding(5), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - } - }; - - Selected.BindValueChanged(value => - { - activationFlash.FadeTo(value.NewValue ? 0.2f : 0, 500, Easing.OutQuint); + computeStarRating(); + updateKeyCount(); }); - KeyboardSelected.BindValueChanged(value => + mods.BindValueChanged(_ => { - if (value.NewValue) - { - BorderThickness = 5; - BorderColour = Color4.Pink; - } - else - { - BorderThickness = 0; - } - }); + computeStarRating(); + updateKeyCount(); + }, true); + + Selected.BindValueChanged(_ => updateSelectionDisplay(), true); + KeyboardSelected.BindValueChanged(_ => updateKeyboardSelectedDisplay(), true); } protected override void PrepareForUse() @@ -89,13 +268,145 @@ namespace osu.Game.Screens.SelectV2 Debug.Assert(Item != null); var beatmap = (BeatmapInfo)Item.Model; - text.Text = $"Difficulty: {beatmap.DifficultyName} ({beatmap.StarRating:N1}*)"; + iconContainer.Icon = beatmap.Ruleset.CreateInstance().CreateIcon(); - this.FadeInFromZero(500, Easing.OutQuint); + difficultyRank.Beatmap = beatmap; + difficultyText.Text = beatmap.DifficultyName; + authorText.Text = BeatmapsetsStrings.ShowDetailsMappedBy(beatmap.Metadata.Author.Username); + + starDifficultyBindable = null; + + computeStarRating(); + updateKeyCount(); + + updateSelectionDisplay(); + FinishTransforms(true); + + this.FadeInFromZero(duration, Easing.OutQuint); + + // todo: only do this when visible. + // starCounter.ReplayAnimation(); + } + + private void updateSelectionDisplay() + { + bool selected = Selected.Value; + + rightContainer.ResizeHeightTo(selected ? HEIGHT - 4 : HEIGHT, duration, Easing.OutQuint); + + updatePanelPosition(); + updateEdgeEffectColour(); + updateHover(); + } + + private void updateKeyboardSelectedDisplay() + { + updatePanelPosition(); + updateHover(); + } + + private void updatePanelPosition() + { + float x = glow_offset + difficulty_x_offset + selected_x_offset + preselected_x_offset; + + if (Selected.Value) + x -= selected_x_offset; + + if (KeyboardSelected.Value) + x -= preselected_x_offset; + + this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); + } + + private void updateHover() + { + bool hovered = IsHovered || (KeyboardSelected.Value && !Selected.Value); + + if (hovered) + hoverLayer.FadeIn(100, Easing.OutQuint); + else + hoverLayer.FadeOut(1000, Easing.OutQuint); + } + + private void computeStarRating() + { + starDifficultyCancellationSource?.Cancel(); + starDifficultyCancellationSource = new CancellationTokenSource(); + + if (Item == null) + return; + + var beatmap = (BeatmapInfo)Item.Model; + + starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token); + starDifficultyBindable.BindValueChanged(d => + { + var value = d.NewValue ?? default; + + starRatingDisplay.Current.Value = value; + starCounter.Current = (float)value.Stars; + + iconContainer.FadeColour(value.Stars > 6.5f ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint); + + var starRatingColour = colours.ForStarDifficulty(value.Stars); + + backgroundBorder.FadeColour(starRatingColour, duration, Easing.OutQuint); + starCounter.FadeColour(starRatingColour, duration, Easing.OutQuint); + starRatingGradient.FadeColour(ColourInfo.GradientHorizontal(starRatingColour.Opacity(0.25f), starRatingColour.Opacity(0)), duration, Easing.OutQuint); + starRatingGradient.FadeIn(duration, Easing.OutQuint); + + // todo: this doesn't work for dark star rating colours, still not sure how to fix. + activationFlash.FadeColour(starRatingColour, duration, Easing.OutQuint); + + updateEdgeEffectColour(); + }, true); + } + + private void updateEdgeEffectColour() + { + panel.FadeEdgeEffectTo(Selected.Value + ? colours.ForStarDifficulty(starDifficultyBindable?.Value?.Stars ?? 0f).Opacity(0.5f) + : Color4.Black.Opacity(0.4f), duration, Easing.OutQuint); + } + + private void updateKeyCount() + { + if (Item == null) + return; + + var beatmap = (BeatmapInfo)Item.Model; + + if (ruleset.Value.OnlineID == 3) + { + // Account for mania differences locally for now. + // Eventually this should be handled in a more modular way, allowing rulesets to add more information to the panel. + ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.Value.CreateInstance(); + int keyCount = legacyRuleset.GetKeyCount(beatmap, mods.Value); + + keyCountText.Alpha = 1; + keyCountText.Text = $"[{keyCount}K] "; + } + else + keyCountText.Alpha = 0; + } + + protected override bool OnHover(HoverEvent e) + { + updateHover(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateHover(); + base.OnHoverLost(e); } protected override bool OnClick(ClickEvent e) { + if (carousel == null) + return true; + if (carousel.CurrentSelection != Item!.Model) { carousel.CurrentSelection = Item!.Model; @@ -115,7 +426,10 @@ namespace osu.Game.Screens.SelectV2 public double DrawYPosition { get; set; } - public void Activated() => activationFlash.FadeOutFromOne(500, Easing.OutQuint); + public void Activated() + { + activationFlash.FadeOutFromOne(500, Easing.OutQuint); + } #endregion } From 696366f8cb13c2dd1ee6f3b02c8c2d7f806d9126 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 07:17:13 -0500 Subject: [PATCH 342/801] Implement beatmap "standalone" panel design --- ...TestSceneBeatmapCarouselStandalonePanel.cs | 101 ++++ .../SelectV2/BeatmapStandalonePanel.cs | 460 ++++++++++++++++++ 2 files changed, 561 insertions(+) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselStandalonePanel.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselStandalonePanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselStandalonePanel.cs new file mode 100644 index 0000000000..76dcfc9507 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselStandalonePanel.cs @@ -0,0 +1,101 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Overlays; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Resources; +using osu.Game.Tests.Visual.UserInterface; +using osuTK; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneBeatmapCarouselStandalonePanel : ThemeComparisonTestScene + { + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; + + private BeatmapInfo beatmap = null!; + + public TestSceneBeatmapCarouselStandalonePanel() + : base(false) + { + } + + [Test] + public void TestDisplay() + { + AddStep("set beatmap", () => + { + var beatmapSet = beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => b.OnlineID == 241526) + ?? beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => !b.Protected) + ?? TestResources.CreateTestBeatmapSetInfo(); + + beatmap = beatmapSet.Beatmaps.First(); + CreateThemedContent(OverlayColourScheme.Aquamarine); + }); + } + + [Test] + public void TestRandomBeatmap() + { + AddStep("random beatmap", () => + { + beatmap = beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next()) + .First().Beatmaps.OrderBy(_ => RNG.Next()).First(); + CreateThemedContent(OverlayColourScheme.Aquamarine); + }); + } + + [Test] + public void TestManiaRuleset() + { + AddToggleStep("mania ruleset", v => Ruleset.Value = v ? new ManiaRuleset().RulesetInfo : new OsuRuleset().RulesetInfo); + } + + protected override Drawable CreateContent() + { + return new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 5f), + Children = new Drawable[] + { + new BeatmapStandalonePanel + { + Item = new CarouselItem(beatmap) + }, + new BeatmapStandalonePanel + { + Item = new CarouselItem(beatmap), + KeyboardSelected = { Value = true } + }, + new BeatmapStandalonePanel + { + Item = new CarouselItem(beatmap), + Selected = { Value = true } + }, + new BeatmapStandalonePanel + { + Item = new CarouselItem(beatmap), + KeyboardSelected = { Value = true }, + Selected = { Value = true } + }, + } + }; + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs b/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs new file mode 100644 index 0000000000..11fa22ab09 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs @@ -0,0 +1,460 @@ +// 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.Diagnostics; +using System.Linq; +using System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapStandalonePanel : PoolableDrawable, ICarouselPanel + { + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; + + private const float difficulty_icon_container_width = 30; + private const float corner_radius = 10; + + private const float glow_offset = 10f; // extra space for the edge effect to not be cutoff by the right edge of the carousel. + private const float preselected_x_offset = 25f; + private const float selected_x_offset = 50f; + + private const float duration = 500; + + [Resolved] + private BeatmapCarousel? carousel { get; set; } + + [Resolved] + private IBindable ruleset { get; set; } = null!; + + [Resolved] + private IBindable> mods { get; set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private BeatmapDifficultyCache difficultyCache { get; set; } = null!; + + private IBindable? starDifficultyBindable; + private CancellationTokenSource? starDifficultyCancellationSource; + + private Container panel = null!; + private Box backgroundBorder = null!; + private BeatmapSetPanelBackground background = null!; + private Container backgroundContainer = null!; + private FillFlowContainer mainFlowContainer = null!; + private Box hoverLayer = null!; + + private OsuSpriteText titleText = null!; + private OsuSpriteText artistText = null!; + private UpdateBeatmapSetButtonV2 updateButton = null!; + private BeatmapSetOnlineStatusPill statusPill = null!; + + private ConstrainedIconContainer difficultyIcon = null!; + private FillFlowContainer difficultyLine = null!; + private StarRatingDisplay difficultyStarRating = null!; + private TopLocalRankV2 difficultyRank = null!; + private OsuSpriteText difficultyKeyCountText = null!; + private OsuSpriteText difficultyName = null!; + private OsuSpriteText difficultyAuthor = null!; + + [BackgroundDependencyLoader] + private void load() + { + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + RelativeSizeAxes = Axes.X; + Width = 1f; + Height = HEIGHT; + + InternalChild = panel = new Container + { + Masking = true, + CornerRadius = corner_radius, + RelativeSizeAxes = Axes.Both, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Radius = 10, + }, + Children = new Drawable[] + { + new BufferedContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + backgroundBorder = new Box + { + RelativeSizeAxes = Axes.Y, + Alpha = 0, + EdgeSmoothness = new Vector2(2, 0), + }, + backgroundContainer = new Container + { + Masking = true, + CornerRadius = corner_radius, + RelativeSizeAxes = Axes.X, + MaskingSmoothness = 2, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Children = new Drawable[] + { + background = new BeatmapSetPanelBackground + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + }, + }, + }, + } + }, + difficultyIcon = new ConstrainedIconContainer + { + X = difficulty_icon_container_width / 2, + Origin = Anchor.Centre, + Anchor = Anchor.CentreLeft, + Size = new Vector2(20), + }, + mainFlowContainer = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Top = 7.5f, Left = 15, Bottom = 5 }, + Children = new Drawable[] + { + titleText = new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), + Shadow = true, + }, + artistText = new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), + Shadow = true, + }, + new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Top = 5f }, + Children = new Drawable[] + { + updateButton = new UpdateBeatmapSetButtonV2 + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Right = 5f, Top = -2f }, + }, + statusPill = new BeatmapSetOnlineStatusPill + { + AutoSizeAxes = Axes.Both, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + TextSize = 11, + TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, + Margin = new MarginPadding { Right = 5f }, + }, + difficultyLine = new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + difficultyStarRating = new StarRatingDisplay(default, StarRatingDisplaySize.Small) + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Scale = new Vector2(8f / 9f), + Margin = new MarginPadding { Right = 5f }, + }, + difficultyRank = new TopLocalRankV2 + { + Scale = new Vector2(8f / 11), + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Margin = new MarginPadding { Right = 5f }, + }, + difficultyKeyCountText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Alpha = 0, + Margin = new MarginPadding { Bottom = 2f }, + }, + difficultyName = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + Margin = new MarginPadding { Right = 5f, Bottom = 2f }, + }, + difficultyAuthor = new OsuSpriteText + { + Colour = colourProvider.Content2, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + Margin = new MarginPadding { Right = 5f, Bottom = 2f }, + } + } + }, + }, + } + } + }, + hoverLayer = new Box + { + Colour = colours.Blue.Opacity(0.1f), + Alpha = 0, + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + }, + new HoverSounds(), + } + }; + } + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + var inputRectangle = panel.DrawRectangle; + + // Cover a gap introduced by the spacing between a BeatmapSetPanel and a BeatmapPanel either above it or below it. + inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); + + return inputRectangle.Contains(panel.ToLocalSpace(screenSpacePos)); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ruleset.BindValueChanged(_ => + { + computeStarRating(); + updateKeyCount(); + }); + + mods.BindValueChanged(_ => + { + computeStarRating(); + updateKeyCount(); + }, true); + + Selected.BindValueChanged(_ => updateSelectedDisplay(), true); + KeyboardSelected.BindValueChanged(_ => updateKeyboardSelectedDisplay(), true); + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + Debug.Assert(Item != null); + + var beatmap = (BeatmapInfo)Item.Model; + var beatmapSet = beatmap.BeatmapSet!; + + // Choice of background image matches BSS implementation (always uses the lowest `beatmap_id` from the set). + background.Beatmap = beatmaps.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID)); + + titleText.Text = new RomanisableString(beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title); + artistText.Text = new RomanisableString(beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist); + updateButton.BeatmapSet = beatmapSet; + statusPill.Status = beatmapSet.Status; + + difficultyIcon.Icon = beatmap.Ruleset.CreateInstance().CreateIcon(); + difficultyIcon.Show(); + + difficultyRank.Beatmap = beatmap; + difficultyName.Text = beatmap.DifficultyName; + difficultyAuthor.Text = BeatmapsetsStrings.ShowDetailsMappedBy(beatmap.Metadata.Author.Username); + difficultyLine.Show(); + + computeStarRating(); + + updateSelectedDisplay(); + FinishTransforms(true); + + this.FadeInFromZero(duration, Easing.OutQuint); + } + + protected override void FreeAfterUse() + { + base.FreeAfterUse(); + + background.Beatmap = null; + updateButton.BeatmapSet = null; + difficultyRank.Beatmap = null; + starDifficultyBindable = null; + } + + private void updateSelectedDisplay() + { + if (Item == null) + return; + + updatePanelPosition(); + + backgroundBorder.RelativeSizeAxes = Selected.Value ? Axes.Both : Axes.Y; + backgroundBorder.Width = Selected.Value ? 1 : difficulty_icon_container_width + corner_radius; + backgroundBorder.FadeTo(Selected.Value ? 1 : 0, duration, Easing.OutQuint); + difficultyIcon.FadeTo(Selected.Value ? 1 : 0, duration, Easing.OutQuint); + + backgroundContainer.ResizeHeightTo(Selected.Value ? HEIGHT - 4 : HEIGHT, duration, Easing.OutQuint); + backgroundContainer.MoveToX(Selected.Value ? difficulty_icon_container_width : 0, duration, Easing.OutQuint); + mainFlowContainer.MoveToX(Selected.Value ? difficulty_icon_container_width : 0, duration, Easing.OutQuint); + + panel.EdgeEffect = panel.EdgeEffect with { Radius = Selected.Value ? 15 : 10 }; + updateEdgeEffectColour(); + } + + private void updateKeyboardSelectedDisplay() + { + updatePanelPosition(); + updateHover(); + } + + private void updatePanelPosition() + { + float x = glow_offset + selected_x_offset + preselected_x_offset; + + if (Selected.Value) + x -= selected_x_offset; + + if (KeyboardSelected.Value) + x -= preselected_x_offset; + + this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); + } + + private void updateHover() + { + bool hovered = IsHovered || KeyboardSelected.Value; + + if (hovered) + hoverLayer.FadeIn(100, Easing.OutQuint); + else + hoverLayer.FadeOut(1000, Easing.OutQuint); + } + + private void computeStarRating() + { + starDifficultyCancellationSource?.Cancel(); + starDifficultyCancellationSource = new CancellationTokenSource(); + + if (Item == null) + return; + + var beatmap = (BeatmapInfo)Item.Model; + + starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token); + starDifficultyBindable.BindValueChanged(d => + { + var value = d.NewValue ?? default; + + backgroundBorder.FadeColour(colours.ForStarDifficulty(value.Stars), duration, Easing.OutQuint); + difficultyIcon.FadeColour(value.Stars > 6.5f ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint); + difficultyStarRating.Current.Value = value; + + updateEdgeEffectColour(); + }, true); + } + + private void updateEdgeEffectColour() + { + panel.FadeEdgeEffectTo(Selected.Value + ? colours.ForStarDifficulty(starDifficultyBindable?.Value?.Stars ?? 0f).Opacity(0.5f) + : Color4.Black.Opacity(0.4f), duration, Easing.OutQuint); + } + + private void updateKeyCount() + { + if (Item == null) + return; + + var beatmap = (BeatmapInfo)Item.Model; + + if (ruleset.Value.OnlineID == 3) + { + // Account for mania differences locally for now. + // Eventually this should be handled in a more modular way, allowing rulesets to add more information to the panel. + ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.Value.CreateInstance(); + int keyCount = legacyRuleset.GetKeyCount(beatmap, mods.Value); + + difficultyKeyCountText.Alpha = 1; + difficultyKeyCountText.Text = $"[{keyCount}K] "; + } + else + difficultyKeyCountText.Alpha = 0; + } + + protected override bool OnHover(HoverEvent e) + { + updateHover(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateHover(); + base.OnHoverLost(e); + } + + protected override bool OnClick(ClickEvent e) + { + if (carousel != null) + carousel.CurrentSelection = Item!.Model; + + return true; + } + + #region ICarouselPanel + + public CarouselItem? Item { get; set; } + public BindableBool Selected { get; } = new BindableBool(); + public BindableBool Expanded { get; } = new BindableBool(); + public BindableBool KeyboardSelected { get; } = new BindableBool(); + + public double DrawYPosition { get; set; } + + public void Activated() + { + // sets should never be activated. + throw new InvalidOperationException(); + } + + #endregion + } +} From c94d11b7fe08b7d2284615049dd0c0f9de14c5d7 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 07:19:12 -0500 Subject: [PATCH 343/801] Add beatmap carousel to new song select screen --- osu.Game/Screens/SelectV2/SongSelectV2.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game/Screens/SelectV2/SongSelectV2.cs b/osu.Game/Screens/SelectV2/SongSelectV2.cs index 2f9667793f..88825d96e0 100644 --- a/osu.Game/Screens/SelectV2/SongSelectV2.cs +++ b/osu.Game/Screens/SelectV2/SongSelectV2.cs @@ -39,6 +39,20 @@ namespace osu.Game.Screens.SelectV2 { AddRangeInternal(new Drawable[] { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Bottom = ScreenFooter.HEIGHT }, + Child = new BeatmapCarousel + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Both, + Width = 0.5f, + // Push the carousel slightly off the right edge of the screen for the ends of the panels to be cut off. + X = 20f, + }, + }, modSelectOverlay, }); } From abce42b1c8fa48dc9fc64ff5e756b90f66d947a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 15:28:27 +0100 Subject: [PATCH 344/801] Improve bookmark controls - Bookmark menu items get disabled when they would do nothing. - Bookmark deletion only deletes the closest bookmark instead of all of them within the proximity of 2 seconds to current clock time. Action is only however *enabled* within 2 seconds of a bookmark. Additionally, logic was moved out of `Editor` because it's a huge class and I dislike huge classes if they can be at all avoided. --- osu.Game/Screens/Edit/BookmarkController.cs | 148 ++++++++++++++++++++ osu.Game/Screens/Edit/Editor.cs | 66 +-------- 2 files changed, 152 insertions(+), 62 deletions(-) create mode 100644 osu.Game/Screens/Edit/BookmarkController.cs diff --git a/osu.Game/Screens/Edit/BookmarkController.cs b/osu.Game/Screens/Edit/BookmarkController.cs new file mode 100644 index 0000000000..8c048ba871 --- /dev/null +++ b/osu.Game/Screens/Edit/BookmarkController.cs @@ -0,0 +1,148 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; +using osu.Game.Localisation; +using osu.Game.Overlays; +using osu.Game.Screens.Edit.Components.Menus; + +namespace osu.Game.Screens.Edit +{ + public class BookmarkController : Component, IKeyBindingHandler + { + public EditorMenuItem Menu { get; private set; } + + [Resolved] + private EditorClock clock { get; set; } = null!; + + [Resolved] + private EditorBeatmap editorBeatmap { get; set; } = null!; + + [Resolved] + private IDialogOverlay? dialogOverlay { get; set; } + + private readonly BindableList bookmarks = new BindableList(); + + private readonly EditorMenuItem removeBookmarkMenuItem; + private readonly EditorMenuItem seekToPreviousBookmarkMenuItem; + private readonly EditorMenuItem seekToNextBookmarkMenuItem; + private readonly EditorMenuItem resetBookmarkMenuItem; + + public BookmarkController() + { + Menu = new EditorMenuItem(EditorStrings.Bookmarks) + { + Items = new MenuItem[] + { + new EditorMenuItem(EditorStrings.AddBookmark, MenuItemType.Standard, addBookmarkAtCurrentTime) + { + Hotkey = new Hotkey(GlobalAction.EditorAddBookmark), + }, + removeBookmarkMenuItem = new EditorMenuItem(EditorStrings.RemoveClosestBookmark, MenuItemType.Destructive, removeClosestBookmark) + { + Hotkey = new Hotkey(GlobalAction.EditorRemoveClosestBookmark) + }, + seekToPreviousBookmarkMenuItem = new EditorMenuItem(EditorStrings.SeekToPreviousBookmark, MenuItemType.Standard, () => seekBookmark(-1)) + { + Hotkey = new Hotkey(GlobalAction.EditorSeekToPreviousBookmark) + }, + seekToNextBookmarkMenuItem = new EditorMenuItem(EditorStrings.SeekToNextBookmark, MenuItemType.Standard, () => seekBookmark(1)) + { + Hotkey = new Hotkey(GlobalAction.EditorSeekToNextBookmark) + }, + resetBookmarkMenuItem = new EditorMenuItem(EditorStrings.ResetBookmarks, MenuItemType.Destructive, () => dialogOverlay?.Push(new BookmarkResetDialog(editorBeatmap))) + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + bookmarks.BindTo(editorBeatmap.Bookmarks); + } + + protected override void Update() + { + base.Update(); + + bool hasAnyBookmark = bookmarks.Count > 0; + bool hasBookmarkCloseEnoughForDeletion = bookmarks.Any(b => Math.Abs(b - clock.CurrentTimeAccurate) < 2000); + + removeBookmarkMenuItem.Action.Disabled = !hasBookmarkCloseEnoughForDeletion; + seekToPreviousBookmarkMenuItem.Action.Disabled = !hasAnyBookmark; + seekToNextBookmarkMenuItem.Action.Disabled = !hasAnyBookmark; + resetBookmarkMenuItem.Action.Disabled = !hasAnyBookmark; + } + + private void addBookmarkAtCurrentTime() + { + int bookmark = (int)clock.CurrentTimeAccurate; + int idx = bookmarks.BinarySearch(bookmark); + if (idx < 0) + bookmarks.Insert(~idx, bookmark); + } + + private void removeClosestBookmark() + { + if (removeBookmarkMenuItem.Action.Disabled) + return; + + int closestBookmark = bookmarks.MinBy(b => Math.Abs(b - clock.CurrentTimeAccurate)); + bookmarks.Remove(closestBookmark); + } + + private void seekBookmark(int direction) + { + int? targetBookmark = direction < 1 + ? bookmarks.Cast().LastOrDefault(b => b < clock.CurrentTimeAccurate) + : bookmarks.Cast().FirstOrDefault(b => b > clock.CurrentTimeAccurate); + + if (targetBookmark != null) + clock.SeekSmoothlyTo(targetBookmark.Value); + } + + public bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) + { + case GlobalAction.EditorSeekToPreviousBookmark: + seekBookmark(-1); + return true; + + case GlobalAction.EditorSeekToNextBookmark: + seekBookmark(1); + return true; + } + + if (e.Repeat) + return false; + + switch (e.Action) + { + case GlobalAction.EditorAddBookmark: + addBookmarkAtCurrentTime(); + return true; + + case GlobalAction.EditorRemoveClosestBookmark: + removeClosestBookmark(); + return true; + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + } +} diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 3302fafbb8..a5dfda9c95 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -317,6 +317,9 @@ namespace osu.Game.Screens.Edit workingBeatmapUpdated = true; }); + var bookmarkController = new BookmarkController(); + AddInternal(bookmarkController); + OsuMenuItem undoMenuItem; OsuMenuItem redoMenuItem; @@ -442,29 +445,7 @@ namespace osu.Game.Screens.Edit Items = new MenuItem[] { new EditorMenuItem(EditorStrings.SetPreviewPointToCurrent, MenuItemType.Standard, SetPreviewPointToCurrentTime), - new EditorMenuItem(EditorStrings.Bookmarks) - { - Items = new MenuItem[] - { - new EditorMenuItem(EditorStrings.AddBookmark, MenuItemType.Standard, addBookmarkAtCurrentTime) - { - Hotkey = new Hotkey(GlobalAction.EditorAddBookmark), - }, - new EditorMenuItem(EditorStrings.RemoveClosestBookmark, MenuItemType.Destructive, removeBookmarksInProximityToCurrentTime) - { - Hotkey = new Hotkey(GlobalAction.EditorRemoveClosestBookmark) - }, - new EditorMenuItem(EditorStrings.SeekToPreviousBookmark, MenuItemType.Standard, () => seekBookmark(-1)) - { - Hotkey = new Hotkey(GlobalAction.EditorSeekToPreviousBookmark) - }, - new EditorMenuItem(EditorStrings.SeekToNextBookmark, MenuItemType.Standard, () => seekBookmark(1)) - { - Hotkey = new Hotkey(GlobalAction.EditorSeekToNextBookmark) - }, - new EditorMenuItem(EditorStrings.ResetBookmarks, MenuItemType.Destructive, () => dialogOverlay?.Push(new BookmarkResetDialog(editorBeatmap))) - } - } + bookmarkController.Menu, } } } @@ -800,14 +781,6 @@ namespace osu.Game.Screens.Edit case GlobalAction.EditorSeekToNextSamplePoint: seekSamplePoint(1); return true; - - case GlobalAction.EditorSeekToPreviousBookmark: - seekBookmark(-1); - return true; - - case GlobalAction.EditorSeekToNextBookmark: - seekBookmark(1); - return true; } if (e.Repeat) @@ -815,14 +788,6 @@ namespace osu.Game.Screens.Edit switch (e.Action) { - case GlobalAction.EditorAddBookmark: - addBookmarkAtCurrentTime(); - return true; - - case GlobalAction.EditorRemoveClosestBookmark: - removeBookmarksInProximityToCurrentTime(); - return true; - case GlobalAction.EditorCloneSelection: Clone(); return true; @@ -855,19 +820,6 @@ namespace osu.Game.Screens.Edit return false; } - private void addBookmarkAtCurrentTime() - { - int bookmark = (int)clock.CurrentTimeAccurate; - int idx = editorBeatmap.Bookmarks.BinarySearch(bookmark); - if (idx < 0) - editorBeatmap.Bookmarks.Insert(~idx, bookmark); - } - - private void removeBookmarksInProximityToCurrentTime() - { - editorBeatmap.Bookmarks.RemoveAll(b => Math.Abs(b - clock.CurrentTimeAccurate) < 2000); - } - public void OnReleased(KeyBindingReleaseEvent e) { } @@ -1202,16 +1154,6 @@ namespace osu.Game.Screens.Edit clock.SeekSmoothlyTo(found.StartTime); } - private void seekBookmark(int direction) - { - int? targetBookmark = direction < 1 - ? editorBeatmap.Bookmarks.Cast().LastOrDefault(b => b < clock.CurrentTimeAccurate) - : editorBeatmap.Bookmarks.Cast().FirstOrDefault(b => b > clock.CurrentTimeAccurate); - - if (targetBookmark != null) - clock.SeekSmoothlyTo(targetBookmark.Value); - } - private void seekSamplePoint(int direction) { double currentTime = clock.CurrentTimeAccurate; From 4cbfb5170790c301ecfa08214df9795e82b754e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 15:30:11 +0100 Subject: [PATCH 345/801] Fix undoing bookmark operations potentially making them unsorted Found in testing of previous commit. This would break seeking between bookmarks. Reproduction steps on `master`: - open map with bookmark - delete the first bookmark - undo the deletion of the first bookmark - seek to previous bookmark will now always seek to the first bookmark rather than closest preceding regardless of current clock time --- osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs index f3d58a3c3c..e84b6bfc72 100644 --- a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs +++ b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs @@ -115,7 +115,9 @@ namespace osu.Game.Screens.Edit if (editorBeatmap.Bookmarks.Contains(newBookmark)) continue; - editorBeatmap.Bookmarks.Add(newBookmark); + int idx = editorBeatmap.Bookmarks.BinarySearch(newBookmark); + if (idx < 0) + editorBeatmap.Bookmarks.Insert(~idx, newBookmark); } } From 10711e5e2721ab11b28c4fc5f00e6769d9aad3ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 15:39:36 +0100 Subject: [PATCH 346/801] Add missing `partial` --- osu.Game/Screens/Edit/BookmarkController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/BookmarkController.cs b/osu.Game/Screens/Edit/BookmarkController.cs index 8c048ba871..3d2cb4663f 100644 --- a/osu.Game/Screens/Edit/BookmarkController.cs +++ b/osu.Game/Screens/Edit/BookmarkController.cs @@ -17,7 +17,7 @@ using osu.Game.Screens.Edit.Components.Menus; namespace osu.Game.Screens.Edit { - public class BookmarkController : Component, IKeyBindingHandler + public partial class BookmarkController : Component, IKeyBindingHandler { public EditorMenuItem Menu { get; private set; } From 29882a2542bb9895a163461055ff7f57f961f022 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 07:19:14 -0500 Subject: [PATCH 347/801] Allow importing real beatmaps in song select test scene --- .../SongSelectV2/TestSceneSongSelect.cs | 62 ++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs index d43026c960..33474d7449 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs @@ -9,15 +9,27 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Database; +using osu.Game.Online.API; +using osu.Game.Overlays; using osu.Game.Overlays.Mods; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Taiko; using osu.Game.Screens; using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; using osu.Game.Screens.SelectV2.Footer; +using osu.Game.Tests.Resources; using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelectV2 @@ -30,6 +42,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Cached] private readonly OsuLogo logo; + private BeatmapManager beatmapManager = null!; + + protected override bool UseOnlineAPI => true; + public TestSceneSongSelect() { Children = new Drawable[] @@ -49,6 +65,35 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }; } + [BackgroundDependencyLoader] + private void load(GameHost host, IAPIProvider onlineAPI) + { + BeatmapStore beatmapStore; + BeatmapUpdater beatmapUpdater; + BeatmapDifficultyCache difficultyCache; + + // These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install. + // At a point we have isolated interactive test runs enough, this can likely be removed. + Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(Realm); + Dependencies.Cache(difficultyCache = new BeatmapDifficultyCache()); + Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, onlineAPI, Audio, Resources, host, Beatmap.Default, difficultyCache)); + Dependencies.CacheAs(beatmapUpdater = new BeatmapUpdater(beatmapManager, difficultyCache, onlineAPI, LocalStorage)); + Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore()); + + beatmapManager.ProcessBeatmap = (set, scope) => beatmapUpdater.Process(set, scope); + + MusicController music; + Dependencies.Cache(music = new MusicController()); + + // required to get bindables attached + Add(difficultyCache); + Add(music); + Add(beatmapStore); + + Dependencies.Cache(new OsuConfigManager(LocalStorage)); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -64,6 +109,16 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("load screen", () => Stack.Push(new Screens.SelectV2.SongSelectV2())); AddUntilStep("wait for load", () => Stack.CurrentScreen is Screens.SelectV2.SongSelectV2 songSelect && songSelect.IsLoaded); + AddStep("import test beatmap", () => beatmapManager.Import(TestResources.GetTestBeatmapForImport())); + } + + [Test] + public void TestRulesets() + { + AddStep("set osu ruleset", () => Ruleset.Value = new OsuRuleset().RulesetInfo); + AddStep("set taiko ruleset", () => Ruleset.Value = new TaikoRuleset().RulesetInfo); + AddStep("set catch ruleset", () => Ruleset.Value = new CatchRuleset().RulesetInfo); + AddStep("set mania ruleset", () => Ruleset.Value = new ManiaRuleset().RulesetInfo); } #region Footer @@ -80,8 +135,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("modified", () => SelectedMods.Value = new List { new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); AddStep("modified + one", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); AddStep("modified + two", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); - AddStep("modified + three", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModClassic(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); - AddStep("modified + four", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModClassic(), new OsuModDifficultyAdjust(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); + AddStep("modified + three", + () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModClassic(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); + AddStep("modified + four", + () => SelectedMods.Value = new List + { new OsuModHidden(), new OsuModHardRock(), new OsuModClassic(), new OsuModDifficultyAdjust(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); AddWaitStep("wait", 3); From f9962f95f098bf3e4076839544090ad7556c3fcd Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 08:16:51 -0500 Subject: [PATCH 348/801] Implement group panel design --- .../TestSceneBeatmapCarouselGroupPanel.cs | 80 +++++ osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 1 + osu.Game/Screens/SelectV2/GroupPanel.cs | 220 ++++++++++--- osu.Game/Screens/SelectV2/StarsGroupPanel.cs | 288 ++++++++++++++++++ 4 files changed, 541 insertions(+), 48 deletions(-) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselGroupPanel.cs create mode 100644 osu.Game/Screens/SelectV2/StarsGroupPanel.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselGroupPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselGroupPanel.cs new file mode 100644 index 0000000000..eea3870117 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselGroupPanel.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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Visual.UserInterface; +using osuTK; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneBeatmapCarouselGroupPanel : ThemeComparisonTestScene + { + public TestSceneBeatmapCarouselGroupPanel() + : base(false) + { + } + + protected override Drawable CreateContent() + { + return new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 5f), + Children = new Drawable[] + { + new GroupPanel + { + Item = new CarouselItem(new GroupDefinition("Group A")) + }, + new GroupPanel + { + Item = new CarouselItem(new GroupDefinition("Group A")), + KeyboardSelected = { Value = true } + }, + new GroupPanel + { + Item = new CarouselItem(new GroupDefinition("Group A")), + Selected = { Value = true } + }, + new GroupPanel + { + Item = new CarouselItem(new GroupDefinition("Group A")), + KeyboardSelected = { Value = true }, + Selected = { Value = true } + }, + new StarsGroupPanel + { + Item = new CarouselItem(new StarsGroupDefinition(1)) + }, + new StarsGroupPanel + { + Item = new CarouselItem(new StarsGroupDefinition(3)), + }, + new StarsGroupPanel + { + Item = new CarouselItem(new StarsGroupDefinition(5)), + }, + new StarsGroupPanel + { + Item = new CarouselItem(new StarsGroupDefinition(7)), + }, + new StarsGroupPanel + { + Item = new CarouselItem(new StarsGroupDefinition(8)), + }, + new StarsGroupPanel + { + Item = new CarouselItem(new StarsGroupDefinition(9)), + }, + } + }; + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 12660d8642..a49dcdd86c 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -264,4 +264,5 @@ namespace osu.Game.Screens.SelectV2 } public record GroupDefinition(string Title); + public record StarsGroupDefinition(int StarNumber); } diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs index df930a3111..8995b93290 100644 --- a/osu.Game/Screens/SelectV2/GroupPanel.cs +++ b/osu.Game/Screens/SelectV2/GroupPanel.cs @@ -7,10 +7,14 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; using osuTK; using osuTK.Graphics; @@ -18,15 +22,20 @@ namespace osu.Game.Screens.SelectV2 { public partial class GroupPanel : PoolableDrawable, ICarouselPanel { - public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 2; + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; + + private const float glow_offset = 10f; // extra space for the edge effect to not be cutoff by the right edge of the carousel. + private const float preselected_x_offset = 25f; + private const float selected_x_offset = 50f; + + private const float duration = 500; [Resolved] - private BeatmapCarousel carousel { get; set; } = null!; + private BeatmapCarousel? carousel { get; set; } private Box activationFlash = null!; - private OsuSpriteText text = null!; - - private Box box = null!; + private OsuSpriteText titleText = null!; + private Box hoverLayer = null!; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { @@ -39,56 +48,128 @@ namespace osu.Game.Screens.SelectV2 } [BackgroundDependencyLoader] - private void load() + private void load(OverlayColourProvider colourProvider, OsuColour colours) { - Size = new Vector2(500, HEIGHT); - Masking = true; + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + RelativeSizeAxes = Axes.X; + Height = HEIGHT; - InternalChildren = new Drawable[] + InternalChild = new Container { - box = new Box + RelativeSizeAxes = Axes.Both, + CornerRadius = 10f, + Masking = true, + Children = new Drawable[] { - Colour = Color4.DarkBlue.Darken(5), - Alpha = 0.8f, - RelativeSizeAxes = Axes.Both, - }, - activationFlash = new Box - { - Colour = Color4.White, - Blending = BlendingParameters.Additive, - Alpha = 0, - RelativeSizeAxes = Axes.Both, - }, - text = new OsuSpriteText - { - Padding = new MarginPadding(5), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 10f }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = 10f, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6, + }, + } + } + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background3, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 10f }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = 10f, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + titleText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + X = 10f, + }, + new CircularContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(50f, 14f), + Margin = new MarginPadding { Right = 30f }, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.7f), + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), + // TODO: requires Carousel/CarouselItem-side implementation + Text = "43", + UseFullGlyphHeight = false, + } + }, + }, + } + } + }, + activationFlash = new Box + { + Colour = Color4.White, + Blending = BlendingParameters.Additive, + Alpha = 0, + RelativeSizeAxes = Axes.Both, + }, + hoverLayer = new Box + { + Colour = colours.Blue.Opacity(0.1f), + Alpha = 0, + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + }, + new HoverSounds(), } }; + } - Selected.BindValueChanged(value => - { - activationFlash.FadeTo(value.NewValue ? 0.2f : 0, 500, Easing.OutQuint); - }); + protected override void LoadComplete() + { + base.LoadComplete(); - Expanded.BindValueChanged(value => - { - box.FadeColour(value.NewValue ? Color4.SkyBlue : Color4.DarkBlue.Darken(5), 500, Easing.OutQuint); - }); + Expanded.BindValueChanged(_ => updateExpandedDisplay(), true); + KeyboardSelected.BindValueChanged(_ => updateKeyboardSelectedDisplay(), true); + } - KeyboardSelected.BindValueChanged(value => - { - if (value.NewValue) - { - BorderThickness = 5; - BorderColour = Color4.Pink; - } - else - { - BorderThickness = 0; - } - }); + private void updateExpandedDisplay() + { + updatePanelPosition(); + + // todo: figma shares no extra visual feedback on this. + + activationFlash.FadeTo(0.2f).FadeTo(0f, 500, Easing.OutQuint); } protected override void PrepareForUse() @@ -99,17 +180,60 @@ namespace osu.Game.Screens.SelectV2 GroupDefinition group = (GroupDefinition)Item.Model; - text.Text = group.Title; + titleText.Text = group.Title; this.FadeInFromZero(500, Easing.OutQuint); } protected override bool OnClick(ClickEvent e) { - carousel.CurrentSelection = Item!.Model; + if (carousel != null) + carousel.CurrentSelection = Item!.Model; + return true; } + private void updateKeyboardSelectedDisplay() + { + updatePanelPosition(); + updateHover(); + } + + private void updatePanelPosition() + { + float x = glow_offset + selected_x_offset + preselected_x_offset; + + if (Expanded.Value) + x -= selected_x_offset; + + if (KeyboardSelected.Value) + x -= preselected_x_offset; + + this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); + } + + private void updateHover() + { + bool hovered = IsHovered || KeyboardSelected.Value; + + if (hovered) + hoverLayer.FadeIn(100, Easing.OutQuint); + else + hoverLayer.FadeOut(1000, Easing.OutQuint); + } + + protected override bool OnHover(HoverEvent e) + { + updateHover(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateHover(); + base.OnHoverLost(e); + } + #region ICarouselPanel public CarouselItem? Item { get; set; } diff --git a/osu.Game/Screens/SelectV2/StarsGroupPanel.cs b/osu.Game/Screens/SelectV2/StarsGroupPanel.cs new file mode 100644 index 0000000000..8ebf3fc7e8 --- /dev/null +++ b/osu.Game/Screens/SelectV2/StarsGroupPanel.cs @@ -0,0 +1,288 @@ +// 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.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class StarsGroupPanel : PoolableDrawable, ICarouselPanel + { + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; + + private const float glow_offset = 10f; // extra space for the edge effect to not be cutoff by the right edge of the carousel. + private const float preselected_x_offset = 25f; + private const float expanded_x_offset = 50f; + + private const float duration = 500; + + [Resolved] + private BeatmapCarousel? carousel { get; set; } + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + private Box activationFlash = null!; + private Box outerLayer = null!; + private Box innerLayer = null!; + private StarRatingDisplay starRatingDisplay = null!; + private StarCounter starCounter = null!; + private Box hoverLayer = null!; + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + var inputRectangle = DrawRectangle; + + // Cover a gap introduced by the spacing between a GroupPanel and a BeatmapPanel either below/above it. + inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); + + return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); + } + + [BackgroundDependencyLoader] + private void load() + { + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + RelativeSizeAxes = Axes.X; + Height = HEIGHT; + + InternalChild = new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = 10f, + Masking = true, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 10f }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = 10f, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6, + }, + } + } + }, + outerLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background3, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 10f }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = 10f, + Masking = true, + Children = new Drawable[] + { + innerLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.2f), + }, + new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(10f, 0f), + Margin = new MarginPadding { Left = 10f }, + Children = new Drawable[] + { + starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + starCounter = new StarCounter + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Scale = new Vector2(8f / 20f), + }, + } + }, + new CircularContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(50f, 14f), + Margin = new MarginPadding { Right = 30f }, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.7f), + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), + // TODO: requires Carousel/CarouselItem-side implementation + Text = "43", + UseFullGlyphHeight = false, + } + }, + }, + } + } + }, + activationFlash = new Box + { + Colour = Color4.White, + Blending = BlendingParameters.Additive, + Alpha = 0, + RelativeSizeAxes = Axes.Both, + }, + hoverLayer = new Box + { + Colour = colours.Blue.Opacity(0.1f), + Alpha = 0, + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + }, + new HoverSounds(), + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Expanded.BindValueChanged(_ => updateExpandedDisplay(), true); + KeyboardSelected.BindValueChanged(_ => updateKeyboardSelectedDisplay(), true); + } + + private void updateExpandedDisplay() + { + updatePanelPosition(); + + // todo: figma shares no extra visual feedback on this. + + activationFlash.FadeTo(0.2f).FadeTo(0f, 500, Easing.OutQuint); + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + Debug.Assert(Item != null); + + StarsGroupDefinition group = (StarsGroupDefinition)Item.Model; + + Color4 colour = group.StarNumber >= 9 ? OsuColour.Gray(0.2f) : colours.ForStarDifficulty(group.StarNumber); + Color4 contentColour = group.StarNumber >= 7 ? colours.Orange1 : colourProvider.Background5; + + outerLayer.Colour = colour; + starCounter.Colour = contentColour; + + starRatingDisplay.Current.Value = new StarDifficulty(group.StarNumber, 0); + starCounter.Current = group.StarNumber; + + this.FadeInFromZero(500, Easing.OutQuint); + } + + protected override bool OnClick(ClickEvent e) + { + if (carousel != null) + carousel.CurrentSelection = Item!.Model; + + return true; + } + + private void updateKeyboardSelectedDisplay() + { + updatePanelPosition(); + updateHover(); + } + + private void updatePanelPosition() + { + float x = glow_offset + expanded_x_offset + preselected_x_offset; + + if (Expanded.Value) + x -= expanded_x_offset; + + if (KeyboardSelected.Value) + x -= preselected_x_offset; + + this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); + } + + private void updateHover() + { + bool hovered = IsHovered || KeyboardSelected.Value; + + if (hovered) + hoverLayer.FadeIn(100, Easing.OutQuint); + else + hoverLayer.FadeOut(1000, Easing.OutQuint); + } + + protected override bool OnHover(HoverEvent e) + { + updateHover(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateHover(); + base.OnHoverLost(e); + } + + #region ICarouselPanel + + public CarouselItem? Item { get; set; } + public BindableBool Selected { get; } = new BindableBool(); + public BindableBool Expanded { get; } = new BindableBool(); + public BindableBool KeyboardSelected { get; } = new BindableBool(); + + public double DrawYPosition { get; set; } + + public void Activated() + { + // sets should never be activated. + throw new InvalidOperationException(); + } + + #endregion + } +} From 04a3ee863c3f1b2f93d4868da9aebb79d2ec2d32 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 08:38:08 -0500 Subject: [PATCH 349/801] Fix design tests --- ...TestSceneBeatmapCarouselDifficultyPanel.cs | 26 +++++++++++-------- .../TestSceneBeatmapCarouselSetPanel.cs | 21 +++++++++------ ...TestSceneBeatmapCarouselStandalonePanel.cs | 26 +++++++++++-------- 3 files changed, 43 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyPanel.cs index c0ecb06085..a9f73759f7 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyPanel.cs @@ -30,18 +30,20 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { } + [SetUp] + public void SetUp() => Schedule(() => + { + var beatmapSet = beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => b.OnlineID == 241526) + ?? beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => !b.Protected) + ?? TestResources.CreateTestBeatmapSetInfo(); + + beatmap = beatmapSet.Beatmaps.First(); + }); + [Test] public void TestDisplay() { - AddStep("set beatmap", () => - { - var beatmapSet = beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => b.OnlineID == 241526) - ?? beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => !b.Protected) - ?? TestResources.CreateTestBeatmapSetInfo(); - - beatmap = beatmapSet.Beatmaps.First(); - CreateThemedContent(OverlayColourScheme.Aquamarine); - }); + AddStep("display", () => CreateThemedContent(OverlayColourScheme.Aquamarine)); } [Test] @@ -49,8 +51,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { AddStep("random beatmap", () => { - beatmap = beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next()) - .First().Beatmaps.OrderBy(_ => RNG.Next()).First(); + var randomSet = beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next()).FirstOrDefault(); + randomSet ??= TestResources.CreateTestBeatmapSetInfo(); + beatmap = randomSet.Beatmaps.OrderBy(_ => RNG.Next()).First(); + CreateThemedContent(OverlayColourScheme.Aquamarine); }); } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetPanel.cs index 6b981d7b33..8f7cac2b58 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetPanel.cs @@ -28,16 +28,18 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { } + [SetUp] + public void SetUp() => Schedule(() => + { + beatmapSet = beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => b.OnlineID == 241526) + ?? beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => !b.Protected) + ?? TestResources.CreateTestBeatmapSetInfo(); + }); + [Test] public void TestDisplay() { - AddStep("set beatmap", () => - { - beatmapSet = beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => b.OnlineID == 241526) - ?? beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => !b.Protected) - ?? TestResources.CreateTestBeatmapSetInfo(); - CreateThemedContent(OverlayColourScheme.Aquamarine); - }); + AddStep("display", () => CreateThemedContent(OverlayColourScheme.Aquamarine)); } [Test] @@ -45,7 +47,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { AddStep("random beatmap", () => { - beatmapSet = beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next()).First(); + var randomSet = beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next()).FirstOrDefault(); + randomSet ??= TestResources.CreateTestBeatmapSetInfo(); + beatmapSet = randomSet; + CreateThemedContent(OverlayColourScheme.Aquamarine); }); } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselStandalonePanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselStandalonePanel.cs index 76dcfc9507..a34ac31d5d 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselStandalonePanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselStandalonePanel.cs @@ -30,18 +30,20 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { } + [SetUp] + public void SetUp() => Schedule(() => + { + var beatmapSet = beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => b.OnlineID == 241526) + ?? beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => !b.Protected) + ?? TestResources.CreateTestBeatmapSetInfo(); + + beatmap = beatmapSet.Beatmaps.First(); + }); + [Test] public void TestDisplay() { - AddStep("set beatmap", () => - { - var beatmapSet = beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => b.OnlineID == 241526) - ?? beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => !b.Protected) - ?? TestResources.CreateTestBeatmapSetInfo(); - - beatmap = beatmapSet.Beatmaps.First(); - CreateThemedContent(OverlayColourScheme.Aquamarine); - }); + AddStep("display", () => CreateThemedContent(OverlayColourScheme.Aquamarine)); } [Test] @@ -49,8 +51,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { AddStep("random beatmap", () => { - beatmap = beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next()) - .First().Beatmaps.OrderBy(_ => RNG.Next()).First(); + var randomSet = beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next()).FirstOrDefault(); + randomSet ??= TestResources.CreateTestBeatmapSetInfo(); + beatmap = randomSet.Beatmaps.OrderBy(_ => RNG.Next()).First(); + CreateThemedContent(OverlayColourScheme.Aquamarine); }); } From 467ea91105249569887ba2c12be021d6292a372e Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 21:47:15 -0500 Subject: [PATCH 350/801] Fix basic code quality issues --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 1 + osu.Game/Screens/SelectV2/StarsGroupPanel.cs | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index a49dcdd86c..4de0041d36 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -264,5 +264,6 @@ namespace osu.Game.Screens.SelectV2 } public record GroupDefinition(string Title); + public record StarsGroupDefinition(int StarNumber); } diff --git a/osu.Game/Screens/SelectV2/StarsGroupPanel.cs b/osu.Game/Screens/SelectV2/StarsGroupPanel.cs index 8ebf3fc7e8..76e3da2500 100644 --- a/osu.Game/Screens/SelectV2/StarsGroupPanel.cs +++ b/osu.Game/Screens/SelectV2/StarsGroupPanel.cs @@ -43,7 +43,6 @@ namespace osu.Game.Screens.SelectV2 private Box activationFlash = null!; private Box outerLayer = null!; - private Box innerLayer = null!; private StarRatingDisplay starRatingDisplay = null!; private StarCounter starCounter = null!; private Box hoverLayer = null!; @@ -108,7 +107,7 @@ namespace osu.Game.Screens.SelectV2 Masking = true, Children = new Drawable[] { - innerLayer = new Box + new Box { RelativeSizeAxes = Axes.Both, Colour = Color4.Black.Opacity(0.2f), From 72a62b70c469407af7a91c7933ec7d6c803858da Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 22:14:38 -0500 Subject: [PATCH 351/801] Simplify some code --- osu.Game/Screens/SelectV2/BeatmapPanel.cs | 10 ++++++---- osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 8 +++++--- osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs | 7 +++++-- osu.Game/Screens/SelectV2/GroupPanel.cs | 2 +- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs index 180acffe80..896b8ea82a 100644 --- a/osu.Game/Screens/SelectV2/BeatmapPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapPanel.cs @@ -36,8 +36,10 @@ namespace osu.Game.Screens.SelectV2 private const float colour_box_width = 30; private const float corner_radius = 10; - private const float glow_offset = 10f; // extra space for the edge effect to not be cutoff by the right edge of the carousel. - private const float difficulty_x_offset = 50f; // constant X offset for beatmap difficulty panels specifically. + // todo: this should be replaced with information from CarouselItem about how deep is BeatmapPanel in the carousel + // (i.e. whether it's under a beatmap set that's under a group, or just under a top-level beatmap set). + private const float difficulty_x_offset = 80f; // constant X offset for beatmap difficulty panels specifically. + private const float preselected_x_offset = 25f; private const float selected_x_offset = 50f; @@ -89,7 +91,7 @@ namespace osu.Game.Screens.SelectV2 Origin = Anchor.TopRight; RelativeSizeAxes = Axes.X; - Width = 0.9f; + Width = 1f; Height = HEIGHT; InternalChild = panel = new Container @@ -307,7 +309,7 @@ namespace osu.Game.Screens.SelectV2 private void updatePanelPosition() { - float x = glow_offset + difficulty_x_offset + selected_x_offset + preselected_x_offset; + float x = difficulty_x_offset + selected_x_offset + preselected_x_offset; if (Selected.Value) x -= selected_x_offset; diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index 4706ea487a..dff563339c 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -33,8 +33,10 @@ namespace osu.Game.Screens.SelectV2 private const float arrow_container_width = 20; private const float corner_radius = 10; - private const float glow_offset = 10f; // extra space for the edge effect to not be cutoff by the right edge of the carousel. - private const float set_x_offset = 20f; // constant X offset for beatmap set panels specifically. + // todo: this should be replaced with information from CarouselItem about how deep is BeatmapPanel in the carousel + // (i.e. whether it's under a beatmap set that's under a group, or just under a top-level beatmap set). + private const float set_x_offset = 20f; // constant X offset for beatmap set/standalone panels specifically. + private const float preselected_x_offset = 25f; private const float expanded_x_offset = 50f; @@ -269,7 +271,7 @@ namespace osu.Game.Screens.SelectV2 private void updatePanelPosition() { - float x = glow_offset + set_x_offset + expanded_x_offset + preselected_x_offset; + float x = set_x_offset + expanded_x_offset + preselected_x_offset; if (Expanded.Value) x -= expanded_x_offset; diff --git a/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs b/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs index 11fa22ab09..c3a773799a 100644 --- a/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs @@ -38,7 +38,10 @@ namespace osu.Game.Screens.SelectV2 private const float difficulty_icon_container_width = 30; private const float corner_radius = 10; - private const float glow_offset = 10f; // extra space for the edge effect to not be cutoff by the right edge of the carousel. + // todo: this should be replaced with information from CarouselItem about how deep is BeatmapPanel in the carousel + // (i.e. whether it's under a beatmap set that's under a group, or just under a top-level beatmap set). + private const float set_x_offset = 20f; // constant X offset for beatmap set/standalone panels specifically. + private const float preselected_x_offset = 25f; private const float selected_x_offset = 50f; @@ -348,7 +351,7 @@ namespace osu.Game.Screens.SelectV2 private void updatePanelPosition() { - float x = glow_offset + selected_x_offset + preselected_x_offset; + float x = set_x_offset + selected_x_offset + preselected_x_offset; if (Selected.Value) x -= selected_x_offset; diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs index 8995b93290..10d3b8934e 100644 --- a/osu.Game/Screens/SelectV2/GroupPanel.cs +++ b/osu.Game/Screens/SelectV2/GroupPanel.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.SelectV2 { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; - private const float glow_offset = 10f; // extra space for the edge effect to not be cutoff by the right edge of the carousel. + private const float glow_offset = 10f; // extra space for any edge effect to not be cutoff by the right edge of the carousel. private const float preselected_x_offset = 25f; private const float selected_x_offset = 50f; From 5e894a6f7e6faff59840d6b923ebd509a32853ee Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 22:24:36 -0500 Subject: [PATCH 352/801] Fix carousel tests failing due to X offsets --- .../TestSceneBeatmapCarouselV2GroupSelection.cs | 10 +++++----- .../TestSceneBeatmapCarouselV2Selection.cs | 13 ++++++------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs index ebdc54864e..4e6aa5d6c2 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs @@ -165,26 +165,26 @@ namespace osu.Game.Tests.Visual.SongSelect WaitForDrawablePanels(); SelectNextGroup(); - clickOnPanel(0, 1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); + clickOnPanel(0, 1, p => p.LayoutRectangle.TopLeft + new Vector2(p.LayoutRectangle.Centre.X, -1f)); WaitForGroupSelection(0, 1); - clickOnPanel(0, 0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + clickOnPanel(0, 0, p => p.LayoutRectangle.BottomLeft + new Vector2(p.LayoutRectangle.Centre.X, 1f)); WaitForGroupSelection(0, 0); SelectNextPanel(); Select(); WaitForGroupSelection(0, 1); - clickOnGroup(0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + clickOnGroup(0, p => p.LayoutRectangle.BottomLeft + new Vector2(p.LayoutRectangle.Centre.X, 1f)); AddAssert("group 0 collapsed", () => this.ChildrenOfType().OrderBy(g => g.Y).ElementAt(0).Expanded.Value, () => Is.False); clickOnGroup(0, p => p.LayoutRectangle.Centre); AddAssert("group 0 expanded", () => this.ChildrenOfType().OrderBy(g => g.Y).ElementAt(0).Expanded.Value, () => Is.True); AddStep("scroll to end", () => Scroll.ScrollToEnd(false)); - clickOnPanel(0, 4, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + clickOnPanel(0, 4, p => p.LayoutRectangle.BottomLeft + new Vector2(p.LayoutRectangle.Centre.X, 1f)); WaitForGroupSelection(0, 4); - clickOnGroup(1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); + clickOnGroup(1, p => p.LayoutRectangle.TopLeft + new Vector2(p.LayoutRectangle.Centre.X, -1f)); AddAssert("group 1 expanded", () => this.ChildrenOfType().OrderBy(g => g.Y).ElementAt(1).Expanded.Value, () => Is.True); } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs index 5541e217cf..3566b5e95f 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs @@ -187,24 +187,23 @@ namespace osu.Game.Tests.Visual.SongSelect WaitForDrawablePanels(); SelectNextGroup(); - clickOnDifficulty(0, 1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); + clickOnDifficulty(0, 1, p => new Vector2(p.LayoutRectangle.Centre.X, -1f)); WaitForSelection(0, 1); - clickOnDifficulty(0, 0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + clickOnDifficulty(0, 0, p => new Vector2(p.LayoutRectangle.Centre.X, BeatmapPanel.HEIGHT + 1f)); WaitForSelection(0, 0); - SelectNextPanel(); - Select(); + clickOnDifficulty(0, 1, p => p.LayoutRectangle.Centre); WaitForSelection(0, 1); - clickOnSet(0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + clickOnSet(0, p => new Vector2(p.LayoutRectangle.Centre.X, BeatmapSetPanel.HEIGHT + 1f)); WaitForSelection(0, 0); AddStep("scroll to end", () => Scroll.ScrollToEnd(false)); - clickOnDifficulty(0, 4, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + clickOnDifficulty(0, 4, p => new Vector2(p.LayoutRectangle.Centre.X, BeatmapPanel.HEIGHT + 1f)); WaitForSelection(0, 4); - clickOnSet(1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); + clickOnSet(1, p => new Vector2(p.LayoutRectangle.Centre.X, -1f)); WaitForSelection(1, 0); } From aab4a79ce4e87ebc24481398b378cc939b64cd7c Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 22:37:03 -0500 Subject: [PATCH 353/801] Push all beatmap panels to hide their tails --- osu.Game/Screens/SelectV2/BeatmapPanel.cs | 3 ++- osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 1 + .../Screens/SelectV2/BeatmapStandalonePanel.cs | 1 + osu.Game/Screens/SelectV2/GroupPanel.cs | 16 ++++++++++------ osu.Game/Screens/SelectV2/SongSelectV2.cs | 4 +--- 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs index 896b8ea82a..e5b612b1b2 100644 --- a/osu.Game/Screens/SelectV2/BeatmapPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapPanel.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.SelectV2 // todo: this should be replaced with information from CarouselItem about how deep is BeatmapPanel in the carousel // (i.e. whether it's under a beatmap set that's under a group, or just under a top-level beatmap set). - private const float difficulty_x_offset = 80f; // constant X offset for beatmap difficulty panels specifically. + private const float difficulty_x_offset = 100f; // constant X offset for beatmap difficulty panels specifically. private const float preselected_x_offset = 25f; private const float selected_x_offset = 50f; @@ -99,6 +99,7 @@ namespace osu.Game.Screens.SelectV2 Masking = true, CornerRadius = corner_radius, RelativeSizeAxes = Axes.Both, + X = corner_radius, EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index dff563339c..aabc39f27f 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -81,6 +81,7 @@ namespace osu.Game.Screens.SelectV2 Masking = true, CornerRadius = corner_radius, RelativeSizeAxes = Axes.Both, + X = corner_radius, EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, diff --git a/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs b/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs index c3a773799a..c0a5f828f4 100644 --- a/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs @@ -105,6 +105,7 @@ namespace osu.Game.Screens.SelectV2 Masking = true, CornerRadius = corner_radius, RelativeSizeAxes = Axes.Both, + X = corner_radius, EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs index 10d3b8934e..b5fa338f82 100644 --- a/osu.Game/Screens/SelectV2/GroupPanel.cs +++ b/osu.Game/Screens/SelectV2/GroupPanel.cs @@ -24,6 +24,8 @@ namespace osu.Game.Screens.SelectV2 { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; + private const float corner_radius = 10; + private const float glow_offset = 10f; // extra space for any edge effect to not be cutoff by the right edge of the carousel. private const float preselected_x_offset = 25f; private const float selected_x_offset = 50f; @@ -33,18 +35,19 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private BeatmapCarousel? carousel { get; set; } + private Container panel = null!; private Box activationFlash = null!; private OsuSpriteText titleText = null!; private Box hoverLayer = null!; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { - var inputRectangle = DrawRectangle; + var inputRectangle = panel.DrawRectangle; // Cover a gap introduced by the spacing between a GroupPanel and a BeatmapPanel either below/above it. inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); + return inputRectangle.Contains(panel.ToLocalSpace(screenSpacePos)); } [BackgroundDependencyLoader] @@ -55,11 +58,12 @@ namespace osu.Game.Screens.SelectV2 RelativeSizeAxes = Axes.X; Height = HEIGHT; - InternalChild = new Container + InternalChild = panel = new Container { RelativeSizeAxes = Axes.Both, - CornerRadius = 10f, + CornerRadius = corner_radius, Masking = true, + X = corner_radius, Children = new Drawable[] { new Container @@ -69,7 +73,7 @@ namespace osu.Game.Screens.SelectV2 Child = new Container { RelativeSizeAxes = Axes.Both, - CornerRadius = 10f, + CornerRadius = corner_radius, Masking = true, Children = new Drawable[] { @@ -93,7 +97,7 @@ namespace osu.Game.Screens.SelectV2 Child = new Container { RelativeSizeAxes = Axes.Both, - CornerRadius = 10f, + CornerRadius = corner_radius, Masking = true, Children = new Drawable[] { diff --git a/osu.Game/Screens/SelectV2/SongSelectV2.cs b/osu.Game/Screens/SelectV2/SongSelectV2.cs index 88825d96e0..3943d059f9 100644 --- a/osu.Game/Screens/SelectV2/SongSelectV2.cs +++ b/osu.Game/Screens/SelectV2/SongSelectV2.cs @@ -48,9 +48,7 @@ namespace osu.Game.Screens.SelectV2 Anchor = Anchor.TopRight, Origin = Anchor.TopRight, RelativeSizeAxes = Axes.Both, - Width = 0.5f, - // Push the carousel slightly off the right edge of the screen for the ends of the panels to be cut off. - X = 20f, + Width = 0.6f, }, }, modSelectOverlay, From ecc3aeadf2f5fbb17008ff1919ba9e68a0e0b77d Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 22:39:42 -0500 Subject: [PATCH 354/801] Make `BeatmapPanel` appear hovered on keyboard selection even if selected Was an intentional choice but appeared weird to others instead. The feedback itself probably needs changing. --- osu.Game/Screens/SelectV2/BeatmapPanel.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs index e5b612b1b2..c36a23e51f 100644 --- a/osu.Game/Screens/SelectV2/BeatmapPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapPanel.cs @@ -299,7 +299,6 @@ namespace osu.Game.Screens.SelectV2 updatePanelPosition(); updateEdgeEffectColour(); - updateHover(); } private void updateKeyboardSelectedDisplay() @@ -323,7 +322,7 @@ namespace osu.Game.Screens.SelectV2 private void updateHover() { - bool hovered = IsHovered || (KeyboardSelected.Value && !Selected.Value); + bool hovered = IsHovered || KeyboardSelected.Value; if (hovered) hoverLayer.FadeIn(100, Easing.OutQuint); From 84206e9ad8253ae0acc5169787fb6d6b516e16ff Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Feb 2025 13:29:16 +0900 Subject: [PATCH 355/801] Initial support for freemod+freestyle --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 11 +-- .../Multiplayer/MultiplayerMatchSubScreen.cs | 89 ++++++++---------- .../Playlists/PlaylistsRoomSubScreen.cs | 93 +++++++++---------- 3 files changed, 86 insertions(+), 107 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index ce51bb3c21..312253774f 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -441,7 +440,9 @@ namespace osu.Game.Screens.OnlinePlay.Match var rulesetInstance = GetGameplayRuleset().CreateInstance(); // Remove any user mods that are no longer allowed. - Mod[] allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); + Mod[] allowedMods = item.Freestyle + ? rulesetInstance.CreateAllMods().Where(m => ModUtils.IsValidFreeModForMatchType(m, Room.Type)).ToArray() + : item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); Mod[] newUserMods = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); if (!newUserMods.SequenceEqual(UserMods.Value)) UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList(); @@ -455,12 +456,8 @@ namespace osu.Game.Screens.OnlinePlay.Match Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray(); Ruleset.Value = GetGameplayRuleset(); - bool freeMod = item.AllowedMods.Any(); bool freestyle = item.Freestyle; - - // For now, the game can never be in a state where freemod and freestyle are on at the same time. - // This will change, but due to the current implementation if this was to occur drawables will overlap so let's assert. - Debug.Assert(!freeMod || !freestyle); + bool freeMod = freestyle || item.AllowedMods.Any(); if (freeMod) { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index b803c5f28b..a16c5c9442 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -98,7 +98,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { new Drawable?[] { - // Participants column new GridContainer { RelativeSizeAxes = Axes.Both, @@ -118,9 +117,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } } }, - // Spacer null, - // Beatmap column new GridContainer { RelativeSizeAxes = Axes.Both, @@ -147,67 +144,63 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer SelectedItem = SelectedItem } }, - new Drawable[] + new[] { - new Container + UserModsSection = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Margin = new MarginPadding { Top = 10 }, - Children = new[] + Alpha = 0, + Children = new Drawable[] { - UserModsSection = new FillFlowContainer + new OverlinedHeader("Extra mods"), + new FillFlowContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Alpha = 0, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), Children = new Drawable[] { - new OverlinedHeader("Extra mods"), - new FillFlowContainer + new UserModSelectButton { - 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), - }, - } + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 90, + Text = "Select", + Action = ShowUserModSelect, }, - } - }, - UserStyleSection = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Alpha = 0, - Children = new Drawable[] - { - new OverlinedHeader("Difficulty"), - UserStyleDisplayContainer = new Container + new ModDisplay { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - } + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Current = UserMods, + Scale = new Vector2(0.8f), + }, } }, } } }, + new[] + { + UserStyleSection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 10 }, + Alpha = 0, + Children = new Drawable[] + { + new OverlinedHeader("Difficulty"), + UserStyleDisplayContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } + } + }, + }, }, RowDimensions = new[] { @@ -218,9 +211,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer new Dimension(GridSizeMode.AutoSize), } }, - // Spacer null, - // Main right column new GridContainer { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 2195ed4722..957a51c467 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -146,7 +146,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { new Drawable?[] { - // Playlist items column new GridContainer { RelativeSizeAxes = Axes.Both, @@ -176,73 +175,66 @@ namespace osu.Game.Screens.OnlinePlay.Playlists new Dimension(), } }, - // Spacer null, - // Middle column (mods and leaderboard) new GridContainer { RelativeSizeAxes = Axes.Both, Content = new[] { - new Drawable[] + new[] { - new Container + UserModsSection = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Margin = new MarginPadding { Bottom = 10 }, - Children = new[] + Alpha = 0, + Children = new Drawable[] { - UserModsSection = new FillFlowContainer + new OverlinedHeader("Extra mods"), + new FillFlowContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Alpha = 0, - Margin = new MarginPadding { Bottom = 10 }, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), Children = new Drawable[] { - new OverlinedHeader("Extra mods"), - new FillFlowContainer + new UserModSelectButton { - 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), - }, - } - } + 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), + }, } - }, - UserStyleSection = new FillFlowContainer + } + } + }, + }, + new[] + { + UserStyleSection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Bottom = 10 }, + Alpha = 0, + Children = new Drawable[] + { + new OverlinedHeader("Difficulty"), + UserStyleDisplayContainer = new Container { RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Alpha = 0, - Children = new Drawable[] - { - new OverlinedHeader("Difficulty"), - UserStyleDisplayContainer = new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - } - } - }, + AutoSizeAxes = Axes.Y + } } }, }, @@ -273,12 +265,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), new Dimension(), } }, - // Spacer null, - // Main right column new GridContainer { RelativeSizeAxes = Axes.Both, From 9cc90a51df7aa2a0043690ff873ed836741993b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 13:32:11 +0900 Subject: [PATCH 356/801] Adjust xmldoc and avoid LINQ overheads --- osu.Game/Screens/SelectV2/Carousel.cs | 7 +++---- osu.Game/Screens/SelectV2/CarouselItem.cs | 5 ++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 5dc8d80476..07d9c988f5 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -550,8 +550,7 @@ namespace osu.Game.Screens.SelectV2 updateDisplayedRange(range); } - double selectedYPos = currentSelection?.CarouselItem?.CarouselYPosition ?? 0; - double maximumDistanceFromSelection = scroll.Panels.Select(p => Math.Abs(((ICarouselPanel)p).DrawYPosition - selectedYPos)).DefaultIfEmpty().Max(); + double selectedYPos = currentSelection.CarouselItem?.CarouselYPosition ?? 0; foreach (var panel in scroll.Panels) { @@ -561,8 +560,8 @@ namespace osu.Game.Screens.SelectV2 if (c.Item == null) continue; - float normalisedDepth = (float)(Math.Abs(selectedYPos - c.DrawYPosition) / maximumDistanceFromSelection); - scroll.Panels.ChangeChildDepth(panel, normalisedDepth + c.Item.DepthLayer); + float normalisedDepth = (float)(Math.Abs(selectedYPos - c.DrawYPosition) / DrawHeight); + scroll.Panels.ChangeChildDepth(panel, c.Item.DepthLayer + normalisedDepth); if (c.DrawYPosition != c.Item.CarouselYPosition) c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed); diff --git a/osu.Game/Screens/SelectV2/CarouselItem.cs b/osu.Game/Screens/SelectV2/CarouselItem.cs index e497c3890c..0ac8180028 100644 --- a/osu.Game/Screens/SelectV2/CarouselItem.cs +++ b/osu.Game/Screens/SelectV2/CarouselItem.cs @@ -30,10 +30,9 @@ namespace osu.Game.Screens.SelectV2 public float DrawHeight { get; set; } = DEFAULT_HEIGHT; /// - /// A number that defines the layer which this should be placed on depth-wise. - /// The higher the number, the farther the panel associated with this item is taken to the background. + /// Defines the display depth relative to other s. /// - public int DepthLayer { get; set; } = 0; + public int DepthLayer { get; set; } /// /// Whether this item is visible or hidden. From d9b370e3a1d384758dee89ef704dd0c38a694ec8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 13:41:16 +0900 Subject: [PATCH 357/801] Add xmldoc for menu implying external consumption --- osu.Game/Screens/Edit/BookmarkController.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Edit/BookmarkController.cs b/osu.Game/Screens/Edit/BookmarkController.cs index 3d2cb4663f..80e77364e5 100644 --- a/osu.Game/Screens/Edit/BookmarkController.cs +++ b/osu.Game/Screens/Edit/BookmarkController.cs @@ -19,6 +19,9 @@ namespace osu.Game.Screens.Edit { public partial class BookmarkController : Component, IKeyBindingHandler { + /// + /// Bookmarks menu item (with submenu containing options). Should be added to the 's global menu. + /// public EditorMenuItem Menu { get; private set; } [Resolved] From 0257b8c2ffd2dffa2b81fbf41ad88889db0ff14a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 13:57:38 +0900 Subject: [PATCH 358/801] Move metadata randomisation local to usage --- osu.Game.Tests/Resources/TestResources.cs | 29 +++++------------- .../SongSelect/BeatmapCarouselV2TestScene.cs | 30 +++++++++++++++++-- .../TestSceneBeatmapCarouselV2Basics.cs | 3 +- 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index bf08097ffd..e0572e604c 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -85,8 +85,7 @@ namespace osu.Game.Tests.Resources /// /// Number of difficulties. If null, a random number between 1 and 20 will be used. /// Rulesets to cycle through when creating difficulties. If null, osu! ruleset will be used. - /// Whether to randomise metadata to create a better distribution. - public static BeatmapSetInfo CreateTestBeatmapSetInfo(int? difficultyCount = null, RulesetInfo[] rulesets = null, bool randomiseMetadata = false) + public static BeatmapSetInfo CreateTestBeatmapSetInfo(int? difficultyCount = null, RulesetInfo[] rulesets = null) { int j = 0; @@ -96,27 +95,13 @@ namespace osu.Game.Tests.Resources int setId = GetNextTestID(); - char getRandomCharacter() + var metadata = new BeatmapMetadata { - const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz*"; - return chars[RNG.Next(chars.Length)]; - } - - var metadata = randomiseMetadata - ? new BeatmapMetadata - { - // Create random metadata, then we can check if sorting works based on these - Artist = $"{getRandomCharacter()}ome Artist " + RNG.Next(0, 9), - Title = $"{getRandomCharacter()}ome Song (set id {setId:000}) {Guid.NewGuid()}", - Author = { Username = $"{getRandomCharacter()}ome Guy " + RNG.Next(0, 9) }, - } - : new BeatmapMetadata - { - // Create random metadata, then we can check if sorting works based on these - Artist = "Some Artist " + RNG.Next(0, 9), - Title = $"Some Song (set id {setId:000}) {Guid.NewGuid()}", - Author = { Username = "Some Guy " + RNG.Next(0, 9) }, - }; + // Create random metadata, then we can check if sorting works based on these + Artist = "Some Artist " + RNG.Next(0, 9), + Title = $"Some Song (set id {setId:000}) {Guid.NewGuid()}", + Author = { Username = "Some Guy " + RNG.Next(0, 9) }, + }; Logger.Log($"🛠️ Generating beatmap set \"{metadata}\" for test consumption."); diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index f5ea959c51..a55f79c42e 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -190,12 +191,37 @@ namespace osu.Game.Tests.Visual.SongSelect /// /// The count of beatmap sets to add. /// If not null, the number of difficulties per set. If null, randomised difficulty count will be used. - protected void AddBeatmaps(int count, int? fixedDifficultiesPerSet = null) => AddStep($"add {count} beatmaps", () => + /// Whether to randomise the metadata to make groupings more uniform. + protected void AddBeatmaps(int count, int? fixedDifficultiesPerSet = null, bool randomMetadata = false) => AddStep($"add {count} beatmaps{(randomMetadata ? " with random data" : "")}", () => { for (int i = 0; i < count; i++) - BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4), randomiseMetadata: true)); + { + var beatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4)); + + if (randomMetadata) + { + var metadata = new BeatmapMetadata + { + // Create random metadata, then we can check if sorting works based on these + Artist = $"{getRandomCharacter()}ome Artist " + RNG.Next(0, 9), + Title = $"{getRandomCharacter()}ome Song (set id {beatmapSetInfo.OnlineID:000}) {Guid.NewGuid()}", + Author = { Username = $"{getRandomCharacter()}ome Guy " + RNG.Next(0, 9) }, + }; + + foreach (var beatmap in beatmapSetInfo.Beatmaps) + beatmap.Metadata = metadata.DeepClone(); + } + + BeatmapSets.Add(beatmapSetInfo); + } }); + private static char getRandomCharacter() + { + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz*"; + return chars[RNG.Next(chars.Length)]; + } + protected void RemoveAllBeatmaps() => AddStep("clear all beatmaps", () => BeatmapSets.Clear()); protected void RemoveFirstBeatmap() => diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs index a173920dc6..41ceff3183 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs @@ -26,8 +26,9 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestBasics() { - AddBeatmaps(1); AddBeatmaps(10); + AddBeatmaps(10, randomMetadata: true); + AddBeatmaps(1); RemoveFirstBeatmap(); RemoveAllBeatmaps(); } From d93f7509b6545489f405faf8e9a60f4800b7e040 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Feb 2025 14:12:15 +0900 Subject: [PATCH 359/801] Fix participant panels not displaying mods from other rulesets correctly --- .../TestSceneMultiplayerParticipantsList.cs | 37 +++++++++++++++++++ .../Participants/ParticipantPanel.cs | 22 ++++++----- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 238a716f91..d3c967a8d5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -12,11 +12,14 @@ using osu.Framework.Utils; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Online; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; +using osu.Game.Rulesets.Catch.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; using osu.Game.Users; using osuTK; @@ -393,6 +396,40 @@ namespace osu.Game.Tests.Visual.Multiplayer }); } + [Test] + public void TestModsAndRuleset() + { + AddStep("add another user", () => + { + MultiplayerClient.AddUser(new APIUser + { + Id = 0, + Username = "User 0", + RulesetsStatistics = new Dictionary + { + { + Ruleset.Value.ShortName, + new UserStatistics { GlobalRank = RNG.Next(1, 100000), } + } + }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }); + + MultiplayerClient.ChangeUserBeatmapAvailability(0, BeatmapAvailability.LocallyAvailable()); + }); + + AddStep("set user styles", () => + { + MultiplayerClient.ChangeUserStyle(API.LocalUser.Value.OnlineID, 259, 1); + MultiplayerClient.ChangeUserMods(API.LocalUser.Value.OnlineID, + [new APIMod(new TaikoModConstantSpeed()), new APIMod(new TaikoModHidden()), new APIMod(new TaikoModFlashlight()), new APIMod(new TaikoModHardRock())]); + + MultiplayerClient.ChangeUserStyle(0, 259, 2); + MultiplayerClient.ChangeUserMods(0, + [new APIMod(new CatchModFloatingFruits()), new APIMod(new CatchModHidden()), new APIMod(new CatchModMirror())]); + }); + } + private void createNewParticipantsList() { ParticipantsList? participantsList = null; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index a2657019a3..d6666de2b6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -27,7 +28,6 @@ using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play.HUD; using osu.Game.Users; using osu.Game.Users.Drawables; @@ -210,13 +210,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants const double fade_time = 50; MultiplayerPlaylistItem? currentItem = client.Room.GetCurrentItem(); - Ruleset? ruleset = currentItem != null ? rulesets.GetRuleset(currentItem.RulesetID)?.CreateInstance() : null; + Debug.Assert(currentItem != null); - int? currentModeRank = ruleset != null ? User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank : null; - userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; + int userBeatmapId = User.BeatmapId ?? currentItem.BeatmapID; + int userRulesetId = User.RulesetId ?? currentItem.RulesetID; + Ruleset? userRuleset = rulesets.GetRuleset(userRulesetId)?.CreateInstance(); + Debug.Assert(userRuleset != null); userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability); + int? currentModeRank = User.User?.RulesetsStatistics?.GetValueOrDefault(userRuleset.ShortName)?.GlobalRank; + userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; + if ((User.BeatmapAvailability.State == DownloadState.LocallyAvailable) && (User.State != MultiplayerUserState.Spectating)) { userModsDisplay.FadeIn(fade_time); @@ -228,20 +233,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants userStyleDisplay.FadeOut(fade_time); } - if ((User.BeatmapId == null && User.RulesetId == null) || (User.BeatmapId == currentItem?.BeatmapID && User.RulesetId == currentItem?.RulesetID)) + if (userBeatmapId == currentItem.BeatmapID && userRulesetId == currentItem.RulesetID) userStyleDisplay.Style = null; else - userStyleDisplay.Style = (User.BeatmapId ?? currentItem?.BeatmapID ?? 0, User.RulesetId ?? currentItem?.RulesetID ?? 0); + userStyleDisplay.Style = (userBeatmapId, userRulesetId); kickButton.Alpha = client.IsHost && !User.Equals(client.LocalUser) ? 1 : 0; crown.Alpha = client.Room.Host?.Equals(User) == true ? 1 : 0; // If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187 // This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix. - Schedule(() => - { - userModsDisplay.Current.Value = ruleset != null ? User.Mods.Select(m => m.ToMod(ruleset)).ToList() : Array.Empty(); - }); + Schedule(() => userModsDisplay.Current.Value = User.Mods.Select(m => m.ToMod(userRuleset)).ToList()); } public MenuItem[]? ContextMenuItems From 885ae7c735a82740710fce395a456d8e1280abf9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Feb 2025 14:25:08 +0900 Subject: [PATCH 360/801] Adjust styling --- .../Multiplayer/Participants/ParticipantPanel.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index d6666de2b6..51ff52c63e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -161,11 +161,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants Origin = Anchor.CentreRight, AutoSizeAxes = Axes.Both, Margin = new MarginPadding { Right = 70 }, + Spacing = new Vector2(2), Children = new Drawable[] { - userStyleDisplay = new StyleDisplayIcon(), + userStyleDisplay = new StyleDisplayIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, userModsDisplay = new ModDisplay { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, Scale = new Vector2(0.5f), ExpansionMode = ExpansionMode.AlwaysContracted, } From 88ad87a78e36a7170d0ce05dd0a0a29433977f88 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 14:30:15 +0900 Subject: [PATCH 361/801] Expose set grouping state --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 2 +- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index e7311fbfbc..36e57c9067 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -140,7 +140,7 @@ namespace osu.Game.Screens.SelectV2 return true; case BeatmapInfo: - return Criteria.SplitOutDifficulties; + return !grouping.BeatmapSetsGroupedTogether; case GroupDefinition: return false; diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index d4e0a166ab..29c534cbe2 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -14,6 +14,8 @@ namespace osu.Game.Screens.SelectV2 { public class BeatmapCarouselFilterGrouping : ICarouselFilter { + public bool BeatmapSetsGroupedTogether { get; private set; } + /// /// Beatmap sets contain difficulties as related panels. This dictionary holds the relationships between set-difficulties to allow expanding them on selection. /// @@ -36,8 +38,6 @@ namespace osu.Game.Screens.SelectV2 public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => { - bool groupSetsTogether; - setItems.Clear(); groupItems.Clear(); @@ -48,12 +48,12 @@ namespace osu.Game.Screens.SelectV2 switch (criteria.Group) { default: - groupSetsTogether = true; + BeatmapSetsGroupedTogether = true; newItems.AddRange(items); break; case GroupMode.Artist: - groupSetsTogether = true; + BeatmapSetsGroupedTogether = true; char groupChar = (char)0; foreach (var item in items) @@ -80,7 +80,7 @@ namespace osu.Game.Screens.SelectV2 break; case GroupMode.Difficulty: - groupSetsTogether = false; + BeatmapSetsGroupedTogether = false; int starGroup = int.MinValue; foreach (var item in items) @@ -108,7 +108,7 @@ namespace osu.Game.Screens.SelectV2 // Add set headers wherever required. CarouselItem? lastItem = null; - if (groupSetsTogether) + if (BeatmapSetsGroupedTogether) { for (int i = 0; i < newItems.Count; i++) { From 342a66b9e21e619c9192a4bd63bb2f32563c2e20 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 14:39:11 +0900 Subject: [PATCH 362/801] Fix keyboard traversal on a collapsed group not working as intended --- osu.Game/Screens/SelectV2/Carousel.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 608ef207d9..6b7b1f3a9b 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -378,7 +378,7 @@ namespace osu.Game.Screens.SelectV2 { TryActivateSelection(); - // There's a chance this couldn't resolve, at which point continue with standard traversal. + // Is the selection actually changed, then we should not perform any further traversal. if (currentSelection.CarouselItem == currentKeyboardSelection.CarouselItem) return; } @@ -386,20 +386,20 @@ namespace osu.Game.Screens.SelectV2 int originalIndex; int newIndex; - if (currentSelection.Index == null) + if (currentKeyboardSelection.Index == null) { // If there's no current selection, start from either end of the full list. newIndex = originalIndex = direction > 0 ? carouselItems.Count - 1 : 0; } else { - newIndex = originalIndex = currentSelection.Index.Value; + newIndex = originalIndex = currentKeyboardSelection.Index.Value; // As a second special case, if we're group selecting backwards and the current selection isn't a group, // make sure to go back to the group header this item belongs to, so that the block below doesn't find it and stop too early. if (direction < 0) { - while (!CheckValidForGroupSelection(carouselItems[newIndex])) + while (newIndex > 0 && !CheckValidForGroupSelection(carouselItems[newIndex])) newIndex--; } } From bf377e081ad36ab88b1c7b6ef415bcb2db888bdd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 14:38:51 +0900 Subject: [PATCH 363/801] Reorganise tests to make more logical when manually testing --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 23 ++--- ...asics.cs => TestSceneBeatmapCarouselV2.cs} | 93 ++++++------------- ...eneBeatmapCarouselV2DifficultyGrouping.cs} | 25 ++--- ...> TestSceneBeatmapCarouselV2NoGrouping.cs} | 12 ++- .../TestSceneBeatmapCarouselV2Scrolling.cs | 65 +++++++++++++ 5 files changed, 118 insertions(+), 100 deletions(-) rename osu.Game.Tests/Visual/SongSelect/{TestSceneBeatmapCarouselV2Basics.cs => TestSceneBeatmapCarouselV2.cs} (52%) rename osu.Game.Tests/Visual/SongSelect/{TestSceneBeatmapCarouselV2GroupSelection.cs => TestSceneBeatmapCarouselV2DifficultyGrouping.cs} (92%) rename osu.Game.Tests/Visual/SongSelect/{TestSceneBeatmapCarouselV2Selection.cs => TestSceneBeatmapCarouselV2NoGrouping.cs} (94%) create mode 100644 osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index a55f79c42e..36226a13cc 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -17,7 +17,6 @@ using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Screens.Select; -using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; @@ -54,16 +53,6 @@ namespace osu.Game.Tests.Visual.SongSelect Scheduler.AddDelayed(updateStats, 100, true); } - [SetUpSteps] - public virtual void SetUpSteps() - { - RemoveAllBeatmaps(); - - CreateCarousel(); - - SortBy(new FilterCriteria { Sort = SortMode.Title }); - } - protected void CreateCarousel() { AddStep("create components", () => @@ -200,12 +189,14 @@ namespace osu.Game.Tests.Visual.SongSelect if (randomMetadata) { + char randomCharacter = getRandomCharacter(); + var metadata = new BeatmapMetadata { // Create random metadata, then we can check if sorting works based on these - Artist = $"{getRandomCharacter()}ome Artist " + RNG.Next(0, 9), - Title = $"{getRandomCharacter()}ome Song (set id {beatmapSetInfo.OnlineID:000}) {Guid.NewGuid()}", - Author = { Username = $"{getRandomCharacter()}ome Guy " + RNG.Next(0, 9) }, + Artist = $"{randomCharacter}ome Artist " + RNG.Next(0, 9), + Title = $"{randomCharacter}ome Song (set id {beatmapSetInfo.OnlineID:000}) {Guid.NewGuid()}", + Author = { Username = $"{randomCharacter}ome Guy " + RNG.Next(0, 9) }, }; foreach (var beatmap in beatmapSetInfo.Beatmaps) @@ -216,10 +207,12 @@ namespace osu.Game.Tests.Visual.SongSelect } }); + private static long randomCharPointer; + private static char getRandomCharacter() { const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz*"; - return chars[RNG.Next(chars.Length)]; + return chars[(int)((randomCharPointer++ / 2) % chars.Length)]; } protected void RemoveAllBeatmaps() => AddStep("clear all beatmaps", () => BeatmapSets.Clear()); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs similarity index 52% rename from osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs rename to osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs index 41ceff3183..3c5cf16e92 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs @@ -2,102 +2,65 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using NUnit.Framework; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; -using osu.Game.Screens.SelectV2; using osu.Game.Tests.Resources; namespace osu.Game.Tests.Visual.SongSelect { /// - /// Currently covers adding and removing of items and scrolling. - /// If we add more tests here, these two categories can likely be split out into separate scenes. + /// Covers common steps which can be used for manual testing. /// [TestFixture] - public partial class TestSceneBeatmapCarouselV2Basics : BeatmapCarouselV2TestScene + public partial class TestSceneBeatmapCarouselV2 : BeatmapCarouselV2TestScene { [Test] + [Explicit] public void TestBasics() { - AddBeatmaps(10); + CreateCarousel(); + RemoveAllBeatmaps(); + AddBeatmaps(10, randomMetadata: true); + AddBeatmaps(10); AddBeatmaps(1); + } + + [Test] + [Explicit] + public void TestSorting() + { + SortBy(new FilterCriteria { Sort = SortMode.Artist }); + SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty }); + SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist }); + } + + [Test] + [Explicit] + public void TestRemovals() + { RemoveFirstBeatmap(); RemoveAllBeatmaps(); } [Test] - public void TestOffScreenLoading() - { - AddStep("disable masking", () => Scroll.Masking = false); - AddStep("enable masking", () => Scroll.Masking = true); - } - - [Test] - public void TestAddRemoveOneByOne() + [Explicit] + public void TestAddRemoveRepeatedOps() { AddRepeatStep("add beatmaps", () => BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))), 20); AddRepeatStep("remove beatmaps", () => BeatmapSets.RemoveAt(RNG.Next(0, BeatmapSets.Count)), 20); } [Test] - public void TestSorting() + [Explicit] + public void TestMasking() { - AddBeatmaps(10); - SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty }); - SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist }); - SortBy(new FilterCriteria { Sort = SortMode.Artist }); - } - - [Test] - public void TestScrollPositionMaintainedOnAddSecondSelected() - { - Quad positionBefore = default; - - AddBeatmaps(10); - WaitForDrawablePanels(); - - AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2)); - AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType().Single(p => p.Selected.Value))); - - WaitForScrolling(); - - AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); - - RemoveFirstBeatmap(); - WaitForSorting(); - - AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, - () => Is.EqualTo(positionBefore)); - } - - [Test] - public void TestScrollPositionMaintainedOnAddLastSelected() - { - Quad positionBefore = default; - - AddBeatmaps(10); - WaitForDrawablePanels(); - - AddStep("scroll to last item", () => Scroll.ScrollToEnd(false)); - - AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.Last()); - - WaitForScrolling(); - - AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); - - RemoveFirstBeatmap(); - WaitForSorting(); - AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, - () => Is.EqualTo(positionBefore)); + AddStep("disable masking", () => Scroll.Masking = false); + AddStep("enable masking", () => Scroll.Masking = true); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs similarity index 92% rename from osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs rename to osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs index f4d97be5a5..e861d8bf30 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs @@ -12,23 +12,22 @@ using osu.Game.Screens.SelectV2; namespace osu.Game.Tests.Visual.SongSelect { [TestFixture] - public partial class TestSceneBeatmapCarouselV2GroupSelection : BeatmapCarouselV2TestScene + public partial class TestSceneBeatmapCarouselV2DifficultyGrouping : BeatmapCarouselV2TestScene { - public override void SetUpSteps() + [SetUpSteps] + public void SetUpSteps() { RemoveAllBeatmaps(); - CreateCarousel(); - SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty }); + + AddBeatmaps(10, 3); + WaitForDrawablePanels(); } [Test] public void TestOpenCloseGroupWithNoSelectionMouse() { - AddBeatmaps(10, 5); - WaitForDrawablePanels(); - AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); CheckNoSelection(); @@ -44,9 +43,6 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestOpenCloseGroupWithNoSelectionKeyboard() { - AddBeatmaps(10, 5); - WaitForDrawablePanels(); - AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); CheckNoSelection(); @@ -67,9 +63,6 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestCarouselRemembersSelection() { - AddBeatmaps(10); - WaitForDrawablePanels(); - SelectNextGroup(); object? selection = null; @@ -107,9 +100,6 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestGroupSelectionOnHeader() { - AddBeatmaps(10, 3); - WaitForDrawablePanels(); - SelectNextGroup(); WaitForGroupSelection(0, 0); @@ -121,9 +111,6 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestKeyboardSelection() { - AddBeatmaps(10, 3); - WaitForDrawablePanels(); - SelectNextPanel(); SelectNextPanel(); SelectNextPanel(); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs similarity index 94% rename from osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs rename to osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs index b087c252e4..82f35af0ec 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs @@ -5,14 +5,24 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelect { [TestFixture] - public partial class TestSceneBeatmapCarouselV2Selection : BeatmapCarouselV2TestScene + public partial class TestSceneBeatmapCarouselV2NoGrouping : BeatmapCarouselV2TestScene { + [SetUpSteps] + public void SetUpSteps() + { + RemoveAllBeatmaps(); + CreateCarousel(); + SortBy(new FilterCriteria { Sort = SortMode.Title }); + } + /// /// Keyboard selection via up and down arrows doesn't actually change the selection until /// the select key is pressed. diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs new file mode 100644 index 0000000000..1d5d8e2a2d --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs @@ -0,0 +1,65 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Testing; +using osu.Game.Screens.Select; +using osu.Game.Screens.SelectV2; + +namespace osu.Game.Tests.Visual.SongSelect +{ + [TestFixture] + public partial class TestSceneBeatmapCarouselV2Scrolling : BeatmapCarouselV2TestScene + { + [SetUpSteps] + public void SetUpSteps() + { + RemoveAllBeatmaps(); + CreateCarousel(); + SortBy(new FilterCriteria()); + + AddBeatmaps(10); + WaitForDrawablePanels(); + } + + [Test] + public void TestScrollPositionMaintainedOnAddSecondSelected() + { + Quad positionBefore = default; + + AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2)); + AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType().Single(p => p.Selected.Value))); + + WaitForScrolling(); + + AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); + + RemoveFirstBeatmap(); + WaitForSorting(); + + AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, + () => Is.EqualTo(positionBefore)); + } + + [Test] + public void TestScrollPositionMaintainedOnAddLastSelected() + { + Quad positionBefore = default; + + AddStep("scroll to last item", () => Scroll.ScrollToEnd(false)); + + AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.Last()); + + WaitForScrolling(); + + AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); + + RemoveFirstBeatmap(); + WaitForSorting(); + AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, + () => Is.EqualTo(positionBefore)); + } + } +} From 5b8b9589d8347fbf4a6d4d0ff9f89f24ed3274a5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Feb 2025 15:25:14 +0900 Subject: [PATCH 364/801] Add ruleset icon to expanded score panel --- .../Expanded/ExpandedPanelMiddleContent.cs | 49 +++++++++---------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index d1dc1a81db..4bc559694a 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -41,7 +41,6 @@ namespace osu.Game.Screens.Ranking.Expanded private readonly List statisticDisplays = new List(); - private FillFlowContainer starAndModDisplay; private RollingCounter scoreCounter; [Resolved] @@ -139,12 +138,35 @@ namespace osu.Game.Screens.Ranking.Expanded Alpha = 0, AlwaysPresent = true }, - starAndModDisplay = new FillFlowContainer + new FillFlowContainer { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, AutoSizeAxes = Axes.Both, Spacing = new Vector2(5, 0), + Children = new Drawable[] + { + new StarRatingDisplay(beatmapDifficultyCache.GetDifficultyAsync(beatmap, score.Ruleset, score.Mods).GetResultSafely() ?? default) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft + }, + new DifficultyIcon(beatmap, score.Ruleset) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(20), + TooltipType = DifficultyIconTooltipType.Extended, + }, + new ModDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + ExpansionMode = ExpansionMode.AlwaysExpanded, + Scale = new Vector2(0.5f), + Current = { Value = score.Mods } + } + } }, new FillFlowContainer { @@ -225,29 +247,6 @@ namespace osu.Game.Screens.Ranking.Expanded if (score.Date != default) AddInternal(new PlayedOnText(score.Date)); - - var starDifficulty = beatmapDifficultyCache.GetDifficultyAsync(beatmap, score.Ruleset, score.Mods).GetResultSafely(); - - if (starDifficulty != null) - { - starAndModDisplay.Add(new StarRatingDisplay(starDifficulty.Value) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft - }); - } - - if (score.Mods.Any()) - { - starAndModDisplay.Add(new ModDisplay - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - ExpansionMode = ExpansionMode.AlwaysExpanded, - Scale = new Vector2(0.5f), - Current = { Value = score.Mods } - }); - } } protected override void LoadComplete() From 134e62c39afb3aa4a36d790c509aff24a7b5bead Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 6 Feb 2025 00:10:42 -0500 Subject: [PATCH 365/801] Abstractify beatmap panel piece and update all panel implementations --- osu.Game/Screens/SelectV2/BeatmapPanel.cs | 249 +++---------- osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 278 ++++---------- .../SelectV2/BeatmapStandalonePanel.cs | 342 ++++++------------ .../Screens/SelectV2/CarouselPanelPiece.cs | 240 ++++++++++++ osu.Game/Screens/SelectV2/GroupPanel.cs | 212 +++-------- osu.Game/Screens/SelectV2/StarsGroupPanel.cs | 234 ++++-------- 6 files changed, 608 insertions(+), 947 deletions(-) create mode 100644 osu.Game/Screens/SelectV2/CarouselPanelPiece.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs index c36a23e51f..bd4cf6d7cf 100644 --- a/osu.Game/Screens/SelectV2/BeatmapPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapPanel.cs @@ -6,13 +6,9 @@ using System.Diagnostics; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Pooling; -using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; @@ -25,7 +21,6 @@ using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osuTK; -using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { @@ -33,36 +28,23 @@ namespace osu.Game.Screens.SelectV2 { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; - private const float colour_box_width = 30; - private const float corner_radius = 10; - // todo: this should be replaced with information from CarouselItem about how deep is BeatmapPanel in the carousel // (i.e. whether it's under a beatmap set that's under a group, or just under a top-level beatmap set). private const float difficulty_x_offset = 100f; // constant X offset for beatmap difficulty panels specifically. - private const float preselected_x_offset = 25f; - private const float selected_x_offset = 50f; - private const float duration = 500; - [Resolved] - private BeatmapCarousel? carousel { get; set; } - - [Resolved] - private IBindable ruleset { get; set; } = null!; - - [Resolved] - private IBindable> mods { get; set; } = null!; - - private Container panel = null!; + private CarouselPanelPiece panel = null!; private StarCounter starCounter = null!; - private ConstrainedIconContainer iconContainer = null!; - private Box hoverLayer = null!; - private Box activationFlash = null!; - - private Box backgroundBorder = null!; - + private ConstrainedIconContainer difficultyIcon = null!; + private OsuSpriteText keyCountText = null!; private StarRatingDisplay starRatingDisplay = null!; + private TopLocalRankV2 difficultyRank = null!; + private OsuSpriteText difficultyText = null!; + private OsuSpriteText authorText = null!; + + private IBindable? starDifficultyBindable; + private CancellationTokenSource? starDifficultyCancellationSource; [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; @@ -73,16 +55,24 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private BeatmapDifficultyCache difficultyCache { get; set; } = null!; - private OsuSpriteText keyCountText = null!; + [Resolved] + private BeatmapCarousel? carousel { get; set; } - private IBindable? starDifficultyBindable; - private CancellationTokenSource? starDifficultyCancellationSource; + [Resolved] + private IBindable ruleset { get; set; } = null!; - private Container rightContainer = null!; - private Box starRatingGradient = null!; - private TopLocalRankV2 difficultyRank = null!; - private OsuSpriteText difficultyText = null!; - private OsuSpriteText authorText = null!; + [Resolved] + private IBindable> mods { get; set; } = null!; + + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) + { + var inputRectangle = panel.TopLevelContent.DrawRectangle; + + // Cover the gaps introduced by the spacing between BeatmapPanels. + inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); + + return inputRectangle.Contains(panel.TopLevelContent.ToLocalSpace(screenSpacePos)); + } [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) @@ -94,67 +84,21 @@ namespace osu.Game.Screens.SelectV2 Width = 1f; Height = HEIGHT; - InternalChild = panel = new Container + InternalChild = panel = new CarouselPanelPiece(difficulty_x_offset) { - Masking = true, - CornerRadius = corner_radius, - RelativeSizeAxes = Axes.Both, - X = corner_radius, - EdgeEffect = new EdgeEffectParameters + Icon = difficultyIcon = new ConstrainedIconContainer { - Type = EdgeEffectType.Shadow, - Offset = new Vector2(1f), - Radius = 10, + Size = new Vector2(20), + Margin = new MarginPadding { Horizontal = 5f }, + Colour = colourProvider.Background5, }, - Children = new Drawable[] + Children = new[] { - new BufferedContainer - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - backgroundBorder = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colours.ForStarDifficulty(0), - EdgeSmoothness = new Vector2(2, 0), - }, - rightContainer = new Container - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Masking = true, - CornerRadius = corner_radius, - RelativeSizeAxes = Axes.X, - Height = HEIGHT, - X = colour_box_width, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(colourProvider.Background3, colourProvider.Background4), - }, - starRatingGradient = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - }, - }, - }, - } - }, - iconContainer = new ConstrainedIconContainer - { - X = colour_box_width / 2, - Origin = Anchor.Centre, - Anchor = Anchor.CentreLeft, - Size = new Vector2(20), - Colour = colourProvider.Background5, - }, new FillFlowContainer { - Padding = new MarginPadding { Top = 8, Left = colour_box_width + corner_radius }, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Padding = new MarginPadding { Left = 10f }, Direction = FillDirection.Vertical, AutoSizeAxes = Axes.Both, Children = new Drawable[] @@ -216,34 +160,10 @@ namespace osu.Game.Screens.SelectV2 } } }, - hoverLayer = new Box - { - Colour = colours.Blue.Opacity(0.1f), - Alpha = 0, - Blending = BlendingParameters.Additive, - RelativeSizeAxes = Axes.Both, - }, - activationFlash = new Box - { - Blending = BlendingParameters.Additive, - Alpha = 0f, - RelativeSizeAxes = Axes.Both, - }, - new HoverSounds(), } }; } - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) - { - var inputRectangle = panel.DrawRectangle; - - // Cover the gaps introduced by the spacing between BeatmapPanels. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - - return inputRectangle.Contains(panel.ToLocalSpace(screenSpacePos)); - } - protected override void LoadComplete() { base.LoadComplete(); @@ -260,8 +180,8 @@ namespace osu.Game.Screens.SelectV2 updateKeyCount(); }, true); - Selected.BindValueChanged(_ => updateSelectionDisplay(), true); - KeyboardSelected.BindValueChanged(_ => updateKeyboardSelectedDisplay(), true); + Selected.BindValueChanged(s => panel.Active.Value = s.NewValue, true); + KeyboardSelected.BindValueChanged(k => panel.KeyboardActive.Value = k.NewValue, true); } protected override void PrepareForUse() @@ -271,63 +191,25 @@ namespace osu.Game.Screens.SelectV2 Debug.Assert(Item != null); var beatmap = (BeatmapInfo)Item.Model; - iconContainer.Icon = beatmap.Ruleset.CreateInstance().CreateIcon(); + difficultyIcon.Icon = beatmap.Ruleset.CreateInstance().CreateIcon(); difficultyRank.Beatmap = beatmap; difficultyText.Text = beatmap.DifficultyName; authorText.Text = BeatmapsetsStrings.ShowDetailsMappedBy(beatmap.Metadata.Author.Username); - starDifficultyBindable = null; - computeStarRating(); updateKeyCount(); - updateSelectionDisplay(); FinishTransforms(true); - this.FadeInFromZero(duration, Easing.OutQuint); - - // todo: only do this when visible. - // starCounter.ReplayAnimation(); } - private void updateSelectionDisplay() + protected override void FreeAfterUse() { - bool selected = Selected.Value; + base.FreeAfterUse(); - rightContainer.ResizeHeightTo(selected ? HEIGHT - 4 : HEIGHT, duration, Easing.OutQuint); - - updatePanelPosition(); - updateEdgeEffectColour(); - } - - private void updateKeyboardSelectedDisplay() - { - updatePanelPosition(); - updateHover(); - } - - private void updatePanelPosition() - { - float x = difficulty_x_offset + selected_x_offset + preselected_x_offset; - - if (Selected.Value) - x -= selected_x_offset; - - if (KeyboardSelected.Value) - x -= preselected_x_offset; - - this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); - } - - private void updateHover() - { - bool hovered = IsHovered || KeyboardSelected.Value; - - if (hovered) - hoverLayer.FadeIn(100, Easing.OutQuint); - else - hoverLayer.FadeOut(1000, Easing.OutQuint); + difficultyRank.Beatmap = null; + starDifficultyBindable = null; } private void computeStarRating() @@ -341,34 +223,7 @@ namespace osu.Game.Screens.SelectV2 var beatmap = (BeatmapInfo)Item.Model; starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token); - starDifficultyBindable.BindValueChanged(d => - { - var value = d.NewValue ?? default; - - starRatingDisplay.Current.Value = value; - starCounter.Current = (float)value.Stars; - - iconContainer.FadeColour(value.Stars > 6.5f ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint); - - var starRatingColour = colours.ForStarDifficulty(value.Stars); - - backgroundBorder.FadeColour(starRatingColour, duration, Easing.OutQuint); - starCounter.FadeColour(starRatingColour, duration, Easing.OutQuint); - starRatingGradient.FadeColour(ColourInfo.GradientHorizontal(starRatingColour.Opacity(0.25f), starRatingColour.Opacity(0)), duration, Easing.OutQuint); - starRatingGradient.FadeIn(duration, Easing.OutQuint); - - // todo: this doesn't work for dark star rating colours, still not sure how to fix. - activationFlash.FadeColour(starRatingColour, duration, Easing.OutQuint); - - updateEdgeEffectColour(); - }, true); - } - - private void updateEdgeEffectColour() - { - panel.FadeEdgeEffectTo(Selected.Value - ? colours.ForStarDifficulty(starDifficultyBindable?.Value?.Stars ?? 0f).Opacity(0.5f) - : Color4.Black.Opacity(0.4f), duration, Easing.OutQuint); + starDifficultyBindable.BindValueChanged(_ => updateDisplay(), true); } private void updateKeyCount() @@ -392,16 +247,18 @@ namespace osu.Game.Screens.SelectV2 keyCountText.Alpha = 0; } - protected override bool OnHover(HoverEvent e) + private void updateDisplay() { - updateHover(); - return true; - } + var starDifficulty = starDifficultyBindable?.Value ?? default; - protected override void OnHoverLost(HoverLostEvent e) - { - updateHover(); - base.OnHoverLost(e); + starRatingDisplay.Current.Value = starDifficulty; + starCounter.Current = (float)starDifficulty.Stars; + + difficultyIcon.FadeColour(starDifficulty.Stars > 6.5f ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint); + + var starRatingColour = colours.ForStarDifficulty(starDifficulty.Stars); + starCounter.FadeColour(starRatingColour, duration, Easing.OutQuint); + panel.AccentColour = starRatingColour; } protected override bool OnClick(ClickEvent e) @@ -430,7 +287,7 @@ namespace osu.Game.Screens.SelectV2 public void Activated() { - activationFlash.FadeOutFromOne(500, Easing.OutQuint); + panel.Flash(); } #endregion diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index aabc39f27f..f5d7e0594b 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -6,12 +6,9 @@ using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Pooling; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Localisation; @@ -19,10 +16,8 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; -using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { @@ -30,18 +25,22 @@ namespace osu.Game.Screens.SelectV2 { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; - private const float arrow_container_width = 20; - private const float corner_radius = 10; - // todo: this should be replaced with information from CarouselItem about how deep is BeatmapPanel in the carousel // (i.e. whether it's under a beatmap set that's under a group, or just under a top-level beatmap set). private const float set_x_offset = 20f; // constant X offset for beatmap set/standalone panels specifically. - private const float preselected_x_offset = 25f; - private const float expanded_x_offset = 50f; - private const float duration = 500; + private CarouselPanelPiece panel = null!; + private BeatmapSetPanelBackground background = null!; + + private OsuSpriteText titleText = null!; + private OsuSpriteText artistText = null!; + private Drawable chevronIcon = null!; + private UpdateBeatmapSetButtonV2 updateButton = null!; + private BeatmapSetOnlineStatusPill statusPill = null!; + private DifficultySpectrumDisplay difficultiesDisplay = null!; + [Resolved] private BeatmapCarousel? carousel { get; set; } @@ -51,22 +50,15 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private BeatmapManager beatmaps { get; set; } = null!; - [Resolved] - private OsuColour colours { get; set; } = null!; + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) + { + var inputRectangle = panel.TopLevelContent.DrawRectangle; - private Container panel = null!; - private Box backgroundBorder = null!; - private BeatmapSetPanelBackground background = null!; - private Container backgroundContainer = null!; - private FillFlowContainer mainFlowContainer = null!; - private SpriteIcon chevronIcon = null!; - private Box hoverLayer = null!; + // Cover a gap introduced by the spacing between a BeatmapSetPanel and a BeatmapPanel either above it or below it. + inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - private OsuSpriteText titleText = null!; - private OsuSpriteText artistText = null!; - private UpdateBeatmapSetButtonV2 updateButton = null!; - private BeatmapSetOnlineStatusPill statusPill = null!; - private DifficultySpectrumDisplay difficultiesDisplay = null!; + return inputRectangle.Contains(panel.TopLevelContent.ToLocalSpace(screenSpacePos)); + } [BackgroundDependencyLoader] private void load() @@ -76,137 +68,89 @@ namespace osu.Game.Screens.SelectV2 RelativeSizeAxes = Axes.X; Height = HEIGHT; - InternalChild = panel = new Container + InternalChild = panel = new CarouselPanelPiece(set_x_offset) { - Masking = true, - CornerRadius = corner_radius, - RelativeSizeAxes = Axes.Both, - X = corner_radius, - EdgeEffect = new EdgeEffectParameters + Icon = chevronIcon = new Container { - Type = EdgeEffectType.Shadow, - Radius = 10, - }, - Children = new Drawable[] - { - new BufferedContainer + Size = new Vector2(22), + Child = new SpriteIcon { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - backgroundBorder = new Box - { - RelativeSizeAxes = Axes.Y, - Alpha = 0, - EdgeSmoothness = new Vector2(2, 0), - }, - backgroundContainer = new Container - { - Masking = true, - CornerRadius = corner_radius, - RelativeSizeAxes = Axes.X, - MaskingSmoothness = 2, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Children = new Drawable[] - { - background = new BeatmapSetPanelBackground - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - }, - }, - }, - } - }, - chevronIcon = new SpriteIcon - { - X = arrow_container_width / 2, + Anchor = Anchor.Centre, Origin = Anchor.Centre, - Anchor = Anchor.CentreLeft, Icon = FontAwesome.Solid.ChevronRight, Size = new Vector2(12), + X = 1f, Colour = colourProvider.Background5, }, - mainFlowContainer = new FillFlowContainer + }, + Background = background = new BeatmapSetPanelBackground + { + RelativeSizeAxes = Axes.Both, + }, + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Top = 7.5f, Left = 15, Bottom = 5 }, + Children = new Drawable[] { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Padding = new MarginPadding { Top = 7.5f, Left = 15, Bottom = 5 }, - Children = new Drawable[] + titleText = new OsuSpriteText { - titleText = new OsuSpriteText + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), + }, + artistText = new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), + }, + new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Top = 5f }, + Children = new Drawable[] { - Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), - Shadow = true, - }, - artistText = new OsuSpriteText - { - Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), - Shadow = true, - }, - new FillFlowContainer - { - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Top = 5f }, - Children = new Drawable[] + updateButton = new UpdateBeatmapSetButtonV2 { - updateButton = new UpdateBeatmapSetButtonV2 - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Margin = new MarginPadding { Right = 5f, Top = -2f }, - }, - statusPill = new BeatmapSetOnlineStatusPill - { - AutoSizeAxes = Axes.Both, - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - TextSize = 11, - TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, - Margin = new MarginPadding { Right = 5f }, - }, - difficultiesDisplay = new DifficultySpectrumDisplay - { - DotSize = new Vector2(5, 10), - DotSpacing = 2, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Right = 5f, Top = -2f }, }, - } + statusPill = new BeatmapSetOnlineStatusPill + { + AutoSizeAxes = Axes.Both, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + TextSize = 11, + TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, + Margin = new MarginPadding { Right = 5f }, + }, + difficultiesDisplay = new DifficultySpectrumDisplay + { + DotSize = new Vector2(5, 10), + DotSpacing = 2, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + }, } - }, - hoverLayer = new Box - { - Colour = colours.Blue.Opacity(0.1f), - Alpha = 0, - Blending = BlendingParameters.Additive, - RelativeSizeAxes = Axes.Both, - }, - new HoverSounds(), + } } }; } - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) - { - var inputRectangle = panel.DrawRectangle; - - // Cover a gap introduced by the spacing between a BeatmapSetPanel and a BeatmapPanel either above it or below it. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - - return inputRectangle.Contains(panel.ToLocalSpace(screenSpacePos)); - } - protected override void LoadComplete() { base.LoadComplete(); - Expanded.BindValueChanged(_ => updateExpandedDisplay(), true); - KeyboardSelected.BindValueChanged(_ => updateKeyboardSelectedDisplay(), true); + Expanded.BindValueChanged(_ => onExpanded(), true); + KeyboardSelected.BindValueChanged(k => panel.KeyboardActive.Value = k.NewValue, true); + } + + private void onExpanded() + { + panel.Active.Value = Expanded.Value; + chevronIcon.ResizeWidthTo(Expanded.Value ? 22 : 0f, duration, Easing.OutQuint); + chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); } protected override void PrepareForUse() @@ -226,9 +170,7 @@ namespace osu.Game.Screens.SelectV2 statusPill.Status = beatmapSet.Status; difficultiesDisplay.BeatmapSet = beatmapSet; - updateExpandedDisplay(); FinishTransforms(true); - this.FadeInFromZero(duration, Easing.OutQuint); } @@ -241,70 +183,6 @@ namespace osu.Game.Screens.SelectV2 difficultiesDisplay.BeatmapSet = null; } - private void updateExpandedDisplay() - { - if (Item == null) - return; - - updatePanelPosition(); - - backgroundBorder.RelativeSizeAxes = Expanded.Value ? Axes.Both : Axes.Y; - backgroundBorder.Width = Expanded.Value ? 1 : arrow_container_width + corner_radius; - backgroundBorder.FadeTo(Expanded.Value ? 1 : 0, duration, Easing.OutQuint); - chevronIcon.FadeTo(Expanded.Value ? 1 : 0, duration, Easing.OutQuint); - - backgroundContainer.ResizeHeightTo(Expanded.Value ? HEIGHT - 4 : HEIGHT, duration, Easing.OutQuint); - backgroundContainer.MoveToX(Expanded.Value ? arrow_container_width : 0, duration, Easing.OutQuint); - mainFlowContainer.MoveToX(Expanded.Value ? arrow_container_width : 0, duration, Easing.OutQuint); - - panel.EdgeEffect = panel.EdgeEffect with { Radius = Expanded.Value ? 15 : 10 }; - - panel.FadeEdgeEffectTo(Expanded.Value - ? Color4Extensions.FromHex(@"4EBFFF").Opacity(0.5f) - : Color4.Black.Opacity(0.4f), duration, Easing.OutQuint); - } - - private void updateKeyboardSelectedDisplay() - { - updatePanelPosition(); - updateHover(); - } - - private void updatePanelPosition() - { - float x = set_x_offset + expanded_x_offset + preselected_x_offset; - - if (Expanded.Value) - x -= expanded_x_offset; - - if (KeyboardSelected.Value) - x -= preselected_x_offset; - - this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); - } - - private void updateHover() - { - bool hovered = IsHovered || KeyboardSelected.Value; - - if (hovered) - hoverLayer.FadeIn(100, Easing.OutQuint); - else - hoverLayer.FadeOut(1000, Easing.OutQuint); - } - - protected override bool OnHover(HoverEvent e) - { - updateHover(); - return true; - } - - protected override void OnHoverLost(HoverLostEvent e) - { - updateHover(); - base.OnHoverLost(e); - } - protected override bool OnClick(ClickEvent e) { if (carousel != null) diff --git a/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs b/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs index c0a5f828f4..a8fa2224d7 100644 --- a/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs @@ -8,12 +8,9 @@ using System.Linq; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Pooling; -using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Beatmaps; @@ -21,13 +18,11 @@ using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osuTK; -using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { @@ -35,15 +30,9 @@ namespace osu.Game.Screens.SelectV2 { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; - private const float difficulty_icon_container_width = 30; - private const float corner_radius = 10; - // todo: this should be replaced with information from CarouselItem about how deep is BeatmapPanel in the carousel // (i.e. whether it's under a beatmap set that's under a group, or just under a top-level beatmap set). - private const float set_x_offset = 20f; // constant X offset for beatmap set/standalone panels specifically. - - private const float preselected_x_offset = 25f; - private const float selected_x_offset = 50f; + private const float standalone_x_offset = 20f; // constant X offset for beatmap set/standalone panels specifically. private const float duration = 500; @@ -71,12 +60,8 @@ namespace osu.Game.Screens.SelectV2 private IBindable? starDifficultyBindable; private CancellationTokenSource? starDifficultyCancellationSource; - private Container panel = null!; - private Box backgroundBorder = null!; + private CarouselPanelPiece panel = null!; private BeatmapSetPanelBackground background = null!; - private Container backgroundContainer = null!; - private FillFlowContainer mainFlowContainer = null!; - private Box hoverLayer = null!; private OsuSpriteText titleText = null!; private OsuSpriteText artistText = null!; @@ -91,6 +76,16 @@ namespace osu.Game.Screens.SelectV2 private OsuSpriteText difficultyName = null!; private OsuSpriteText difficultyAuthor = null!; + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) + { + var inputRectangle = panel.TopLevelContent.DrawRectangle; + + // Cover a gap introduced by the spacing between a BeatmapSetPanel and a BeatmapPanel either above it or below it. + inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); + + return inputRectangle.Contains(panel.TopLevelContent.ToLocalSpace(screenSpacePos)); + } + [BackgroundDependencyLoader] private void load() { @@ -100,167 +95,109 @@ namespace osu.Game.Screens.SelectV2 Width = 1f; Height = HEIGHT; - InternalChild = panel = new Container + InternalChild = panel = new CarouselPanelPiece(standalone_x_offset) { - Masking = true, - CornerRadius = corner_radius, - RelativeSizeAxes = Axes.Both, - X = corner_radius, - EdgeEffect = new EdgeEffectParameters + Icon = difficultyIcon = new ConstrainedIconContainer { - Type = EdgeEffectType.Shadow, - Radius = 10, + Size = new Vector2(20), + Margin = new MarginPadding { Horizontal = 5f }, + Colour = colourProvider.Background5, }, - Children = new Drawable[] + Background = background = new BeatmapSetPanelBackground { - new BufferedContainer + RelativeSizeAxes = Axes.Both, + }, + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Top = 7.5f, Left = 15, Bottom = 5 }, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + titleText = new OsuSpriteText { - backgroundBorder = new Box + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), + Shadow = true, + }, + artistText = new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), + Shadow = true, + }, + new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Top = 5f }, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Y, - Alpha = 0, - EdgeSmoothness = new Vector2(2, 0), - }, - backgroundContainer = new Container - { - Masking = true, - CornerRadius = corner_radius, - RelativeSizeAxes = Axes.X, - MaskingSmoothness = 2, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Children = new Drawable[] + updateButton = new UpdateBeatmapSetButtonV2 { - background = new BeatmapSetPanelBackground - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - }, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Right = 5f, Top = -2f }, }, - }, - } - }, - difficultyIcon = new ConstrainedIconContainer - { - X = difficulty_icon_container_width / 2, - Origin = Anchor.Centre, - Anchor = Anchor.CentreLeft, - Size = new Vector2(20), - }, - mainFlowContainer = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Padding = new MarginPadding { Top = 7.5f, Left = 15, Bottom = 5 }, - Children = new Drawable[] - { - titleText = new OsuSpriteText - { - Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), - Shadow = true, - }, - artistText = new OsuSpriteText - { - Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), - Shadow = true, - }, - new FillFlowContainer - { - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Top = 5f }, - Children = new Drawable[] + statusPill = new BeatmapSetOnlineStatusPill { - updateButton = new UpdateBeatmapSetButtonV2 + AutoSizeAxes = Axes.Both, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + TextSize = 11, + TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, + Margin = new MarginPadding { Right = 5f }, + }, + difficultyLine = new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Margin = new MarginPadding { Right = 5f, Top = -2f }, - }, - statusPill = new BeatmapSetOnlineStatusPill - { - AutoSizeAxes = Axes.Both, - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - TextSize = 11, - TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, - Margin = new MarginPadding { Right = 5f }, - }, - difficultyLine = new FillFlowContainer - { - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - Children = new Drawable[] + difficultyStarRating = new StarRatingDisplay(default, StarRatingDisplaySize.Small) { - difficultyStarRating = new StarRatingDisplay(default, StarRatingDisplaySize.Small) - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Scale = new Vector2(8f / 9f), - Margin = new MarginPadding { Right = 5f }, - }, - difficultyRank = new TopLocalRankV2 - { - Scale = new Vector2(8f / 11), - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Margin = new MarginPadding { Right = 5f }, - }, - difficultyKeyCountText = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Alpha = 0, - Margin = new MarginPadding { Bottom = 2f }, - }, - difficultyName = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, - Margin = new MarginPadding { Right = 5f, Bottom = 2f }, - }, - difficultyAuthor = new OsuSpriteText - { - Colour = colourProvider.Content2, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold), - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, - Margin = new MarginPadding { Right = 5f, Bottom = 2f }, - } + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Scale = new Vector2(8f / 9f), + Margin = new MarginPadding { Right = 5f }, + }, + difficultyRank = new TopLocalRankV2 + { + Scale = new Vector2(8f / 11), + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Margin = new MarginPadding { Right = 5f }, + }, + difficultyKeyCountText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Alpha = 0, + Margin = new MarginPadding { Bottom = 2f }, + }, + difficultyName = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + Margin = new MarginPadding { Right = 5f, Bottom = 2f }, + }, + difficultyAuthor = new OsuSpriteText + { + Colour = colourProvider.Content2, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + Margin = new MarginPadding { Right = 5f, Bottom = 2f }, } - }, + } }, - } + }, } - }, - hoverLayer = new Box - { - Colour = colours.Blue.Opacity(0.1f), - Alpha = 0, - Blending = BlendingParameters.Additive, - RelativeSizeAxes = Axes.Both, - }, - new HoverSounds(), - } + } + }, }; } - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) - { - var inputRectangle = panel.DrawRectangle; - - // Cover a gap introduced by the spacing between a BeatmapSetPanel and a BeatmapPanel either above it or below it. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - - return inputRectangle.Contains(panel.ToLocalSpace(screenSpacePos)); - } - protected override void LoadComplete() { base.LoadComplete(); @@ -277,8 +214,8 @@ namespace osu.Game.Screens.SelectV2 updateKeyCount(); }, true); - Selected.BindValueChanged(_ => updateSelectedDisplay(), true); - KeyboardSelected.BindValueChanged(_ => updateKeyboardSelectedDisplay(), true); + Selected.BindValueChanged(s => panel.Active.Value = s.NewValue, true); + KeyboardSelected.BindValueChanged(k => panel.KeyboardActive.Value = k.NewValue, true); } protected override void PrepareForUse() @@ -308,7 +245,6 @@ namespace osu.Game.Screens.SelectV2 computeStarRating(); - updateSelectedDisplay(); FinishTransforms(true); this.FadeInFromZero(duration, Easing.OutQuint); @@ -324,55 +260,6 @@ namespace osu.Game.Screens.SelectV2 starDifficultyBindable = null; } - private void updateSelectedDisplay() - { - if (Item == null) - return; - - updatePanelPosition(); - - backgroundBorder.RelativeSizeAxes = Selected.Value ? Axes.Both : Axes.Y; - backgroundBorder.Width = Selected.Value ? 1 : difficulty_icon_container_width + corner_radius; - backgroundBorder.FadeTo(Selected.Value ? 1 : 0, duration, Easing.OutQuint); - difficultyIcon.FadeTo(Selected.Value ? 1 : 0, duration, Easing.OutQuint); - - backgroundContainer.ResizeHeightTo(Selected.Value ? HEIGHT - 4 : HEIGHT, duration, Easing.OutQuint); - backgroundContainer.MoveToX(Selected.Value ? difficulty_icon_container_width : 0, duration, Easing.OutQuint); - mainFlowContainer.MoveToX(Selected.Value ? difficulty_icon_container_width : 0, duration, Easing.OutQuint); - - panel.EdgeEffect = panel.EdgeEffect with { Radius = Selected.Value ? 15 : 10 }; - updateEdgeEffectColour(); - } - - private void updateKeyboardSelectedDisplay() - { - updatePanelPosition(); - updateHover(); - } - - private void updatePanelPosition() - { - float x = set_x_offset + selected_x_offset + preselected_x_offset; - - if (Selected.Value) - x -= selected_x_offset; - - if (KeyboardSelected.Value) - x -= preselected_x_offset; - - this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); - } - - private void updateHover() - { - bool hovered = IsHovered || KeyboardSelected.Value; - - if (hovered) - hoverLayer.FadeIn(100, Easing.OutQuint); - else - hoverLayer.FadeOut(1000, Easing.OutQuint); - } - private void computeStarRating() { starDifficultyCancellationSource?.Cancel(); @@ -384,23 +271,7 @@ namespace osu.Game.Screens.SelectV2 var beatmap = (BeatmapInfo)Item.Model; starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token); - starDifficultyBindable.BindValueChanged(d => - { - var value = d.NewValue ?? default; - - backgroundBorder.FadeColour(colours.ForStarDifficulty(value.Stars), duration, Easing.OutQuint); - difficultyIcon.FadeColour(value.Stars > 6.5f ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint); - difficultyStarRating.Current.Value = value; - - updateEdgeEffectColour(); - }, true); - } - - private void updateEdgeEffectColour() - { - panel.FadeEdgeEffectTo(Selected.Value - ? colours.ForStarDifficulty(starDifficultyBindable?.Value?.Stars ?? 0f).Opacity(0.5f) - : Color4.Black.Opacity(0.4f), duration, Easing.OutQuint); + starDifficultyBindable.BindValueChanged(_ => updateDisplay(), true); } private void updateKeyCount() @@ -424,16 +295,13 @@ namespace osu.Game.Screens.SelectV2 difficultyKeyCountText.Alpha = 0; } - protected override bool OnHover(HoverEvent e) + private void updateDisplay() { - updateHover(); - return true; - } + var starDifficulty = starDifficultyBindable?.Value ?? default; - protected override void OnHoverLost(HoverLostEvent e) - { - updateHover(); - base.OnHoverLost(e); + panel.AccentColour = colours.ForStarDifficulty(starDifficulty.Stars); + difficultyIcon.FadeColour(starDifficulty.Stars > 6.5f ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint); + difficultyStarRating.Current.Value = starDifficulty; } protected override bool OnClick(ClickEvent e) diff --git a/osu.Game/Screens/SelectV2/CarouselPanelPiece.cs b/osu.Game/Screens/SelectV2/CarouselPanelPiece.cs new file mode 100644 index 0000000000..a7f2b3a163 --- /dev/null +++ b/osu.Game/Screens/SelectV2/CarouselPanelPiece.cs @@ -0,0 +1,240 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class CarouselPanelPiece : Container + { + private const float corner_radius = 10; + + private const float left_edge_x_offset = 20f; + private const float keyboard_active_x_offset = 25f; + private const float active_x_offset = 50f; + + private const float duration = 500; + + private readonly float panelXOffset; + + private readonly Box backgroundBorder; + private readonly Box backgroundGradient; + private readonly Box backgroundAccentGradient; + private readonly Container backgroundLayer; + private readonly Container backgroundLayerHorizontalPadding; + private readonly Container backgroundContainer; + private readonly Container iconContainer; + private readonly Box activationFlash; + private readonly Box hoverLayer; + + public Container TopLevelContent { get; } + + protected override Container Content { get; } + + public Drawable Background + { + set => backgroundContainer.Child = value; + } + + public Drawable Icon + { + set => iconContainer.Child = value; + } + + private Color4? accentColour; + + public Color4? AccentColour + { + get => accentColour; + set + { + accentColour = value; + updateDisplay(); + } + } + + public readonly BindableBool Active = new BindableBool(); + public readonly BindableBool KeyboardActive = new BindableBool(); + + public CarouselPanelPiece(float panelXOffset) + { + this.panelXOffset = panelXOffset; + + RelativeSizeAxes = Axes.Both; + + InternalChild = TopLevelContent = new Container + { + Masking = true, + CornerRadius = corner_radius, + RelativeSizeAxes = Axes.Both, + X = corner_radius, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Offset = new Vector2(1f), + Radius = 10, + }, + Children = new Drawable[] + { + new BufferedContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + backgroundBorder = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + }, + backgroundLayerHorizontalPadding = new Container + { + RelativeSizeAxes = Axes.Both, + Child = backgroundLayer = new Container + { + RelativeSizeAxes = Axes.Both, + Child = new Container + { + Masking = true, + CornerRadius = corner_radius, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + backgroundGradient = new Box + { + RelativeSizeAxes = Axes.Both, + }, + backgroundAccentGradient = new Box + { + RelativeSizeAxes = Axes.Both, + }, + backgroundContainer = new Container + { + RelativeSizeAxes = Axes.Both, + }, + } + }, + }, + } + }, + }, + iconContainer = new Container + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + }, + Content = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Right = panelXOffset + corner_radius }, + }, + hoverLayer = new Box + { + Alpha = 0, + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + }, + activationFlash = new Box + { + Colour = Color4.White.Opacity(0.4f), + Blending = BlendingParameters.Additive, + Alpha = 0f, + RelativeSizeAxes = Axes.Both, + }, + new HoverSounds(), + } + }; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider, OsuColour colours) + { + hoverLayer.Colour = colours.Blue.Opacity(0.1f); + backgroundGradient.Colour = ColourInfo.GradientHorizontal(colourProvider.Background3, colourProvider.Background4); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Active.BindValueChanged(_ => updateDisplay()); + KeyboardActive.BindValueChanged(_ => updateDisplay(), true); + } + + public void Flash() + { + activationFlash.FadeOutFromOne(500, Easing.OutQuint); + } + + private void updateDisplay() + { + backgroundLayer.TransformTo(nameof(Padding), backgroundLayer.Padding with { Vertical = Active.Value ? 2f : 0f }, duration, Easing.OutQuint); + + var backgroundColour = accentColour ?? Color4.White; + var edgeEffectColour = accentColour ?? Color4Extensions.FromHex(@"4EBFFF"); + + backgroundAccentGradient.FadeColour(ColourInfo.GradientHorizontal(backgroundColour.Opacity(0.25f), backgroundColour.Opacity(0f)), duration, Easing.OutQuint); + backgroundBorder.FadeColour(backgroundColour, duration, Easing.OutQuint); + + TopLevelContent.FadeEdgeEffectTo(Active.Value ? edgeEffectColour.Opacity(0.5f) : Color4.Black.Opacity(0.4f), duration, Easing.OutQuint); + + updateXOffset(); + updateHover(); + } + + private void updateXOffset() + { + float x = panelXOffset + active_x_offset + keyboard_active_x_offset + left_edge_x_offset; + + if (Active.Value) + x -= active_x_offset; + + if (KeyboardActive.Value) + x -= keyboard_active_x_offset; + + this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); + } + + private void updateHover() + { + bool hovered = IsHovered || KeyboardActive.Value; + + if (hovered) + hoverLayer.FadeIn(100, Easing.OutQuint); + else + hoverLayer.FadeOut(1000, Easing.OutQuint); + } + + protected override bool OnHover(HoverEvent e) + { + updateDisplay(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateDisplay(); + base.OnHoverLost(e); + } + + protected override void Update() + { + base.Update(); + Content.Padding = Content.Padding with { Left = iconContainer.DrawWidth }; + backgroundLayerHorizontalPadding.Padding = new MarginPadding { Left = iconContainer.DrawWidth }; + } + } +} diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs index b5fa338f82..12c4df830c 100644 --- a/osu.Game/Screens/SelectV2/GroupPanel.cs +++ b/osu.Game/Screens/SelectV2/GroupPanel.cs @@ -10,10 +10,10 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; using osuTK.Graphics; @@ -24,137 +24,83 @@ namespace osu.Game.Screens.SelectV2 { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; - private const float corner_radius = 10; - - private const float glow_offset = 10f; // extra space for any edge effect to not be cutoff by the right edge of the carousel. - private const float preselected_x_offset = 25f; - private const float selected_x_offset = 50f; - private const float duration = 500; [Resolved] private BeatmapCarousel? carousel { get; set; } - private Container panel = null!; - private Box activationFlash = null!; + private CarouselPanelPiece panel = null!; + private Drawable chevronIcon = null!; private OsuSpriteText titleText = null!; - private Box hoverLayer = null!; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) { - var inputRectangle = panel.DrawRectangle; + var inputRectangle = panel.TopLevelContent.DrawRectangle; - // Cover a gap introduced by the spacing between a GroupPanel and a BeatmapPanel either below/above it. + // Cover a gap introduced by the spacing between a GroupPanel and other panel types either below/above it. inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - return inputRectangle.Contains(panel.ToLocalSpace(screenSpacePos)); + return inputRectangle.Contains(panel.TopLevelContent.ToLocalSpace(screenSpacePos)); } [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider, OsuColour colours) + private void load(OverlayColourProvider colourProvider) { Anchor = Anchor.TopRight; Origin = Anchor.TopRight; RelativeSizeAxes = Axes.X; Height = HEIGHT; - InternalChild = panel = new Container + InternalChild = panel = new CarouselPanelPiece(0) { - RelativeSizeAxes = Axes.Both, - CornerRadius = corner_radius, - Masking = true, - X = corner_radius, + Icon = chevronIcon = new SpriteIcon + { + AlwaysPresent = true, + Icon = FontAwesome.Solid.ChevronDown, + Size = new Vector2(12), + Margin = new MarginPadding { Horizontal = 5f }, + X = 2f, + Colour = colourProvider.Background3, + }, + Background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Dark1, + }, + AccentColour = colourProvider.Highlight1, Children = new Drawable[] { - new Container + titleText = new OsuSpriteText { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = 10f }, - Child = new Container + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + X = 10f, + }, + new CircularContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(50f, 14f), + Margin = new MarginPadding { Right = 20f }, + Masking = true, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - CornerRadius = corner_radius, - Masking = true, - Children = new Drawable[] + new Box { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background6, - }, - } - } - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background3, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = 10f }, - Child = new Container - { - RelativeSizeAxes = Axes.Both, - CornerRadius = corner_radius, - Masking = true, - Children = new Drawable[] + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.7f), + }, + new OsuSpriteText { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background5, - }, - titleText = new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - X = 10f, - }, - new CircularContainer - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Size = new Vector2(50f, 14f), - Margin = new MarginPadding { Right = 30f }, - Masking = true, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black.Opacity(0.7f), - }, - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), - // TODO: requires Carousel/CarouselItem-side implementation - Text = "43", - UseFullGlyphHeight = false, - } - }, - }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), + // TODO: requires Carousel/CarouselItem-side implementation + Text = "43", + UseFullGlyphHeight = false, } - } - }, - activationFlash = new Box - { - Colour = Color4.White, - Blending = BlendingParameters.Additive, - Alpha = 0, - RelativeSizeAxes = Axes.Both, - }, - hoverLayer = new Box - { - Colour = colours.Blue.Opacity(0.1f), - Alpha = 0, - Blending = BlendingParameters.Additive, - RelativeSizeAxes = Axes.Both, - }, - new HoverSounds(), + }, + } } }; } @@ -163,17 +109,17 @@ namespace osu.Game.Screens.SelectV2 { base.LoadComplete(); - Expanded.BindValueChanged(_ => updateExpandedDisplay(), true); - KeyboardSelected.BindValueChanged(_ => updateKeyboardSelectedDisplay(), true); + Expanded.BindValueChanged(_ => onExpanded(), true); + KeyboardSelected.BindValueChanged(k => panel.KeyboardActive.Value = k.NewValue, true); } - private void updateExpandedDisplay() + private void onExpanded() { - updatePanelPosition(); + panel.Active.Value = Expanded.Value; + panel.Flash(); - // todo: figma shares no extra visual feedback on this. - - activationFlash.FadeTo(0.2f).FadeTo(0f, 500, Easing.OutQuint); + chevronIcon.ResizeWidthTo(Expanded.Value ? 12f : 0f, duration, Easing.OutQuint); + chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); } protected override void PrepareForUse() @@ -186,6 +132,7 @@ namespace osu.Game.Screens.SelectV2 titleText.Text = group.Title; + FinishTransforms(true); this.FadeInFromZero(500, Easing.OutQuint); } @@ -197,47 +144,6 @@ namespace osu.Game.Screens.SelectV2 return true; } - private void updateKeyboardSelectedDisplay() - { - updatePanelPosition(); - updateHover(); - } - - private void updatePanelPosition() - { - float x = glow_offset + selected_x_offset + preselected_x_offset; - - if (Expanded.Value) - x -= selected_x_offset; - - if (KeyboardSelected.Value) - x -= preselected_x_offset; - - this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); - } - - private void updateHover() - { - bool hovered = IsHovered || KeyboardSelected.Value; - - if (hovered) - hoverLayer.FadeIn(100, Easing.OutQuint); - else - hoverLayer.FadeOut(1000, Easing.OutQuint); - } - - protected override bool OnHover(HoverEvent e) - { - updateHover(); - return true; - } - - protected override void OnHoverLost(HoverLostEvent e) - { - updateHover(); - base.OnHoverLost(e); - } - #region ICarouselPanel public CarouselItem? Item { get; set; } @@ -249,7 +155,7 @@ namespace osu.Game.Screens.SelectV2 public void Activated() { - // sets should never be activated. + // groups should never be activated. throw new InvalidOperationException(); } diff --git a/osu.Game/Screens/SelectV2/StarsGroupPanel.cs b/osu.Game/Screens/SelectV2/StarsGroupPanel.cs index 76e3da2500..8e179ec5c1 100644 --- a/osu.Game/Screens/SelectV2/StarsGroupPanel.cs +++ b/osu.Game/Screens/SelectV2/StarsGroupPanel.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; @@ -26,10 +27,6 @@ namespace osu.Game.Screens.SelectV2 { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; - private const float glow_offset = 10f; // extra space for the edge effect to not be cutoff by the right edge of the carousel. - private const float preselected_x_offset = 25f; - private const float expanded_x_offset = 50f; - private const float duration = 500; [Resolved] @@ -41,20 +38,20 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - private Box activationFlash = null!; - private Box outerLayer = null!; + private CarouselPanelPiece panel = null!; + private Drawable chevronIcon = null!; + private Box contentBackground = null!; private StarRatingDisplay starRatingDisplay = null!; private StarCounter starCounter = null!; - private Box hoverLayer = null!; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) { - var inputRectangle = DrawRectangle; + var inputRectangle = panel.TopLevelContent.DrawRectangle; - // Cover a gap introduced by the spacing between a GroupPanel and a BeatmapPanel either below/above it. + // Cover a gap introduced by the spacing between a GroupPanel and other panel types either below/above it. inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); + return inputRectangle.Contains(panel.TopLevelContent.ToLocalSpace(screenSpacePos)); } [BackgroundDependencyLoader] @@ -65,118 +62,71 @@ namespace osu.Game.Screens.SelectV2 RelativeSizeAxes = Axes.X; Height = HEIGHT; - InternalChild = new Container + InternalChild = panel = new CarouselPanelPiece(0) { - RelativeSizeAxes = Axes.Both, - CornerRadius = 10f, - Masking = true, + Icon = chevronIcon = new SpriteIcon + { + AlwaysPresent = true, + Icon = FontAwesome.Solid.ChevronDown, + Size = new Vector2(12), + Margin = new MarginPadding { Horizontal = 5f }, + X = 2f, + }, + Background = contentBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Dark1, + }, + AccentColour = colourProvider.Highlight1, Children = new Drawable[] { - new Container + new FillFlowContainer { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = 10f }, - Child = new Container + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(10f, 0f), + Margin = new MarginPadding { Left = 10f }, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - CornerRadius = 10f, - Masking = true, - Children = new Drawable[] + starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small) { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background6, - }, - } + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + starCounter = new StarCounter + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Scale = new Vector2(8f / 20f), + }, } }, - outerLayer = new Box + new CircularContainer { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background3, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = 10f }, - Child = new Container + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(50f, 14f), + Margin = new MarginPadding { Right = 20f }, + Masking = true, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - CornerRadius = 10f, - Masking = true, - Children = new Drawable[] + new Box { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black.Opacity(0.2f), - }, - new FillFlowContainer - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(10f, 0f), - Margin = new MarginPadding { Left = 10f }, - Children = new Drawable[] - { - starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - starCounter = new StarCounter - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Scale = new Vector2(8f / 20f), - }, - } - }, - new CircularContainer - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Size = new Vector2(50f, 14f), - Margin = new MarginPadding { Right = 30f }, - Masking = true, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black.Opacity(0.7f), - }, - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), - // TODO: requires Carousel/CarouselItem-side implementation - Text = "43", - UseFullGlyphHeight = false, - } - }, - }, + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.7f), + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), + // TODO: requires Carousel/CarouselItem-side implementation + Text = "43", + UseFullGlyphHeight = false, } - } - }, - activationFlash = new Box - { - Colour = Color4.White, - Blending = BlendingParameters.Additive, - Alpha = 0, - RelativeSizeAxes = Axes.Both, - }, - hoverLayer = new Box - { - Colour = colours.Blue.Opacity(0.1f), - Alpha = 0, - Blending = BlendingParameters.Additive, - RelativeSizeAxes = Axes.Both, - }, - new HoverSounds(), + }, + } } }; } @@ -185,17 +135,17 @@ namespace osu.Game.Screens.SelectV2 { base.LoadComplete(); - Expanded.BindValueChanged(_ => updateExpandedDisplay(), true); - KeyboardSelected.BindValueChanged(_ => updateKeyboardSelectedDisplay(), true); + Expanded.BindValueChanged(_ => onExpanded(), true); + KeyboardSelected.BindValueChanged(k => panel.KeyboardActive.Value = k.NewValue, true); } - private void updateExpandedDisplay() + private void onExpanded() { - updatePanelPosition(); + panel.Active.Value = Expanded.Value; + panel.Flash(); - // todo: figma shares no extra visual feedback on this. - - activationFlash.FadeTo(0.2f).FadeTo(0f, 500, Easing.OutQuint); + chevronIcon.ResizeWidthTo(Expanded.Value ? 12f : 0f, duration, Easing.OutQuint); + chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); } protected override void PrepareForUse() @@ -209,12 +159,15 @@ namespace osu.Game.Screens.SelectV2 Color4 colour = group.StarNumber >= 9 ? OsuColour.Gray(0.2f) : colours.ForStarDifficulty(group.StarNumber); Color4 contentColour = group.StarNumber >= 7 ? colours.Orange1 : colourProvider.Background5; - outerLayer.Colour = colour; - starCounter.Colour = contentColour; + panel.AccentColour = colour; + contentBackground.Colour = colour.Darken(0.3f); starRatingDisplay.Current.Value = new StarDifficulty(group.StarNumber, 0); starCounter.Current = group.StarNumber; + chevronIcon.Colour = contentColour; + starCounter.Colour = contentColour; + this.FadeInFromZero(500, Easing.OutQuint); } @@ -226,47 +179,6 @@ namespace osu.Game.Screens.SelectV2 return true; } - private void updateKeyboardSelectedDisplay() - { - updatePanelPosition(); - updateHover(); - } - - private void updatePanelPosition() - { - float x = glow_offset + expanded_x_offset + preselected_x_offset; - - if (Expanded.Value) - x -= expanded_x_offset; - - if (KeyboardSelected.Value) - x -= preselected_x_offset; - - this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); - } - - private void updateHover() - { - bool hovered = IsHovered || KeyboardSelected.Value; - - if (hovered) - hoverLayer.FadeIn(100, Easing.OutQuint); - else - hoverLayer.FadeOut(1000, Easing.OutQuint); - } - - protected override bool OnHover(HoverEvent e) - { - updateHover(); - return true; - } - - protected override void OnHoverLost(HoverLostEvent e) - { - updateHover(); - base.OnHoverLost(e); - } - #region ICarouselPanel public CarouselItem? Item { get; set; } From 3ab208bb4643e6bd0512bd5b274d958cbef3a8fc Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 6 Feb 2025 02:21:44 -0500 Subject: [PATCH 366/801] Fix group visual test scene --- .../SongSelectV2/TestSceneBeatmapCarouselGroupPanel.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselGroupPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselGroupPanel.cs index eea3870117..d9f4a1630f 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselGroupPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselGroupPanel.cs @@ -41,13 +41,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2 new GroupPanel { Item = new CarouselItem(new GroupDefinition("Group A")), - Selected = { Value = true } + Expanded = { Value = true } }, new GroupPanel { Item = new CarouselItem(new GroupDefinition("Group A")), KeyboardSelected = { Value = true }, - Selected = { Value = true } + Expanded = { Value = true } }, new StarsGroupPanel { @@ -56,6 +56,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 new StarsGroupPanel { Item = new CarouselItem(new StarsGroupDefinition(3)), + Expanded = { Value = true } }, new StarsGroupPanel { @@ -64,6 +65,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 new StarsGroupPanel { Item = new CarouselItem(new StarsGroupDefinition(7)), + Expanded = { Value = true } }, new StarsGroupPanel { @@ -72,6 +74,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 new StarsGroupPanel { Item = new CarouselItem(new StarsGroupDefinition(9)), + Expanded = { Value = true } }, } }; From e1d6ce5ff44569c0b911540ebabbf116319b0eab Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 6 Feb 2025 02:25:12 -0500 Subject: [PATCH 367/801] Add V2 suffix for easier test browsing --- ...yPanel.cs => TestSceneBeatmapCarouselV2DifficultyPanel.cs} | 4 ++-- ...lGroupPanel.cs => TestSceneBeatmapCarouselV2GroupPanel.cs} | 4 ++-- ...ouselSetPanel.cs => TestSceneBeatmapCarouselV2SetPanel.cs} | 4 ++-- ...ePanel.cs => TestSceneBeatmapCarouselV2StandalonePanel.cs} | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) rename osu.Game.Tests/Visual/SongSelectV2/{TestSceneBeatmapCarouselDifficultyPanel.cs => TestSceneBeatmapCarouselV2DifficultyPanel.cs} (95%) rename osu.Game.Tests/Visual/SongSelectV2/{TestSceneBeatmapCarouselGroupPanel.cs => TestSceneBeatmapCarouselV2GroupPanel.cs} (95%) rename osu.Game.Tests/Visual/SongSelectV2/{TestSceneBeatmapCarouselSetPanel.cs => TestSceneBeatmapCarouselV2SetPanel.cs} (95%) rename osu.Game.Tests/Visual/SongSelectV2/{TestSceneBeatmapCarouselStandalonePanel.cs => TestSceneBeatmapCarouselV2StandalonePanel.cs} (95%) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs similarity index 95% rename from osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyPanel.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs index a9f73759f7..93472e7b81 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs @@ -18,14 +18,14 @@ using osuTK; namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneBeatmapCarouselDifficultyPanel : ThemeComparisonTestScene + public partial class TestSceneBeatmapCarouselV2DifficultyPanel : ThemeComparisonTestScene { [Resolved] private BeatmapManager beatmaps { get; set; } = null!; private BeatmapInfo beatmap = null!; - public TestSceneBeatmapCarouselDifficultyPanel() + public TestSceneBeatmapCarouselV2DifficultyPanel() : base(false) { } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselGroupPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs similarity index 95% rename from osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselGroupPanel.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs index d9f4a1630f..9808e41f73 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselGroupPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs @@ -9,9 +9,9 @@ using osuTK; namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneBeatmapCarouselGroupPanel : ThemeComparisonTestScene + public partial class TestSceneBeatmapCarouselV2GroupPanel : ThemeComparisonTestScene { - public TestSceneBeatmapCarouselGroupPanel() + public TestSceneBeatmapCarouselV2GroupPanel() : base(false) { } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs similarity index 95% rename from osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetPanel.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs index 8f7cac2b58..540eae3be0 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs @@ -16,14 +16,14 @@ using osuTK; namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneBeatmapCarouselSetPanel : ThemeComparisonTestScene + public partial class TestSceneBeatmapCarouselV2SetPanel : ThemeComparisonTestScene { [Resolved] private BeatmapManager beatmaps { get; set; } = null!; private BeatmapSetInfo beatmapSet = null!; - public TestSceneBeatmapCarouselSetPanel() + public TestSceneBeatmapCarouselV2SetPanel() : base(false) { } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselStandalonePanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs similarity index 95% rename from osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselStandalonePanel.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs index a34ac31d5d..72f7a9e98c 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselStandalonePanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs @@ -18,14 +18,14 @@ using osuTK; namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneBeatmapCarouselStandalonePanel : ThemeComparisonTestScene + public partial class TestSceneBeatmapCarouselV2StandalonePanel : ThemeComparisonTestScene { [Resolved] private BeatmapManager beatmaps { get; set; } = null!; private BeatmapInfo beatmap = null!; - public TestSceneBeatmapCarouselStandalonePanel() + public TestSceneBeatmapCarouselV2StandalonePanel() : base(false) { } From 5e74d82fc101f03d945033be96e184b0199016a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Feb 2025 08:32:08 +0100 Subject: [PATCH 368/801] Suppress inspections for now --- osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs index 85981448da..bb9d32f77b 100644 --- a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs +++ b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs @@ -25,7 +25,11 @@ namespace osu.Game.Online.API.Requests protected override string Target => throw new NotSupportedException(); public uint BeatmapSetID { get; } + + // ReSharper disable once CollectionNeverUpdated.Global public Dictionary FilesChanged { get; } = new Dictionary(); + + // ReSharper disable once CollectionNeverUpdated.Global public HashSet FilesDeleted { get; } = new HashSet(); public PatchBeatmapPackageRequest(uint beatmapSetId) From e1a146d487300feb616adcf100563945aa3d17e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Feb 2025 08:38:28 +0100 Subject: [PATCH 369/801] Remove unnecessary suppressions --- osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs index bb9d32f77b..a59a708079 100644 --- a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs +++ b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs @@ -26,10 +26,8 @@ namespace osu.Game.Online.API.Requests public uint BeatmapSetID { get; } - // ReSharper disable once CollectionNeverUpdated.Global public Dictionary FilesChanged { get; } = new Dictionary(); - // ReSharper disable once CollectionNeverUpdated.Global public HashSet FilesDeleted { get; } = new HashSet(); public PatchBeatmapPackageRequest(uint beatmapSetId) From 78cd093a47f70403428eb40f020c8a8beffc522e Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 6 Feb 2025 02:44:40 -0500 Subject: [PATCH 370/801] Fix broken input handling with structural changes --- osu.Game/Screens/SelectV2/BeatmapPanel.cs | 24 ++++--------------- osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 16 ++----------- .../SelectV2/BeatmapStandalonePanel.cs | 23 +++++++----------- .../Screens/SelectV2/CarouselPanelPiece.cs | 21 +++++++++++++++- osu.Game/Screens/SelectV2/GroupPanel.cs | 16 ++----------- osu.Game/Screens/SelectV2/StarsGroupPanel.cs | 16 ++----------- 6 files changed, 39 insertions(+), 77 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs index bd4cf6d7cf..a878f966b8 100644 --- a/osu.Game/Screens/SelectV2/BeatmapPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapPanel.cs @@ -9,7 +9,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; -using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; @@ -64,16 +63,6 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private IBindable> mods { get; set; } = null!; - protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) - { - var inputRectangle = panel.TopLevelContent.DrawRectangle; - - // Cover the gaps introduced by the spacing between BeatmapPanels. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - - return inputRectangle.Contains(panel.TopLevelContent.ToLocalSpace(screenSpacePos)); - } - [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { @@ -86,6 +75,7 @@ namespace osu.Game.Screens.SelectV2 InternalChild = panel = new CarouselPanelPiece(difficulty_x_offset) { + Action = onAction, Icon = difficultyIcon = new ConstrainedIconContainer { Size = new Vector2(20), @@ -261,19 +251,15 @@ namespace osu.Game.Screens.SelectV2 panel.AccentColour = starRatingColour; } - protected override bool OnClick(ClickEvent e) + private void onAction() { if (carousel == null) - return true; + return; if (carousel.CurrentSelection != Item!.Model) - { carousel.CurrentSelection = Item!.Model; - return true; - } - - carousel.TryActivateSelection(); - return true; + else + carousel.TryActivateSelection(); } #region ICarouselPanel diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index f5d7e0594b..951e76e0bc 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; @@ -50,16 +49,6 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private BeatmapManager beatmaps { get; set; } = null!; - protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) - { - var inputRectangle = panel.TopLevelContent.DrawRectangle; - - // Cover a gap introduced by the spacing between a BeatmapSetPanel and a BeatmapPanel either above it or below it. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - - return inputRectangle.Contains(panel.TopLevelContent.ToLocalSpace(screenSpacePos)); - } - [BackgroundDependencyLoader] private void load() { @@ -70,6 +59,7 @@ namespace osu.Game.Screens.SelectV2 InternalChild = panel = new CarouselPanelPiece(set_x_offset) { + Action = onAction, Icon = chevronIcon = new Container { Size = new Vector2(22), @@ -183,12 +173,10 @@ namespace osu.Game.Screens.SelectV2 difficultiesDisplay.BeatmapSet = null; } - protected override bool OnClick(ClickEvent e) + private void onAction() { if (carousel != null) carousel.CurrentSelection = Item!.Model; - - return true; } #region ICarouselPanel diff --git a/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs b/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs index a8fa2224d7..8e201ec5bc 100644 --- a/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs @@ -11,7 +11,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; -using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; @@ -76,16 +75,6 @@ namespace osu.Game.Screens.SelectV2 private OsuSpriteText difficultyName = null!; private OsuSpriteText difficultyAuthor = null!; - protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) - { - var inputRectangle = panel.TopLevelContent.DrawRectangle; - - // Cover a gap introduced by the spacing between a BeatmapSetPanel and a BeatmapPanel either above it or below it. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - - return inputRectangle.Contains(panel.TopLevelContent.ToLocalSpace(screenSpacePos)); - } - [BackgroundDependencyLoader] private void load() { @@ -97,6 +86,7 @@ namespace osu.Game.Screens.SelectV2 InternalChild = panel = new CarouselPanelPiece(standalone_x_offset) { + Action = onAction, Icon = difficultyIcon = new ConstrainedIconContainer { Size = new Vector2(20), @@ -304,12 +294,15 @@ namespace osu.Game.Screens.SelectV2 difficultyStarRating.Current.Value = starDifficulty; } - protected override bool OnClick(ClickEvent e) + private void onAction() { - if (carousel != null) - carousel.CurrentSelection = Item!.Model; + if (carousel == null) + return; - return true; + if (carousel.CurrentSelection != Item!.Model) + carousel.CurrentSelection = Item!.Model; + else + carousel.TryActivateSelection(); } #region ICarouselPanel diff --git a/osu.Game/Screens/SelectV2/CarouselPanelPiece.cs b/osu.Game/Screens/SelectV2/CarouselPanelPiece.cs index a7f2b3a163..4b533e362a 100644 --- a/osu.Game/Screens/SelectV2/CarouselPanelPiece.cs +++ b/osu.Game/Screens/SelectV2/CarouselPanelPiece.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -69,6 +70,18 @@ namespace osu.Game.Screens.SelectV2 public readonly BindableBool Active = new BindableBool(); public readonly BindableBool KeyboardActive = new BindableBool(); + public Action? Action; + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + var inputRectangle = TopLevelContent.DrawRectangle; + + // Cover potential gaps introduced by the spacing between panels. + inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); + + return inputRectangle.Contains(TopLevelContent.ToLocalSpace(screenSpacePos)); + } + public CarouselPanelPiece(float panelXOffset) { this.panelXOffset = panelXOffset; @@ -221,7 +234,7 @@ namespace osu.Game.Screens.SelectV2 protected override bool OnHover(HoverEvent e) { updateDisplay(); - return base.OnHover(e); + return true; } protected override void OnHoverLost(HoverLostEvent e) @@ -230,6 +243,12 @@ namespace osu.Game.Screens.SelectV2 base.OnHoverLost(e); } + protected override bool OnClick(ClickEvent e) + { + Action?.Invoke(); + return true; + } + protected override void Update() { base.Update(); diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs index 12c4df830c..a757293e57 100644 --- a/osu.Game/Screens/SelectV2/GroupPanel.cs +++ b/osu.Game/Screens/SelectV2/GroupPanel.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; @@ -33,16 +32,6 @@ namespace osu.Game.Screens.SelectV2 private Drawable chevronIcon = null!; private OsuSpriteText titleText = null!; - protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) - { - var inputRectangle = panel.TopLevelContent.DrawRectangle; - - // Cover a gap introduced by the spacing between a GroupPanel and other panel types either below/above it. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - - return inputRectangle.Contains(panel.TopLevelContent.ToLocalSpace(screenSpacePos)); - } - [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { @@ -53,6 +42,7 @@ namespace osu.Game.Screens.SelectV2 InternalChild = panel = new CarouselPanelPiece(0) { + Action = onAction, Icon = chevronIcon = new SpriteIcon { AlwaysPresent = true, @@ -136,12 +126,10 @@ namespace osu.Game.Screens.SelectV2 this.FadeInFromZero(500, Easing.OutQuint); } - protected override bool OnClick(ClickEvent e) + private void onAction() { if (carousel != null) carousel.CurrentSelection = Item!.Model; - - return true; } #region ICarouselPanel diff --git a/osu.Game/Screens/SelectV2/StarsGroupPanel.cs b/osu.Game/Screens/SelectV2/StarsGroupPanel.cs index 8e179ec5c1..d345f9687e 100644 --- a/osu.Game/Screens/SelectV2/StarsGroupPanel.cs +++ b/osu.Game/Screens/SelectV2/StarsGroupPanel.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; @@ -44,16 +43,6 @@ namespace osu.Game.Screens.SelectV2 private StarRatingDisplay starRatingDisplay = null!; private StarCounter starCounter = null!; - protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) - { - var inputRectangle = panel.TopLevelContent.DrawRectangle; - - // Cover a gap introduced by the spacing between a GroupPanel and other panel types either below/above it. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - - return inputRectangle.Contains(panel.TopLevelContent.ToLocalSpace(screenSpacePos)); - } - [BackgroundDependencyLoader] private void load() { @@ -64,6 +53,7 @@ namespace osu.Game.Screens.SelectV2 InternalChild = panel = new CarouselPanelPiece(0) { + Action = onAction, Icon = chevronIcon = new SpriteIcon { AlwaysPresent = true, @@ -171,12 +161,10 @@ namespace osu.Game.Screens.SelectV2 this.FadeInFromZero(500, Easing.OutQuint); } - protected override bool OnClick(ClickEvent e) + private void onAction() { if (carousel != null) carousel.CurrentSelection = Item!.Model; - - return true; } #region ICarouselPanel From aa9727c020051e38d3ffc45f462e8f062ff11752 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 6 Feb 2025 02:44:52 -0500 Subject: [PATCH 371/801] Fix helper method in carousel test scene --- osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index a3f6eaf152..9f7b4468dc 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -185,6 +185,7 @@ namespace osu.Game.Tests.Visual.SongSelect .Where(p => ((ICarouselPanel)p).Item?.IsVisible == true) .OrderBy(p => p.Y) .ElementAt(index) + .ChildrenOfType().Single() .TriggerClick(); }); } From a25e1f4f9b3e9796905419b8aa310a356a3276e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 15:13:17 +0900 Subject: [PATCH 372/801] Add test coverage of artist grouping --- ...estSceneBeatmapCarouselV2ArtistGrouping.cs | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs new file mode 100644 index 0000000000..c7ab9de5e5 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs @@ -0,0 +1,170 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; +using osu.Game.Screens.SelectV2; + +namespace osu.Game.Tests.Visual.SongSelect +{ + [TestFixture] + public partial class TestSceneBeatmapCarouselV2ArtistGrouping : BeatmapCarouselV2TestScene + { + [SetUpSteps] + public void SetUpSteps() + { + RemoveAllBeatmaps(); + CreateCarousel(); + SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist }); + + AddBeatmaps(10, 3, true); + WaitForDrawablePanels(); + } + + [Test] + public void TestOpenCloseGroupWithNoSelectionMouse() + { + AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + CheckNoSelection(); + + ClickVisiblePanel(0); + + AddUntilStep("some sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + CheckNoSelection(); + + ClickVisiblePanel(0); + + AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + CheckNoSelection(); + } + + [Test] + public void TestOpenCloseGroupWithNoSelectionKeyboard() + { + AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + CheckNoSelection(); + + SelectNextPanel(); + Select(); + + AddUntilStep("some sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddAssert("keyboard selected is expanded", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); + CheckNoSelection(); + + Select(); + + AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddAssert("keyboard selected is collapsed", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); + CheckNoSelection(); + + GroupPanel? getKeyboardSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.KeyboardSelected.Value); + } + + [Test] + public void TestCarouselRemembersSelection() + { + SelectNextGroup(); + + object? selection = null; + + AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model); + + CheckHasSelection(); + AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null); + AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); + + RemoveAllBeatmaps(); + AddUntilStep("no drawable selection", getSelectedPanel, () => Is.Null); + + AddBeatmaps(10); + WaitForDrawablePanels(); + + CheckHasSelection(); + AddAssert("no drawable selection", getSelectedPanel, () => Is.Null); + + AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!)); + + AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); + AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); + AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); + + ClickVisiblePanel(0); + AddUntilStep("carousel item not visible", getSelectedPanel, () => Is.Null); + + ClickVisiblePanel(0); + AddUntilStep("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); + + BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); + } + + [Test] + public void TestGroupSelectionOnHeader() + { + SelectNextGroup(); + WaitForGroupSelection(0, 1); + + SelectPrevPanel(); + SelectPrevGroup(); + WaitForGroupSelection(4, 5); + } + + [Test] + public void TestKeyboardSelection() + { + SelectNextPanel(); + SelectNextPanel(); + SelectNextPanel(); + SelectNextPanel(); + CheckNoSelection(); + + // open first group + Select(); + CheckNoSelection(); + AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + + SelectNextPanel(); + Select(); + WaitForGroupSelection(3, 1); + + SelectNextGroup(); + WaitForGroupSelection(3, 5); + + SelectNextGroup(); + WaitForGroupSelection(4, 1); + + SelectPrevGroup(); + WaitForGroupSelection(3, 5); + + SelectNextGroup(); + WaitForGroupSelection(4, 1); + + SelectNextGroup(); + WaitForGroupSelection(4, 5); + + SelectNextGroup(); + WaitForGroupSelection(0, 1); + + SelectNextPanel(); + SelectNextPanel(); + SelectNextPanel(); + SelectNextPanel(); + + SelectNextGroup(); + WaitForGroupSelection(0, 1); + + SelectNextPanel(); + SelectNextGroup(); + WaitForGroupSelection(1, 1); + } + } +} From 4026ca84f887979555d32484f32ec8f20f178c7d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 15:41:57 +0900 Subject: [PATCH 373/801] Move selected retrieval functions to base class --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 3 +++ ...estSceneBeatmapCarouselV2ArtistGrouping.cs | 22 +++++++---------- ...ceneBeatmapCarouselV2DifficultyGrouping.cs | 24 ++++++++----------- .../TestSceneBeatmapCarouselV2NoGrouping.cs | 14 ++++------- 4 files changed, 27 insertions(+), 36 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 36226a13cc..5ace306c7d 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -136,6 +136,9 @@ namespace osu.Game.Tests.Visual.SongSelect protected void CheckNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null); protected void CheckHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null); + protected BeatmapPanel? GetSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); + protected GroupPanel? GetKeyboardSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.KeyboardSelected.Value); + protected void WaitForGroupSelection(int group, int panel) { AddUntilStep($"selected is group{group} panel{panel}", () => diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs index c7ab9de5e5..3c518fc7a6 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs @@ -57,17 +57,15 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("some sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); - AddAssert("keyboard selected is expanded", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); + AddAssert("keyboard selected is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); CheckNoSelection(); Select(); AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); - AddAssert("keyboard selected is collapsed", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); + AddAssert("keyboard selected is collapsed", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); CheckNoSelection(); - - GroupPanel? getKeyboardSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.KeyboardSelected.Value); } [Test] @@ -77,34 +75,32 @@ namespace osu.Game.Tests.Visual.SongSelect object? selection = null; - AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model); + AddStep("store drawable selection", () => selection = GetSelectedPanel()?.Item?.Model); CheckHasSelection(); AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null); AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); RemoveAllBeatmaps(); - AddUntilStep("no drawable selection", getSelectedPanel, () => Is.Null); + AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null); AddBeatmaps(10); WaitForDrawablePanels(); CheckHasSelection(); - AddAssert("no drawable selection", getSelectedPanel, () => Is.Null); + AddAssert("no drawable selection", GetSelectedPanel, () => Is.Null); AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!)); AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); - AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); - AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); + AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); + AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); ClickVisiblePanel(0); - AddUntilStep("carousel item not visible", getSelectedPanel, () => Is.Null); + AddUntilStep("carousel item not visible", GetSelectedPanel, () => Is.Null); ClickVisiblePanel(0); - AddUntilStep("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); - - BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); + AddUntilStep("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs index e861d8bf30..da3ef75487 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs @@ -49,15 +49,13 @@ namespace osu.Game.Tests.Visual.SongSelect SelectNextPanel(); Select(); AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); - AddAssert("keyboard selected is expanded", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); + AddAssert("keyboard selected is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); CheckNoSelection(); Select(); AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); - AddAssert("keyboard selected is collapsed", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); + AddAssert("keyboard selected is collapsed", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); CheckNoSelection(); - - GroupPanel? getKeyboardSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.KeyboardSelected.Value); } [Test] @@ -67,34 +65,32 @@ namespace osu.Game.Tests.Visual.SongSelect object? selection = null; - AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model); + AddStep("store drawable selection", () => selection = GetSelectedPanel()?.Item?.Model); CheckHasSelection(); AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null); AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); RemoveAllBeatmaps(); - AddUntilStep("no drawable selection", getSelectedPanel, () => Is.Null); + AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null); AddBeatmaps(10); WaitForDrawablePanels(); CheckHasSelection(); - AddAssert("no drawable selection", getSelectedPanel, () => Is.Null); + AddAssert("no drawable selection", GetSelectedPanel, () => Is.Null); AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!)); AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); - AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); - AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); + AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); + AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); ClickVisiblePanel(0); - AddUntilStep("carousel item not visible", getSelectedPanel, () => Is.Null); + AddUntilStep("carousel item not visible", GetSelectedPanel, () => Is.Null); ClickVisiblePanel(0); - AddUntilStep("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); - - BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); + AddUntilStep("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); } [Test] @@ -105,7 +101,7 @@ namespace osu.Game.Tests.Visual.SongSelect SelectPrevPanel(); SelectPrevGroup(); - WaitForGroupSelection(2, 9); + WaitForGroupSelection(0, 0); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs index 82f35af0ec..56bc7790bf 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs @@ -1,13 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; -using osu.Game.Screens.SelectV2; using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelect @@ -87,28 +85,26 @@ namespace osu.Game.Tests.Visual.SongSelect object? selection = null; - AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model); + AddStep("store drawable selection", () => selection = GetSelectedPanel()?.Item?.Model); CheckHasSelection(); AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null); AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); RemoveAllBeatmaps(); - AddUntilStep("no drawable selection", getSelectedPanel, () => Is.Null); + AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null); AddBeatmaps(10); WaitForDrawablePanels(); CheckHasSelection(); - AddAssert("no drawable selection", getSelectedPanel, () => Is.Null); + AddAssert("no drawable selection", GetSelectedPanel, () => Is.Null); AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!)); AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); - AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); - AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); - - BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); + AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); + AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); } [Test] From 024fbde0fd723a721eba48279085e0539bec0dde Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 16:21:18 +0900 Subject: [PATCH 374/801] Refactor selection and activation handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I had a bit of a struggle getting header traversal logic to work well. The constraints I had in place were a bit weird: - Group panels should toggle or potentially fall into the prev/next group - Set panels should just traverse around them The current method of using `CheckValidForGroupSelection` return type for traversal did not mesh with the above two cases. Just trust me on this one since it's quite hard to explain in words. After some re-thinking, I've gone with a simpler approach with one important change to UX: Now when group traversing with a beatmap set header currently keyboard focused, the first operation will be to reset keyboard selection to the selected beatmap, rather than traverse. I find this non-offensive – at most it means a user will need to press their group traversal key one extra time. I've also changed group headers to always toggle expansion when doing group traversal with them selected. To make all this work, the meaning of `Activation` has changed somewhat. It is now the primary path for carousel implementations to change selection of an item. It is what the `Drawable` panels call when they are clicked. Selection changes are not performed implicitly by `Carousel` – an implementation should decide when it actually wants to change the selection, usually in `HandleItemActivated`. Having less things mutating `CurrentSelection` is better in my eyes, as we see this variable as only being mutated internally when utmost required (ie the user has requested the change). With this change, `CurrentSelection` can no longer become of a non-`T` type (in the beatmap carousel implementation at least). This might pave a path forward for making `CurrentSelection` typed, but that comes with a few other concerns so I'll look at that as a follow-up. --- ...estSceneBeatmapCarouselV2ArtistGrouping.cs | 13 ++- ...ceneBeatmapCarouselV2DifficultyGrouping.cs | 9 ++ .../TestSceneBeatmapCarouselV2NoGrouping.cs | 6 +- .../TestSceneBeatmapCarouselV2Scrolling.cs | 4 +- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 33 ++++--- osu.Game/Screens/SelectV2/BeatmapPanel.cs | 8 +- osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 5 +- osu.Game/Screens/SelectV2/Carousel.cs | 98 +++++++++---------- osu.Game/Screens/SelectV2/GroupPanel.cs | 5 +- 9 files changed, 98 insertions(+), 83 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs index 3c518fc7a6..d3eeee151a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs @@ -110,8 +110,19 @@ namespace osu.Game.Tests.Visual.SongSelect WaitForGroupSelection(0, 1); SelectPrevPanel(); + SelectPrevPanel(); + + AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); + SelectPrevGroup(); - WaitForGroupSelection(4, 5); + + WaitForGroupSelection(0, 1); + AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); + + SelectPrevGroup(); + + WaitForGroupSelection(0, 1); + AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs index da3ef75487..151f1f5fec 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs @@ -100,8 +100,17 @@ namespace osu.Game.Tests.Visual.SongSelect WaitForGroupSelection(0, 0); SelectPrevPanel(); + AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); + SelectPrevGroup(); + WaitForGroupSelection(0, 0); + AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); + + SelectPrevGroup(); + + WaitForGroupSelection(0, 0); + AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs index 56bc7790bf..34bdd1265d 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs @@ -147,7 +147,11 @@ namespace osu.Game.Tests.Visual.SongSelect SelectPrevPanel(); SelectPrevGroup(); - WaitForSelection(0, 0); + WaitForSelection(1, 0); + + SelectPrevPanel(); + SelectNextGroup(); + WaitForSelection(1, 0); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs index 1d5d8e2a2d..ee6c11595a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs @@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.SongSelect { Quad positionBefore = default; - AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2)); + AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2).Beatmaps.First()); AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType().Single(p => p.Selected.Value))); WaitForScrolling(); @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("scroll to last item", () => Scroll.ScrollToEnd(false)); - AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.Last()); + AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.Last().Beatmaps.Last()); WaitForScrolling(); diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 36e57c9067..6032989ad0 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -95,11 +95,9 @@ namespace osu.Game.Screens.SelectV2 private GroupDefinition? lastSelectedGroup; private BeatmapInfo? lastSelectedBeatmap; - protected override bool HandleItemSelected(object? model) + protected override void HandleItemActivated(CarouselItem item) { - base.HandleItemSelected(model); - - switch (model) + switch (item.Model) { case GroupDefinition group: // Special case – collapsing an open group. @@ -107,16 +105,32 @@ namespace osu.Game.Screens.SelectV2 { setExpansionStateOfGroup(lastSelectedGroup, false); lastSelectedGroup = null; - return false; + return; } setExpandedGroup(group); - return false; + return; case BeatmapSetInfo setInfo: // Selecting a set isn't valid – let's re-select the first difficulty. CurrentSelection = setInfo.Beatmaps.First(); - return false; + return; + + case BeatmapInfo beatmapInfo: + CurrentSelection = beatmapInfo; + return; + } + } + + protected override void HandleItemSelected(object? model) + { + base.HandleItemSelected(model); + + switch (model) + { + case BeatmapSetInfo: + case GroupDefinition: + throw new InvalidOperationException("Groups should never become selected"); case BeatmapInfo beatmapInfo: // Find any containing group. There should never be too many groups so iterating is efficient enough. @@ -125,11 +139,8 @@ namespace osu.Game.Screens.SelectV2 if (containingGroup != null) setExpandedGroup(containingGroup); setExpandedSet(beatmapInfo); - - return true; + break; } - - return true; } protected override bool CheckValidForGroupSelection(CarouselItem item) diff --git a/osu.Game/Screens/SelectV2/BeatmapPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs index 3edfd4203b..9280e1c2c1 100644 --- a/osu.Game/Screens/SelectV2/BeatmapPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapPanel.cs @@ -86,13 +86,7 @@ namespace osu.Game.Screens.SelectV2 protected override bool OnClick(ClickEvent e) { - if (carousel.CurrentSelection != Item!.Model) - { - carousel.CurrentSelection = Item!.Model; - return true; - } - - carousel.TryActivateSelection(); + carousel.Activate(Item!); return true; } diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index 79ffe0f68a..f6c9324077 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -83,7 +82,7 @@ namespace osu.Game.Screens.SelectV2 protected override bool OnClick(ClickEvent e) { - carousel.CurrentSelection = Item!.Model; + carousel.Activate(Item!); return true; } @@ -98,8 +97,6 @@ namespace osu.Game.Screens.SelectV2 public void Activated() { - // sets should never be activated. - throw new InvalidOperationException(); } #endregion diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 6b7b1f3a9b..603a792847 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -94,26 +94,39 @@ namespace osu.Game.Screens.SelectV2 public object? CurrentSelection { get => currentSelection.Model; - set => setSelection(value); + set + { + if (currentSelection.Model != value) + { + HandleItemSelected(value); + + if (currentSelection.Model != null) + HandleItemDeselected(currentSelection.Model); + + currentKeyboardSelection = new Selection(value); + currentSelection = currentKeyboardSelection; + selectionValid.Invalidate(); + } + else if (currentKeyboardSelection.Model != value) + { + // Even if the current selection matches, let's ensure the keyboard selection is reset + // to the newly selected object. This matches user expectations (for now). + currentKeyboardSelection = currentSelection; + selectionValid.Invalidate(); + } + } } /// - /// Activate the current selection, if a selection exists and matches keyboard selection. - /// If keyboard selection does not match selection, this will transfer the selection on first invocation. + /// Activate the specified item. /// - public void TryActivateSelection() + /// + public void Activate(CarouselItem item) { - if (currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) - { - CurrentSelection = currentKeyboardSelection.Model; - return; - } + (GetMaterialisedDrawableForItem(item) as ICarouselPanel)?.Activated(); + HandleItemActivated(item); - if (currentSelection.CarouselItem != null) - { - (GetMaterialisedDrawableForItem(currentSelection.CarouselItem) as ICarouselPanel)?.Activated(); - HandleItemActivated(currentSelection.CarouselItem); - } + selectionValid.Invalidate(); } #endregion @@ -176,30 +189,28 @@ namespace osu.Game.Screens.SelectV2 protected virtual bool CheckValidForGroupSelection(CarouselItem item) => true; /// - /// Called when an item is "selected". + /// Called after an item becomes the . + /// Should be used to handle any group expansion, item visibility changes, etc. /// - /// Whether the item should be selected. - protected virtual bool HandleItemSelected(object? model) => true; + protected virtual void HandleItemSelected(object? model) { } /// - /// Called when an item is "deselected". + /// Called when the changes to a new selection. + /// Should be used to handle any group expansion, item visibility changes, etc. /// - protected virtual void HandleItemDeselected(object? model) - { - } + protected virtual void HandleItemDeselected(object? model) { } /// - /// Called when an item is "activated". + /// Called when an item is activated via user input (keyboard traversal or a mouse click). /// /// - /// An activated item should for instance: - /// - Open or close a folder - /// - Start gameplay on a beatmap difficulty. + /// An activated item should decide to perform an action, such as: + /// - Change its expanded state (and show / hide children items). + /// - Set the item to the . + /// - Start gameplay on a beatmap difficulty if already selected. /// /// The carousel item which was activated. - protected virtual void HandleItemActivated(CarouselItem item) - { - } + protected virtual void HandleItemActivated(CarouselItem item) { } #endregion @@ -305,7 +316,8 @@ namespace osu.Game.Screens.SelectV2 switch (e.Action) { case GlobalAction.Select: - TryActivateSelection(); + if (currentKeyboardSelection.CarouselItem != null) + Activate(currentKeyboardSelection.CarouselItem); return true; case GlobalAction.SelectNext: @@ -374,13 +386,10 @@ namespace osu.Game.Screens.SelectV2 // If the user has a different keyboard selection and requests // group selection, first transfer the keyboard selection to actual selection. - if (currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) + if (currentKeyboardSelection.CarouselItem != null && currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) { - TryActivateSelection(); - - // Is the selection actually changed, then we should not perform any further traversal. - if (currentSelection.CarouselItem == currentKeyboardSelection.CarouselItem) - return; + Activate(currentKeyboardSelection.CarouselItem); + return; } int originalIndex; @@ -413,7 +422,7 @@ namespace osu.Game.Screens.SelectV2 if (CheckValidForGroupSelection(newItem)) { - setSelection(newItem.Model); + HandleItemActivated(newItem); return; } } while (newIndex != originalIndex); @@ -428,23 +437,6 @@ namespace osu.Game.Screens.SelectV2 private Selection currentKeyboardSelection = new Selection(); private Selection currentSelection = new Selection(); - private void setSelection(object? model) - { - if (currentSelection.Model == model) - return; - - if (HandleItemSelected(model)) - { - if (currentSelection.Model != null) - HandleItemDeselected(currentSelection.Model); - - currentKeyboardSelection = new Selection(model); - currentSelection = currentKeyboardSelection; - } - - selectionValid.Invalidate(); - } - private void setKeyboardSelection(object? model) { currentKeyboardSelection = new Selection(model); diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs index 7ed256ca6a..e10521f63e 100644 --- a/osu.Game/Screens/SelectV2/GroupPanel.cs +++ b/osu.Game/Screens/SelectV2/GroupPanel.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -96,7 +95,7 @@ namespace osu.Game.Screens.SelectV2 protected override bool OnClick(ClickEvent e) { - carousel.CurrentSelection = Item!.Model; + carousel.Activate(Item!); return true; } @@ -111,8 +110,6 @@ namespace osu.Game.Screens.SelectV2 public void Activated() { - // sets should never be activated. - throw new InvalidOperationException(); } #endregion From 05a9160884a6426159539c9b9b7b326156cbeabd Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 6 Feb 2025 03:10:21 -0500 Subject: [PATCH 375/801] Simplify LINQ expressions to appease CI don't ask me --- .../SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs | 4 ++-- .../Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs | 2 +- .../SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs index 93472e7b81..f843c2cded 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs @@ -51,9 +51,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { AddStep("random beatmap", () => { - var randomSet = beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next()).FirstOrDefault(); + var randomSet = beatmaps.GetAllUsableBeatmapSets().MinBy(_ => RNG.Next()); randomSet ??= TestResources.CreateTestBeatmapSetInfo(); - beatmap = randomSet.Beatmaps.OrderBy(_ => RNG.Next()).First(); + beatmap = randomSet.Beatmaps.MinBy(_ => RNG.Next())!; CreateThemedContent(OverlayColourScheme.Aquamarine); }); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs index 540eae3be0..382357b67e 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs @@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { AddStep("random beatmap", () => { - var randomSet = beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next()).FirstOrDefault(); + var randomSet = beatmaps.GetAllUsableBeatmapSets().MinBy(_ => RNG.Next()); randomSet ??= TestResources.CreateTestBeatmapSetInfo(); beatmapSet = randomSet; diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs index 72f7a9e98c..41eb5c3683 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs @@ -51,9 +51,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { AddStep("random beatmap", () => { - var randomSet = beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next()).FirstOrDefault(); + var randomSet = beatmaps.GetAllUsableBeatmapSets().MinBy(_ => RNG.Next()); randomSet ??= TestResources.CreateTestBeatmapSetInfo(); - beatmap = randomSet.Beatmaps.OrderBy(_ => RNG.Next()).First(); + beatmap = randomSet.Beatmaps.MinBy(_ => RNG.Next())!; CreateThemedContent(OverlayColourScheme.Aquamarine); }); From bff686f01289f68ec8b12de2bb62107ddb49d76a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 17:09:58 +0900 Subject: [PATCH 376/801] Avoid double iteration when updating group states --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 47 +++++++++++++++----- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 6032989ad0..4126889892 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -173,22 +173,45 @@ namespace osu.Game.Screens.SelectV2 { if (grouping.GroupItems.TryGetValue(group, out var items)) { - // First pass ignoring set groupings. - foreach (var i in items) - { - if (i.Model is GroupDefinition) - i.IsExpanded = expanded; - else - i.IsVisible = expanded; - } - - // Second pass to hide set children when not meant to be displayed. if (expanded) { foreach (var i in items) { - if (i.Model is BeatmapSetInfo set) - setExpansionStateOfSetItems(set, i.IsExpanded); + switch (i.Model) + { + case GroupDefinition: + i.IsExpanded = true; + break; + + case BeatmapSetInfo set: + // Case where there are set headers, header should be visible + // and items should use the set's expanded state. + i.IsVisible = true; + setExpansionStateOfSetItems(set, i.IsExpanded); + break; + + default: + // Case where there are no set headers, all items should be visible. + if (!grouping.BeatmapSetsGroupedTogether) + i.IsVisible = true; + break; + } + } + } + else + { + foreach (var i in items) + { + switch (i.Model) + { + case GroupDefinition: + i.IsExpanded = false; + break; + + default: + i.IsVisible = false; + break; + } } } } From cb42ef95c57cf6f86c66dd882962b1532401d823 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 17:48:42 +0900 Subject: [PATCH 377/801] Add invalidation on draw size change in beatmap carousel v2 Matching old implementation. --- osu.Game/Screens/SelectV2/Carousel.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 1fd2f0a9b0..4248641a43 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -16,6 +16,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Pooling; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Layout; using osu.Framework.Logging; using osu.Framework.Utils; using osu.Game.Graphics.Containers; @@ -678,6 +679,15 @@ namespace osu.Game.Screens.SelectV2 carouselPanel.Expanded.Value = false; } + protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) + { + // handles the vertical size of the carousel changing (ie. on window resize when aspect ratio has changed). + if (invalidation.HasFlag(Invalidation.DrawSize)) + selectionValid.Invalidate(); + + return base.OnInvalidate(invalidation, source); + } + #endregion #region Internal helper classes From b7483b9442596fa367105f62effe81addb8bd8ec Mon Sep 17 00:00:00 2001 From: Layendan Date: Thu, 6 Feb 2025 07:25:45 -0700 Subject: [PATCH 378/801] Add playlist collection button w/ tests --- .../TestSceneAddPlaylistToCollectionButton.cs | 94 +++++++++++++++++++ .../AddPlaylistToCollectionButton.cs | 78 +++++++++++++++ .../Playlists/PlaylistsRoomSubScreen.cs | 10 ++ 3 files changed, 182 insertions(+) create mode 100644 osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs create mode 100644 osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs diff --git a/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs new file mode 100644 index 0000000000..acf2c4b3f9 --- /dev/null +++ b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs @@ -0,0 +1,94 @@ +// 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.Diagnostics; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Graphics; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.OnlinePlay.Playlists; +using osuTK; + +namespace osu.Game.Tests.Visual.Playlists +{ + public partial class TestSceneAddPlaylistToCollectionButton : OsuTestScene + { + private BeatmapManager manager = null!; + private BeatmapSetInfo importedBeatmap = null!; + private Room room = null!; + + [BackgroundDependencyLoader] + private void load(GameHost host, AudioManager audio) + { + Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); + } + + [Cached(typeof(INotificationOverlay))] + private NotificationOverlay notificationOverlay = new NotificationOverlay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }; + + [SetUpSteps] + public void SetUpSteps() + { + importBeatmap(); + + setupRoom(); + + AddStep("create button", () => + { + AddRange(new Drawable[] + { + notificationOverlay, + new AddPlaylistToCollectionButton(room) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(300, 40), + } + }); + }); + } + + private void importBeatmap() => AddStep("import beatmap", () => + { + var beatmap = CreateBeatmap(new OsuRuleset().RulesetInfo); + + Debug.Assert(beatmap.BeatmapInfo.BeatmapSet != null); + + importedBeatmap = manager.Import(beatmap.BeatmapInfo.BeatmapSet)!.Value.Detach(); + }); + + private void setupRoom() => AddStep("setup room", () => + { + room = new Room + { + Name = "my awesome room", + MaxAttempts = 5, + Host = API.LocalUser.Value + }; + room.RecentParticipants = [room.Host]; + room.EndDate = DateTimeOffset.Now.AddMinutes(5); + room.Playlist = + [ + new PlaylistItem(importedBeatmap.Beatmaps.First()) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID + } + ]; + }); + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs new file mode 100644 index 0000000000..643e274335 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -0,0 +1,78 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Game.Collections; +using osu.Game.Database; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; + +namespace osu.Game.Screens.OnlinePlay.Playlists +{ + public partial class AddPlaylistToCollectionButton : RoundedButton + { + private readonly Room room; + + [Resolved] + private RealmAccess realmAccess { get; set; } = null!; + + [Resolved] + private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; + + [Resolved(canBeNull: true)] + private INotificationOverlay? notifications { get; set; } + + public AddPlaylistToCollectionButton(Room room) + { + this.room = room; + Text = "Add Maps to Collection"; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + BackgroundColour = colours.Gray5; + + Action = () => + { + int[] ids = room.Playlist.Select(item => item.Beatmap.OnlineID).Where(onlineId => onlineId > 0).ToArray(); + + if (ids.Length == 0) + { + notifications?.Post(new SimpleErrorNotification { Text = "Cannot add local beatmaps" }); + return; + } + + beatmapLookupCache.GetBeatmapsAsync(ids).ContinueWith(task => Schedule(() => + { + var beatmaps = task.GetResultSafely().Where(item => item?.BeatmapSet != null).ToList(); + + var collection = realmAccess.Realm.All().FirstOrDefault(c => c.Name == room.Name); + + if (collection == null) + { + collection = new BeatmapCollection(room.Name, beatmaps.Select(i => i!.MD5Hash).Distinct().ToList()); + realmAccess.Realm.Write(() => realmAccess.Realm.Add(collection)); + notifications?.Post(new SimpleNotification { Text = $"Created new playlist: {room.Name}" }); + } + else + { + collection.ToLive(realmAccess).PerformWrite(c => + { + beatmaps = beatmaps.Where(i => !c.BeatmapMD5Hashes.Contains(i!.MD5Hash)).ToList(); + foreach (var item in beatmaps) + c.BeatmapMD5Hashes.Add(item!.MD5Hash); + notifications?.Post(new SimpleNotification { Text = $"Updated playlist: {room.Name}" }); + }); + } + }), TaskContinuationOptions.OnlyOnRanToCompletion); + }; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 9b4630ac0b..afab8a9721 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -153,11 +153,21 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } } }, + new Drawable[] + { + new AddPlaylistToCollectionButton(Room) + { + Margin = new MarginPadding { Top = 5 }, + RelativeSizeAxes = Axes.X, + Size = new Vector2(1, 40) + } + } }, RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize), new Dimension(), + new Dimension(GridSizeMode.AutoSize), } }, // Spacer From 6769a74c92937eead5628a4a3b0080059c2d2e85 Mon Sep 17 00:00:00 2001 From: Layendan Date: Thu, 6 Feb 2025 17:23:06 -0700 Subject: [PATCH 379/801] Add loading in case cache lookup takes longer than expected --- .../Playlists/AddPlaylistToCollectionButton.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index 643e274335..d28776cac2 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -8,6 +8,7 @@ using osu.Framework.Extensions; using osu.Game.Collections; using osu.Game.Database; using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -19,6 +20,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { private readonly Room room; + private LoadingLayer loading = null!; + [Resolved] private RealmAccess realmAccess { get; set; } = null!; @@ -39,6 +42,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { BackgroundColour = colours.Gray5; + Add(loading = new LoadingLayer(true, false)); + Action = () => { int[] ids = room.Playlist.Select(item => item.Beatmap.OnlineID).Where(onlineId => onlineId > 0).ToArray(); @@ -49,6 +54,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return; } + Enabled.Value = false; + loading.Show(); beatmapLookupCache.GetBeatmapsAsync(ids).ContinueWith(task => Schedule(() => { var beatmaps = task.GetResultSafely().Where(item => item?.BeatmapSet != null).ToList(); @@ -71,6 +78,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists notifications?.Post(new SimpleNotification { Text = $"Updated playlist: {room.Name}" }); }); } + + loading.Hide(); + Enabled.Value = true; }), TaskContinuationOptions.OnlyOnRanToCompletion); }; } From 2aa930a36c87d579c1cde09a11a56342f8ca960f Mon Sep 17 00:00:00 2001 From: Layendan Date: Thu, 6 Feb 2025 17:46:49 -0700 Subject: [PATCH 380/801] Corrected notification strings --- .../OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index d28776cac2..ab3e481f9f 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -66,7 +66,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { collection = new BeatmapCollection(room.Name, beatmaps.Select(i => i!.MD5Hash).Distinct().ToList()); realmAccess.Realm.Write(() => realmAccess.Realm.Add(collection)); - notifications?.Post(new SimpleNotification { Text = $"Created new playlist: {room.Name}" }); + notifications?.Post(new SimpleNotification { Text = $"Created new collection: {room.Name}" }); } else { @@ -75,7 +75,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists beatmaps = beatmaps.Where(i => !c.BeatmapMD5Hashes.Contains(i!.MD5Hash)).ToList(); foreach (var item in beatmaps) c.BeatmapMD5Hashes.Add(item!.MD5Hash); - notifications?.Post(new SimpleNotification { Text = $"Updated playlist: {room.Name}" }); + notifications?.Post(new SimpleNotification { Text = $"Updated collection: {room.Name}" }); }); } From 4f6fd68a9195d170eeca5983e0a76d5e5fcc78b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 13:54:35 +0900 Subject: [PATCH 381/801] Fix inspections --- .../Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs index 6c4a332624..247fb06dc0 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm } double actualRatio = current.DeltaTime / previous.DeltaTime; - double closestRatio = common_ratios.OrderBy(r => Math.Abs(r - actualRatio)).First(); + double closestRatio = common_ratios.MinBy(r => Math.Abs(r - actualRatio)); Ratio = closestRatio; } @@ -63,8 +63,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm /// speeding up is generally harder than slowing down (with exceptions of rhythm changes requiring a hand switch). /// /// - private static readonly double[] common_ratios = new[] - { + private static readonly double[] common_ratios = + [ 1.0 / 1, 2.0 / 1, 1.0 / 2, @@ -74,6 +74,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm 2.0 / 3, 5.0 / 4, 4.0 / 5 - }; + ]; } } From 25846b232748ae71b288fec35a43534512bdf5ed Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 14:21:43 +0900 Subject: [PATCH 382/801] Adjust results screen designs and tests slightly --- .../Visual/Ranking/TestSceneResultsScreen.cs | 16 ++++++++++------ .../Contracted/ContractedPanelMiddleContent.cs | 6 +++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index fca1d0f82a..3a08756090 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -62,12 +62,6 @@ namespace osu.Game.Tests.Visual.Ranking if (beatmapInfo != null) Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo); }); - - AddToggleStep("toggle legacy classic skin", v => - { - if (skins != null) - skins.CurrentSkinInfo.Value = v ? skins.DefaultClassicSkin.SkinInfo : skins.CurrentSkinInfo.Default; - }); } [SetUp] @@ -84,6 +78,16 @@ namespace osu.Game.Tests.Visual.Ranking })); } + [Test] + public void TestLegacySkin() + { + AddToggleStep("toggle legacy classic skin", v => + { + if (skins != null) + skins.CurrentSkinInfo.Value = v ? skins.DefaultClassicSkin.SkinInfo : skins.CurrentSkinInfo.Default; + }); + } + private int onlineScoreID = 1; [TestCase(1, ScoreRank.X, 0)] diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs index cfb6465e62..2f863a95ec 100644 --- a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.Ranking.Contracted Colour = Color4.Black.Opacity(0.25f), Type = EdgeEffectType.Shadow, Radius = 1, - Offset = new Vector2(0, 4) + Offset = new Vector2(0, 2) }, Children = new Drawable[] { @@ -100,10 +100,10 @@ namespace osu.Game.Screens.Ranking.Contracted CornerRadius = 20, EdgeEffect = new EdgeEffectParameters { - Colour = Color4.Black.Opacity(0.25f), + Colour = Color4.Black.Opacity(0.15f), Type = EdgeEffectType.Shadow, Radius = 8, - Offset = new Vector2(0, 4), + Offset = new Vector2(0, 1), } }, new OsuSpriteText From 975c35f5ac48735367ba792784d5572b69fe9a4c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 14:27:37 +0900 Subject: [PATCH 383/801] Also add difficulty icon to contracted panel --- .../ContractedPanelMiddleContent.cs | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs index 2f863a95ec..e9d0bf3403 100644 --- a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs @@ -11,13 +11,15 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; +using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.Leaderboards; using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; using osu.Game.Scoring; -using osu.Game.Screens.Play.HUD; using osu.Game.Users; using osu.Game.Users.Drawables; using osu.Game.Utils; @@ -134,14 +136,33 @@ namespace osu.Game.Screens.Ranking.Contracted createStatistic(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy, $"{score.Accuracy.FormatAccuracy()}"), } }, - new ModFlowDisplay + new FillFlowContainer { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, - Current = { Value = score.Mods }, - IconScale = 0.5f, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Full, + Spacing = new Vector2(3), + ChildrenEnumerable = + [ + new DifficultyIcon(score.BeatmapInfo!, score.Ruleset) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Size = new Vector2(20), + TooltipType = DifficultyIconTooltipType.Extended, + Margin = new MarginPadding { Right = 2 } + }, + .. + score.Mods.AsOrdered().Select(m => new ModIcon(m) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Scale = new Vector2(0.3f), + Margin = new MarginPadding { Top = -6 } + }) + ] } } } From d73f275143a7c36a8b629f15ea061678cba5ba1e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 15:15:58 +0900 Subject: [PATCH 384/801] Don't inflate set / group panels for simplicity --- osu.Game/Screens/SelectV2/BeatmapPanel.cs | 7 +++++-- osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 10 ---------- osu.Game/Screens/SelectV2/GroupPanel.cs | 10 ---------- 3 files changed, 5 insertions(+), 22 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs index b690e35a48..ddf2fdcb57 100644 --- a/osu.Game/Screens/SelectV2/BeatmapPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapPanel.cs @@ -28,8 +28,11 @@ namespace osu.Game.Screens.SelectV2 { var inputRectangle = DrawRectangle; - // Cover the gaps introduced by the spacing between BeatmapPanels. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); + // Cover the gaps introduced by the spacing between BeatmapPanels so that clicks will not fall through the carousel. + // + // Caveat is that for simplicity, we are covering the full spacing, so panels with frontmost depth will have a slightly + // larger hit target. + inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING }); return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); } diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index d869e0af75..f6c9324077 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -26,16 +26,6 @@ namespace osu.Game.Screens.SelectV2 private OsuSpriteText text = null!; private Box box = null!; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) - { - var inputRectangle = DrawRectangle; - - // Cover a gap introduced by the spacing between a BeatmapSetPanel and a BeatmapPanel either below/above it. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - - return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); - } - [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs index c5e5c7745f..e10521f63e 100644 --- a/osu.Game/Screens/SelectV2/GroupPanel.cs +++ b/osu.Game/Screens/SelectV2/GroupPanel.cs @@ -27,16 +27,6 @@ namespace osu.Game.Screens.SelectV2 private Box box = null!; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) - { - var inputRectangle = DrawRectangle; - - // Cover a gap introduced by the spacing between a GroupPanel and a BeatmapPanel either below/above it. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - - return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); - } - [BackgroundDependencyLoader] private void load() { From d505c529cd217abfbf697a5e9f9f8c1ebb2da14c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 15:06:21 +0900 Subject: [PATCH 385/801] Adjust tests in line with new expectations --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 28 ++++++++ ...ceneBeatmapCarouselV2DifficultyGrouping.cs | 64 ++++--------------- .../TestSceneBeatmapCarouselV2NoGrouping.cs | 54 +++++----------- 3 files changed, 58 insertions(+), 88 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index f17f312e9f..be0d0bf79a 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -2,6 +2,7 @@ // 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; @@ -20,6 +21,7 @@ using osu.Game.Screens.Select; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; +using osuTK; using osuTK.Graphics; using osuTK.Input; using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel; @@ -164,6 +166,15 @@ namespace osu.Game.Tests.Visual.SongSelect }); } + protected IEnumerable GetVisiblePanels() + where T : Drawable + { + return Carousel.ChildrenOfType().Single() + .ChildrenOfType() + .Where(p => ((ICarouselPanel)p).Item?.IsVisible == true) + .OrderBy(p => p.Y); + } + protected void ClickVisiblePanel(int index) where T : Drawable { @@ -178,6 +189,23 @@ namespace osu.Game.Tests.Visual.SongSelect }); } + protected void ClickVisiblePanelWithOffset(int index, Vector2 positionOffsetFromCentre) + where T : Drawable + { + AddStep($"move mouse to panel {index} with offset {positionOffsetFromCentre}", () => + { + var panel = Carousel.ChildrenOfType().Single() + .ChildrenOfType() + .Where(p => ((ICarouselPanel)p).Item?.IsVisible == true) + .OrderBy(p => p.Y) + .ElementAt(index); + + InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre + panel.ToScreenSpace(positionOffsetFromCentre) - panel.ToScreenSpace(Vector2.Zero)); + }); + + AddStep("click", () => InputManager.Click(MouseButton.Left)); + } + /// /// Add requested beatmap sets count to list. /// diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs index 83e0e77fa6..f631dfc562 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Linq; using NUnit.Framework; using osu.Framework.Testing; @@ -10,7 +9,6 @@ using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; using osuTK; -using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelect { @@ -153,60 +151,24 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestInputHandlingWithinGaps() { - AddBeatmaps(5, 2); - WaitForDrawablePanels(); - SelectNextGroup(); + AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); - clickOnPanel(0, 1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); - WaitForGroupSelection(0, 1); + // Clicks just above the first group panel should not actuate any action. + ClickVisiblePanelWithOffset(0, new Vector2(0, -(GroupPanel.HEIGHT / 2 + 1))); - clickOnPanel(0, 0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); + + ClickVisiblePanelWithOffset(0, new Vector2(0, -(GroupPanel.HEIGHT / 2))); + + AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels().Any()); + CheckNoSelection(); + + // Beatmap panels expand their selection area to cover holes from spacing. + ClickVisiblePanelWithOffset(0, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1))); WaitForGroupSelection(0, 0); - SelectNextPanel(); - Select(); + ClickVisiblePanelWithOffset(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); WaitForGroupSelection(0, 1); - - clickOnGroup(0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); - AddAssert("group 0 collapsed", () => this.ChildrenOfType().OrderBy(g => g.Y).ElementAt(0).Expanded.Value, () => Is.False); - clickOnGroup(0, p => p.LayoutRectangle.Centre); - AddAssert("group 0 expanded", () => this.ChildrenOfType().OrderBy(g => g.Y).ElementAt(0).Expanded.Value, () => Is.True); - - AddStep("scroll to end", () => Scroll.ScrollToEnd(false)); - clickOnPanel(0, 4, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); - WaitForGroupSelection(0, 4); - - clickOnGroup(1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); - AddAssert("group 1 expanded", () => this.ChildrenOfType().OrderBy(g => g.Y).ElementAt(1).Expanded.Value, () => Is.True); - } - - private void clickOnGroup(int group, Func pos) - { - AddStep($"click on group{group}", () => - { - var groupingFilter = Carousel.Filters.OfType().Single(); - var model = groupingFilter.GroupItems.Keys.ElementAt(group); - - var panel = this.ChildrenOfType().Single(b => ReferenceEquals(b.Item!.Model, model)); - InputManager.MoveMouseTo(panel.ToScreenSpace(pos(panel))); - InputManager.Click(MouseButton.Left); - }); - } - - private void clickOnPanel(int group, int panel, Func pos) - { - AddStep($"click on group{group} panel{panel}", () => - { - var groupingFilter = Carousel.Filters.OfType().Single(); - - var g = groupingFilter.GroupItems.Keys.ElementAt(group); - // offset by one because the group itself is included in the items list. - object model = groupingFilter.GroupItems[g].ElementAt(panel + 1).Model; - - var p = this.ChildrenOfType().Single(b => ReferenceEquals(b.Item!.Model, model)); - InputManager.MoveMouseTo(p.ToScreenSpace(pos(p))); - InputManager.Click(MouseButton.Left); - }); } } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs index 566c2f1798..1359b5c58e 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Linq; using NUnit.Framework; using osu.Framework.Testing; @@ -209,31 +208,34 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] + [Solo] public void TestInputHandlingWithinGaps() { AddBeatmaps(2, 5); WaitForDrawablePanels(); - SelectNextGroup(); - clickOnDifficulty(0, 1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); - WaitForSelection(0, 1); + AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); - clickOnDifficulty(0, 0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + // Clicks just above the first group panel should not actuate any action. + ClickVisiblePanelWithOffset(0, new Vector2(0, -(BeatmapSetPanel.HEIGHT / 2 + 1))); + + AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); + + ClickVisiblePanelWithOffset(0, new Vector2(0, -(BeatmapSetPanel.HEIGHT / 2))); + + AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels().Any()); WaitForSelection(0, 0); - SelectNextPanel(); - Select(); - WaitForSelection(0, 1); - - clickOnSet(0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + // Beatmap panels expand their selection area to cover holes from spacing. + ClickVisiblePanelWithOffset(1, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1))); WaitForSelection(0, 0); - AddStep("scroll to end", () => Scroll.ScrollToEnd(false)); - clickOnDifficulty(0, 4, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); - WaitForSelection(0, 4); + // Panels with higher depth will handle clicks in the gutters for simplicity. + ClickVisiblePanelWithOffset(2, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + WaitForSelection(0, 2); - clickOnSet(1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); - WaitForSelection(1, 0); + ClickVisiblePanelWithOffset(3, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + WaitForSelection(0, 3); } private void checkSelectionIterating(bool isIterating) @@ -249,27 +251,5 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("selection not changed", () => Carousel.CurrentSelection == selection); } } - - private void clickOnSet(int set, Func pos) - { - AddStep($"click on set{set}", () => - { - var model = BeatmapSets[set]; - var panel = this.ChildrenOfType().Single(b => ReferenceEquals(b.Item!.Model, model)); - InputManager.MoveMouseTo(panel.ToScreenSpace(pos(panel))); - InputManager.Click(MouseButton.Left); - }); - } - - private void clickOnDifficulty(int set, int diff, Func pos) - { - AddStep($"click on set{set} diff{diff}", () => - { - var model = BeatmapSets[set].Beatmaps[diff]; - var panel = this.ChildrenOfType().Single(b => ReferenceEquals(b.Item!.Model, model)); - InputManager.MoveMouseTo(panel.ToScreenSpace(pos(panel))); - InputManager.Click(MouseButton.Left); - }); - } } } From aa329f397e684f41e5ca040d4d33a13026d464a5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 15:30:31 +0900 Subject: [PATCH 386/801] Remove stray `[Solo]`s --- osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs | 1 - .../Visual/Navigation/TestSceneBeatmapEditorNavigation.cs | 1 - .../Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs | 1 - osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs | 1 - 4 files changed, 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs index 966e6513bb..4953cf83c9 100644 --- a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs @@ -74,7 +74,6 @@ namespace osu.Game.Tests.Visual.Editing } [Test] - [Solo] public void TestCommitPlacementViaRightClick() { Playfield playfield = null!; diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs index d76e0290ef..ee5b1797ed 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs @@ -165,7 +165,6 @@ namespace osu.Game.Tests.Visual.Navigation } [Test] - [Solo] public void TestEditorGameplayTestAlwaysUsesOriginalRuleset() { prepareBeatmap(); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs index 1359b5c58e..09ded342c3 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs @@ -208,7 +208,6 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] - [Solo] public void TestInputHandlingWithinGaps() { AddBeatmaps(2, 5); diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index c415fc876f..d8ab367ebd 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -1239,7 +1239,6 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] - [Solo] public void TestHardDeleteHandledCorrectly() { createSongSelect(); From 4d1167fdccbfee3d0ecf425a969925c9baf5b222 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 15:36:59 +0900 Subject: [PATCH 387/801] Don't attempt to submit zero scores --- osu.Game/Screens/Play/SubmittingPlayer.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 0a230ea00b..b667963a70 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -284,6 +284,13 @@ namespace osu.Game.Screens.Play return Task.CompletedTask; } + // zero scores should also never be submitted. + if (score.ScoreInfo.TotalScore == 0) + { + Logger.Log("Zero score, skipping score submission"); + return Task.CompletedTask; + } + // mind the timing of this. // once `scoreSubmissionSource` is created, it is presumed that submission is taking place in the background, // so all exceptional circumstances that would disallow submission must be handled above. From 75ef6f6a0e02e1bf4b898186141376d2ccf7b80a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 02:10:08 +0900 Subject: [PATCH 388/801] Use random generation in carousel stress test --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 45 ++++++++++--------- .../SongSelect/TestSceneBeatmapCarouselV2.cs | 2 +- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index f17f312e9f..db433b93d2 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -187,29 +187,32 @@ namespace osu.Game.Tests.Visual.SongSelect protected void AddBeatmaps(int count, int? fixedDifficultiesPerSet = null, bool randomMetadata = false) => AddStep($"add {count} beatmaps{(randomMetadata ? " with random data" : "")}", () => { for (int i = 0; i < count; i++) - { - var beatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4)); - - if (randomMetadata) - { - char randomCharacter = getRandomCharacter(); - - var metadata = new BeatmapMetadata - { - // Create random metadata, then we can check if sorting works based on these - Artist = $"{randomCharacter}ome Artist " + RNG.Next(0, 9), - Title = $"{randomCharacter}ome Song (set id {beatmapSetInfo.OnlineID:000}) {Guid.NewGuid()}", - Author = { Username = $"{randomCharacter}ome Guy " + RNG.Next(0, 9) }, - }; - - foreach (var beatmap in beatmapSetInfo.Beatmaps) - beatmap.Metadata = metadata.DeepClone(); - } - - BeatmapSets.Add(beatmapSetInfo); - } + BeatmapSets.Add(CreateTestBeatmapSetInfo(fixedDifficultiesPerSet, randomMetadata)); }); + protected static BeatmapSetInfo CreateTestBeatmapSetInfo(int? fixedDifficultiesPerSet, bool randomMetadata) + { + var beatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4)); + + if (randomMetadata) + { + char randomCharacter = getRandomCharacter(); + + var metadata = new BeatmapMetadata + { + // Create random metadata, then we can check if sorting works based on these + Artist = $"{randomCharacter}ome Artist " + RNG.Next(0, 9), + Title = $"{randomCharacter}ome Song (set id {beatmapSetInfo.OnlineID:000}) {Guid.NewGuid()}", + Author = { Username = $"{randomCharacter}ome Guy " + RNG.Next(0, 9) }, + }; + + foreach (var beatmap in beatmapSetInfo.Beatmaps) + beatmap.Metadata = metadata.DeepClone(); + } + + return beatmapSetInfo; + } + private static long randomCharPointer; private static char getRandomCharacter() diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs index 3c5cf16e92..30ca26ce68 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.SongSelect Task.Run(() => { for (int j = 0; j < count; j++) - generated.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); + generated.Add(CreateTestBeatmapSetInfo(3, true)); }).ConfigureAwait(true); }); From 50d880e2ae3e3abfd58a795d26461ff39aa82070 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 02:10:38 +0900 Subject: [PATCH 389/801] Fix unnecessary `BeatmapSet.Metadata` lookups --- osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs index 0298616aa8..3cdbbb4fed 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs @@ -36,7 +36,7 @@ namespace osu.Game.Screens.SelectV2 switch (criteria.Sort) { case SortMode.Artist: - comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.BeatmapSet!.Metadata.Artist, bb.BeatmapSet!.Metadata.Artist); + comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.Metadata.Artist, bb.Metadata.Artist); if (comparison == 0) goto case SortMode.Title; break; @@ -46,7 +46,7 @@ namespace osu.Game.Screens.SelectV2 break; case SortMode.Title: - comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.BeatmapSet!.Metadata.Title, bb.BeatmapSet!.Metadata.Title); + comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.Metadata.Title, bb.Metadata.Title); break; default: From a49b1b61b4dfe7ff394f8f68c70d0e61bbc657d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Feb 2025 08:21:34 +0100 Subject: [PATCH 390/801] Add test coverage for scores with zero total not submitting --- .../TestScenePlayerScoreSubmission.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs index c382f0828b..381f49d9eb 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs @@ -16,6 +16,7 @@ using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Online.Solo; using osu.Game.Rulesets; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -234,6 +235,31 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("ensure no submission", () => Player.SubmittedScore == null); } + [Test] + public void TestNoSubmissionWhenScoreZero() + { + prepareTestAPI(true); + + createPlayerTest(); + + AddUntilStep("wait for token request", () => Player.TokenCreationRequested); + + AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); + AddUntilStep("wait for first result", () => Player.Results.Count > 0); + + AddStep("add fake non-scoring hit", () => + { + Player.ScoreProcessor.RevertResult(Player.Results.First()); + Player.ScoreProcessor.ApplyResult(new OsuJudgementResult(Beatmap.Value.Beatmap.HitObjects.First(), new IgnoreJudgement()) + { + Type = HitResult.IgnoreHit, + }); + }); + + AddStep("exit", () => Player.Exit()); + AddAssert("ensure no submission", () => Player.SubmittedScore == null); + } + [Test] public void TestSubmissionOnExit() { From 9d979dc3f4adb523269fb14f6d049986dab9d61b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 02:37:16 +0900 Subject: [PATCH 391/801] Refactor grouping to be much more efficient --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 2 +- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 207 ++++++++---------- 2 files changed, 93 insertions(+), 116 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 4126889892..137a8e8eab 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -289,5 +289,5 @@ namespace osu.Game.Screens.SelectV2 #endregion } - public record GroupDefinition(string Title); + public record GroupDefinition(object Data, string Title); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index a8caebad7a..8838ce67ad 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Game.Beatmaps; @@ -36,137 +35,115 @@ namespace osu.Game.Screens.SelectV2 this.getCriteria = getCriteria; } - public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => + public async Task> Run(IEnumerable items, CancellationToken cancellationToken) { - setItems.Clear(); - groupItems.Clear(); + return await Task.Run(() => + { + setItems.Clear(); + groupItems.Clear(); - var criteria = getCriteria(); - var newItems = new List(items.Count()); + var criteria = getCriteria(); + var newItems = new List(); - // Add criteria groups. + BeatmapInfo? lastBeatmap = null; + GroupDefinition? lastGroup = null; + + HashSet? groupRefItems = null; + HashSet? setRefItems = null; + + switch (criteria.Group) + { + default: + BeatmapSetsGroupedTogether = true; + break; + + case GroupMode.Difficulty: + BeatmapSetsGroupedTogether = false; + break; + } + + foreach (var item in items) + { + cancellationToken.ThrowIfCancellationRequested(); + + var beatmap = (BeatmapInfo)item.Model; + + if (createGroupIfRequired(criteria, beatmap, lastGroup) is GroupDefinition newGroup) + { + // When reaching a new group, ensure we reset any beatmap set tracking. + setRefItems = null; + lastBeatmap = null; + + groupItems[newGroup] = groupRefItems = new HashSet(); + lastGroup = newGroup; + + addItem(new CarouselItem(newGroup) + { + DrawHeight = GroupPanel.HEIGHT, + DepthLayer = -2, + }); + } + + if (BeatmapSetsGroupedTogether) + { + bool newBeatmapSet = lastBeatmap?.BeatmapSet!.ID != beatmap.BeatmapSet!.ID; + + if (newBeatmapSet) + { + setItems[beatmap.BeatmapSet!] = setRefItems = new HashSet(); + + addItem(new CarouselItem(beatmap.BeatmapSet!) + { + DrawHeight = BeatmapSetPanel.HEIGHT, + DepthLayer = -1 + }); + } + } + + addItem(item); + lastBeatmap = beatmap; + + void addItem(CarouselItem i) + { + newItems.Add(i); + + groupRefItems?.Add(i); + setRefItems?.Add(i); + + i.IsVisible = i.Model is GroupDefinition || (lastGroup == null && (i.Model is BeatmapSetInfo || setRefItems == null)); + } + } + + return newItems; + }, cancellationToken).ConfigureAwait(false); + } + + private GroupDefinition? createGroupIfRequired(FilterCriteria criteria, BeatmapInfo beatmap, GroupDefinition? lastGroup) + { switch (criteria.Group) { - default: - BeatmapSetsGroupedTogether = true; - newItems.AddRange(items); - break; - case GroupMode.Artist: - BeatmapSetsGroupedTogether = true; - char groupChar = (char)0; + char groupChar = lastGroup?.Data as char? ?? (char)0; + char beatmapFirstChar = char.ToUpperInvariant(beatmap.Metadata.Artist[0]); - foreach (var item in items) - { - cancellationToken.ThrowIfCancellationRequested(); - - var b = (BeatmapInfo)item.Model; - - char beatmapFirstChar = char.ToUpperInvariant(b.Metadata.Artist[0]); - - if (beatmapFirstChar > groupChar) - { - groupChar = beatmapFirstChar; - var groupDefinition = new GroupDefinition($"{groupChar}"); - var groupItem = new CarouselItem(groupDefinition) { DrawHeight = GroupPanel.HEIGHT }; - - newItems.Add(groupItem); - groupItems[groupDefinition] = new HashSet { groupItem }; - } - - newItems.Add(item); - } + if (beatmapFirstChar > groupChar) + return new GroupDefinition(beatmapFirstChar, $"{beatmapFirstChar}"); break; case GroupMode.Difficulty: - BeatmapSetsGroupedTogether = false; - int starGroup = int.MinValue; + int starGroup = lastGroup?.Data as int? ?? -1; - foreach (var item in items) + if (beatmap.StarRating > starGroup) { - cancellationToken.ThrowIfCancellationRequested(); - - var b = (BeatmapInfo)item.Model; - - if (b.StarRating > starGroup) - { - starGroup = (int)Math.Floor(b.StarRating); - var groupDefinition = new GroupDefinition($"{starGroup} - {++starGroup} *"); - - var groupItem = new CarouselItem(groupDefinition) - { - DrawHeight = GroupPanel.HEIGHT, - DepthLayer = -2 - }; - - newItems.Add(groupItem); - groupItems[groupDefinition] = new HashSet { groupItem }; - } - - newItems.Add(item); + starGroup = (int)Math.Floor(beatmap.StarRating); + return new GroupDefinition(starGroup + 1, $"{starGroup} - {starGroup + 1} *"); } break; } - // Add set headers wherever required. - CarouselItem? lastItem = null; - - if (BeatmapSetsGroupedTogether) - { - for (int i = 0; i < newItems.Count; i++) - { - cancellationToken.ThrowIfCancellationRequested(); - - var item = newItems[i]; - - if (item.Model is BeatmapInfo beatmap) - { - bool newBeatmapSet = lastItem?.Model is not BeatmapInfo lastBeatmap || lastBeatmap.BeatmapSet!.ID != beatmap.BeatmapSet!.ID; - - if (newBeatmapSet) - { - var setItem = new CarouselItem(beatmap.BeatmapSet!) - { - DrawHeight = BeatmapSetPanel.HEIGHT, - DepthLayer = -1 - }; - - setItems[beatmap.BeatmapSet!] = new HashSet { setItem }; - newItems.Insert(i, setItem); - i++; - } - - setItems[beatmap.BeatmapSet!].Add(item); - item.IsVisible = false; - } - - lastItem = item; - } - } - - // Link group items to their headers. - GroupDefinition? lastGroup = null; - - foreach (var item in newItems) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (item.Model is GroupDefinition group) - { - lastGroup = group; - continue; - } - - if (lastGroup != null) - { - groupItems[lastGroup].Add(item); - item.IsVisible = false; - } - } - - return newItems; - }, cancellationToken).ConfigureAwait(false); + return null; + } } } From c935c3154b33739020c42b597fbd83480e6cd0e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 16:54:30 +0900 Subject: [PATCH 392/801] Always transfer keyboard selection on activation --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 4 ++-- ...ceneBeatmapCarouselV2DifficultyGrouping.cs | 22 ++++++++++++++++++- osu.Game/Screens/SelectV2/Carousel.cs | 5 +++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index f17f312e9f..cfef2882be 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -136,8 +136,8 @@ namespace osu.Game.Tests.Visual.SongSelect protected void CheckNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null); protected void CheckHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null); - protected BeatmapPanel? GetSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); - protected GroupPanel? GetKeyboardSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.KeyboardSelected.Value); + protected ICarouselPanel? GetSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); + protected ICarouselPanel? GetKeyboardSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.KeyboardSelected.Value); protected void WaitForGroupSelection(int group, int panel) { diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs index 151f1f5fec..f46e79caf7 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs @@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] - public void TestGroupSelectionOnHeader() + public void TestGroupSelectionOnHeaderKeyboard() { SelectNextGroup(); WaitForGroupSelection(0, 0); @@ -113,6 +113,26 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); } + [Test] + public void TestGroupSelectionOnHeaderMouse() + { + SelectNextGroup(); + WaitForGroupSelection(0, 0); + + AddAssert("keyboard selected panel is beatmap", GetKeyboardSelectedPanel, Is.TypeOf); + AddAssert("selected panel is beatmap", GetSelectedPanel, Is.TypeOf); + + ClickVisiblePanel(0); + AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf); + AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); + + ClickVisiblePanel(0); + AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf); + AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); + + AddAssert("selected panel is still beatmap", GetSelectedPanel, Is.TypeOf); + } + [Test] public void TestKeyboardSelection() { diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 5220781ce8..89dec6a7ae 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -124,6 +124,11 @@ namespace osu.Game.Screens.SelectV2 /// public void Activate(CarouselItem item) { + // Regardless of how the item handles activation, update keyboard selection to the activated panel. + // In other words, when a panel is clicked, keyboard selection should default to matching the clicked + // item. + setKeyboardSelection(item.Model); + (GetMaterialisedDrawableForItem(item) as ICarouselPanel)?.Activated(); HandleItemActivated(item); From cef9d2eac50ac11bdbc964fcb243d1225c06dae3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 16:55:02 +0900 Subject: [PATCH 393/801] Reduce number of beatmaps added in selection test This is because with the new keyboard selection logic, adding too many can cause the re-added selection to be off-screen in the headless test setup. --- .../SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs index f46e79caf7..33d9d3a363 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs @@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual.SongSelect RemoveAllBeatmaps(); AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null); - AddBeatmaps(10); + AddBeatmaps(5); WaitForDrawablePanels(); CheckHasSelection(); From 41c8f648063d2cef2ba36021095cb7ca4e5fd0c0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 17:33:32 +0900 Subject: [PATCH 394/801] Simplify naming of endpoints --- osu.Desktop/DiscordRichPresence.cs | 2 +- .../Visual/Menus/TestSceneMainMenu.cs | 2 +- .../Online/TestSceneWikiMarkdownContainer.cs | 10 ++--- osu.Game/Beatmaps/BeatmapInfoExtensions.cs | 2 +- osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs | 4 +- osu.Game/Online/API/APIAccess.cs | 12 +++--- osu.Game/Online/API/APIRequest.cs | 2 +- osu.Game/Online/API/DummyAPIAccess.cs | 6 +-- osu.Game/Online/API/IAPIProvider.cs | 2 +- .../Requests/PatchBeatmapPackageRequest.cs | 4 +- .../API/Requests/PutBeatmapSetRequest.cs | 4 +- .../Requests/ReplaceBeatmapPackageRequest.cs | 4 +- osu.Game/Online/Chat/ExternalLinkOpener.cs | 4 +- osu.Game/Online/Chat/NowPlayingCommand.cs | 2 +- .../DevelopmentEndpointConfiguration.cs | 8 ++-- osu.Game/Online/EndpointConfiguration.cs | 38 +++++++++---------- .../Online/Leaderboards/LeaderboardScore.cs | 2 +- .../Online/Metadata/OnlineMetadataClient.cs | 2 +- .../Multiplayer/OnlineMultiplayerClient.cs | 2 +- .../Online/ProductionEndpointConfiguration.cs | 8 ++-- .../Online/Spectator/OnlineSpectatorClient.cs | 2 +- osu.Game/OsuGameBase.cs | 2 +- osu.Game/Overlays/Comments/DrawableComment.cs | 2 +- osu.Game/Overlays/Login/LoginForm.cs | 2 +- .../Overlays/Login/SecondFactorAuthForm.cs | 2 +- .../Profile/Header/BottomHeaderContainer.cs | 4 +- .../Components/DrawableTournamentBanner.cs | 2 +- .../Profile/Header/TopHeaderContainer.cs | 2 +- .../Sections/Recent/DrawableRecentActivity.cs | 2 +- osu.Game/Overlays/Wiki/WikiPanelContainer.cs | 2 +- osu.Game/Overlays/WikiOverlay.cs | 4 +- .../ScreenFrequentlyAskedQuestions.cs | 4 +- .../Lounge/Components/DrawableRoom.cs | 2 +- .../Leaderboards/LeaderboardScoreV2.cs | 2 +- osu.Game/Utils/SentryLogger.cs | 2 +- 35 files changed, 78 insertions(+), 78 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index cf56fe6115..668f63b910 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -173,7 +173,7 @@ namespace osu.Desktop new Button { Label = "View beatmap", - Url = $@"{api.EndpointConfiguration.WebsiteRootUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}" + Url = $@"{api.Endpoints.WebsiteUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}" } }; } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs index e2d5bc2917..cd391519f4 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs @@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Menus new APIMenuImage { Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png", - Url = $@"{API.EndpointConfiguration.WebsiteRootUrl}/home/news/2023-12-21-project-loved-december-2023", + Url = $@"{API.Endpoints.WebsiteUrl}/home/news/2023-12-21-project-loved-december-2023", } } }); diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs index cee3f37aea..e453a32652 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs @@ -67,19 +67,19 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestLink() { - AddStep("set current path", () => markdownContainer.CurrentPath = $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/Article_styling_criteria/"); + AddStep("set current path", () => markdownContainer.CurrentPath = $"{API.Endpoints.WebsiteUrl}/wiki/Article_styling_criteria/"); AddStep("set '/wiki/Main_page''", () => markdownContainer.Text = "[wiki main page](/wiki/Main_page)"); - AddAssert("check url", () => markdownContainer.Link.Url == $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/Main_page"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.Endpoints.WebsiteUrl}/wiki/Main_page"); AddStep("set '../FAQ''", () => markdownContainer.Text = "[FAQ](../FAQ)"); - AddAssert("check url", () => markdownContainer.Link.Url == $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/FAQ"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.Endpoints.WebsiteUrl}/wiki/FAQ"); AddStep("set './Writing''", () => markdownContainer.Text = "[wiki writing guidline](./Writing)"); - AddAssert("check url", () => markdownContainer.Link.Url == $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/Article_styling_criteria/Writing"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.Endpoints.WebsiteUrl}/wiki/Article_styling_criteria/Writing"); AddStep("set 'Formatting''", () => markdownContainer.Text = "[wiki formatting guidline](Formatting)"); - AddAssert("check url", () => markdownContainer.Link.Url == $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/Article_styling_criteria/Formatting"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.Endpoints.WebsiteUrl}/wiki/Article_styling_criteria/Formatting"); } [Test] diff --git a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs index d0625c64e3..16b4b04ce4 100644 --- a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs @@ -59,7 +59,7 @@ namespace osu.Game.Beatmaps if (beatmapInfo.OnlineID <= 0 || beatmapInfo.BeatmapSet == null) return null; - return $@"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapInfo.BeatmapSet.OnlineID}#{ruleset?.ShortName ?? beatmapInfo.Ruleset.ShortName}/{beatmapInfo.OnlineID}"; + return $@"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapInfo.BeatmapSet.OnlineID}#{ruleset?.ShortName ?? beatmapInfo.Ruleset.ShortName}/{beatmapInfo.OnlineID}"; } } } diff --git a/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs index ac191d36a9..1af0e7a9ee 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs @@ -41,9 +41,9 @@ namespace osu.Game.Beatmaps return null; if (ruleset != null) - return $@"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapSetInfo.OnlineID}#{ruleset.ShortName}"; + return $@"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetInfo.OnlineID}#{ruleset.ShortName}"; - return $@"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapSetInfo.OnlineID}"; + return $@"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetInfo.OnlineID}"; } } } diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index ef7b49868c..88f9b3f242 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -40,7 +40,7 @@ namespace osu.Game.Online.API private readonly Queue queue = new Queue(); - public EndpointConfiguration EndpointConfiguration { get; } + public EndpointConfiguration Endpoints { get; } /// /// The API response version. @@ -73,7 +73,7 @@ namespace osu.Game.Online.API private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource(); private readonly Logger log; - public APIAccess(OsuGameBase game, OsuConfigManager config, EndpointConfiguration endpointConfiguration, string versionHash) + public APIAccess(OsuGameBase game, OsuConfigManager config, EndpointConfiguration endpoints, string versionHash) { this.game = game; this.config = config; @@ -87,13 +87,13 @@ namespace osu.Game.Online.API APIVersion = now.Year * 10000 + now.Month * 100 + now.Day; } - EndpointConfiguration = endpointConfiguration; + Endpoints = endpoints; NotificationsClient = setUpNotificationsClient(); - authentication = new OAuth(endpointConfiguration.APIClientID, endpointConfiguration.APIClientSecret, EndpointConfiguration.APIEndpointUrl); + authentication = new OAuth(endpoints.APIClientID, endpoints.APIClientSecret, Endpoints.APIUrl); log = Logger.GetLogger(LoggingTarget.Network); - log.Add($@"API endpoint root: {EndpointConfiguration.APIEndpointUrl}"); + log.Add($@"API endpoint root: {Endpoints.APIUrl}"); log.Add($@"API request version: {APIVersion}"); ProvidedUsername = config.Get(OsuSetting.Username); @@ -405,7 +405,7 @@ namespace osu.Game.Online.API var req = new RegistrationRequest { - Url = $@"{EndpointConfiguration.APIEndpointUrl}/users", + Url = $@"{Endpoints.APIUrl}/users", Method = HttpMethod.Post, Username = username, Email = email, diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 575e6f8a10..9d9873cc6f 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -71,7 +71,7 @@ namespace osu.Game.Online.API protected virtual WebRequest CreateWebRequest() => new OsuWebRequest(Uri); - protected virtual string Uri => $@"{API!.EndpointConfiguration.APIEndpointUrl}/api/v2/{Target}"; + protected virtual string Uri => $@"{API!.Endpoints.APIUrl}/api/v2/{Target}"; protected IAPIProvider? API; diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 7b3a8f357b..f9649cdd88 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -41,10 +41,10 @@ namespace osu.Game.Online.API public string ProvidedUsername => LocalUser.Value.Username; - public EndpointConfiguration EndpointConfiguration { get; } = new EndpointConfiguration + public EndpointConfiguration Endpoints { get; } = new EndpointConfiguration { - APIEndpointUrl = "http://localhost", - WebsiteRootUrl = "http://localhost", + APIUrl = "http://localhost", + WebsiteUrl = "http://localhost", }; public int APIVersion => int.Parse(DateTime.Now.ToString("yyyyMMdd")); diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 048193def7..54eaaaafc2 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -53,7 +53,7 @@ namespace osu.Game.Online.API /// /// Holds configuration for online endpoints. /// - EndpointConfiguration EndpointConfiguration { get; } + EndpointConfiguration Endpoints { get; } /// /// The version of the API. diff --git a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs index bb9d32f77b..ffe7b5d1ec 100644 --- a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs +++ b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs @@ -15,10 +15,10 @@ namespace osu.Game.Online.API.Requests get { // can be removed once the service has been successfully deployed to production - if (API!.EndpointConfiguration.BeatmapSubmissionServiceUrl == null) + if (API!.Endpoints.BeatmapSubmissionServiceUrl == null) throw new NotSupportedException("Beatmap submission not supported in this configuration!"); - return $@"{API!.EndpointConfiguration.BeatmapSubmissionServiceUrl!}/beatmapsets/{BeatmapSetID}"; + return $@"{API!.Endpoints.BeatmapSubmissionServiceUrl!}/beatmapsets/{BeatmapSetID}"; } } diff --git a/osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs b/osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs index 03b8397681..fb25749786 100644 --- a/osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs +++ b/osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs @@ -21,10 +21,10 @@ namespace osu.Game.Online.API.Requests get { // can be removed once the service has been successfully deployed to production - if (API!.EndpointConfiguration.BeatmapSubmissionServiceUrl == null) + if (API!.Endpoints.BeatmapSubmissionServiceUrl == null) throw new NotSupportedException("Beatmap submission not supported in this configuration!"); - return $@"{API!.EndpointConfiguration.BeatmapSubmissionServiceUrl}/beatmapsets"; + return $@"{API!.Endpoints.BeatmapSubmissionServiceUrl}/beatmapsets"; } } diff --git a/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs index c9dd12d61e..2e224ce602 100644 --- a/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs +++ b/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs @@ -14,10 +14,10 @@ namespace osu.Game.Online.API.Requests get { // can be removed once the service has been successfully deployed to production - if (API!.EndpointConfiguration.BeatmapSubmissionServiceUrl == null) + if (API!.Endpoints.BeatmapSubmissionServiceUrl == null) throw new NotSupportedException("Beatmap submission not supported in this configuration!"); - return $@"{API!.EndpointConfiguration.BeatmapSubmissionServiceUrl}/beatmapsets/{BeatmapSetID}"; + return $@"{API!.Endpoints.BeatmapSubmissionServiceUrl}/beatmapsets/{BeatmapSetID}"; } } diff --git a/osu.Game/Online/Chat/ExternalLinkOpener.cs b/osu.Game/Online/Chat/ExternalLinkOpener.cs index 1615b72033..258cca2ad5 100644 --- a/osu.Game/Online/Chat/ExternalLinkOpener.cs +++ b/osu.Game/Online/Chat/ExternalLinkOpener.cs @@ -49,12 +49,12 @@ namespace osu.Game.Online.Chat if (url.StartsWith('/')) { - url = $"{api.EndpointConfiguration.WebsiteRootUrl}{url}"; + url = $"{api.Endpoints.WebsiteUrl}{url}"; isTrustedDomain = true; } else { - isTrustedDomain = url.StartsWith(api.EndpointConfiguration.WebsiteRootUrl, StringComparison.Ordinal); + isTrustedDomain = url.StartsWith(api.Endpoints.WebsiteUrl, StringComparison.Ordinal); } if (!url.CheckIsValidUrl()) diff --git a/osu.Game/Online/Chat/NowPlayingCommand.cs b/osu.Game/Online/Chat/NowPlayingCommand.cs index 5e71980a55..43452a768c 100644 --- a/osu.Game/Online/Chat/NowPlayingCommand.cs +++ b/osu.Game/Online/Chat/NowPlayingCommand.cs @@ -95,7 +95,7 @@ namespace osu.Game.Online.Chat string getBeatmapPart() { - return beatmapOnlineID > 0 ? $"[{api.EndpointConfiguration.WebsiteRootUrl}/b/{beatmapOnlineID} {beatmapDisplayTitle}]" : beatmapDisplayTitle; + return beatmapOnlineID > 0 ? $"[{api.Endpoints.WebsiteUrl}/b/{beatmapOnlineID} {beatmapDisplayTitle}]" : beatmapDisplayTitle; } string getRulesetPart() diff --git a/osu.Game/Online/DevelopmentEndpointConfiguration.cs b/osu.Game/Online/DevelopmentEndpointConfiguration.cs index 5f3c353f4d..f4e1b257ee 100644 --- a/osu.Game/Online/DevelopmentEndpointConfiguration.cs +++ b/osu.Game/Online/DevelopmentEndpointConfiguration.cs @@ -7,12 +7,12 @@ namespace osu.Game.Online { public DevelopmentEndpointConfiguration() { - WebsiteRootUrl = APIEndpointUrl = @"https://dev.ppy.sh"; + WebsiteUrl = APIUrl = @"https://dev.ppy.sh"; APIClientSecret = @"3LP2mhUrV89xxzD1YKNndXHEhWWCRLPNKioZ9ymT"; APIClientID = "5"; - SpectatorEndpointUrl = $@"{APIEndpointUrl}/signalr/spectator"; - MultiplayerEndpointUrl = $@"{APIEndpointUrl}/signalr/multiplayer"; - MetadataEndpointUrl = $@"{APIEndpointUrl}/signalr/metadata"; + SpectatorUrl = $@"{APIUrl}/signalr/spectator"; + MultiplayerUrl = $@"{APIUrl}/signalr/multiplayer"; + MetadataUrl = $@"{APIUrl}/signalr/metadata"; } } } diff --git a/osu.Game/Online/EndpointConfiguration.cs b/osu.Game/Online/EndpointConfiguration.cs index 39dd72d41a..2d5ea32345 100644 --- a/osu.Game/Online/EndpointConfiguration.cs +++ b/osu.Game/Online/EndpointConfiguration.cs @@ -8,16 +8,6 @@ namespace osu.Game.Online /// public class EndpointConfiguration { - /// - /// The base URL for the website. Does not include a trailing slash. - /// - public string WebsiteRootUrl { get; set; } = string.Empty; - - /// - /// The endpoint for the main (osu-web) API. Does not include a trailing slash. - /// - public string APIEndpointUrl { get; set; } = string.Empty; - /// /// The OAuth client secret. /// @@ -29,23 +19,33 @@ namespace osu.Game.Online public string APIClientID { get; set; } = string.Empty; /// - /// The endpoint for the SignalR spectator server. + /// The base URL for the website. Does not include a trailing slash. /// - public string SpectatorEndpointUrl { get; set; } = string.Empty; + public string WebsiteUrl { get; set; } = string.Empty; /// - /// The endpoint for the SignalR multiplayer server. + /// The endpoint for the main (osu-web) API. Does not include a trailing slash. /// - public string MultiplayerEndpointUrl { get; set; } = string.Empty; - - /// - /// The endpoint for the SignalR metadata server. - /// - public string MetadataEndpointUrl { get; set; } = string.Empty; + public string APIUrl { get; set; } = string.Empty; /// /// The root URL for the service handling beatmap submission. Does not include a trailing slash. /// public string? BeatmapSubmissionServiceUrl { get; set; } + + /// + /// The endpoint for the SignalR spectator server. + /// + public string SpectatorUrl { get; set; } = string.Empty; + + /// + /// The endpoint for the SignalR multiplayer server. + /// + public string MultiplayerUrl { get; set; } = string.Empty; + + /// + /// The endpoint for the SignalR metadata server. + /// + public string MetadataUrl { get; set; } = string.Empty; } } diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index f7efa08969..52074119b8 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -436,7 +436,7 @@ namespace osu.Game.Online.Leaderboards items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods)); if (Score.OnlineID > 0) - items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.EndpointConfiguration.WebsiteRootUrl}/scores/{Score.OnlineID}"))); + items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{Score.OnlineID}"))); if (Score.Files.Count > 0) { diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index c7c7dfc58b..6637fc8dba 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -47,7 +47,7 @@ namespace osu.Game.Online.Metadata public OnlineMetadataClient(EndpointConfiguration endpoints) { - endpoint = endpoints.MetadataEndpointUrl; + endpoint = endpoints.MetadataUrl; } [BackgroundDependencyLoader] diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 2660cd94e4..a485a6b262 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -32,7 +32,7 @@ namespace osu.Game.Online.Multiplayer public OnlineMultiplayerClient(EndpointConfiguration endpoints) { - endpoint = endpoints.MultiplayerEndpointUrl; + endpoint = endpoints.MultiplayerUrl; } [BackgroundDependencyLoader] diff --git a/osu.Game/Online/ProductionEndpointConfiguration.cs b/osu.Game/Online/ProductionEndpointConfiguration.cs index 0244761b65..6e06abbeed 100644 --- a/osu.Game/Online/ProductionEndpointConfiguration.cs +++ b/osu.Game/Online/ProductionEndpointConfiguration.cs @@ -7,12 +7,12 @@ namespace osu.Game.Online { public ProductionEndpointConfiguration() { - WebsiteRootUrl = APIEndpointUrl = @"https://osu.ppy.sh"; + WebsiteUrl = APIUrl = @"https://osu.ppy.sh"; APIClientSecret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk"; APIClientID = "5"; - SpectatorEndpointUrl = "https://spectator.ppy.sh/spectator"; - MultiplayerEndpointUrl = "https://spectator.ppy.sh/multiplayer"; - MetadataEndpointUrl = "https://spectator.ppy.sh/metadata"; + SpectatorUrl = "https://spectator.ppy.sh/spectator"; + MultiplayerUrl = "https://spectator.ppy.sh/multiplayer"; + MetadataUrl = "https://spectator.ppy.sh/metadata"; } } } diff --git a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs index 645d7054dc..29d174f8e3 100644 --- a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs +++ b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs @@ -24,7 +24,7 @@ namespace osu.Game.Online.Spectator public OnlineSpectatorClient(EndpointConfiguration endpoints) { - endpoint = endpoints.SpectatorEndpointUrl; + endpoint = endpoints.SpectatorUrl; } [BackgroundDependencyLoader] diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 5e247ca877..7d35207bbe 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -295,7 +295,7 @@ namespace osu.Game EndpointConfiguration endpoints = CreateEndpoints(); - MessageFormatter.WebsiteRootUrl = endpoints.WebsiteRootUrl; + MessageFormatter.WebsiteRootUrl = endpoints.WebsiteUrl; frameworkLocale = frameworkConfig.GetBindable(FrameworkSetting.Locale); frameworkLocale.BindValueChanged(_ => updateLanguage()); diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index b06be3e74a..0d566174bb 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -419,7 +419,7 @@ namespace osu.Game.Overlays.Comments private void copyUrl() { - clipboard.SetText($@"{api.EndpointConfiguration.APIEndpointUrl}/comments/{Comment.Id}"); + clipboard.SetText($@"{api.Endpoints.APIUrl}/comments/{Comment.Id}"); onScreenDisplay?.Display(new CopyUrlToast()); } diff --git a/osu.Game/Overlays/Login/LoginForm.cs b/osu.Game/Overlays/Login/LoginForm.cs index 2b6d523b95..215a946b42 100644 --- a/osu.Game/Overlays/Login/LoginForm.cs +++ b/osu.Game/Overlays/Login/LoginForm.cs @@ -129,7 +129,7 @@ namespace osu.Game.Overlays.Login } }; - forgottenPasswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.EndpointConfiguration.WebsiteRootUrl}/home/password-reset"); + forgottenPasswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.Endpoints.WebsiteUrl}/home/password-reset"); password.OnCommit += (_, _) => performLogin(); diff --git a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs index e36d62f827..74db58e225 100644 --- a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs +++ b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs @@ -98,7 +98,7 @@ namespace osu.Game.Overlays.Login explainText.AddParagraph(UserVerificationStrings.BoxInfoCheckSpam); // We can't support localisable strings with nested links yet. Not sure if we even can (probably need to allow markdown link formatting or something). explainText.AddParagraph("If you can't access your email or have forgotten what you used, please follow the "); - explainText.AddLink(UserVerificationStrings.BoxInfoRecoverLink, $"{api.EndpointConfiguration.WebsiteRootUrl}/home/password-reset"); + explainText.AddLink(UserVerificationStrings.BoxInfoRecoverLink, $"{api.Endpoints.WebsiteUrl}/home/password-reset"); explainText.AddText(". You can also "); explainText.AddLink(UserVerificationStrings.BoxInfoReissueLink, () => { diff --git a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs index d9d23f16fd..03c849052b 100644 --- a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs @@ -124,12 +124,12 @@ namespace osu.Game.Overlays.Profile.Header } topLinkContainer.AddText("Contributed "); - topLinkContainer.AddLink("forum post".ToQuantity(user.PostCount, "#,##0"), $"{api.EndpointConfiguration.WebsiteRootUrl}/users/{user.Id}/posts", creationParameters: embolden); + topLinkContainer.AddLink("forum post".ToQuantity(user.PostCount, "#,##0"), $"{api.Endpoints.WebsiteUrl}/users/{user.Id}/posts", creationParameters: embolden); addSpacer(topLinkContainer); topLinkContainer.AddText("Posted "); - topLinkContainer.AddLink("comment".ToQuantity(user.CommentsCount, "#,##0"), $"{api.EndpointConfiguration.WebsiteRootUrl}/comments?user_id={user.Id}", creationParameters: embolden); + topLinkContainer.AddLink("comment".ToQuantity(user.CommentsCount, "#,##0"), $"{api.Endpoints.WebsiteUrl}/comments?user_id={user.Id}", creationParameters: embolden); string websiteWithoutProtocol = user.Website; diff --git a/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs b/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs index a66a5c8fe9..b036b0a305 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs @@ -33,7 +33,7 @@ namespace osu.Game.Overlays.Profile.Header.Components Texture = textures.Get(banner.Image), }; - Action = () => game?.OpenUrlExternally($@"{api.EndpointConfiguration.WebsiteRootUrl}/community/tournaments/{banner.TournamentId}"); + Action = () => game?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/community/tournaments/{banner.TournamentId}"); } protected override void LoadComplete() diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index fb1bdca57c..ba2cd5b705 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -213,7 +213,7 @@ namespace osu.Game.Overlays.Profile.Header cover.User = user; avatar.User = user; usernameText.Text = user?.Username ?? string.Empty; - openUserExternally.Link = $@"{api.EndpointConfiguration.WebsiteRootUrl}/users/{user?.Id ?? 0}"; + openUserExternally.Link = $@"{api.Endpoints.WebsiteUrl}/users/{user?.Id ?? 0}"; userFlag.CountryCode = user?.CountryCode ?? default; userCountryText.Text = (user?.CountryCode ?? default).GetDescription(); userCountryContainer.Action = () => rankingsOverlay?.ShowCountry(user?.CountryCode ?? default); diff --git a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs index a0bcf2dc47..05762f29f9 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs @@ -223,7 +223,7 @@ namespace osu.Game.Overlays.Profile.Sections.Recent private void addBeatmapsetLink() => content.AddLink(activity.Beatmapset.AsNonNull().Title, LinkAction.OpenBeatmapSet, getLinkArgument(activity.Beatmapset.AsNonNull().Url), creationParameters: t => t.Font = getLinkFont()); - private object getLinkArgument(string url) => MessageFormatter.GetLinkDetails($"{api.EndpointConfiguration.WebsiteRootUrl}{url}").Argument.AsNonNull(); + private object getLinkArgument(string url) => MessageFormatter.GetLinkDetails($"{api.Endpoints.WebsiteUrl}{url}").Argument.AsNonNull(); private FontUsage getLinkFont(FontWeight fontWeight = FontWeight.Regular) => OsuFont.GetFont(size: font_size, weight: fontWeight, italics: true); diff --git a/osu.Game/Overlays/Wiki/WikiPanelContainer.cs b/osu.Game/Overlays/Wiki/WikiPanelContainer.cs index 773dde6436..81bdae5525 100644 --- a/osu.Game/Overlays/Wiki/WikiPanelContainer.cs +++ b/osu.Game/Overlays/Wiki/WikiPanelContainer.cs @@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Wiki Padding = new MarginPadding(padding), Child = new WikiPanelMarkdownContainer(isFullWidth) { - CurrentPath = $@"{api.EndpointConfiguration.WebsiteRootUrl}/wiki/", + CurrentPath = $@"{api.Endpoints.WebsiteUrl}/wiki/", Text = text, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y diff --git a/osu.Game/Overlays/WikiOverlay.cs b/osu.Game/Overlays/WikiOverlay.cs index c360d1eb9e..e9099f1deb 100644 --- a/osu.Game/Overlays/WikiOverlay.cs +++ b/osu.Game/Overlays/WikiOverlay.cs @@ -167,7 +167,7 @@ namespace osu.Game.Overlays } else { - LoadDisplay(articlePage = new WikiArticlePage($@"{api.EndpointConfiguration.WebsiteRootUrl}/wiki/{path.Value}/", response.Markdown)); + LoadDisplay(articlePage = new WikiArticlePage($@"{api.Endpoints.WebsiteUrl}/wiki/{path.Value}/", response.Markdown)); } } @@ -176,7 +176,7 @@ namespace osu.Game.Overlays wikiData.Value = null; path.Value = "error"; - LoadDisplay(articlePage = new WikiArticlePage($@"{api.EndpointConfiguration.WebsiteRootUrl}/wiki/", + LoadDisplay(articlePage = new WikiArticlePage($@"{api.Endpoints.WebsiteUrl}/wiki/", $"Something went wrong when trying to fetch page \"{originalPath}\".\n\n[Return to the main page]({INDEX_PATH}).")); } diff --git a/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs b/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs index ff9cb07e2d..861c5051f4 100644 --- a/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs +++ b/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs @@ -46,14 +46,14 @@ namespace osu.Game.Screens.Edit.Submission RelativeSizeAxes = Axes.X, Caption = BeatmapSubmissionStrings.MappingHelpForumDescription, ButtonText = BeatmapSubmissionStrings.MappingHelpForum, - Action = () => game?.OpenUrlExternally($@"{api.EndpointConfiguration.WebsiteRootUrl}/community/forums/56"), + Action = () => game?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/community/forums/56"), }, new FormButton { RelativeSizeAxes = Axes.X, Caption = BeatmapSubmissionStrings.ModdingQueuesForumDescription, ButtonText = BeatmapSubmissionStrings.ModdingQueuesForum, - Action = () => game?.OpenUrlExternally($@"{api.EndpointConfiguration.WebsiteRootUrl}/community/forums/60"), + Action = () => game?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/community/forums/60"), }, }, }); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 7b2e2c02f7..de5813ce0d 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -361,7 +361,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components return items.ToArray(); - string formatRoomUrl(long id) => $@"{api.EndpointConfiguration.WebsiteRootUrl}/multiplayer/rooms/{id}"; + string formatRoomUrl(long id) => $@"{api.Endpoints.WebsiteUrl}/multiplayer/rooms/{id}"; } } diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs index 2460fbe6f8..a2253b413c 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs @@ -778,7 +778,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => SelectedMods.Value = score.Mods.Where(m => IsValidMod.Invoke(m)).ToArray())); if (score.OnlineID > 0) - items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.EndpointConfiguration.WebsiteRootUrl}/scores/{score.OnlineID}"))); + items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{score.OnlineID}"))); if (score.Files.Count <= 0) return items.ToArray(); diff --git a/osu.Game/Utils/SentryLogger.cs b/osu.Game/Utils/SentryLogger.cs index ed644bf5cb..2172ea895e 100644 --- a/osu.Game/Utils/SentryLogger.cs +++ b/osu.Game/Utils/SentryLogger.cs @@ -41,7 +41,7 @@ namespace osu.Game.Utils { this.game = game; - if (!game.IsDeployedBuild || !game.CreateEndpoints().WebsiteRootUrl.EndsWith(@".ppy.sh", StringComparison.Ordinal)) + if (!game.IsDeployedBuild || !game.CreateEndpoints().WebsiteUrl.EndsWith(@".ppy.sh", StringComparison.Ordinal)) return; sentrySession = SentrySdk.Init(options => From 3da615481eb59a2aad22501e74121f2b0e06323e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 17:38:24 +0900 Subject: [PATCH 395/801] Change `switch` to simple conditional for now --- .../Screens/SelectV2/BeatmapCarouselFilterGrouping.cs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 8838ce67ad..db407fd647 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -51,16 +51,7 @@ namespace osu.Game.Screens.SelectV2 HashSet? groupRefItems = null; HashSet? setRefItems = null; - switch (criteria.Group) - { - default: - BeatmapSetsGroupedTogether = true; - break; - - case GroupMode.Difficulty: - BeatmapSetsGroupedTogether = false; - break; - } + BeatmapSetsGroupedTogether = criteria.Group != GroupMode.Difficulty; foreach (var item in items) { From 29b0b62ffa55ebb4ac4b107a691c95a3f72f516d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 17:39:38 +0900 Subject: [PATCH 396/801] Rename variables to something more sane --- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index db407fd647..cb5a40918c 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -48,8 +48,8 @@ namespace osu.Game.Screens.SelectV2 BeatmapInfo? lastBeatmap = null; GroupDefinition? lastGroup = null; - HashSet? groupRefItems = null; - HashSet? setRefItems = null; + HashSet? currentGroupItems = null; + HashSet? currentSetItems = null; BeatmapSetsGroupedTogether = criteria.Group != GroupMode.Difficulty; @@ -62,10 +62,10 @@ namespace osu.Game.Screens.SelectV2 if (createGroupIfRequired(criteria, beatmap, lastGroup) is GroupDefinition newGroup) { // When reaching a new group, ensure we reset any beatmap set tracking. - setRefItems = null; + currentSetItems = null; lastBeatmap = null; - groupItems[newGroup] = groupRefItems = new HashSet(); + groupItems[newGroup] = currentGroupItems = new HashSet(); lastGroup = newGroup; addItem(new CarouselItem(newGroup) @@ -81,7 +81,7 @@ namespace osu.Game.Screens.SelectV2 if (newBeatmapSet) { - setItems[beatmap.BeatmapSet!] = setRefItems = new HashSet(); + setItems[beatmap.BeatmapSet!] = currentSetItems = new HashSet(); addItem(new CarouselItem(beatmap.BeatmapSet!) { @@ -98,10 +98,10 @@ namespace osu.Game.Screens.SelectV2 { newItems.Add(i); - groupRefItems?.Add(i); - setRefItems?.Add(i); + currentGroupItems?.Add(i); + currentSetItems?.Add(i); - i.IsVisible = i.Model is GroupDefinition || (lastGroup == null && (i.Model is BeatmapSetInfo || setRefItems == null)); + i.IsVisible = i.Model is GroupDefinition || (lastGroup == null && (i.Model is BeatmapSetInfo || currentSetItems == null)); } } From bf57fef4125bba86595850a6ec13f5f1fcb3f980 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 17:50:32 +0900 Subject: [PATCH 397/801] Fix missing cached settings in `BetamapSubmissionOverlay` test --- .../Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs index e3e8c0de39..f83d424d56 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs @@ -24,7 +24,11 @@ namespace osu.Game.Tests.Visual.Editing Child = new DependencyProvidingContainer { RelativeSizeAxes = Axes.Both, - CachedDependencies = new[] { (typeof(ScreenFooter), (object)footer) }, + CachedDependencies = new[] + { + (typeof(ScreenFooter), (object)footer), + (typeof(BeatmapSubmissionSettings), new BeatmapSubmissionSettings()), + }, Children = new Drawable[] { receptor, From 46290ae76b81d953253b670c752968906ced6e5f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:05:47 +0900 Subject: [PATCH 398/801] Disallow changing beatmap / ruleset while submitting beatmap --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 9794402061..4c7ea39c35 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -40,6 +40,8 @@ namespace osu.Game.Screens.Edit.Submission public override bool AllowUserExit => false; + public override bool DisallowExternalBeatmapRulesetChanges => true; + [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); From 12881f3f366625ecdd861c66e24120541c428995 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:06:31 +0900 Subject: [PATCH 399/801] Don't show informational screens for subsequent submissions These are historically only presented to the user when uploading a new beatmap for the first time. --- .../Edit/Submission/BeatmapSubmissionOverlay.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs index da2abd8c23..cf2fef25d5 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Localisation; @@ -15,10 +17,14 @@ namespace osu.Game.Screens.Edit.Submission } [BackgroundDependencyLoader] - private void load() + private void load(IBindable beatmap) { - AddStep(); - AddStep(); + if (beatmap.Value.BeatmapSetInfo.OnlineID <= 0) + { + AddStep(); + AddStep(); + } + AddStep(); Header.Title = BeatmapSubmissionStrings.BeatmapSubmissionTitle; From 95967a2fde5ae2015c206d35f3edc86eff318388 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:17:49 +0900 Subject: [PATCH 400/801] Adjust beatmap stream creation to make a bit more sense --- .../Edit/Submission/BeatmapSubmissionScreen.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 4c7ea39c35..44b2778869 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -76,10 +76,10 @@ namespace osu.Game.Screens.Edit.Submission private RoundedButton backButton = null!; private uint? beatmapSetId; + private MemoryStream? beatmapPackageStream; private SubmissionBeatmapExporter legacyBeatmapExporter = null!; private ProgressNotification? exportProgressNotification; - private MemoryStream beatmapPackageStream = null!; private ProgressNotification? updateProgressNotification; [BackgroundDependencyLoader] @@ -189,7 +189,6 @@ namespace osu.Game.Screens.Edit.Submission } } }); - beatmapPackageStream = new MemoryStream(); } private void createBeatmapSet() @@ -239,10 +238,12 @@ namespace osu.Game.Screens.Edit.Submission private async Task createBeatmapPackage(ICollection onlineFiles) { Debug.Assert(ThreadSafety.IsUpdateThread); + exportStep.SetInProgress(); try { + beatmapPackageStream = new MemoryStream(); await legacyBeatmapExporter.ExportToStreamAsync(Beatmap.Value.BeatmapSetInfo.ToLive(realmAccess), beatmapPackageStream, exportProgressNotification = new ProgressNotification()) .ConfigureAwait(true); } @@ -266,6 +267,7 @@ namespace osu.Game.Screens.Edit.Submission private async Task patchBeatmapSet(ICollection onlineFiles) { Debug.Assert(beatmapSetId != null); + Debug.Assert(beatmapPackageStream != null); var onlineFilesByFilename = onlineFiles.ToDictionary(f => f.Filename, f => f.SHA2Hash); @@ -320,6 +322,7 @@ namespace osu.Game.Screens.Edit.Submission private void replaceBeatmapSet() { Debug.Assert(beatmapSetId != null); + Debug.Assert(beatmapPackageStream != null); var uploadRequest = new ReplaceBeatmapPackageRequest(beatmapSetId.Value, beatmapPackageStream.ToArray()); @@ -347,6 +350,8 @@ namespace osu.Game.Screens.Edit.Submission private async Task updateLocalBeatmap() { Debug.Assert(beatmapSetId != null); + Debug.Assert(beatmapPackageStream != null); + updateStep.SetInProgress(); Live? importedSet; @@ -420,5 +425,12 @@ namespace osu.Game.Screens.Edit.Submission overlay.Show(); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + beatmapPackageStream?.Dispose(); + } } } From 783ef0078533c7bf90f13675861a88c03c4242e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:34:48 +0900 Subject: [PATCH 401/801] Change `BeatmapSubmissionScreen` to use global back button instead of custom implementation --- .../Submission/BeatmapSubmissionScreen.cs | 81 ++++++++++--------- 1 file changed, 44 insertions(+), 37 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 44b2778869..8536ba5f02 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -21,7 +21,6 @@ using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Extensions; -using osu.Game.Graphics.UserInterfaceV2; using osu.Game.IO.Archives; using osu.Game.Localisation; using osu.Game.Online.API; @@ -38,8 +37,6 @@ namespace osu.Game.Screens.Edit.Submission { private BeatmapSubmissionOverlay overlay = null!; - public override bool AllowUserExit => false; - public override bool DisallowExternalBeatmapRulesetChanges => true; [Cached] @@ -73,7 +70,6 @@ namespace osu.Game.Screens.Edit.Submission private SubmissionStageProgress updateStep = null!; private Container successContainer = null!; private Container flashLayer = null!; - private RoundedButton backButton = null!; private uint? beatmapSetId; private MemoryStream? beatmapPackageStream; @@ -82,6 +78,8 @@ namespace osu.Game.Screens.Edit.Submission private ProgressNotification? exportProgressNotification; private ProgressNotification? updateProgressNotification; + private Live? importedSet; + [BackgroundDependencyLoader] private void load() { @@ -161,15 +159,6 @@ namespace osu.Game.Screens.Edit.Submission } } }, - backButton = new RoundedButton - { - Text = CommonStrings.Back, - Width = 150, - Action = this.Exit, - Enabled = { Value = false }, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - } } } } @@ -181,7 +170,10 @@ namespace osu.Game.Screens.Edit.Submission if (overlay.State.Value == Visibility.Hidden) { if (!overlay.Completed) + { + allowExit(); this.Exit(); + } else { submissionProgress.FadeIn(200, Easing.OutQuint); @@ -227,8 +219,8 @@ namespace osu.Game.Screens.Edit.Submission createRequest.Failure += ex => { createSetStep.SetFailed(ex.Message); - backButton.Enabled.Value = true; Logger.Log($"Beatmap set submission failed on creation: {ex}"); + allowExit(); }; createSetStep.SetInProgress(); @@ -250,9 +242,9 @@ namespace osu.Game.Screens.Edit.Submission catch (Exception ex) { exportStep.SetFailed(ex.Message); - Logger.Log($"Beatmap set submission failed on export: {ex}"); - backButton.Enabled.Value = true; exportProgressNotification = null; + Logger.Log($"Beatmap set submission failed on export: {ex}"); + allowExit(); } exportStep.SetCompleted(); @@ -311,7 +303,7 @@ namespace osu.Game.Screens.Edit.Submission { uploadStep.SetFailed(ex.Message); Logger.Log($"Beatmap submission failed on upload: {ex}"); - backButton.Enabled.Value = true; + allowExit(); }; patchRequest.Progressed += (current, total) => uploadStep.SetInProgress((float)current / total); @@ -339,7 +331,7 @@ namespace osu.Game.Screens.Edit.Submission { uploadStep.SetFailed(ex.Message); Logger.Log($"Beatmap submission failed on upload: {ex}"); - backButton.Enabled.Value = true; + allowExit(); }; uploadRequest.Progressed += (current, total) => uploadStep.SetInProgress((float)current / Math.Max(total, 1)); @@ -354,8 +346,6 @@ namespace osu.Game.Screens.Edit.Submission updateStep.SetInProgress(); - Live? importedSet; - try { importedSet = await beatmaps.ImportAsUpdate( @@ -367,28 +357,13 @@ namespace osu.Game.Screens.Edit.Submission { updateStep.SetFailed(ex.Message); Logger.Log($"Beatmap submission failed on local update: {ex}"); - Schedule(() => backButton.Enabled.Value = true); + allowExit(); return; } updateStep.SetCompleted(); - backButton.Enabled.Value = true; - backButton.Action = () => - { - game?.PerformFromScreen(s => - { - if (s is OsuScreen osuScreen) - { - Debug.Assert(importedSet != null); - var targetBeatmap = importedSet.Value.Beatmaps.FirstOrDefault(b => b.DifficultyName == Beatmap.Value.BeatmapInfo.DifficultyName) - ?? importedSet.Value.Beatmaps.First(); - osuScreen.Beatmap.Value = beatmaps.GetWorkingBeatmap(targetBeatmap); - } - - s.Push(new EditorLoader()); - }, [typeof(MainMenu)]); - }; showBeatmapCard(); + allowExit(); } private void showBeatmapCard() @@ -408,6 +383,11 @@ namespace osu.Game.Screens.Edit.Submission api.Queue(getBeatmapSetRequest); } + private void allowExit() + { + BackButtonVisibility.Value = true; + } + protected override void Update() { base.Update(); @@ -419,6 +399,33 @@ namespace osu.Game.Screens.Edit.Submission updateStep.SetInProgress(updateProgressNotification.Progress); } + public override bool OnExiting(ScreenExitEvent e) + { + // We probably want a method of cancelling in the future… + if (!BackButtonVisibility.Value) + return true; + + if (importedSet != null) + { + game?.PerformFromScreen(s => + { + if (s is OsuScreen osuScreen) + { + Debug.Assert(importedSet != null); + var targetBeatmap = importedSet.Value.Beatmaps.FirstOrDefault(b => b.DifficultyName == Beatmap.Value.BeatmapInfo.DifficultyName) + ?? importedSet.Value.Beatmaps.First(); + osuScreen.Beatmap.Value = beatmaps.GetWorkingBeatmap(targetBeatmap); + } + + s.Push(new EditorLoader()); + }, [typeof(MainMenu)]); + + return true; + } + + return base.OnExiting(e); + } + public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); From ce88ecfb3cbfb2df90663b6f7ac1d3b8021da22e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:39:01 +0900 Subject: [PATCH 402/801] Adjust timeouts to be much higher for upload requests It seems that right now these timeouts do not check for actual data movement, which is to say if a user with a very slow connection is uploading and it takes more than `Timeout`, their upload will fail. For now let's set these values high enough that most users will not be affected. --- osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs | 2 +- osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs index 5728dbe3fa..df3c9d071c 100644 --- a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs +++ b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs @@ -46,7 +46,7 @@ namespace osu.Game.Online.API.Requests foreach (string filename in FilesDeleted) request.AddParameter(@"filesDeleted", filename, RequestParameterType.Form); - request.Timeout = 60_000; + request.Timeout = 600_000; return request; } } diff --git a/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs index 2e224ce602..de8af6a623 100644 --- a/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs +++ b/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs @@ -38,7 +38,7 @@ namespace osu.Game.Online.API.Requests var request = base.CreateWebRequest(); request.AddFile(@"beatmapArchive", oszPackage); request.Method = HttpMethod.Put; - request.Timeout = 60_000; + request.Timeout = 600_000; return request; } } From 753eae426d7c33978621025424b8dd43081a31fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:42:36 +0900 Subject: [PATCH 403/801] Update strings --- .../Localisation/BeatmapSubmissionStrings.cs | 20 +++++++++---------- .../Submission/BeatmapSubmissionScreen.cs | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game/Localisation/BeatmapSubmissionStrings.cs b/osu.Game/Localisation/BeatmapSubmissionStrings.cs index 50b65ab572..3abe8cc515 100644 --- a/osu.Game/Localisation/BeatmapSubmissionStrings.cs +++ b/osu.Game/Localisation/BeatmapSubmissionStrings.cs @@ -45,24 +45,24 @@ namespace osu.Game.Localisation public static LocalisableString ConfirmSubmission => new TranslatableString(getKey(@"confirm_submission"), @"Submit beatmap!"); /// - /// "Exporting beatmap set in compatibility mode..." + /// "Exporting beatmap for compatibility..." /// - public static LocalisableString ExportingBeatmapSet => new TranslatableString(getKey(@"exporting_beatmap_set"), @"Exporting beatmap set in compatibility mode..."); + public static LocalisableString Exporting => new TranslatableString(getKey(@"exporting"), @"Exporting beatmap for compatibility..."); /// - /// "Preparing beatmap set online..." + /// "Preparing for upload..." /// - public static LocalisableString PreparingBeatmapSet => new TranslatableString(getKey(@"preparing_beatmap_set"), @"Preparing beatmap set online..."); + public static LocalisableString Preparing => new TranslatableString(getKey(@"preparing"), @"Preparing for upload..."); /// - /// "Uploading beatmap set contents..." + /// "Uploading beatmap contents..." /// - public static LocalisableString UploadingBeatmapSetContents => new TranslatableString(getKey(@"uploading_beatmap_set_contents"), @"Uploading beatmap set contents..."); + public static LocalisableString Uploading => new TranslatableString(getKey(@"uploading"), @"Uploading beatmap contents..."); /// - /// "Updating local beatmap with relevant changes..." + /// "Finishing up..." /// - public static LocalisableString UpdatingLocalBeatmap => new TranslatableString(getKey(@"updating_local_beatmap"), @"Updating local beatmap with relevant changes..."); + public static LocalisableString Finishing => new TranslatableString(getKey(@"finishing"), @"Finishing up..."); /// /// "Before you continue, we ask you to check whether the content you are uploading has been cleared for upload. Please understand that you are responsible for the content you upload to the platform and if in doubt, should ask permission from the creators before uploading!" @@ -140,9 +140,9 @@ namespace osu.Game.Localisation public static LocalisableString LoadInBrowserAfterSubmission => new TranslatableString(getKey(@"load_in_browser_after_submission"), @"Load in browser after submission"); /// - /// "Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost." + /// "Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost." /// - public static LocalisableString LegacyExportDisclaimer => new TranslatableString(getKey(@"legacy_export_disclaimer"), @"Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost."); + public static LocalisableString LegacyExportDisclaimer => new TranslatableString(getKey(@"legacy_export_disclaimer"), @"Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost."); /// /// "Empty beatmaps cannot be submitted." diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 8536ba5f02..41c875ac1f 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -114,25 +114,25 @@ namespace osu.Game.Screens.Edit.Submission { createSetStep = new SubmissionStageProgress { - StageDescription = BeatmapSubmissionStrings.PreparingBeatmapSet, + StageDescription = BeatmapSubmissionStrings.Preparing, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, exportStep = new SubmissionStageProgress { - StageDescription = BeatmapSubmissionStrings.ExportingBeatmapSet, + StageDescription = BeatmapSubmissionStrings.Exporting, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, uploadStep = new SubmissionStageProgress { - StageDescription = BeatmapSubmissionStrings.UploadingBeatmapSetContents, + StageDescription = BeatmapSubmissionStrings.Uploading, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, updateStep = new SubmissionStageProgress { - StageDescription = BeatmapSubmissionStrings.UpdatingLocalBeatmap, + StageDescription = BeatmapSubmissionStrings.Finishing, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, From fab5cfd275bb827ab9c81c7d1e1be2a298a403d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:57:26 +0900 Subject: [PATCH 404/801] Fix slider ball rotation not being updated when rewinding to a slider --- .../Objects/Drawables/DrawableSliderBall.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs index 24c0d0fcf0..9b8b197804 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs @@ -66,8 +66,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Slider slider = drawableSlider.HitObject; Position = slider.CurvePositionAt(completionProgress); - //0.1 / slider.Path.Distance is the additional progress needed to ensure the diff length is 0.1 - var diff = slider.CurvePositionAt(completionProgress) - slider.CurvePositionAt(Math.Min(1, completionProgress + 0.1 / slider.Path.Distance)); + // 0.1 / slider.Path.Distance is the additional progress needed to ensure the diff length is 0.1 + double checkDistance = 0.1 / slider.Path.Distance; + var diff = slider.CurvePositionAt(Math.Min(1 - checkDistance, completionProgress)) - slider.CurvePositionAt(Math.Min(1, completionProgress + checkDistance)); // Ensure the value is substantially high enough to allow for Atan2 to get a valid angle. // Needed for when near completion, or in case of a very short slider. From aad12024b0db512c32b77cd2b48dd50a64cb7d05 Mon Sep 17 00:00:00 2001 From: Layendan Date: Fri, 7 Feb 2025 03:13:51 -0700 Subject: [PATCH 405/801] remove using cache, improve tests, and revert loading --- .../TestSceneAddPlaylistToCollectionButton.cs | 37 ++++++++--- .../AddPlaylistToCollectionButton.cs | 62 +++++++------------ 2 files changed, 50 insertions(+), 49 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs index acf2c4b3f9..f18488170d 100644 --- a/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs +++ b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs @@ -4,12 +4,14 @@ using System; using System.Diagnostics; using System.Linq; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Collections; using osu.Game.Database; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -17,14 +19,17 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Screens.OnlinePlay.Playlists; using osuTK; +using osuTK.Input; +using SharpCompress; namespace osu.Game.Tests.Visual.Playlists { - public partial class TestSceneAddPlaylistToCollectionButton : OsuTestScene + public partial class TestSceneAddPlaylistToCollectionButton : OsuManualInputManagerTestScene { private BeatmapManager manager = null!; private BeatmapSetInfo importedBeatmap = null!; private Room room = null!; + private AddPlaylistToCollectionButton button = null!; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -32,6 +37,8 @@ namespace osu.Game.Tests.Visual.Playlists Dependencies.Cache(new RealmRulesetStore(Realm)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); + + Add(notificationOverlay); } [Cached(typeof(INotificationOverlay))] @@ -44,25 +51,37 @@ namespace osu.Game.Tests.Visual.Playlists [SetUpSteps] public void SetUpSteps() { + AddStep("clear realm", () => Realm.Realm.Write(() => Realm.Realm.RemoveAll())); + + AddStep("clear notifications", () => notificationOverlay.AllNotifications.Empty()); + importBeatmap(); setupRoom(); AddStep("create button", () => { - AddRange(new Drawable[] + Add(button = new AddPlaylistToCollectionButton(room) { - notificationOverlay, - new AddPlaylistToCollectionButton(room) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(300, 40), - } + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(300, 40), }); }); } + [Test] + public void TestButtonFlow() + { + AddStep("move mouse to button", () => InputManager.MoveMouseTo(button)); + + AddStep("click button", () => InputManager.Click(MouseButton.Left)); + + AddAssert("notification shown", () => notificationOverlay.AllNotifications.FirstOrDefault(n => n.Text.ToString().StartsWith("Created", StringComparison.Ordinal)) != null); + + AddAssert("realm is updated", () => Realm.Realm.All().FirstOrDefault(c => c.Name == room.Name) != null); + } + private void importBeatmap() => AddStep("import beatmap", () => { var beatmap = CreateBeatmap(new OsuRuleset().RulesetInfo); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index ab3e481f9f..8801d73e9e 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -2,17 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; -using System.Threading.Tasks; using osu.Framework.Allocation; -using osu.Framework.Extensions; +using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Database; -using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; +using Realms; namespace osu.Game.Screens.OnlinePlay.Playlists { @@ -20,14 +18,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { private readonly Room room; - private LoadingLayer loading = null!; - [Resolved] private RealmAccess realmAccess { get; set; } = null!; - [Resolved] - private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; - [Resolved(canBeNull: true)] private INotificationOverlay? notifications { get; set; } @@ -38,12 +31,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { - BackgroundColour = colours.Gray5; - - Add(loading = new LoadingLayer(true, false)); - Action = () => { int[] ids = room.Playlist.Select(item => item.Beatmap.OnlineID).Where(onlineId => onlineId > 0).ToArray(); @@ -54,34 +43,27 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return; } - Enabled.Value = false; - loading.Show(); - beatmapLookupCache.GetBeatmapsAsync(ids).ContinueWith(task => Schedule(() => + string filter = string.Join(" OR ", ids.Select(id => $"(OnlineID == {id})")); + var beatmaps = realmAccess.Realm.All().Filter(filter).ToList(); + + var collection = realmAccess.Realm.All().FirstOrDefault(c => c.Name == room.Name); + + if (collection == null) { - var beatmaps = task.GetResultSafely().Where(item => item?.BeatmapSet != null).ToList(); - - var collection = realmAccess.Realm.All().FirstOrDefault(c => c.Name == room.Name); - - if (collection == null) + collection = new BeatmapCollection(room.Name, beatmaps.Select(i => i.MD5Hash).Distinct().ToList()); + realmAccess.Realm.Write(() => realmAccess.Realm.Add(collection)); + notifications?.Post(new SimpleNotification { Text = $"Created new collection: {room.Name}" }); + } + else + { + collection.ToLive(realmAccess).PerformWrite(c => { - collection = new BeatmapCollection(room.Name, beatmaps.Select(i => i!.MD5Hash).Distinct().ToList()); - realmAccess.Realm.Write(() => realmAccess.Realm.Add(collection)); - notifications?.Post(new SimpleNotification { Text = $"Created new collection: {room.Name}" }); - } - else - { - collection.ToLive(realmAccess).PerformWrite(c => - { - beatmaps = beatmaps.Where(i => !c.BeatmapMD5Hashes.Contains(i!.MD5Hash)).ToList(); - foreach (var item in beatmaps) - c.BeatmapMD5Hashes.Add(item!.MD5Hash); - notifications?.Post(new SimpleNotification { Text = $"Updated collection: {room.Name}" }); - }); - } - - loading.Hide(); - Enabled.Value = true; - }), TaskContinuationOptions.OnlyOnRanToCompletion); + beatmaps = beatmaps.Where(i => !c.BeatmapMD5Hashes.Contains(i.MD5Hash)).ToList(); + foreach (var item in beatmaps) + c.BeatmapMD5Hashes.Add(item.MD5Hash); + notifications?.Post(new SimpleNotification { Text = $"Updated collection: {room.Name}" }); + }); + } }; } } From 9f90ebb2f774bd023befdc73a849fe087cca9550 Mon Sep 17 00:00:00 2001 From: James Wilson Date: Fri, 7 Feb 2025 10:21:12 +0000 Subject: [PATCH 406/801] Calculate hit windows in performance calculator instead of databased difficulty attributes (#31735) * Calculate hit windows in performance calculator instead of databased difficulty attributes * Apply mods to beatmap difficulty in osu! performance calculator * Remove `GreatHitWindow` difficulty attribute for osu!mania * Remove use of approach rate and overall difficulty attributes for osu! * Remove use of hit window difficulty attributes in osu!taiko * Remove use of approach rate attribute in osu!catch * Remove unused attribute IDs * Code quality * Fix `computeDeviationUpperBound` being called before `greatHitWindow` is set --- .../Difficulty/CatchDifficultyAttributes.cs | 12 --- .../Difficulty/CatchDifficultyCalculator.cs | 4 - .../Difficulty/CatchPerformanceCalculator.cs | 17 ++++- .../Difficulty/ManiaDifficultyAttributes.cs | 12 --- .../Difficulty/ManiaDifficultyCalculator.cs | 29 -------- .../Difficulty/OsuDifficultyAttributes.cs | 41 ---------- .../Difficulty/OsuDifficultyCalculator.cs | 15 ---- .../Difficulty/OsuPerformanceCalculator.cs | 74 +++++++++++++------ .../Difficulty/TaikoDifficultyAttributes.cs | 22 ------ .../Difficulty/TaikoDifficultyCalculator.cs | 5 -- .../Difficulty/TaikoPerformanceCalculator.cs | 30 ++++++-- .../Difficulty/DifficultyAttributes.cs | 6 -- 12 files changed, 92 insertions(+), 175 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs index 5c64643fd4..82c3cfe735 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using Newtonsoft.Json; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; @@ -10,15 +9,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty { public class CatchDifficultyAttributes : DifficultyAttributes { - /// - /// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc). - /// - /// - /// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing. - /// - [JsonProperty("approach_rate")] - public double ApproachRate { get; set; } - public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() { foreach (var v in base.ToDatabaseAttributes()) @@ -26,7 +16,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty // Todo: osu!catch should not output star rating in the 'aim' attribute. yield return (ATTRIB_ID_AIM, StarRating); - yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate); } public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) @@ -34,7 +23,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty base.FromDatabaseAttributes(values, onlineInfo); StarRating = values[ATTRIB_ID_AIM]; - ApproachRate = values[ATTRIB_ID_APPROACH_RATE]; } } } diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 99df2731ff..6434adb63c 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -36,14 +36,10 @@ namespace osu.Game.Rulesets.Catch.Difficulty if (beatmap.HitObjects.Count == 0) return new CatchDifficultyAttributes { Mods = mods }; - // this is the same as osu!, so there's potential to share the implementation... maybe - double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; - CatchDifficultyAttributes attributes = new CatchDifficultyAttributes { StarRating = Math.Sqrt(skills.OfType().Single().DifficultyValue()) * difficulty_multiplier, Mods = mods, - ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0, MaxCombo = beatmap.GetMaxCombo(), }; diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs index 55232a9598..62a9fe250e 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs @@ -3,6 +3,9 @@ using System; using System.Linq; +using osu.Framework.Audio.Track; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; @@ -50,7 +53,19 @@ namespace osu.Game.Rulesets.Catch.Difficulty if (catchAttributes.MaxCombo > 0) value *= Math.Min(Math.Pow(score.MaxCombo, 0.8) / Math.Pow(catchAttributes.MaxCombo, 0.8), 1.0); - double approachRate = catchAttributes.ApproachRate; + var difficulty = score.BeatmapInfo!.Difficulty.Clone(); + + score.Mods.OfType().ForEach(m => m.ApplyToDifficulty(difficulty)); + + var track = new TrackVirtual(10000); + score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); + double clockRate = track.Rate; + + // this is the same as osu!, so there's potential to share the implementation... maybe + double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate; + + double approachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0; + double approachRateFactor = 1.0; if (approachRate > 9.0) approachRateFactor += 0.1 * (approachRate - 9.0); // 10% for each AR above 9 diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs index db60e757e1..512d98f713 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using Newtonsoft.Json; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; @@ -10,22 +9,12 @@ namespace osu.Game.Rulesets.Mania.Difficulty { public class ManiaDifficultyAttributes : DifficultyAttributes { - /// - /// The hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc). - /// - /// - /// Rate-adjusting mods do not affect the hit window at all in osu-stable. - /// - [JsonProperty("great_hit_window")] - public double GreatHitWindow { get; set; } - public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() { foreach (var v in base.ToDatabaseAttributes()) yield return v; yield return (ATTRIB_ID_DIFFICULTY, StarRating); - yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow); } public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) @@ -33,7 +22,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty base.FromDatabaseAttributes(values, onlineInfo); StarRating = values[ATTRIB_ID_DIFFICULTY]; - GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW]; } } } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 1efa7cb42f..06b8018f2b 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -27,7 +27,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty private const double difficulty_multiplier = 0.018; private readonly bool isForCurrentRuleset; - private readonly double originalOverallDifficulty; public override int Version => 20241007; @@ -35,7 +34,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty : base(ruleset, beatmap) { isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset); - originalOverallDifficulty = beatmap.BeatmapInfo.Difficulty.OverallDifficulty; } protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) @@ -50,9 +48,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty { StarRating = skills.OfType().Single().DifficultyValue() * difficulty_multiplier, Mods = mods, - // In osu-stable mania, rate-adjustment mods don't affect the hit window. - // This is done the way it is to introduce fractional differences in order to match osu-stable for the time being. - GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate), MaxCombo = beatmap.HitObjects.Sum(maxComboForObject), }; @@ -124,29 +119,5 @@ namespace osu.Game.Rulesets.Mania.Difficulty }).ToArray(); } } - - private double getHitWindow300(Mod[] mods) - { - if (isForCurrentRuleset) - { - double od = Math.Min(10.0, Math.Max(0, 10.0 - originalOverallDifficulty)); - return applyModAdjustments(34 + 3 * od, mods); - } - - if (Math.Round(originalOverallDifficulty) > 4) - return applyModAdjustments(34, mods); - - return applyModAdjustments(47, mods); - - static double applyModAdjustments(double value, Mod[] mods) - { - if (mods.Any(m => m is ManiaModHardRock)) - value /= 1.4; - else if (mods.Any(m => m is ManiaModEasy)) - value *= 1.4; - - return value; - } - } } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index 395f581b65..f7d8c649c1 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -59,36 +59,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty [JsonProperty("speed_difficult_strain_count")] public double SpeedDifficultStrainCount { get; set; } - /// - /// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc). - /// - [JsonProperty("approach_rate")] - public double ApproachRate { get; set; } - - /// - /// The perceived overall difficulty inclusive of rate-adjusting mods (DT/HT/etc). - /// - [JsonProperty("overall_difficulty")] - public double OverallDifficulty { get; set; } - - /// - /// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc). - /// - [JsonProperty("great_hit_window")] - public double GreatHitWindow { get; set; } - - /// - /// The perceived hit window for an OK hit inclusive of rate-adjusting mods (DT/HT/etc). - /// - [JsonProperty("ok_hit_window")] - public double OkHitWindow { get; set; } - - /// - /// The perceived hit window for a MEH hit inclusive of rate-adjusting mods (DT/HT/etc). - /// - [JsonProperty("meh_hit_window")] - public double MehHitWindow { get; set; } - /// /// The beatmap's drain rate. This doesn't scale with rate-adjusting mods. /// @@ -116,10 +86,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty yield return (ATTRIB_ID_AIM, AimDifficulty); yield return (ATTRIB_ID_SPEED, SpeedDifficulty); - yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty); - yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate); yield return (ATTRIB_ID_DIFFICULTY, StarRating); - yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow); if (ShouldSerializeFlashlightDifficulty()) yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty); @@ -130,9 +97,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty yield return (ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount); yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount); yield return (ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT, AimDifficultSliderCount); - - yield return (ATTRIB_ID_OK_HIT_WINDOW, OkHitWindow); - yield return (ATTRIB_ID_MEH_HIT_WINDOW, MehHitWindow); } public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) @@ -141,18 +105,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty AimDifficulty = values[ATTRIB_ID_AIM]; SpeedDifficulty = values[ATTRIB_ID_SPEED]; - OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY]; - ApproachRate = values[ATTRIB_ID_APPROACH_RATE]; StarRating = values[ATTRIB_ID_DIFFICULTY]; - GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW]; FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT); SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR]; AimDifficultStrainCount = values[ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT]; SpeedDifficultStrainCount = values[ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT]; SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT]; AimDifficultSliderCount = values[ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT]; - OkHitWindow = values[ATTRIB_ID_OK_HIT_WINDOW]; - MehHitWindow = values[ATTRIB_ID_MEH_HIT_WINDOW]; DrainRate = onlineInfo.DrainRate; HitCircleCount = onlineInfo.CircleCount; SliderCount = onlineInfo.SliderCount; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 1505c51592..30339fbaa7 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -15,8 +15,6 @@ using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Scoring; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Difficulty { @@ -90,20 +88,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty ? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0; - double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; double drainRate = beatmap.Difficulty.DrainRate; int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle); int sliderCount = beatmap.HitObjects.Count(h => h is Slider); int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner); - HitWindows hitWindows = new OsuHitWindows(); - hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); - - double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate; - double hitWindowOk = hitWindows.WindowFor(HitResult.Ok) / clockRate; - double hitWindowMeh = hitWindows.WindowFor(HitResult.Meh) / clockRate; - OsuDifficultyAttributes attributes = new OsuDifficultyAttributes { StarRating = starRating, @@ -116,11 +106,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty SliderFactor = sliderFactor, AimDifficultStrainCount = aimDifficultyStrainCount, SpeedDifficultStrainCount = speedDifficultyStrainCount, - ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5, - OverallDifficulty = (80 - hitWindowGreat) / 6, - GreatHitWindow = hitWindowGreat, - OkHitWindow = hitWindowOk, - MehHitWindow = hitWindowMeh, DrainRate = drainRate, MaxCombo = beatmap.GetMaxCombo(), HitCircleCount = hitCirclesCount, diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index dc2df39cdb..09ec890926 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -4,10 +4,15 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Audio.Track; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -41,6 +46,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty /// private double effectiveMissCount; + private double clockRate; + private double greatHitWindow; + private double okHitWindow; + private double mehHitWindow; + private double overallDifficulty; + private double approachRate; + private double? speedDeviation; public OsuPerformanceCalculator() @@ -64,6 +76,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty countSliderTickMiss = score.Statistics.GetValueOrDefault(HitResult.LargeTickMiss); effectiveMissCount = countMiss; + var difficulty = score.BeatmapInfo!.Difficulty.Clone(); + + score.Mods.OfType().ForEach(m => m.ApplyToDifficulty(difficulty)); + + var track = new TrackVirtual(10000); + score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); + clockRate = track.Rate; + + HitWindows hitWindows = new OsuHitWindows(); + hitWindows.SetDifficulty(difficulty.OverallDifficulty); + + greatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate; + okHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate; + mehHitWindow = hitWindows.WindowFor(HitResult.Meh) / clockRate; + + double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate; + + overallDifficulty = (80 - greatHitWindow) / 6; + approachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5; + if (osuAttributes.SliderCount > 0) { if (usingClassicSliderAccuracy) @@ -106,8 +138,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty // https://www.desmos.com/calculator/bc9eybdthb // we use OD13.3 as maximum since it's the value at which great hitwidow becomes 0 // this is well beyond currently maximum achievable OD which is 12.17 (DTx2 + DA with OD11) - double okMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 1.8) : 1.0); - double mehMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 5) : 1.0); + double okMultiplier = Math.Max(0.0, overallDifficulty > 0.0 ? 1 - Math.Pow(overallDifficulty / 13.33, 1.8) : 1.0); + double mehMultiplier = Math.Max(0.0, overallDifficulty > 0.0 ? 1 - Math.Pow(overallDifficulty / 13.33, 5) : 1.0); // As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it. effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits); @@ -178,10 +210,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= calculateMissPenalty(effectiveMissCount, attributes.AimDifficultStrainCount); double approachRateFactor = 0.0; - if (attributes.ApproachRate > 10.33) - approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33); - else if (attributes.ApproachRate < 8.0) - approachRateFactor = 0.05 * (8.0 - attributes.ApproachRate); + if (approachRate > 10.33) + approachRateFactor = 0.3 * (approachRate - 10.33); + else if (approachRate < 8.0) + approachRateFactor = 0.05 * (8.0 - approachRate); if (score.Mods.Any(h => h is OsuModRelax)) approachRateFactor = 0.0; @@ -193,12 +225,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable)) { // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. - aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); + aimValue *= 1.0 + 0.04 * (12.0 - approachRate); } aimValue *= accuracy; // It is important to consider accuracy difficulty when scaling with accuracy. - aimValue *= 0.98 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 2500; + aimValue *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500; return aimValue; } @@ -218,8 +250,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= calculateMissPenalty(effectiveMissCount, attributes.SpeedDifficultStrainCount); double approachRateFactor = 0.0; - if (attributes.ApproachRate > 10.33) - approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33); + if (approachRate > 10.33) + approachRateFactor = 0.3 * (approachRate - 10.33); if (score.Mods.Any(h => h is OsuModAutopilot)) approachRateFactor = 0.0; @@ -234,7 +266,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable)) { // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. - speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); + speedValue *= 1.0 + 0.04 * (12.0 - approachRate); } double speedHighDeviationMultiplier = calculateSpeedHighDeviationNerf(attributes); @@ -248,7 +280,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0); // Scale the speed value with accuracy and OD. - speedValue *= (0.95 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 2); + speedValue *= (0.95 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - overallDifficulty) / 2); return speedValue; } @@ -275,7 +307,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Lots of arbitrary values from testing. // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution. - double accuracyValue = Math.Pow(1.52163, attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83; + double accuracyValue = Math.Pow(1.52163, overallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83; // Bonus for many hitcircles - it's harder to keep good accuracy up for longer. accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3)); @@ -312,7 +344,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Scale the flashlight value with accuracy _slightly_. flashlightValue *= 0.5 + accuracy / 2.0; // It is important to also consider accuracy difficulty when doing that. - flashlightValue *= 0.98 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 2500; + flashlightValue *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500; return flashlightValue; } @@ -352,10 +384,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty double objectCount = relevantCountGreat + relevantCountOk + relevantCountMeh + relevantCountMiss; - double hitWindowGreat = attributes.GreatHitWindow; - double hitWindowOk = attributes.OkHitWindow; - double hitWindowMeh = attributes.MehHitWindow; - // The probability that a player hits a circle is unknown, but we can estimate it to be // the number of greats on circles divided by the number of circles, and then add one // to the number of circles as a bias correction. @@ -370,22 +398,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Compute the deviation assuming greats and oks are normally distributed, and mehs are uniformly distributed. // Begin with greats and oks first. Ignoring mehs, we can be 99% confident that the deviation is not higher than: - double deviation = hitWindowGreat / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound)); + double deviation = greatHitWindow / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound)); - double randomValue = Math.Sqrt(2 / Math.PI) * hitWindowOk * Math.Exp(-0.5 * Math.Pow(hitWindowOk / deviation, 2)) - / (deviation * DifficultyCalculationUtils.Erf(hitWindowOk / (Math.Sqrt(2) * deviation))); + double randomValue = Math.Sqrt(2 / Math.PI) * okHitWindow * Math.Exp(-0.5 * Math.Pow(okHitWindow / deviation, 2)) + / (deviation * DifficultyCalculationUtils.Erf(okHitWindow / (Math.Sqrt(2) * deviation))); deviation *= Math.Sqrt(1 - randomValue); // Value deviation approach as greatCount approaches 0 - double limitValue = hitWindowOk / Math.Sqrt(3); + double limitValue = okHitWindow / Math.Sqrt(3); // If precision is not enough to compute true deviation - use limit value if (pLowerBound == 0 || randomValue >= 1 || deviation > limitValue) deviation = limitValue; // Then compute the variance for mehs. - double mehVariance = (hitWindowMeh * hitWindowMeh + hitWindowOk * hitWindowMeh + hitWindowOk * hitWindowOk) / 3; + double mehVariance = (mehHitWindow * mehHitWindow + okHitWindow * mehHitWindow + okHitWindow * okHitWindow) / 3; // Find the total deviation. deviation = Math.Sqrt(((relevantCountGreat + relevantCountOk) * Math.Pow(deviation, 2) + relevantCountMeh * mehVariance) / (relevantCountGreat + relevantCountOk + relevantCountMeh)); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index 37e6996e5a..b43468ab18 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -49,32 +49,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("stamina_difficult_strains")] public double StaminaTopStrains { get; set; } - /// - /// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc). - /// - /// - /// Rate-adjusting mods don't directly affect the hit window, but have a perceived effect as a result of adjusting audio timing. - /// - [JsonProperty("great_hit_window")] - public double GreatHitWindow { get; set; } - - /// - /// The perceived hit window for an OK hit inclusive of rate-adjusting mods (DT/HT/etc). - /// - /// - /// Rate-adjusting mods don't directly affect the hit window, but have a perceived effect as a result of adjusting audio timing. - /// - [JsonProperty("ok_hit_window")] - public double OkHitWindow { get; set; } - public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() { foreach (var v in base.ToDatabaseAttributes()) yield return v; yield return (ATTRIB_ID_DIFFICULTY, StarRating); - yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow); - yield return (ATTRIB_ID_OK_HIT_WINDOW, OkHitWindow); yield return (ATTRIB_ID_MONO_STAMINA_FACTOR, MonoStaminaFactor); } @@ -83,8 +63,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty base.FromDatabaseAttributes(values, onlineInfo); StarRating = values[ATTRIB_ID_DIFFICULTY]; - GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW]; - OkHitWindow = values[ATTRIB_ID_OK_HIT_WINDOW]; MonoStaminaFactor = values[ATTRIB_ID_MONO_STAMINA_FACTOR]; } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 6b9986bd68..7bc050d2df 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -129,9 +129,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax, isConvert); double starRating = rescale(combinedRating * 1.4); - HitWindows hitWindows = new TaikoHitWindows(); - hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); - TaikoDifficultyAttributes attributes = new TaikoDifficultyAttributes { StarRating = starRating, @@ -144,8 +141,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty RhythmTopStrains = rhythmDifficultStrains, ColourTopStrains = colourDifficultStrains, StaminaTopStrains = staminaDifficultStrains, - GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate, - OkHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate, MaxCombo = beatmap.GetMaxCombo(), }; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index bcd3693119..9e049df87c 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -4,11 +4,14 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Audio.Track; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Scoring; namespace osu.Game.Rulesets.Taiko.Difficulty @@ -21,6 +24,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private int countMiss; private double? estimatedUnstableRate; + private double clockRate; + private double greatHitWindow; + private double effectiveMissCount; public TaikoPerformanceCalculator() @@ -36,7 +42,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty countOk = score.Statistics.GetValueOrDefault(HitResult.Ok); countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); - estimatedUnstableRate = computeDeviationUpperBound(taikoAttributes) * 10; + + var track = new TrackVirtual(10000); + score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); + clockRate = track.Rate; + + var difficulty = score.BeatmapInfo!.Difficulty.Clone(); + + score.Mods.OfType().ForEach(m => m.ApplyToDifficulty(difficulty)); + + HitWindows hitWindows = new TaikoHitWindows(); + hitWindows.SetDifficulty(difficulty.OverallDifficulty); + + greatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate; + + estimatedUnstableRate = computeDeviationUpperBound() * 10; // The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000. if (totalSuccessfulHits > 0) @@ -104,7 +124,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool isConvert) { - if (attributes.GreatHitWindow <= 0 || estimatedUnstableRate == null) + if (greatHitWindow <= 0 || estimatedUnstableRate == null) return 0; double accuracyValue = Math.Pow(70 / estimatedUnstableRate.Value, 1.1) * Math.Pow(attributes.StarRating, 0.4) * 100.0; @@ -123,9 +143,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// and the hit judgements, assuming the player's mean hit error is 0. The estimation is consistent in that /// two SS scores on the same map with the same settings will always return the same deviation. /// - private double? computeDeviationUpperBound(TaikoDifficultyAttributes attributes) + private double? computeDeviationUpperBound() { - if (countGreat == 0 || attributes.GreatHitWindow <= 0) + if (countGreat == 0 || greatHitWindow <= 0) return null; const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed). @@ -139,7 +159,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4); // We can be 99% confident that the deviation is not higher than: - return attributes.GreatHitWindow / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound)); + return greatHitWindow / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound)); } private int totalHits => countGreat + countOk + countMeh + countMiss; diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index 1d6cee043b..59511973f7 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -17,21 +17,15 @@ namespace osu.Game.Rulesets.Difficulty { protected const int ATTRIB_ID_AIM = 1; protected const int ATTRIB_ID_SPEED = 3; - protected const int ATTRIB_ID_OVERALL_DIFFICULTY = 5; - protected const int ATTRIB_ID_APPROACH_RATE = 7; protected const int ATTRIB_ID_MAX_COMBO = 9; protected const int ATTRIB_ID_DIFFICULTY = 11; - protected const int ATTRIB_ID_GREAT_HIT_WINDOW = 13; - protected const int ATTRIB_ID_SCORE_MULTIPLIER = 15; protected const int ATTRIB_ID_FLASHLIGHT = 17; protected const int ATTRIB_ID_SLIDER_FACTOR = 19; protected const int ATTRIB_ID_SPEED_NOTE_COUNT = 21; protected const int ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT = 23; protected const int ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT = 25; - protected const int ATTRIB_ID_OK_HIT_WINDOW = 27; protected const int ATTRIB_ID_MONO_STAMINA_FACTOR = 29; protected const int ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT = 31; - protected const int ATTRIB_ID_MEH_HIT_WINDOW = 33; /// /// The mods which were applied to the beatmap. From d4c69f0c9063c7c4d56f75ecc37a1819b616e4dc Mon Sep 17 00:00:00 2001 From: Layendan Date: Fri, 7 Feb 2025 04:04:29 -0700 Subject: [PATCH 407/801] Assume room is setup correctly and remove duplicate maps before querying realm --- .../OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index 8801d73e9e..c24c7d834d 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -35,15 +35,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { Action = () => { - int[] ids = room.Playlist.Select(item => item.Beatmap.OnlineID).Where(onlineId => onlineId > 0).ToArray(); - - if (ids.Length == 0) + if (room.Playlist.Count == 0) { notifications?.Post(new SimpleErrorNotification { Text = "Cannot add local beatmaps" }); return; } - string filter = string.Join(" OR ", ids.Select(id => $"(OnlineID == {id})")); + string filter = string.Join(" OR ", room.Playlist.Select(item => $"(OnlineID == {item.Beatmap.OnlineID})").Distinct()); var beatmaps = realmAccess.Realm.All().Filter(filter).ToList(); var collection = realmAccess.Realm.All().FirstOrDefault(c => c.Name == room.Name); From 5ace8e911bde4a5f9c9318f57a98519995b5b55f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 21:45:31 +0900 Subject: [PATCH 408/801] Fix failing test --- .../SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs index c043fd87a9..a0c56020ab 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs @@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual.SongSelect RemoveAllBeatmaps(); AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null); - AddBeatmaps(5); + AddBeatmaps(3); WaitForDrawablePanels(); CheckHasSelection(); From de0aabbfc59963923637bc08edcc3c205a3e1f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Feb 2025 15:34:52 +0100 Subject: [PATCH 409/801] Add staging submission service URL to development endpoint config --- osu.Game/Online/DevelopmentEndpointConfiguration.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/DevelopmentEndpointConfiguration.cs b/osu.Game/Online/DevelopmentEndpointConfiguration.cs index f4e1b257ee..e36e36ee9f 100644 --- a/osu.Game/Online/DevelopmentEndpointConfiguration.cs +++ b/osu.Game/Online/DevelopmentEndpointConfiguration.cs @@ -13,6 +13,7 @@ namespace osu.Game.Online SpectatorUrl = $@"{APIUrl}/signalr/spectator"; MultiplayerUrl = $@"{APIUrl}/signalr/multiplayer"; MetadataUrl = $@"{APIUrl}/signalr/metadata"; + BeatmapSubmissionServiceUrl = $@"{APIUrl}/beatmap-submission"; } } } From 64f0d234d84222b00363397b43c9cda55c772a04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Feb 2025 15:37:27 +0100 Subject: [PATCH 410/801] Fix exiting being eternally blocked after successful beatmap submission --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 41c875ac1f..9dfe998138 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -420,7 +420,7 @@ namespace osu.Game.Screens.Edit.Submission s.Push(new EditorLoader()); }, [typeof(MainMenu)]); - return true; + return false; } return base.OnExiting(e); From bcd4fcbeed3a2a4057849f3e01defdcea17b849e Mon Sep 17 00:00:00 2001 From: SebastianPeP Date: Sun, 9 Feb 2025 01:29:22 -0300 Subject: [PATCH 411/801] Changed the Currently Playing Text when no track is selected Changed the currently playing text for when the track isnt selected/loaded --- osu.Game/Beatmaps/DummyWorkingBeatmap.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs index 35067f4055..5dc73d8679 100644 --- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs @@ -30,8 +30,8 @@ namespace osu.Game.Beatmaps { Metadata = new BeatmapMetadata { - Artist = "please load a beatmap!", - Title = "no beatmaps available!" + Artist = "please select or load a beatmap!", + Title = "no beatmap selected!" }, BeatmapSet = new BeatmapSetInfo(), Difficulty = new BeatmapDifficulty From f9bda0524ada81a9bbc440b88195af3d8ec9786e Mon Sep 17 00:00:00 2001 From: Layendan Date: Sun, 9 Feb 2025 18:45:13 -0700 Subject: [PATCH 412/801] Update button text to include downloaded beatmaps and collection status --- .../AddPlaylistToCollectionButton.cs | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index c24c7d834d..cc875b707d 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -1,8 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Database; @@ -17,6 +20,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public partial class AddPlaylistToCollectionButton : RoundedButton { private readonly Room room; + private readonly Bindable downloadedBeatmapsCount = new Bindable(0); + private readonly Bindable collectionExists = new Bindable(false); + private IDisposable? beatmapSubscription; + private IDisposable? collectionSubscription; [Resolved] private RealmAccess realmAccess { get; set; } = null!; @@ -27,7 +34,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public AddPlaylistToCollectionButton(Room room) { this.room = room; - Text = "Add Maps to Collection"; + Text = formatButtonText(downloadedBeatmapsCount.Value, collectionExists.Value); } [BackgroundDependencyLoader] @@ -41,8 +48,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return; } - string filter = string.Join(" OR ", room.Playlist.Select(item => $"(OnlineID == {item.Beatmap.OnlineID})").Distinct()); - var beatmaps = realmAccess.Realm.All().Filter(filter).ToList(); + var beatmaps = realmAccess.Realm.All().Filter(formatFilterQuery(room.Playlist)).ToList(); var collection = realmAccess.Realm.All().FirstOrDefault(c => c.Name == room.Name); @@ -64,5 +70,30 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } }; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + beatmapSubscription = realmAccess.RegisterForNotifications(r => r.All().Filter(formatFilterQuery(room.Playlist)), (sender, _) => downloadedBeatmapsCount.Value = sender.Count); + + collectionSubscription = realmAccess.RegisterForNotifications(r => r.All().Where(c => c.Name == room.Name), (sender, _) => collectionExists.Value = sender.Count > 0); + + downloadedBeatmapsCount.BindValueChanged(_ => Text = formatButtonText(downloadedBeatmapsCount.Value, collectionExists.Value)); + + collectionExists.BindValueChanged(_ => Text = formatButtonText(downloadedBeatmapsCount.Value, collectionExists.Value), true); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + beatmapSubscription?.Dispose(); + collectionSubscription?.Dispose(); + } + + private string formatFilterQuery(IReadOnlyList playlistItems) => string.Join(" OR ", playlistItems.Select(item => $"(OnlineID == {item.Beatmap.OnlineID})").Distinct()); + + private string formatButtonText(int count, bool collectionExists) => $"Add {count} {(count == 1 ? "beatmap" : "beatmaps")} to {(collectionExists ? "collection" : "new collection")}"; } } From 274b4221398ba232edfd101ebaab862cbd12c6c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 14:51:48 +0900 Subject: [PATCH 413/801] Add percent progress display to editor footer --- .../Edit/Components/TimeInfoContainer.cs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs index 8f2a3d49ca..d17f9011f4 100644 --- a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs +++ b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs @@ -18,6 +18,7 @@ namespace osu.Game.Screens.Edit.Components public partial class TimeInfoContainer : BottomBarContainer { private OsuSpriteText bpm = null!; + private OsuSpriteText progress = null!; [Resolved] private EditorBeatmap editorBeatmap { get; set; } = null!; @@ -36,26 +37,44 @@ namespace osu.Game.Screens.Edit.Components bpm = new OsuSpriteText { Colour = colours.Orange1, + Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold, fixedWidth: true), + Spacing = new Vector2(-1, 0), + Position = new Vector2(0, 4), + Anchor = Anchor.CentreRight, + Origin = Anchor.TopRight, + }, + progress = new OsuSpriteText + { + Colour = colours.Purple1, + Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold, fixedWidth: true), + Spacing = new Vector2(-1, 0), Anchor = Anchor.CentreLeft, - Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), Position = new Vector2(2, 4), } }; } private double? lastBPM; + private double? lastProgress; protected override void Update() { base.Update(); double newBPM = editorBeatmap.ControlPointInfo.TimingPointAt(editorClock.CurrentTime).BPM; + double newProgress = (int)(editorClock.CurrentTime / editorClock.TrackLength * 100); if (lastBPM != newBPM) { lastBPM = newBPM; bpm.Text = @$"{newBPM:0} BPM"; } + + if (lastProgress != newProgress) + { + lastProgress = newProgress; + progress.Text = @$"{newProgress:0}%"; + } } private partial class TimestampControl : OsuClickableContainer From 7853456c06abf8c7e46d233580b50cdf070f2efe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 15:12:59 +0900 Subject: [PATCH 414/801] Add delay before browser displays beatmap --- .../Submission/BeatmapSubmissionScreen.cs | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 9dfe998138..039c919ed6 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -290,15 +290,7 @@ namespace osu.Game.Screens.Edit.Submission var patchRequest = new PatchBeatmapPackageRequest(beatmapSetId.Value); patchRequest.FilesChanged.AddRange(changedFiles); patchRequest.FilesDeleted.AddRange(onlineFilesByFilename.Keys); - patchRequest.Success += async () => - { - uploadStep.SetCompleted(); - - if (configManager.Get(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission)) - game?.OpenUrlExternally($"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetId}"); - - await updateLocalBeatmap().ConfigureAwait(true); - }; + patchRequest.Success += uploadCompleted; patchRequest.Failure += ex => { uploadStep.SetFailed(ex.Message); @@ -318,15 +310,7 @@ namespace osu.Game.Screens.Edit.Submission var uploadRequest = new ReplaceBeatmapPackageRequest(beatmapSetId.Value, beatmapPackageStream.ToArray()); - uploadRequest.Success += async () => - { - uploadStep.SetCompleted(); - - if (configManager.Get(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission)) - game?.OpenUrlExternally($"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetId}"); - - await updateLocalBeatmap().ConfigureAwait(true); - }; + uploadRequest.Success += uploadCompleted; uploadRequest.Failure += ex => { uploadStep.SetFailed(ex.Message); @@ -339,6 +323,12 @@ namespace osu.Game.Screens.Edit.Submission uploadStep.SetInProgress(); } + private void uploadCompleted() + { + uploadStep.SetCompleted(); + updateLocalBeatmap().ConfigureAwait(true); + } + private async Task updateLocalBeatmap() { Debug.Assert(beatmapSetId != null); @@ -364,6 +354,12 @@ namespace osu.Game.Screens.Edit.Submission updateStep.SetCompleted(); showBeatmapCard(); allowExit(); + + if (configManager.Get(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission)) + { + await Task.Delay(1000).ConfigureAwait(true); + game?.OpenUrlExternally($"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetId}"); + } } private void showBeatmapCard() From 930aaecd7fc39a9455f3e56fe7baffe97b9dc360 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 15:22:31 +0900 Subject: [PATCH 415/801] Fix back button displaying before it should --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 039c919ed6..0967bcfc65 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -39,6 +39,8 @@ namespace osu.Game.Screens.Edit.Submission public override bool DisallowExternalBeatmapRulesetChanges => true; + protected override bool InitialBackButtonVisibility => false; + [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); From eae1ea7e32484c03cd24b656c68c3138f4197b82 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 15:23:25 +0900 Subject: [PATCH 416/801] Adjust animations and induce some short delays to make things more graceful --- .../Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 0967bcfc65..121e25d8b7 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -92,6 +92,8 @@ namespace osu.Game.Screens.Edit.Submission { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + AutoSizeDuration = 400, + AutoSizeEasing = Easing.OutQuint, Alpha = 0, Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -144,9 +146,6 @@ namespace osu.Game.Screens.Edit.Submission Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, AutoSizeAxes = Axes.Both, - AutoSizeDuration = 500, - AutoSizeEasing = Easing.OutQuint, - Masking = true, CornerRadius = BeatmapCard.CORNER_RADIUS, Child = flashLayer = new Container { @@ -252,6 +251,8 @@ namespace osu.Game.Screens.Edit.Submission exportStep.SetCompleted(); exportProgressNotification = null; + await Task.Delay(200).ConfigureAwait(true); + if (onlineFiles.Count > 0) await patchBeatmapSet(onlineFiles).ConfigureAwait(true); else @@ -337,6 +338,7 @@ namespace osu.Game.Screens.Edit.Submission Debug.Assert(beatmapPackageStream != null); updateStep.SetInProgress(); + await Task.Delay(200).ConfigureAwait(true); try { From 5e9f195117307feb555e663fe8544c9a2527bc51 Mon Sep 17 00:00:00 2001 From: Layendan Date: Sun, 9 Feb 2025 23:27:28 -0700 Subject: [PATCH 417/801] Fix tests failing if playlist was empty --- .../OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index cc875b707d..8b5d5c752c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -75,7 +75,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { base.LoadComplete(); - beatmapSubscription = realmAccess.RegisterForNotifications(r => r.All().Filter(formatFilterQuery(room.Playlist)), (sender, _) => downloadedBeatmapsCount.Value = sender.Count); + if (room.Playlist.Count > 0) + beatmapSubscription = realmAccess.RegisterForNotifications(r => r.All().Filter(formatFilterQuery(room.Playlist)), (sender, _) => downloadedBeatmapsCount.Value = sender.Count); collectionSubscription = realmAccess.RegisterForNotifications(r => r.All().Where(c => c.Name == room.Name), (sender, _) => collectionExists.Value = sender.Count > 0); From a3cd62ec7295b3dd5f16350ae6439a623acab111 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 25 Jan 2025 20:17:21 -0500 Subject: [PATCH 418/801] Flip mania judgement anchor on flipped scroll direction --- .../UI/DrawableManiaJudgement.cs | 65 +++++-------------- osu.Game.Rulesets.Mania/UI/Stage.cs | 8 +-- 2 files changed, 17 insertions(+), 56 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index 75f56bffa4..40fef1a56a 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -3,65 +3,32 @@ #nullable disable +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; -using osuTK; +using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Rulesets.Mania.UI { public partial class DrawableManiaJudgement : DrawableJudgement { - protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result); + private IBindable direction; - private partial class DefaultManiaJudgementPiece : DefaultJudgementPiece + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) { - private const float judgement_y_position = -180f; - - public DefaultManiaJudgementPiece(HitResult result) - : base(result) - { - Y = judgement_y_position; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - JudgementText.Font = JudgementText.Font.With(size: 25); - } - - public override void PlayAnimation() - { - switch (Result) - { - case HitResult.None: - this.FadeOutFromOne(800); - break; - - case HitResult.Miss: - this.ScaleTo(1.6f); - this.ScaleTo(1, 100, Easing.In); - - this.MoveToY(judgement_y_position); - this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); - - this.RotateTo(0); - this.RotateTo(40, 800, Easing.InQuint); - - this.FadeOutFromOne(800); - break; - - default: - this.ScaleTo(0.8f); - this.ScaleTo(1, 250, Easing.OutElastic); - - this.Delay(50) - .ScaleTo(0.75f, 250) - .FadeOut(200); - break; - } - } + direction = scrollingInfo.Direction.GetBoundCopy(); + direction.BindValueChanged(_ => onDirectionChanged(), true); } + + private void onDirectionChanged() + { + Anchor = direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; + Origin = Anchor.Centre; + } + + protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result); } } diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index fb9671c14d..faa9fc318c 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -216,13 +216,7 @@ namespace osu.Game.Rulesets.Mania.UI return; judgements.Clear(false); - judgements.Add(judgementPooler.Get(result.Type, j => - { - j.Apply(result, judgedObject); - - j.Anchor = Anchor.BottomCentre; - j.Origin = Anchor.Centre; - })!); + judgements.Add(judgementPooler.Get(result.Type, j => j.Apply(result, judgedObject))!); } protected override void Update() From 1e06c5cc4ac823f773101899b0ea55e23e82e291 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 25 Jan 2025 20:17:36 -0500 Subject: [PATCH 419/801] Flip the Y offset of skin judgement pieces on flipped scroll direction --- .../Skinning/Argon/ArgonJudgementPiece.cs | 14 +++- .../Legacy/LegacyManiaJudgementPiece.cs | 29 +++++-- .../UI/DefaultManiaJudgementPiece.cs | 75 +++++++++++++++++++ 3 files changed, 108 insertions(+), 10 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs index a1c81d3a6a..6098459f6b 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -12,6 +13,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI.Scrolling; using osuTK; using osuTK.Graphics; @@ -26,18 +28,22 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon [Resolved] private OsuColour colours { get; set; } = null!; + private IBindable direction = null!; + public ArgonJudgementPiece(HitResult result) : base(result) { AutoSizeAxes = Axes.Both; Origin = Anchor.Centre; - Y = judgement_y_position; } [BackgroundDependencyLoader] - private void load() + private void load(IScrollingInfo scrollingInfo) { + direction = scrollingInfo.Direction.GetBoundCopy(); + direction.BindValueChanged(_ => onDirectionChanged(), true); + if (Result.IsHit()) { AddInternal(ringExplosion = new RingExplosion(Result) @@ -47,6 +53,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon } } + private void onDirectionChanged() => Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position; + protected override SpriteText CreateJudgementText() => new OsuSpriteText { @@ -78,7 +86,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon this.ScaleTo(1.6f); this.ScaleTo(1, 100, Easing.In); - this.MoveToY(judgement_y_position); + this.MoveToY(direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position); this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); this.RotateTo(0); diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs index 4b0cc482d9..3752c5f27a 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs @@ -2,12 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.Skinning.Legacy @@ -28,14 +30,16 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy AutoSizeAxes = Axes.Both; } - [BackgroundDependencyLoader] - private void load(ISkinSource skin) - { - float hitPosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? 0; - float scorePosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value ?? 0; + private IBindable direction = null!; - float absoluteHitPosition = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition; - Y = scorePosition - absoluteHitPosition; + [Resolved] + private ISkinSource skin { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { + direction = scrollingInfo.Direction.GetBoundCopy(); + direction.BindValueChanged(_ => onDirectionChanged(), true); InternalChild = animation.With(d => { @@ -44,6 +48,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy }); } + private void onDirectionChanged() + { + float hitPosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? 0; + float scorePosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value ?? 0; + + float absoluteHitPosition = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition; + float finalPosition = scorePosition - absoluteHitPosition; + + Y = direction.Value == ScrollingDirection.Up ? -finalPosition : finalPosition; + } + public void PlayAnimation() { (animation as IFramedAnimation)?.GotoFrame(0); diff --git a/osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.cs b/osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.cs new file mode 100644 index 0000000000..f0af6085d0 --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.cs @@ -0,0 +1,75 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI.Scrolling; +using osuTK; + +namespace osu.Game.Rulesets.Mania.UI +{ + public partial class DefaultManiaJudgementPiece : DefaultJudgementPiece + { + private const float judgement_y_position = -180f; + + private IBindable direction = null!; + + public DefaultManiaJudgementPiece(HitResult result) + : base(result) + { + } + + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { + direction = scrollingInfo.Direction.GetBoundCopy(); + direction.BindValueChanged(_ => onDirectionChanged(), true); + } + + private void onDirectionChanged() => Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position; + + protected override void LoadComplete() + { + base.LoadComplete(); + + JudgementText.Font = JudgementText.Font.With(size: 25); + } + + public override void PlayAnimation() + { + switch (Result) + { + case HitResult.None: + this.FadeOutFromOne(800); + break; + + case HitResult.Miss: + this.ScaleTo(1.6f); + this.ScaleTo(1, 100, Easing.In); + + this.MoveToY(direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position); + this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); + + this.RotateTo(0); + this.RotateTo(40, 800, Easing.InQuint); + + this.FadeOutFromOne(800); + break; + + default: + this.ScaleTo(0.8f); + this.ScaleTo(1, 250, Easing.OutElastic); + + this.Delay(50) + .ScaleTo(0.75f, 250) + .FadeOut(200); + + // osu!mania uses a custom fade length, so the base call is intentionally omitted. + break; + } + } + } +} From 895493877cd0f04699099a4228657b05365c7b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2025 09:02:47 +0100 Subject: [PATCH 420/801] Allow performing beatmap reload after submission from song select --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 121e25d8b7..f53d10d23b 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -29,6 +29,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Screens.Menu; +using osu.Game.Screens.Select; using osuTK; namespace osu.Game.Screens.Edit.Submission @@ -418,7 +419,7 @@ namespace osu.Game.Screens.Edit.Submission } s.Push(new EditorLoader()); - }, [typeof(MainMenu)]); + }, [typeof(SongSelect)]); return false; } From 45259b374a2fdd6626e06a7ed9c526cf28cd5fb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2025 09:09:43 +0100 Subject: [PATCH 421/801] Remove unused using --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index f53d10d23b..9672e4360a 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -28,7 +28,6 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; -using osu.Game.Screens.Menu; using osu.Game.Screens.Select; using osuTK; From b8e33a28d25c8590cf4d0b93e59deeaa21daa1d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 17:40:00 +0900 Subject: [PATCH 422/801] Minor code refactors --- .../Submission/BeatmapSubmissionScreen.cs | 19 ++++++++++------- .../Submission/SubmissionBeatmapExporter.cs | 21 +++++++------------ 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 9672e4360a..201888e078 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -76,7 +76,6 @@ namespace osu.Game.Screens.Edit.Submission private uint? beatmapSetId; private MemoryStream? beatmapPackageStream; - private SubmissionBeatmapExporter legacyBeatmapExporter = null!; private ProgressNotification? exportProgressNotification; private ProgressNotification? updateProgressNotification; @@ -214,8 +213,7 @@ namespace osu.Game.Screens.Edit.Submission }).ConfigureAwait(true); } - legacyBeatmapExporter = new SubmissionBeatmapExporter(storage, response); - await createBeatmapPackage(response.Files).ConfigureAwait(true); + await createBeatmapPackage(response).ConfigureAwait(true); }; createRequest.Failure += ex => { @@ -228,7 +226,7 @@ namespace osu.Game.Screens.Edit.Submission api.Queue(createRequest); } - private async Task createBeatmapPackage(ICollection onlineFiles) + private async Task createBeatmapPackage(PutBeatmapSetResponse response) { Debug.Assert(ThreadSafety.IsUpdateThread); @@ -237,8 +235,13 @@ namespace osu.Game.Screens.Edit.Submission try { beatmapPackageStream = new MemoryStream(); - await legacyBeatmapExporter.ExportToStreamAsync(Beatmap.Value.BeatmapSetInfo.ToLive(realmAccess), beatmapPackageStream, exportProgressNotification = new ProgressNotification()) - .ConfigureAwait(true); + exportProgressNotification = new ProgressNotification(); + + var legacyBeatmapExporter = new SubmissionBeatmapExporter(storage, response); + + await legacyBeatmapExporter + .ExportToStreamAsync(Beatmap.Value.BeatmapSetInfo.ToLive(realmAccess), beatmapPackageStream, exportProgressNotification) + .ConfigureAwait(true); } catch (Exception ex) { @@ -253,8 +256,8 @@ namespace osu.Game.Screens.Edit.Submission await Task.Delay(200).ConfigureAwait(true); - if (onlineFiles.Count > 0) - await patchBeatmapSet(onlineFiles).ConfigureAwait(true); + if (response.Files.Count > 0) + await patchBeatmapSet(response.Files).ConfigureAwait(true); else replaceBeatmapSet(); } diff --git a/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs b/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs index 3c50a1bf80..fab080cdba 100644 --- a/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs +++ b/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs @@ -14,43 +14,38 @@ namespace osu.Game.Screens.Edit.Submission public class SubmissionBeatmapExporter : LegacyBeatmapExporter { private readonly uint? beatmapSetId; - private readonly HashSet? beatmapIds; - - public SubmissionBeatmapExporter(Storage storage) - : base(storage) - { - } + private readonly HashSet? allocatedBeatmapIds; public SubmissionBeatmapExporter(Storage storage, PutBeatmapSetResponse putBeatmapSetResponse) : base(storage) { beatmapSetId = putBeatmapSetResponse.BeatmapSetId; - beatmapIds = putBeatmapSetResponse.BeatmapIds.Select(id => (int)id).ToHashSet(); + allocatedBeatmapIds = putBeatmapSetResponse.BeatmapIds.Select(id => (int)id).ToHashSet(); } protected override void MutateBeatmap(BeatmapSetInfo beatmapSet, IBeatmap playableBeatmap) { base.MutateBeatmap(beatmapSet, playableBeatmap); - if (beatmapSetId != null && beatmapIds != null) + if (beatmapSetId != null && allocatedBeatmapIds != null) { playableBeatmap.BeatmapInfo.BeatmapSet = beatmapSet; playableBeatmap.BeatmapInfo.BeatmapSet!.OnlineID = (int)beatmapSetId; - if (beatmapIds.Contains(playableBeatmap.BeatmapInfo.OnlineID)) + if (allocatedBeatmapIds.Contains(playableBeatmap.BeatmapInfo.OnlineID)) { - beatmapIds.Remove(playableBeatmap.BeatmapInfo.OnlineID); + allocatedBeatmapIds.Remove(playableBeatmap.BeatmapInfo.OnlineID); return; } if (playableBeatmap.BeatmapInfo.OnlineID > 0) throw new InvalidOperationException(@"Encountered beatmap with ID that has not been assigned to it by the server!"); - if (beatmapIds.Count == 0) + if (allocatedBeatmapIds.Count == 0) throw new InvalidOperationException(@"Ran out of new beatmap IDs to assign to unsubmitted beatmaps!"); - int newId = beatmapIds.First(); - beatmapIds.Remove(newId); + int newId = allocatedBeatmapIds.First(); + allocatedBeatmapIds.Remove(newId); playableBeatmap.BeatmapInfo.OnlineID = newId; } } From d4ce71267256590ff170281b17ef471fdb497653 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 17:57:01 +0900 Subject: [PATCH 423/801] Add note about weird taiko iteration --- .../Difficulty/Utils/IntervalGroupingUtils.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs index 7bd7aa7677..5ab58ad4f3 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs @@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Utils { const double margin_of_error = 5; + // This never compares the first two elements in the group. + // This sounds wrong but is apparently "as intended" (https://github.com/ppy/osu/pull/31636#discussion_r1942673329) var groupedObjects = new List { objects[i] }; i++; From 340e081965355fc1f36acdd1acce1d7c6b773780 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 18:05:08 +0900 Subject: [PATCH 424/801] Rename buzz variable per review --- osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index 2d1adbd056..559e9dafa0 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills private float lastDistanceMoved; private float lastExactDistanceMoved; private double lastStrainTime; - private bool isBuzzSliderTriggered; + private bool isInBuzzSection; /// /// The speed multiplier applied to the player's catcher. @@ -107,14 +107,14 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills // To achieve that, we need to store the exact distances (distance ignoring absolute_player_positioning_error and normalized_hitobject_radius) if (Math.Abs(exactDistanceMoved) <= HalfCatcherWidth * 2 && exactDistanceMoved == -lastExactDistanceMoved && catchCurrent.StrainTime == lastStrainTime) { - if (isBuzzSliderTriggered) + if (isInBuzzSection) distanceAddition = 0; else - isBuzzSliderTriggered = true; + isInBuzzSection = true; } else { - isBuzzSliderTriggered = false; + isInBuzzSection = false; } lastPlayerPosition = playerPosition; From 3ba56e009e347942089dbfe8533020ad7a4b63e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2025 10:41:10 +0100 Subject: [PATCH 425/801] Privatise a few members --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 35 +++++++++++----------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index c784fc298a..72e866cb24 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -28,17 +28,16 @@ namespace osu.Game.Screens.Play.HUD { private const int max_spectators_displayed = 10; - public BindableList Spectators { get; } = new BindableList(); - public Bindable UserPlayingState { get; } = new Bindable(); - [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))] public Bindable Font { get; } = new Bindable(Typeface.Torus); [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour), nameof(SkinnableComponentStrings.TextColourDescription))] public BindableColour4 HeaderColour { get; } = new BindableColour4(Colour4.White); - protected OsuSpriteText Header { get; private set; } = null!; + private BindableList spectators { get; } = new BindableList(); + private Bindable userPlayingState { get; } = new Bindable(); + private OsuSpriteText header = null!; private FillFlowContainer mainFlow = null!; private FillFlowContainer spectatorsFlow = null!; private DrawablePool pool = null!; @@ -63,7 +62,7 @@ namespace osu.Game.Screens.Play.HUD Direction = FillDirection.Vertical, Children = new Drawable[] { - Header = new OsuSpriteText + header = new OsuSpriteText { Colour = colours.Blue0, Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), @@ -78,18 +77,18 @@ namespace osu.Game.Screens.Play.HUD pool = new DrawablePool(max_spectators_displayed), }; - HeaderColour.Value = Header.Colour; + HeaderColour.Value = header.Colour; } protected override void LoadComplete() { base.LoadComplete(); - ((IBindableList)Spectators).BindTo(client.WatchingUsers); - ((IBindable)UserPlayingState).BindTo(gameplayState.PlayingState); + ((IBindableList)spectators).BindTo(client.WatchingUsers); + ((IBindable)userPlayingState).BindTo(gameplayState.PlayingState); - Spectators.BindCollectionChanged(onSpectatorsChanged, true); - UserPlayingState.BindValueChanged(_ => updateVisibility()); + spectators.BindCollectionChanged(onSpectatorsChanged, true); + userPlayingState.BindValueChanged(_ => updateVisibility()); Font.BindValueChanged(_ => updateAppearance()); HeaderColour.BindValueChanged(_ => updateAppearance(), true); @@ -125,10 +124,10 @@ namespace osu.Game.Screens.Play.HUD for (int i = 0; i < spectatorsFlow.Count; i++) spectatorsFlow.SetLayoutPosition(spectatorsFlow[i], i); - if (Spectators.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed) + if (spectators.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed) { for (int i = spectatorsFlow.Count; i < max_spectators_displayed; i++) - addNewSpectatorToList(i, Spectators[i]); + addNewSpectatorToList(i, spectators[i]); } break; @@ -144,7 +143,7 @@ namespace osu.Game.Screens.Play.HUD throw new NotSupportedException(); } - Header.Text = SpectatorListStrings.SpectatorCount(Spectators.Count).ToUpper(); + header.Text = SpectatorListStrings.SpectatorCount(spectators.Count).ToUpper(); updateVisibility(); for (int i = 0; i < spectatorsFlow.Count; i++) @@ -160,7 +159,7 @@ namespace osu.Game.Screens.Play.HUD var entry = pool.Get(entry => { entry.Current.Value = spectator; - entry.UserPlayingState = UserPlayingState; + entry.UserPlayingState = userPlayingState; }); spectatorsFlow.Insert(i, entry); @@ -169,15 +168,15 @@ namespace osu.Game.Screens.Play.HUD private void updateVisibility() { // We don't want to show spectators when we are watching a replay. - mainFlow.FadeTo(Spectators.Count > 0 && UserPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); + mainFlow.FadeTo(spectators.Count > 0 && userPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); } private void updateAppearance() { - Header.Font = OsuFont.GetFont(Font.Value, 12, FontWeight.Bold); - Header.Colour = HeaderColour.Value; + header.Font = OsuFont.GetFont(Font.Value, 12, FontWeight.Bold); + header.Colour = HeaderColour.Value; - Width = Header.DrawWidth; + Width = header.DrawWidth; } private partial class SpectatorListEntry : PoolableDrawable From ad642b84258497b0140ae3d45680f52988a1429f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2025 11:17:17 +0100 Subject: [PATCH 426/801] Fix spectator list showing other users in multiplayer room even if they're not spectating --- .../Visual/Gameplay/TestSceneSpectatorList.cs | 17 ++++++--- osu.Game/Screens/Play/HUD/SpectatorList.cs | 37 +++++++++++++++---- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs index 66a87c0715..66c465cbed 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs @@ -8,11 +8,14 @@ using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Graphics; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; +using osu.Game.Tests.Visual.Multiplayer; +using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Tests.Visual.Spectator; namespace osu.Game.Tests.Visual.Gameplay @@ -28,20 +31,23 @@ namespace osu.Game.Tests.Visual.Gameplay SpectatorList list = null!; Bindable playingState = new Bindable(); GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), healthProcessor: new OsuHealthProcessor(0), localUserPlayingState: playingState); - TestSpectatorClient client = new TestSpectatorClient(); + TestSpectatorClient spectatorClient = new TestSpectatorClient(); + TestMultiplayerClient multiplayerClient = new TestMultiplayerClient(new TestMultiplayerRoomManager(new TestRoomRequestsHandler())); AddStep("create spectator list", () => { Children = new Drawable[] { - client, + spectatorClient, + multiplayerClient, new DependencyProvidingContainer { RelativeSizeAxes = Axes.Both, CachedDependencies = [ (typeof(GameplayState), gameplayState), - (typeof(SpectatorClient), client) + (typeof(SpectatorClient), spectatorClient), + (typeof(MultiplayerClient), multiplayerClient), ], Child = list = new SpectatorList { @@ -57,7 +63,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddRepeatStep("add a user", () => { int id = Interlocked.Increment(ref counter); - ((ISpectatorClient)client).UserStartedWatching([ + ((ISpectatorClient)spectatorClient).UserStartedWatching([ new SpectatorUser { OnlineID = id, @@ -66,7 +72,8 @@ namespace osu.Game.Tests.Visual.Gameplay ]); }, 10); - AddRepeatStep("remove random user", () => ((ISpectatorClient)client).UserEndedWatching(client.WatchingUsers[RNG.Next(client.WatchingUsers.Count)].OnlineID), 5); + AddRepeatStep("remove random user", () => ((ISpectatorClient)spectatorClient).UserEndedWatching( + spectatorClient.WatchingUsers[RNG.Next(spectatorClient.WatchingUsers.Count)].OnlineID), 5); AddStep("change font to venera", () => list.Font.Value = Typeface.Venera); AddStep("change font to torus", () => list.Font.Value = Typeface.Torus); diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 72e866cb24..9f97121a92 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -17,6 +19,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; using osu.Game.Localisation.HUD; using osu.Game.Localisation.SkinComponents; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Skinning; using osuTK; @@ -34,8 +37,9 @@ namespace osu.Game.Screens.Play.HUD [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour), nameof(SkinnableComponentStrings.TextColourDescription))] public BindableColour4 HeaderColour { get; } = new BindableColour4(Colour4.White); - private BindableList spectators { get; } = new BindableList(); + private BindableList watchingUsers { get; } = new BindableList(); private Bindable userPlayingState { get; } = new Bindable(); + private int displayedSpectatorCount; private OsuSpriteText header = null!; private FillFlowContainer mainFlow = null!; @@ -48,6 +52,9 @@ namespace osu.Game.Screens.Play.HUD [Resolved] private GameplayState gameplayState { get; set; } = null!; + [Resolved] + private MultiplayerClient multiplayerClient { get; set; } = null!; + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -84,10 +91,10 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); - ((IBindableList)spectators).BindTo(client.WatchingUsers); + ((IBindableList)watchingUsers).BindTo(client.WatchingUsers); ((IBindable)userPlayingState).BindTo(gameplayState.PlayingState); - spectators.BindCollectionChanged(onSpectatorsChanged, true); + watchingUsers.BindCollectionChanged(onSpectatorsChanged, true); userPlayingState.BindValueChanged(_ => updateVisibility()); Font.BindValueChanged(_ => updateAppearance()); @@ -99,6 +106,18 @@ namespace osu.Game.Screens.Play.HUD private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArgs e) { + // the multiplayer gameplay leaderboard relies on calling `SpectatorClient.WatchUser()` to get updates on users' total scores. + // this has an unfortunate side effect of other players showing up in `SpectatorClient.WatchingUsers`. + // we do not generally wish to display other players in the room as spectators due to that implementation detail, + // therefore this code is intended to filter out those players on the client side. + // note that the way that this is done is rather specific to the multiplayer use case and therefore carries a lot of assumptions + // (e.g. that the `MultiplayerRoomUser`s have the correct `State` at the point wherein they issue the `WatchUser()` calls). + // the more proper way to do this (which is by subscribing to `WatchingUsers` and `RoomUpdated`, and doing a proper diff to a third list on any change of either) + // is a lot more difficult to write correctly, given that we also rely on `BindableList`'s collection changed event arguments to properly animate this component. + var excludedUserIds = new HashSet(); + if (multiplayerClient.Room != null) + excludedUserIds.UnionWith(multiplayerClient.Room.Users.Where(u => u.State != MultiplayerUserState.Spectating).Select(u => u.UserID)); + switch (e.Action) { case NotifyCollectionChangedAction.Add: @@ -108,6 +127,9 @@ namespace osu.Game.Screens.Play.HUD var spectator = (SpectatorUser)e.NewItems![i]!; int index = Math.Max(e.NewStartingIndex, 0) + i; + if (excludedUserIds.Contains(spectator.OnlineID)) + continue; + if (index >= max_spectators_displayed) break; @@ -124,10 +146,10 @@ namespace osu.Game.Screens.Play.HUD for (int i = 0; i < spectatorsFlow.Count; i++) spectatorsFlow.SetLayoutPosition(spectatorsFlow[i], i); - if (spectators.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed) + if (watchingUsers.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed) { for (int i = spectatorsFlow.Count; i < max_spectators_displayed; i++) - addNewSpectatorToList(i, spectators[i]); + addNewSpectatorToList(i, watchingUsers[i]); } break; @@ -143,7 +165,8 @@ namespace osu.Game.Screens.Play.HUD throw new NotSupportedException(); } - header.Text = SpectatorListStrings.SpectatorCount(spectators.Count).ToUpper(); + displayedSpectatorCount = watchingUsers.Count(s => !excludedUserIds.Contains(s.OnlineID)); + header.Text = SpectatorListStrings.SpectatorCount(displayedSpectatorCount).ToUpper(); updateVisibility(); for (int i = 0; i < spectatorsFlow.Count; i++) @@ -168,7 +191,7 @@ namespace osu.Game.Screens.Play.HUD private void updateVisibility() { // We don't want to show spectators when we are watching a replay. - mainFlow.FadeTo(spectators.Count > 0 && userPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); + mainFlow.FadeTo(displayedSpectatorCount > 0 && userPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); } private void updateAppearance() From 4b8890ef0c86e7ccdd415181b89ca132331a7024 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 10 Feb 2025 05:55:21 -0500 Subject: [PATCH 427/801] Fix incorrect thread access in recent iOS orientation changes --- osu.iOS/OsuGameIOS.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index a5a42c1e66..a0132de966 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -41,7 +41,7 @@ namespace osu.iOS updateOrientation(); } - private void updateOrientation() + private void updateOrientation() => UIApplication.SharedApplication.InvokeOnMainThread(() => { bool iPad = UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad; var orientation = MobileUtils.GetOrientation(this, (IOsuScreen)ScreenStack.CurrentScreen, iPad); @@ -60,7 +60,7 @@ namespace osu.iOS appDelegate.Orientations = null; break; } - } + }); protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier(); From 288851c606682165d7bb9f0cc8604eefa2b1604b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2025 12:13:46 +0100 Subject: [PATCH 428/801] Fix score position not being displayed in solo results screen Closes https://github.com/ppy/osu/issues/31842. To be honest, I recall this working too, but I don't recall when it might have broken, nor do I want to go look for the point of breakage because it might be borderline impossible to find it now. So I'm just fixing as if it was just a straight omission. Opting for a client-side fix because server-side inclusion of the score position for an entire leaderboard has been previously rejected as too expensive: https://github.com/ppy/osu-web/pull/11354#discussion_r1689217450 --- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 33b4bf976b..9f7604aa82 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Extensions; @@ -35,7 +34,32 @@ namespace osu.Game.Screens.Ranking return null; getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset); - getScoreRequest.Success += r => scoresCallback.Invoke(r.Scores.Where(s => !s.MatchesOnlineID(Score)).Select(s => s.ToScoreInfo(rulesets, Beatmap.Value.BeatmapInfo))); + getScoreRequest.Success += r => + { + var toDisplay = new List(); + + for (int i = 0; i < r.Scores.Count; ++i) + { + var score = r.Scores[i]; + int position = i + 1; + + if (score.MatchesOnlineID(Score)) + { + // we don't want to add the same score twice, but also setting any properties of `Score` this late will have no visible effect, + // so we have to fish out the actual drawable panel and set the position to it directly. + var panel = ScorePanelList.GetPanelForScore(Score); + Score.Position = panel.ScorePosition.Value = position; + } + else + { + var converted = score.ToScoreInfo(rulesets, Beatmap.Value.BeatmapInfo); + converted.Position = position; + toDisplay.Add(converted); + } + } + + scoresCallback.Invoke(toDisplay); + }; return getScoreRequest; } From 38e2f793cae38148dc5c2eae7af7c20dd46c98b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2025 12:47:38 +0100 Subject: [PATCH 429/801] Add menu items to open beatmap info & discussion pages in browser from editor --- osu.Game/Localisation/EditorStrings.cs | 12 +++++++++++- osu.Game/Screens/Edit/Editor.cs | 9 +++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 3b4026be11..1681e541fc 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -184,6 +184,16 @@ namespace osu.Game.Localisation /// public static LocalisableString ResetBookmarks => new TranslatableString(getKey(@"reset_bookmarks"), @"Reset bookmarks"); + /// + /// "Open beatmap info page in browser" + /// + public static LocalisableString OpenInfoPageInBrowser => new TranslatableString(getKey(@"open_info_page_in_browser"), @"Open beatmap info page in browser"); + + /// + /// "Open beatmap discussion page in browser" + /// + public static LocalisableString OpenDiscussionPageInBrowser => new TranslatableString(getKey(@"open_discussion_page_in_browser"), @"Open beatmap discussion page in browser"); + private static string getKey(string key) => $@"{prefix}:{key}"; } -} \ No newline at end of file +} diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index a5dfda9c95..ecb0731c16 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1256,6 +1256,15 @@ namespace osu.Game.Screens.Edit yield return externalEdit; } + if (editorBeatmap.BeatmapInfo.OnlineID > 0) + { + yield return new OsuMenuItemSpacer(); + yield return new EditorMenuItem(EditorStrings.OpenInfoPageInBrowser, MenuItemType.Standard, + () => (Game as OsuGame)?.OpenUrlExternally(editorBeatmap.BeatmapInfo.GetOnlineURL(api, editorBeatmap.BeatmapInfo.Ruleset))); + yield return new EditorMenuItem(EditorStrings.OpenDiscussionPageInBrowser, MenuItemType.Standard, + () => (Game as OsuGame)?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/beatmapsets/{editorBeatmap.BeatmapInfo.BeatmapSet!.OnlineID}/discussion/{editorBeatmap.BeatmapInfo.OnlineID}")); + } + yield return new OsuMenuItemSpacer(); yield return new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, this.Exit); } From 310700b4e7a4d3570605195babc78826751f0de6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 21:48:27 +0900 Subject: [PATCH 430/801] Space out comment --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 9f97121a92..4297c62712 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -108,8 +108,10 @@ namespace osu.Game.Screens.Play.HUD { // the multiplayer gameplay leaderboard relies on calling `SpectatorClient.WatchUser()` to get updates on users' total scores. // this has an unfortunate side effect of other players showing up in `SpectatorClient.WatchingUsers`. + // // we do not generally wish to display other players in the room as spectators due to that implementation detail, // therefore this code is intended to filter out those players on the client side. + // // note that the way that this is done is rather specific to the multiplayer use case and therefore carries a lot of assumptions // (e.g. that the `MultiplayerRoomUser`s have the correct `State` at the point wherein they issue the `WatchUser()` calls). // the more proper way to do this (which is by subscribing to `WatchingUsers` and `RoomUpdated`, and doing a proper diff to a third list on any change of either) From 78e5e0eddd1e20e480b3e49b59c2f1c3f5319e8e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 11 Feb 2025 12:17:00 +0900 Subject: [PATCH 431/801] Refactor with a bit more null safety In particular I don't like the non-null assert around `GetCurrentItem()`, because there's no reason why it _couldn't_ be `null`. Consider, for example, if these panels are used in matchmaking where there are no items initially present in the playlist. The ruleset nullability part is debatable, but I've chosen to restore the original code here. --- .../Participants/ParticipantPanel.cs | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 51ff52c63e..230245e926 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -28,6 +27,7 @@ using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play.HUD; using osu.Game.Users; using osu.Game.Users.Drawables; @@ -216,20 +216,28 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants const double fade_time = 50; - MultiplayerPlaylistItem? currentItem = client.Room.GetCurrentItem(); - Debug.Assert(currentItem != null); + if (client.Room.GetCurrentItem() is MultiplayerPlaylistItem currentItem) + { + int userBeatmapId = User.BeatmapId ?? currentItem.BeatmapID; + int userRulesetId = User.RulesetId ?? currentItem.RulesetID; + Ruleset? userRuleset = rulesets.GetRuleset(userRulesetId)?.CreateInstance(); - int userBeatmapId = User.BeatmapId ?? currentItem.BeatmapID; - int userRulesetId = User.RulesetId ?? currentItem.RulesetID; - Ruleset? userRuleset = rulesets.GetRuleset(userRulesetId)?.CreateInstance(); - Debug.Assert(userRuleset != null); + int? currentModeRank = User.User?.RulesetsStatistics?.GetValueOrDefault(userRuleset?.ShortName)?.GlobalRank; + userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; + + if (userBeatmapId == currentItem.BeatmapID && userRulesetId == currentItem.RulesetID) + userStyleDisplay.Style = null; + else + userStyleDisplay.Style = (userBeatmapId, userRulesetId); + + // If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187 + // This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix. + Schedule(() => userModsDisplay.Current.Value = userRuleset == null ? Array.Empty() : User.Mods.Select(m => m.ToMod(userRuleset)).ToList()); + } userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability); - int? currentModeRank = User.User?.RulesetsStatistics?.GetValueOrDefault(userRuleset.ShortName)?.GlobalRank; - userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; - - if ((User.BeatmapAvailability.State == DownloadState.LocallyAvailable) && (User.State != MultiplayerUserState.Spectating)) + if (User.BeatmapAvailability.State == DownloadState.LocallyAvailable && User.State != MultiplayerUserState.Spectating) { userModsDisplay.FadeIn(fade_time); userStyleDisplay.FadeIn(fade_time); @@ -240,17 +248,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants userStyleDisplay.FadeOut(fade_time); } - if (userBeatmapId == currentItem.BeatmapID && userRulesetId == currentItem.RulesetID) - userStyleDisplay.Style = null; - else - userStyleDisplay.Style = (userBeatmapId, userRulesetId); - kickButton.Alpha = client.IsHost && !User.Equals(client.LocalUser) ? 1 : 0; crown.Alpha = client.Room.Host?.Equals(User) == true ? 1 : 0; - - // If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187 - // This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix. - Schedule(() => userModsDisplay.Current.Value = User.Mods.Select(m => m.ToMod(userRuleset)).ToList()); } public MenuItem[]? ContextMenuItems From 748c2eb3904bdd23ab60bd2e1dbb5a2c772aecb8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 11 Feb 2025 12:43:51 +0900 Subject: [PATCH 432/801] Refactor `RoomSubScreen` update --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 312253774f..59acd3c17f 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -439,13 +439,14 @@ namespace osu.Game.Screens.OnlinePlay.Match var rulesetInstance = GetGameplayRuleset().CreateInstance(); - // Remove any user mods that are no longer allowed. Mod[] allowedMods = item.Freestyle - ? rulesetInstance.CreateAllMods().Where(m => ModUtils.IsValidFreeModForMatchType(m, Room.Type)).ToArray() + ? rulesetInstance.AllMods.OfType().Where(m => ModUtils.IsValidFreeModForMatchType(m, Room.Type)).ToArray() : item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); + + // Remove any user mods that are no longer allowed. Mod[] newUserMods = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); if (!newUserMods.SequenceEqual(UserMods.Value)) - UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList(); + UserMods.Value = newUserMods; // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info int beatmapId = GetGameplayBeatmap().OnlineID; @@ -456,10 +457,7 @@ namespace osu.Game.Screens.OnlinePlay.Match Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray(); Ruleset.Value = GetGameplayRuleset(); - bool freestyle = item.Freestyle; - bool freeMod = freestyle || item.AllowedMods.Any(); - - if (freeMod) + if (allowedMods.Length > 0) { UserModsSection.Show(); UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); @@ -471,7 +469,7 @@ namespace osu.Game.Screens.OnlinePlay.Match UserModsSelectOverlay.IsValidMod = _ => false; } - if (freestyle) + if (item.Freestyle) { UserStyleSection.Show(); @@ -484,7 +482,7 @@ namespace osu.Game.Screens.OnlinePlay.Match UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true) { AllowReordering = false, - AllowEditing = freestyle, + AllowEditing = true, RequestEdit = _ => OpenStyleSelection() }; } From e51c09ec3d94823ea6707b3541da6d74a738344a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 11 Feb 2025 14:23:51 +0900 Subject: [PATCH 433/801] Fix inspection Interestingly, this is not a compiler error nor does R# warn about it. No problem, because this is just restoring the original code anyway. --- .../OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 230245e926..0fa2be44f3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -222,7 +222,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants int userRulesetId = User.RulesetId ?? currentItem.RulesetID; Ruleset? userRuleset = rulesets.GetRuleset(userRulesetId)?.CreateInstance(); - int? currentModeRank = User.User?.RulesetsStatistics?.GetValueOrDefault(userRuleset?.ShortName)?.GlobalRank; + int? currentModeRank = userRuleset == null ? null : User.User?.RulesetsStatistics?.GetValueOrDefault(userRuleset.ShortName)?.GlobalRank; userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; if (userBeatmapId == currentItem.BeatmapID && userRulesetId == currentItem.RulesetID) From daf0130b2307e435641a9485fb026f1071aaff6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Feb 2025 08:06:12 +0100 Subject: [PATCH 434/801] Reword copy to be less verbose --- osu.Game/Localisation/EditorStrings.cs | 4 ++-- osu.Game/Screens/Edit/Editor.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 1681e541fc..0a15752961 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -187,12 +187,12 @@ namespace osu.Game.Localisation /// /// "Open beatmap info page in browser" /// - public static LocalisableString OpenInfoPageInBrowser => new TranslatableString(getKey(@"open_info_page_in_browser"), @"Open beatmap info page in browser"); + public static LocalisableString OpenInfoPage => new TranslatableString(getKey(@"open_info_page"), @"Open beatmap info page"); /// /// "Open beatmap discussion page in browser" /// - public static LocalisableString OpenDiscussionPageInBrowser => new TranslatableString(getKey(@"open_discussion_page_in_browser"), @"Open beatmap discussion page in browser"); + public static LocalisableString OpenDiscussionPage => new TranslatableString(getKey(@"open_discussion_page"), @"Open beatmap discussion page"); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index ecb0731c16..d73384af7f 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1259,9 +1259,9 @@ namespace osu.Game.Screens.Edit if (editorBeatmap.BeatmapInfo.OnlineID > 0) { yield return new OsuMenuItemSpacer(); - yield return new EditorMenuItem(EditorStrings.OpenInfoPageInBrowser, MenuItemType.Standard, + yield return new EditorMenuItem(EditorStrings.OpenInfoPage, MenuItemType.Standard, () => (Game as OsuGame)?.OpenUrlExternally(editorBeatmap.BeatmapInfo.GetOnlineURL(api, editorBeatmap.BeatmapInfo.Ruleset))); - yield return new EditorMenuItem(EditorStrings.OpenDiscussionPageInBrowser, MenuItemType.Standard, + yield return new EditorMenuItem(EditorStrings.OpenDiscussionPage, MenuItemType.Standard, () => (Game as OsuGame)?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/beatmapsets/{editorBeatmap.BeatmapInfo.BeatmapSet!.OnlineID}/discussion/{editorBeatmap.BeatmapInfo.OnlineID}")); } From 7db0a6f81775248bbcb41a57f363b2d6a73b8875 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Feb 2025 08:06:12 +0100 Subject: [PATCH 435/801] Update xmldoc --- osu.Game/Localisation/EditorStrings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 0a15752961..b74a546eca 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -185,12 +185,12 @@ namespace osu.Game.Localisation public static LocalisableString ResetBookmarks => new TranslatableString(getKey(@"reset_bookmarks"), @"Reset bookmarks"); /// - /// "Open beatmap info page in browser" + /// "Open beatmap info page" /// public static LocalisableString OpenInfoPage => new TranslatableString(getKey(@"open_info_page"), @"Open beatmap info page"); /// - /// "Open beatmap discussion page in browser" + /// "Open beatmap discussion page" /// public static LocalisableString OpenDiscussionPage => new TranslatableString(getKey(@"open_discussion_page"), @"Open beatmap discussion page"); From 1fa8d53232931e0edc37ddba22cde7aacb48e799 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Feb 2025 17:11:20 +0900 Subject: [PATCH 436/801] Disable scale animation when holding editor "test" button --- .../Timelines/Summary/TestGameplayButton.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs index 169e72fe3f..065f52b929 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -33,5 +34,16 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary Text = EditorStrings.TestBeatmap; } + + protected override bool OnMouseDown(MouseDownEvent e) + { + // block scale animation + return false; + } + + protected override void OnMouseUp(MouseUpEvent e) + { + // block scale animation + } } } From 884fa20b286264482f6e965f946369e42d9fe356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Feb 2025 09:13:08 +0100 Subject: [PATCH 437/801] Remove completely unnecessary subscriptions per collection --- .../Collections/DrawableCollectionList.cs | 1 - .../Collections/DrawableCollectionListItem.cs | 32 +++++++------------ 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionList.cs b/osu.Game/Collections/DrawableCollectionList.cs index 85af1d383d..c494b830d1 100644 --- a/osu.Game/Collections/DrawableCollectionList.cs +++ b/osu.Game/Collections/DrawableCollectionList.cs @@ -96,7 +96,6 @@ namespace osu.Game.Collections lastCreated = collections[changes.InsertedIndices[0]].ID; foreach (int i in changes.NewModifiedIndices) - { var updatedItem = collections[i]; diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 0060dacc01..703def9546 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -126,15 +126,10 @@ namespace osu.Game.Collections private const float count_text_size = 12; - [Resolved] - private RealmAccess realm { get; set; } = null!; - private readonly Live collection; private OsuSpriteText countText = null!; - private IDisposable? itemCountSubscription; - public ItemTextBox(Live collection) { this.collection = collection; @@ -163,29 +158,24 @@ namespace osu.Game.Collections Colour = colours.Yellow }); - itemCountSubscription = realm.SubscribeToPropertyChanged(r => r.Find(collection.ID), c => c.BeatmapMD5Hashes, _ => - Scheduler.AddOnce(() => - { - int count = collection.PerformRead(c => c.BeatmapMD5Hashes.Count); + // interestingly, it is not required to subscribe to change notifications on this collection at all for this to work correctly. + // the reasoning for this is that `DrawableCollectionList` already takes out a subscription on the set of all `BeatmapCollection`s - + // but that subscription does not only cover *changes to the set of collections* (i.e. addition/removal/rearrangement of collections), + // but also covers *changes to the properties of collections*, which `BeatmapMD5Hashes` is one. + // when a collection item changes due to `BeatmapMD5Hashes` changing, the list item is deleted and re-inserted, thus guaranteeing this to work correctly. + int count = collection.PerformRead(c => c.BeatmapMD5Hashes.Count); - countText.Text = count == 1 - // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 - // but also in this case we want support for formatting a number within a string). - ? $"{count:#,0} beatmap" - : $"{count:#,0} beatmaps"; - })); + countText.Text = count == 1 + // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 + // but also in this case we want support for formatting a number within a string). + ? $"{count:#,0} beatmap" + : $"{count:#,0} beatmaps"; } else { PlaceholderText = "Create a new collection"; } } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - itemCountSubscription?.Dispose(); - } } public partial class DeleteButton : OsuClickableContainer From b9ed217308f3ebe7405274d8fbd257835bc259dd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Feb 2025 17:13:34 +0900 Subject: [PATCH 438/801] Add basic brighten animation instead --- .../Timelines/Summary/TestGameplayButton.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs index 065f52b929..f5c0ed2382 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs @@ -15,6 +15,9 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary { public partial class TestGameplayButton : OsuButton { + [Resolved] + private OsuColour colours { get; set; } = null!; + protected override SpriteText CreateText() => new OsuSpriteText { Depth = -1, @@ -25,7 +28,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary }; [BackgroundDependencyLoader] - private void load(OsuColour colours, OverlayColourProvider colourProvider) + private void load(OverlayColourProvider colourProvider) { BackgroundColour = colours.Orange1; SpriteText.Colour = colourProvider.Background6; @@ -37,13 +40,15 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary protected override bool OnMouseDown(MouseDownEvent e) { - // block scale animation + Background.FadeColour(colours.Orange0, 500, Easing.OutQuint); + // don't call base in order to block scale animation return false; } protected override void OnMouseUp(MouseUpEvent e) { - // block scale animation + Background.FadeColour(colours.Orange1, 300, Easing.OutQuint); + // don't call base in order to block scale animation } } } From d8b3c28c2e5cb3c666ae937d4cd13feb7d5475d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Feb 2025 09:17:11 +0100 Subject: [PATCH 439/801] Use more neutral terminology to avoid contentious 'beatmap' term --- osu.Game/Collections/DrawableCollectionListItem.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 703def9546..f2b00004e2 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -168,8 +168,8 @@ namespace osu.Game.Collections countText.Text = count == 1 // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 // but also in this case we want support for formatting a number within a string). - ? $"{count:#,0} beatmap" - : $"{count:#,0} beatmaps"; + ? $"{count:#,0} item" + : $"{count:#,0} items"; } else { From b9c4e235958796bb4f85b9734b5f685541ea13d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Feb 2025 20:05:48 +0900 Subject: [PATCH 440/801] Fix potential bad realm access to collection name --- osu.Game/Collections/DrawableCollectionListItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index f2b00004e2..b0dd70227c 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -255,7 +255,7 @@ namespace osu.Game.Collections private void deleteCollection() => collection.PerformWrite(c => c.Realm!.Remove(c)); } - public IEnumerable FilterTerms => [(LocalisableString)Model.Value.Name]; + public IEnumerable FilterTerms => Model.PerformRead(m => m.IsValid ? new[] { (LocalisableString)m.Name } : []); private bool matchingFilter = true; From 8c85616d1c8677a859bf007291997b092786f94c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 11 Feb 2025 21:28:21 +0900 Subject: [PATCH 441/801] Fix test --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs index 66c465cbed..bd1e15d06d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay Bindable playingState = new Bindable(); GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), healthProcessor: new OsuHealthProcessor(0), localUserPlayingState: playingState); TestSpectatorClient spectatorClient = new TestSpectatorClient(); - TestMultiplayerClient multiplayerClient = new TestMultiplayerClient(new TestMultiplayerRoomManager(new TestRoomRequestsHandler())); + TestMultiplayerClient multiplayerClient = new TestMultiplayerClient(new TestRoomRequestsHandler()); AddStep("create spectator list", () => { From be035538c241f29ef609c9f73c670b0056278222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Feb 2025 14:01:32 +0100 Subject: [PATCH 442/801] Fix remaining hit counter scaling in the incorrect direction --- osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 0eb80d333f..c819cb7937 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -108,8 +108,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private void animateSwellProgress(int numHits, int requiredHits) { - remainingHitsText.Text = $"{requiredHits - numHits}"; - remainingHitsText.ScaleTo(1.6f - (0.6f * ((float)numHits / requiredHits)), 60, Easing.OutQuad); + int remainingHits = requiredHits - numHits; + remainingHitsText.Text = remainingHits.ToString(); + remainingHitsText.ScaleTo(1.6f - (0.6f * ((float)remainingHits / requiredHits)), 60, Easing.OutQuad); spinnerCircle.ClearTransforms(); spinnerCircle From 231988bc9de21a5e7cdc0fbd838e6cb20c75990a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Feb 2025 15:20:36 +0100 Subject: [PATCH 443/801] Adjust things to be closer to stable (but not close enough yet) --- .../Skinning/Legacy/LegacySwell.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index c819cb7937..d3b5d54828 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -14,6 +14,7 @@ using osuTK; using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Extensions.ObjectExtensions; using System; +using System.Globalization; namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { @@ -39,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } [BackgroundDependencyLoader] - private void load(DrawableHitObject hitObject, ISkinSource skin, SkinManager skinManager) + private void load(DrawableHitObject hitObject, ISkinSource skin) { Children = new Drawable[] { @@ -54,7 +55,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Position = new Vector2(200f, 100f), + Position = new Vector2(250f, 100f), // ballparked to be horizontally centred on 4:3 resolution Children = new Drawable[] { @@ -109,14 +110,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private void animateSwellProgress(int numHits, int requiredHits) { int remainingHits = requiredHits - numHits; - remainingHitsText.Text = remainingHits.ToString(); - remainingHitsText.ScaleTo(1.6f - (0.6f * ((float)remainingHits / requiredHits)), 60, Easing.OutQuad); + remainingHitsText.Text = remainingHits.ToString(CultureInfo.InvariantCulture); + remainingHitsText.ScaleTo(1.6f - (0.6f * ((float)remainingHits / requiredHits)), 60, Easing.Out); spinnerCircle.ClearTransforms(); spinnerCircle .RotateTo(180f * numHits, 1000, Easing.OutQuint) .ScaleTo(Math.Min(0.94f, spinnerCircle.Scale.X + 0.02f)) - .ScaleTo(0.8f, 400, Easing.OutQuad); + .ScaleTo(0.8f, 400, Easing.Out); } private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) @@ -134,7 +135,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy samplePlayed = false; } - const double body_transition_duration = 100; + const double body_transition_duration = 200; warning.FadeOut(body_transition_duration); bodyContainer.FadeIn(body_transition_duration); @@ -146,9 +147,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy const double clear_transition_duration = 300; const double clear_fade_in = 120; - bodyContainer - .FadeOut(clear_transition_duration, Easing.OutQuad) - .ScaleTo(1.05f, clear_transition_duration, Easing.OutQuad); + bodyContainer.FadeOut(clear_transition_duration, Easing.OutQuad); + spinnerCircle.ScaleTo(spinnerCircle.Scale.X + 0.05f, clear_transition_duration, Easing.OutQuad); if (state == ArmedState.Hit) { @@ -159,11 +159,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } clearAnimation - .FadeIn(clear_fade_in) .MoveTo(new Vector2(0, 0)) + .MoveTo(new Vector2(0, -90), clear_fade_in * 2, Easing.Out) .ScaleTo(0.4f) - .MoveTo(new Vector2(0, -90), clear_fade_in * 2, Easing.OutQuad) .ScaleTo(1f, clear_fade_in * 2, Easing.Out) + .FadeIn(clear_fade_in) .Delay(clear_fade_in * 3) .FadeOut(clear_fade_in * 2.5); } From a8f07ae7b1ebce7579cc97a14264b7132b017f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Tue, 11 Feb 2025 18:04:23 +0100 Subject: [PATCH 444/801] Add comment warning about enum entry order in `GlobalAction` --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 599ca6d6c1..e4dc2d503b 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -227,6 +227,10 @@ namespace osu.Game.Input.Bindings }; } + /// + /// IMPORTANT: New entries should always be added at the end of the enum, as key bindings are stored using the enum's numeric value and + /// changes in order would cause key bindings to get associated with the wrong action. + /// public enum GlobalAction { [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleChat))] From ffd8bd7bf4dd4d238986c90e598ad11580667d01 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 16:14:12 +0900 Subject: [PATCH 445/801] Rename `ParentObject` to `DrawableObject` It's not a parent. The follow circle is directly part of the slider itself. --- .../Skinning/FollowCircle.cs | 51 ++++++++++--------- .../Skinning/Legacy/LegacyFollowCircle.cs | 4 +- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs index 4fadb09948..d1836010fb 100644 --- a/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs @@ -13,8 +13,7 @@ namespace osu.Game.Rulesets.Osu.Skinning { public abstract partial class FollowCircle : CompositeDrawable { - [Resolved] - protected DrawableHitObject? ParentObject { get; private set; } + protected DrawableSlider? DrawableObject { get; private set; } protected FollowCircle() { @@ -22,16 +21,18 @@ namespace osu.Game.Rulesets.Osu.Skinning } [BackgroundDependencyLoader] - private void load() + private void load(DrawableHitObject? hitObject) { - ((DrawableSlider?)ParentObject)?.Tracking.BindValueChanged(tracking => - { - Debug.Assert(ParentObject != null); + DrawableObject = hitObject as DrawableSlider; - if (ParentObject.Judged) + DrawableObject?.Tracking.BindValueChanged(tracking => + { + Debug.Assert(DrawableObject != null); + + if (DrawableObject.Judged) return; - using (BeginAbsoluteSequence(Math.Max(Time.Current, ParentObject.HitObject?.StartTime ?? 0))) + using (BeginAbsoluteSequence(Math.Max(Time.Current, DrawableObject.HitObject?.StartTime ?? 0))) { if (tracking.NewValue) OnSliderPress(); @@ -45,13 +46,13 @@ namespace osu.Game.Rulesets.Osu.Skinning { base.LoadComplete(); - if (ParentObject != null) + if (DrawableObject != null) { - ParentObject.HitObjectApplied += onHitObjectApplied; - onHitObjectApplied(ParentObject); + DrawableObject.HitObjectApplied += onHitObjectApplied; + onHitObjectApplied(DrawableObject); - ParentObject.ApplyCustomUpdateState += updateStateTransforms; - updateStateTransforms(ParentObject, ParentObject.State.Value); + DrawableObject.ApplyCustomUpdateState += updateStateTransforms; + updateStateTransforms(DrawableObject, DrawableObject.State.Value); } } @@ -61,26 +62,26 @@ namespace osu.Game.Rulesets.Osu.Skinning .FadeOut(); } - private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState state) + private void updateStateTransforms(DrawableHitObject d, ArmedState state) { - Debug.Assert(ParentObject != null); + Debug.Assert(DrawableObject != null); switch (state) { case ArmedState.Hit: - switch (drawableObject) + switch (d) { case DrawableSliderTail: - // Use ParentObject instead of drawableObject because slider tail's + // Use DrawableObject instead of local object because slider tail's // HitStateUpdateTime is ~36ms before the actual slider end (aka slider // tail leniency) - using (BeginAbsoluteSequence(ParentObject.HitStateUpdateTime)) + using (BeginAbsoluteSequence(DrawableObject.HitStateUpdateTime)) OnSliderEnd(); break; case DrawableSliderTick: case DrawableSliderRepeat: - using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime)) + using (BeginAbsoluteSequence(d.HitStateUpdateTime)) OnSliderTick(); break; } @@ -88,15 +89,15 @@ namespace osu.Game.Rulesets.Osu.Skinning break; case ArmedState.Miss: - switch (drawableObject) + switch (d) { case DrawableSliderTail: case DrawableSliderTick: case DrawableSliderRepeat: - // Despite above comment, ok to use drawableObject.HitStateUpdateTime + // Despite above comment, ok to use d.HitStateUpdateTime // here, since on stable, the break anim plays right when the tail is // missed, not when the slider ends - using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime)) + using (BeginAbsoluteSequence(d.HitStateUpdateTime)) OnSliderBreak(); break; } @@ -109,10 +110,10 @@ namespace osu.Game.Rulesets.Osu.Skinning { base.Dispose(isDisposing); - if (ParentObject != null) + if (DrawableObject != null) { - ParentObject.HitObjectApplied -= onHitObjectApplied; - ParentObject.ApplyCustomUpdateState -= updateStateTransforms; + DrawableObject.HitObjectApplied -= onHitObjectApplied; + DrawableObject.ApplyCustomUpdateState -= updateStateTransforms; } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs index 4a8b737206..f60b5cfe12 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs @@ -22,9 +22,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected override void OnSliderPress() { - Debug.Assert(ParentObject != null); + Debug.Assert(DrawableObject != null); - double remainingTime = Math.Max(0, ParentObject.HitStateUpdateTime - Time.Current); + double remainingTime = Math.Max(0, DrawableObject.HitStateUpdateTime - Time.Current); // Note that the scale adjust here is 2 instead of DrawableSliderBall.FOLLOW_AREA to match legacy behaviour. // This means the actual tracking area for gameplay purposes is larger than the sprite (but skins may be accounting for this). From f97708e6b3bd4bc516e7837e43599b5f1c88c6f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 16:28:14 +0900 Subject: [PATCH 446/801] Avoid binding directly to DHO's bindable --- .../Skinning/FollowCircle.cs | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs index d1836010fb..903ba08010 100644 --- a/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; @@ -15,6 +16,8 @@ namespace osu.Game.Rulesets.Osu.Skinning { protected DrawableSlider? DrawableObject { get; private set; } + private readonly IBindable tracking = new Bindable(); + protected FollowCircle() { RelativeSizeAxes = Axes.Both; @@ -25,21 +28,23 @@ namespace osu.Game.Rulesets.Osu.Skinning { DrawableObject = hitObject as DrawableSlider; - DrawableObject?.Tracking.BindValueChanged(tracking => + if (DrawableObject != null) { - Debug.Assert(DrawableObject != null); - - if (DrawableObject.Judged) - return; - - using (BeginAbsoluteSequence(Math.Max(Time.Current, DrawableObject.HitObject?.StartTime ?? 0))) + tracking.BindTo(DrawableObject.Tracking); + tracking.BindValueChanged(tracking => { - if (tracking.NewValue) - OnSliderPress(); - else - OnSliderRelease(); - } - }, true); + if (DrawableObject.Judged) + return; + + using (BeginAbsoluteSequence(Math.Max(Time.Current, DrawableObject.HitObject?.StartTime ?? 0))) + { + if (tracking.NewValue) + OnSliderPress(); + else + OnSliderRelease(); + } + }, true); + } } protected override void LoadComplete() From 84b5ea3dbf6ab7b6209820468d3369e477f9d1b1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 16:33:23 +0900 Subject: [PATCH 447/801] Fix weird follow circle display when rewinding through sliders in editor Closes https://github.com/ppy/osu/issues/31812. --- osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs index 903ba08010..db789166c6 100644 --- a/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs @@ -63,8 +63,12 @@ namespace osu.Game.Rulesets.Osu.Skinning private void onHitObjectApplied(DrawableHitObject drawableObject) { + // Sane defaults when a new hitobject is applied to the drawable slider. this.ScaleTo(1f) .FadeOut(); + + // Immediately play out any pending transforms from press/release + FinishTransforms(true); } private void updateStateTransforms(DrawableHitObject d, ArmedState state) From b92e9f515bd291a19546538355aeb48001933829 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 17:31:55 +0900 Subject: [PATCH 448/801] Fix layout of user setting areas when aspect ratio is vertically tall --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index a16c5c9442..ff4c8c2fd9 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -121,9 +121,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer new GridContainer { RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, 5), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + }, Content = new[] { - new Drawable[] { new OverlinedHeader("Beatmap") }, + new Drawable[] { new OverlinedHeader("Beatmap queue") }, new Drawable[] { addItemButton = new AddItemButton @@ -202,14 +211,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer }, }, }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Absolute, 5), - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - } }, null, new GridContainer From 9aef95c38127ae72b2538326e561a28db5d3acda Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 17:43:49 +0900 Subject: [PATCH 449/801] Adjust some paddings and text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mostly trying to give more space to the queue as we add more vertical elements to the middle area of multiplayer / playerlists. This whole UI will likely change – this is just a stop-gap fix. --- osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs | 2 -- .../Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs | 2 +- .../Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 1 + 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs b/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs index d9cdcac7d7..6dfde183f0 100644 --- a/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs @@ -53,13 +53,11 @@ namespace osu.Game.Screens.OnlinePlay.Components { RelativeSizeAxes = Axes.X, Height = 2, - Margin = new MarginPadding { Bottom = 2 } }, new FillFlowContainer { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Top = 5 }, Spacing = new Vector2(10, 0), Children = new Drawable[] { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs index e5d94c5358..a7f3e17efa 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist protected override void LoadComplete() { base.LoadComplete(); - QueueItems.BindCollectionChanged((_, _) => Text.Text = QueueItems.Count > 0 ? $"Queue ({QueueItems.Count})" : "Queue", true); + QueueItems.BindCollectionChanged((_, _) => Text.Text = QueueItems.Count > 0 ? $"Up next ({QueueItems.Count})" : "Up next", true); } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index ff4c8c2fd9..083c8e070e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -176,6 +176,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Width = 90, + Height = 30, Text = "Select", Action = ShowUserModSelect, }, From 9c3e9e7c55b8aad452151c2c1b13a00660b3f52d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 17:56:15 +0900 Subject: [PATCH 450/801] Change free mods button to show "all" when freestyle is enabled --- .../TestSceneFreeModSelectOverlay.cs | 2 +- .../OnlinePlay/FooterButtonFreeMods.cs | 28 ++++++------------- .../OnlinePlay/FooterButtonFreestyle.cs | 15 ++++------ .../OnlinePlay/OnlinePlaySongSelect.cs | 20 +++++++++---- 4 files changed, 29 insertions(+), 36 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs index fb54b89a4b..fd589e928a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs @@ -166,7 +166,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, Y = -ScreenFooter.HEIGHT, - Current = { BindTarget = freeModSelectOverlay.SelectedMods }, + FreeMods = { BindTarget = freeModSelectOverlay.SelectedMods }, }, footer = new ScreenFooter(), }, diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs index 402f538716..695ed74ab9 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs @@ -11,31 +11,20 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select; using osuTK; -using osu.Game.Localisation; namespace osu.Game.Screens.OnlinePlay { - public partial class FooterButtonFreeMods : FooterButton, IHasCurrentValue> + public partial class FooterButtonFreeMods : FooterButton { - private readonly BindableWithCurrent> current = new BindableWithCurrent>(Array.Empty()); - - public Bindable> Current - { - get => current.Current; - set - { - ArgumentNullException.ThrowIfNull(value); - - current.Current = value; - } - } + public readonly Bindable> FreeMods = new Bindable>(); + public readonly IBindable Freestyle = new Bindable(); public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); } @@ -104,7 +93,8 @@ namespace osu.Game.Screens.OnlinePlay { base.LoadComplete(); - Current.BindValueChanged(_ => updateModDisplay(), true); + Freestyle.BindValueChanged(_ => updateModDisplay()); + FreeMods.BindValueChanged(_ => updateModDisplay(), true); } /// @@ -114,16 +104,16 @@ namespace osu.Game.Screens.OnlinePlay { var availableMods = allAvailableAndValidMods.ToArray(); - Current.Value = Current.Value.Count == availableMods.Length + FreeMods.Value = FreeMods.Value.Count == availableMods.Length ? Array.Empty() : availableMods; } private void updateModDisplay() { - int currentCount = Current.Value.Count; + int currentCount = FreeMods.Value.Count; - if (currentCount == allAvailableAndValidMods.Count()) + if (currentCount == allAvailableAndValidMods.Count() || Freestyle.Value) { count.Text = "all"; count.FadeColour(colours.Gray2, 200, Easing.OutQuint); diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs index 157f90d078..d907fec489 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs @@ -16,15 +16,10 @@ using osu.Game.Localisation; namespace osu.Game.Screens.OnlinePlay { - public partial class FooterButtonFreestyle : FooterButton, IHasCurrentValue + public partial class FooterButtonFreestyle : FooterButton { - private readonly BindableWithCurrent current = new BindableWithCurrent(); + public readonly Bindable Freestyle = new Bindable(); - public Bindable Current - { - get => current.Current; - set => current.Current = value; - } public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); } @@ -37,7 +32,7 @@ namespace osu.Game.Screens.OnlinePlay public FooterButtonFreestyle() { // Overwrite any external behaviour as we delegate the main toggle action to a sub-button. - base.Action = () => current.Value = !current.Value; + base.Action = () => Freestyle.Value = !Freestyle.Value; } [BackgroundDependencyLoader] @@ -81,12 +76,12 @@ namespace osu.Game.Screens.OnlinePlay { base.LoadComplete(); - Current.BindValueChanged(_ => updateDisplay(), true); + Freestyle.BindValueChanged(_ => updateDisplay(), true); } private void updateDisplay() { - if (current.Value) + if (Freestyle.Value) { text.Text = "on"; text.FadeColour(colours.Gray2, 200, Easing.OutQuint); diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 1164c4c0fc..cf351b31bf 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -126,6 +126,7 @@ namespace osu.Game.Screens.OnlinePlay { if (enabled.NewValue) { + freeModsFooterButton.Enabled.Value = false; freeModsFooterButton.Enabled.Value = false; ModsFooterButton.Enabled.Value = false; @@ -205,8 +206,15 @@ namespace osu.Game.Screens.OnlinePlay baseButtons.InsertRange(baseButtons.FindIndex(b => b.button is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[] { - (freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) { Current = FreeMods }, null), - (new FooterButtonFreestyle { Current = Freestyle }, null) + (freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) + { + FreeMods = { BindTarget = FreeMods }, + Freestyle = { BindTarget = Freestyle } + }, null), + (new FooterButtonFreestyle + { + Freestyle = { BindTarget = Freestyle } + }, null) }); return baseButtons; @@ -225,10 +233,10 @@ namespace osu.Game.Screens.OnlinePlay /// The to check. /// Whether is a selectable free-mod. private bool isValidFreeMod(Mod mod) => ModUtils.IsValidFreeModForMatchType(mod, room.Type) - // Mod must not be contained in the required mods. - && Mods.Value.All(m => m.Acronym != mod.Acronym) - // Mod must be compatible with all the required mods. - && ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray()); + // Mod must not be contained in the required mods. + && Mods.Value.All(m => m.Acronym != mod.Acronym) + // Mod must be compatible with all the required mods. + && ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray()); protected override void Dispose(bool isDisposing) { From 218151bb3c7af0fe77b32e55757cc0079b40cce6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 18:27:53 +0900 Subject: [PATCH 451/801] Flash footer freemod/freestyle buttons when active --- .../Screens/OnlinePlay/FooterButtonFreeMods.cs | 2 ++ .../Screens/OnlinePlay/FooterButtonFreestyle.cs | 4 ++-- .../Screens/OnlinePlay/OnlinePlaySongSelect.cs | 2 +- osu.Game/Screens/Select/FooterButton.cs | 17 +++++++++++++++++ 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs index 695ed74ab9..3605412b2b 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs @@ -26,6 +26,8 @@ namespace osu.Game.Screens.OnlinePlay public readonly Bindable> FreeMods = new Bindable>(); public readonly IBindable Freestyle = new Bindable(); + protected override bool IsActive => FreeMods.Value.Count > 0; + public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); } private OsuSpriteText count = null!; diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs index d907fec489..6ee983af20 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs @@ -8,11 +8,10 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Screens.Select; using osu.Game.Localisation; +using osu.Game.Screens.Select; namespace osu.Game.Screens.OnlinePlay { @@ -20,6 +19,7 @@ namespace osu.Game.Screens.OnlinePlay { public readonly Bindable Freestyle = new Bindable(); + protected override bool IsActive => Freestyle.Value; public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index cf351b31bf..9bedecc221 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -42,7 +42,7 @@ namespace osu.Game.Screens.OnlinePlay protected override UserActivity InitialActivity => new UserActivity.InLobby(room); protected readonly Bindable> FreeMods = new Bindable>(Array.Empty()); - protected readonly Bindable Freestyle = new Bindable(); + protected readonly Bindable Freestyle = new Bindable(true); private readonly Room room; private readonly PlaylistItem? initialItem; diff --git a/osu.Game/Screens/Select/FooterButton.cs b/osu.Game/Screens/Select/FooterButton.cs index 128e750dca..dafa0b0c1c 100644 --- a/osu.Game/Screens/Select/FooterButton.cs +++ b/osu.Game/Screens/Select/FooterButton.cs @@ -25,6 +25,11 @@ namespace osu.Game.Screens.Select protected static readonly Vector2 SHEAR = new Vector2(SHEAR_WIDTH / Footer.HEIGHT, 0); + /// + /// Used to show an initial animation hinting at the enabled state. + /// + protected virtual bool IsActive => false; + public LocalisableString Text { get => SpriteText?.Text ?? default; @@ -124,6 +129,18 @@ namespace osu.Game.Screens.Select { base.LoadComplete(); Enabled.BindValueChanged(_ => updateDisplay(), true); + + if (IsActive) + { + box.ClearTransforms(); + + using (box.BeginDelayedSequence(200)) + { + box.FadeIn(200) + .Then() + .FadeOut(1500, Easing.OutQuint); + } + } } public Action Hovered; From c049ae69370629f8c8c888705b6cb6feb7ad2ef4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 18:45:00 +0900 Subject: [PATCH 452/801] Update height specification for playlist screen too --- osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 957a51c467..7f2255e482 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -204,6 +204,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Width = 90, + Height = 30, Text = "Select", Action = ShowUserModSelect, }, From 3a0464299af5bde7527d48bcdb8f3a1a85d67d85 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 19:22:57 +0900 Subject: [PATCH 453/801] Remove unnecessary V2 suffixes --- .../SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs | 4 ++-- osu.Game/Screens/SelectV2/BeatmapPanel.cs | 4 ++-- osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 4 ++-- osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs | 8 ++++---- .../SelectV2/{TopLocalRankV2.cs => TopLocalRank.cs} | 6 ++---- ...ateBeatmapSetButtonV2.cs => UpdateBeatmapSetButton.cs} | 4 ++-- 6 files changed, 14 insertions(+), 16 deletions(-) rename osu.Game/Screens/SelectV2/{TopLocalRankV2.cs => TopLocalRank.cs} (94%) rename osu.Game/Screens/SelectV2/{UpdateBeatmapSetButtonV2.cs => UpdateBeatmapSetButton.cs} (98%) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs index 6e5d731453..ba3f2635b0 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs @@ -11,12 +11,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { public partial class TestSceneUpdateBeatmapSetButtonV2 : OsuTestScene { - private UpdateBeatmapSetButtonV2 button = null!; + private UpdateBeatmapSetButton button = null!; [SetUp] public void SetUp() => Schedule(() => { - Child = button = new UpdateBeatmapSetButtonV2 + Child = button = new UpdateBeatmapSetButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/SelectV2/BeatmapPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs index a888c0331f..3db60876a1 100644 --- a/osu.Game/Screens/SelectV2/BeatmapPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapPanel.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.SelectV2 private ConstrainedIconContainer difficultyIcon = null!; private OsuSpriteText keyCountText = null!; private StarRatingDisplay starRatingDisplay = null!; - private TopLocalRankV2 difficultyRank = null!; + private TopLocalRank difficultyRank = null!; private OsuSpriteText difficultyText = null!; private OsuSpriteText authorText = null!; @@ -118,7 +118,7 @@ namespace osu.Game.Screens.SelectV2 Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, - difficultyRank = new TopLocalRankV2 + difficultyRank = new TopLocalRank { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index 85e97a8464..6caabb79c3 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -35,7 +35,7 @@ namespace osu.Game.Screens.SelectV2 private OsuSpriteText titleText = null!; private OsuSpriteText artistText = null!; private Drawable chevronIcon = null!; - private UpdateBeatmapSetButtonV2 updateButton = null!; + private UpdateBeatmapSetButton updateButton = null!; private BeatmapSetOnlineStatusPill statusPill = null!; private DifficultySpectrumDisplay difficultiesDisplay = null!; @@ -98,7 +98,7 @@ namespace osu.Game.Screens.SelectV2 Margin = new MarginPadding { Top = 5f }, Children = new Drawable[] { - updateButton = new UpdateBeatmapSetButtonV2 + updateButton = new UpdateBeatmapSetButton { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, diff --git a/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs b/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs index 32a729c95d..e8628d5b78 100644 --- a/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs @@ -64,13 +64,13 @@ namespace osu.Game.Screens.SelectV2 private OsuSpriteText titleText = null!; private OsuSpriteText artistText = null!; - private UpdateBeatmapSetButtonV2 updateButton = null!; + private UpdateBeatmapSetButton updateButton = null!; private BeatmapSetOnlineStatusPill statusPill = null!; private ConstrainedIconContainer difficultyIcon = null!; private FillFlowContainer difficultyLine = null!; private StarRatingDisplay difficultyStarRating = null!; - private TopLocalRankV2 difficultyRank = null!; + private TopLocalRank difficultyRank = null!; private OsuSpriteText difficultyKeyCountText = null!; private OsuSpriteText difficultyName = null!; private OsuSpriteText difficultyAuthor = null!; @@ -121,7 +121,7 @@ namespace osu.Game.Screens.SelectV2 Margin = new MarginPadding { Top = 5f }, Children = new Drawable[] { - updateButton = new UpdateBeatmapSetButtonV2 + updateButton = new UpdateBeatmapSetButton { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -149,7 +149,7 @@ namespace osu.Game.Screens.SelectV2 Scale = new Vector2(8f / 9f), Margin = new MarginPadding { Right = 5f }, }, - difficultyRank = new TopLocalRankV2 + difficultyRank = new TopLocalRank { Scale = new Vector2(8f / 11), Origin = Anchor.CentreLeft, diff --git a/osu.Game/Screens/SelectV2/TopLocalRankV2.cs b/osu.Game/Screens/SelectV2/TopLocalRank.cs similarity index 94% rename from osu.Game/Screens/SelectV2/TopLocalRankV2.cs rename to osu.Game/Screens/SelectV2/TopLocalRank.cs index 241e92a67d..2a72a05db7 100644 --- a/osu.Game/Screens/SelectV2/TopLocalRankV2.cs +++ b/osu.Game/Screens/SelectV2/TopLocalRank.cs @@ -19,7 +19,7 @@ using Realms; namespace osu.Game.Screens.SelectV2 { - public partial class TopLocalRankV2 : CompositeDrawable + public partial class TopLocalRank : CompositeDrawable { private BeatmapInfo? beatmap; @@ -48,9 +48,7 @@ namespace osu.Game.Screens.SelectV2 private readonly UpdateableRank updateable; - public ScoreRank? DisplayedRank => updateable.Rank; - - public TopLocalRankV2(BeatmapInfo? beatmap = null) + public TopLocalRank(BeatmapInfo? beatmap = null) { AutoSizeAxes = Axes.Both; diff --git a/osu.Game/Screens/SelectV2/UpdateBeatmapSetButtonV2.cs b/osu.Game/Screens/SelectV2/UpdateBeatmapSetButton.cs similarity index 98% rename from osu.Game/Screens/SelectV2/UpdateBeatmapSetButtonV2.cs rename to osu.Game/Screens/SelectV2/UpdateBeatmapSetButton.cs index 2d1ce4ba48..e2c841f88a 100644 --- a/osu.Game/Screens/SelectV2/UpdateBeatmapSetButtonV2.cs +++ b/osu.Game/Screens/SelectV2/UpdateBeatmapSetButton.cs @@ -22,7 +22,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public partial class UpdateBeatmapSetButtonV2 : OsuAnimatedButton + public partial class UpdateBeatmapSetButton : OsuAnimatedButton { private BeatmapSetInfo? beatmapSet; @@ -53,7 +53,7 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private IDialogOverlay? dialogOverlay { get; set; } - public UpdateBeatmapSetButtonV2() + public UpdateBeatmapSetButton() { Size = new Vector2(75f, 22f); } From 151101be7031c8b87716bbb24411f44658567482 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 19:24:30 +0900 Subject: [PATCH 454/801] Mark `Action` as `init` only --- osu.Game/Screens/SelectV2/CarouselPanelPiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/CarouselPanelPiece.cs b/osu.Game/Screens/SelectV2/CarouselPanelPiece.cs index 4b533e362a..5aefa57bb5 100644 --- a/osu.Game/Screens/SelectV2/CarouselPanelPiece.cs +++ b/osu.Game/Screens/SelectV2/CarouselPanelPiece.cs @@ -70,7 +70,7 @@ namespace osu.Game.Screens.SelectV2 public readonly BindableBool Active = new BindableBool(); public readonly BindableBool KeyboardActive = new BindableBool(); - public Action? Action; + public Action? Action { get; init; } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { From 554884710cd4bb9749e337ed25297304cfdb3541 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 19:30:27 +0900 Subject: [PATCH 455/801] Rename classes for better discoverability / grouping --- ...estSceneBeatmapCarouselV2ArtistGrouping.cs | 34 ++++++------- ...ceneBeatmapCarouselV2DifficultyGrouping.cs | 50 +++++++++---------- .../TestSceneBeatmapCarouselV2NoGrouping.cs | 16 +++--- .../TestSceneBeatmapCarouselV2Scrolling.cs | 10 ++-- ...stSceneBeatmapCarouselV2DifficultyPanel.cs | 8 +-- .../TestSceneBeatmapCarouselV2GroupPanel.cs | 20 ++++---- .../TestSceneBeatmapCarouselV2SetPanel.cs | 8 +-- ...stSceneBeatmapCarouselV2StandalonePanel.cs | 8 +-- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 6 +-- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 4 +- .../{BeatmapPanel.cs => PanelBeatmap.cs} | 4 +- ...{BeatmapSetPanel.cs => PanelBeatmapSet.cs} | 4 +- ...lonePanel.cs => PanelBeatmapStandalone.cs} | 4 +- .../SelectV2/{GroupPanel.cs => PanelGroup.cs} | 2 +- ...oupPanel.cs => PanelGroupStarDificulty.cs} | 2 +- 15 files changed, 90 insertions(+), 90 deletions(-) rename osu.Game/Screens/SelectV2/{BeatmapPanel.cs => PanelBeatmap.cs} (98%) rename osu.Game/Screens/SelectV2/{BeatmapSetPanel.cs => PanelBeatmapSet.cs} (98%) rename osu.Game/Screens/SelectV2/{BeatmapStandalonePanel.cs => PanelBeatmapStandalone.cs} (99%) rename osu.Game/Screens/SelectV2/{GroupPanel.cs => PanelGroup.cs} (98%) rename osu.Game/Screens/SelectV2/{StarsGroupPanel.cs => PanelGroupStarDificulty.cs} (98%) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs index d3eeee151a..c378871eac 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs @@ -28,42 +28,42 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestOpenCloseGroupWithNoSelectionMouse() { - AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); - AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); CheckNoSelection(); - ClickVisiblePanel(0); + ClickVisiblePanel(0); - AddUntilStep("some sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); - AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("some sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); CheckNoSelection(); - ClickVisiblePanel(0); + ClickVisiblePanel(0); - AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); - AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); CheckNoSelection(); } [Test] public void TestOpenCloseGroupWithNoSelectionKeyboard() { - AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); - AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); CheckNoSelection(); SelectNextPanel(); Select(); - AddUntilStep("some sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); - AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("some sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); AddAssert("keyboard selected is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); CheckNoSelection(); Select(); - AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); - AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); AddAssert("keyboard selected is collapsed", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); CheckNoSelection(); } @@ -96,10 +96,10 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); - ClickVisiblePanel(0); + ClickVisiblePanel(0); AddUntilStep("carousel item not visible", GetSelectedPanel, () => Is.Null); - ClickVisiblePanel(0); + ClickVisiblePanel(0); AddUntilStep("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); } @@ -137,7 +137,7 @@ namespace osu.Game.Tests.Visual.SongSelect // open first group Select(); CheckNoSelection(); - AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); SelectNextPanel(); Select(); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs index c043fd87a9..f3c1634cb2 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs @@ -29,32 +29,32 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestOpenCloseGroupWithNoSelectionMouse() { - AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); CheckNoSelection(); - ClickVisiblePanel(0); - AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + ClickVisiblePanel(0); + AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); CheckNoSelection(); - ClickVisiblePanel(0); - AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + ClickVisiblePanel(0); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); CheckNoSelection(); } [Test] public void TestOpenCloseGroupWithNoSelectionKeyboard() { - AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); CheckNoSelection(); SelectNextPanel(); Select(); - AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); AddAssert("keyboard selected is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); CheckNoSelection(); Select(); - AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); AddAssert("keyboard selected is collapsed", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); CheckNoSelection(); } @@ -87,10 +87,10 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); - ClickVisiblePanel(0); + ClickVisiblePanel(0); AddUntilStep("carousel item not visible", GetSelectedPanel, () => Is.Null); - ClickVisiblePanel(0); + ClickVisiblePanel(0); AddUntilStep("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); } @@ -120,18 +120,18 @@ namespace osu.Game.Tests.Visual.SongSelect SelectNextGroup(); WaitForGroupSelection(0, 0); - AddAssert("keyboard selected panel is beatmap", GetKeyboardSelectedPanel, Is.TypeOf); - AddAssert("selected panel is beatmap", GetSelectedPanel, Is.TypeOf); + AddAssert("keyboard selected panel is beatmap", GetKeyboardSelectedPanel, Is.TypeOf); + AddAssert("selected panel is beatmap", GetSelectedPanel, Is.TypeOf); - ClickVisiblePanel(0); - AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf); + ClickVisiblePanel(0); + AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf); AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); - ClickVisiblePanel(0); - AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf); + ClickVisiblePanel(0); + AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf); AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); - AddAssert("selected panel is still beatmap", GetSelectedPanel, Is.TypeOf); + AddAssert("selected panel is still beatmap", GetSelectedPanel, Is.TypeOf); } [Test] @@ -146,7 +146,7 @@ namespace osu.Game.Tests.Visual.SongSelect // open first group Select(); CheckNoSelection(); - AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); SelectNextPanel(); Select(); @@ -171,23 +171,23 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestInputHandlingWithinGaps() { - AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); + AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); // Clicks just above the first group panel should not actuate any action. - ClickVisiblePanelWithOffset(0, new Vector2(0, -(GroupPanel.HEIGHT / 2 + 1))); + ClickVisiblePanelWithOffset(0, new Vector2(0, -(PanelGroup.HEIGHT / 2 + 1))); - AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); + AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); - ClickVisiblePanelWithOffset(0, new Vector2(0, -(GroupPanel.HEIGHT / 2))); + ClickVisiblePanelWithOffset(0, new Vector2(0, -(PanelGroup.HEIGHT / 2))); - AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels().Any()); + AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels().Any()); CheckNoSelection(); // Beatmap panels expand their selection area to cover holes from spacing. - ClickVisiblePanelWithOffset(0, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + ClickVisiblePanelWithOffset(0, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1))); WaitForGroupSelection(0, 0); - ClickVisiblePanelWithOffset(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + ClickVisiblePanelWithOffset(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); WaitForGroupSelection(0, 1); } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs index 09ded342c3..b4048a5355 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs @@ -213,27 +213,27 @@ namespace osu.Game.Tests.Visual.SongSelect AddBeatmaps(2, 5); WaitForDrawablePanels(); - AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); + AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); // Clicks just above the first group panel should not actuate any action. - ClickVisiblePanelWithOffset(0, new Vector2(0, -(BeatmapSetPanel.HEIGHT / 2 + 1))); + ClickVisiblePanelWithOffset(0, new Vector2(0, -(PanelBeatmapSet.HEIGHT / 2 + 1))); - AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); + AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); - ClickVisiblePanelWithOffset(0, new Vector2(0, -(BeatmapSetPanel.HEIGHT / 2))); + ClickVisiblePanelWithOffset(0, new Vector2(0, -(PanelBeatmapSet.HEIGHT / 2))); - AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels().Any()); + AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels().Any()); WaitForSelection(0, 0); // Beatmap panels expand their selection area to cover holes from spacing. - ClickVisiblePanelWithOffset(1, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + ClickVisiblePanelWithOffset(1, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1))); WaitForSelection(0, 0); // Panels with higher depth will handle clicks in the gutters for simplicity. - ClickVisiblePanelWithOffset(2, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + ClickVisiblePanelWithOffset(2, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); WaitForSelection(0, 2); - ClickVisiblePanelWithOffset(3, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + ClickVisiblePanelWithOffset(3, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); WaitForSelection(0, 3); } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs index ee6c11595a..890e1dd6e3 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs @@ -30,16 +30,16 @@ namespace osu.Game.Tests.Visual.SongSelect Quad positionBefore = default; AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2).Beatmaps.First()); - AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType().Single(p => p.Selected.Value))); + AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType().Single(p => p.Selected.Value))); WaitForScrolling(); - AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); + AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); RemoveFirstBeatmap(); WaitForSorting(); - AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, + AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, () => Is.EqualTo(positionBefore)); } @@ -54,11 +54,11 @@ namespace osu.Game.Tests.Visual.SongSelect WaitForScrolling(); - AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); + AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); RemoveFirstBeatmap(); WaitForSorting(); - AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, + AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, () => Is.EqualTo(positionBefore)); } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs index f843c2cded..1947721d5d 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs @@ -78,21 +78,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Spacing = new Vector2(0f, 5f), Children = new Drawable[] { - new BeatmapPanel + new PanelBeatmap { Item = new CarouselItem(beatmap) }, - new BeatmapPanel + new PanelBeatmap { Item = new CarouselItem(beatmap), KeyboardSelected = { Value = true } }, - new BeatmapPanel + new PanelBeatmap { Item = new CarouselItem(beatmap), Selected = { Value = true } }, - new BeatmapPanel + new PanelBeatmap { Item = new CarouselItem(beatmap), KeyboardSelected = { Value = true }, diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs index 5c94addc74..711a3b881d 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs @@ -29,49 +29,49 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Spacing = new Vector2(0f, 5f), Children = new Drawable[] { - new GroupPanel + new PanelGroup { Item = new CarouselItem(new GroupDefinition('A', "Group A")) }, - new GroupPanel + new PanelGroup { Item = new CarouselItem(new GroupDefinition('A', "Group A")), KeyboardSelected = { Value = true } }, - new GroupPanel + new PanelGroup { Item = new CarouselItem(new GroupDefinition('A', "Group A")), Expanded = { Value = true } }, - new GroupPanel + new PanelGroup { Item = new CarouselItem(new GroupDefinition('A', "Group A")), KeyboardSelected = { Value = true }, Expanded = { Value = true } }, - new StarsGroupPanel + new PanelGroupStarDificulty { Item = new CarouselItem(new GroupDefinition(1, "1")) }, - new StarsGroupPanel + new PanelGroupStarDificulty { Item = new CarouselItem(new GroupDefinition(3, "3")), Expanded = { Value = true } }, - new StarsGroupPanel + new PanelGroupStarDificulty { Item = new CarouselItem(new GroupDefinition(5, "5")), }, - new StarsGroupPanel + new PanelGroupStarDificulty { Item = new CarouselItem(new GroupDefinition(7, "7")), Expanded = { Value = true } }, - new StarsGroupPanel + new PanelGroupStarDificulty { Item = new CarouselItem(new GroupDefinition(8, "8")), }, - new StarsGroupPanel + new PanelGroupStarDificulty { Item = new CarouselItem(new GroupDefinition(9, "9")), Expanded = { Value = true } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs index 382357b67e..ef34394e12 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs @@ -68,21 +68,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Spacing = new Vector2(0f, 5f), Children = new Drawable[] { - new BeatmapSetPanel + new PanelBeatmapSet { Item = new CarouselItem(beatmapSet) }, - new BeatmapSetPanel + new PanelBeatmapSet { Item = new CarouselItem(beatmapSet), KeyboardSelected = { Value = true } }, - new BeatmapSetPanel + new PanelBeatmapSet { Item = new CarouselItem(beatmapSet), Expanded = { Value = true } }, - new BeatmapSetPanel + new PanelBeatmapSet { Item = new CarouselItem(beatmapSet), KeyboardSelected = { Value = true }, diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs index 41eb5c3683..2dbe9e6cd1 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs @@ -78,21 +78,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Spacing = new Vector2(0f, 5f), Children = new Drawable[] { - new BeatmapStandalonePanel + new PanelBeatmapStandalone { Item = new CarouselItem(beatmap) }, - new BeatmapStandalonePanel + new PanelBeatmapStandalone { Item = new CarouselItem(beatmap), KeyboardSelected = { Value = true } }, - new BeatmapStandalonePanel + new PanelBeatmapStandalone { Item = new CarouselItem(beatmap), Selected = { Value = true } }, - new BeatmapStandalonePanel + new PanelBeatmapStandalone { Item = new CarouselItem(beatmap), KeyboardSelected = { Value = true }, diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 5ae227f86c..c6bce228dc 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -267,9 +267,9 @@ namespace osu.Game.Screens.SelectV2 #region Drawable pooling - private readonly DrawablePool beatmapPanelPool = new DrawablePool(100); - private readonly DrawablePool setPanelPool = new DrawablePool(100); - private readonly DrawablePool groupPanelPool = new DrawablePool(100); + private readonly DrawablePool beatmapPanelPool = new DrawablePool(100); + private readonly DrawablePool setPanelPool = new DrawablePool(100); + private readonly DrawablePool groupPanelPool = new DrawablePool(100); private void setupPools() { diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index cb5a40918c..8f9d5cc31b 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -70,7 +70,7 @@ namespace osu.Game.Screens.SelectV2 addItem(new CarouselItem(newGroup) { - DrawHeight = GroupPanel.HEIGHT, + DrawHeight = PanelGroup.HEIGHT, DepthLayer = -2, }); } @@ -85,7 +85,7 @@ namespace osu.Game.Screens.SelectV2 addItem(new CarouselItem(beatmap.BeatmapSet!) { - DrawHeight = BeatmapSetPanel.HEIGHT, + DrawHeight = PanelBeatmapSet.HEIGHT, DepthLayer = -1 }); } diff --git a/osu.Game/Screens/SelectV2/BeatmapPanel.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs similarity index 98% rename from osu.Game/Screens/SelectV2/BeatmapPanel.cs rename to osu.Game/Screens/SelectV2/PanelBeatmap.cs index 3db60876a1..93ef814f2e 100644 --- a/osu.Game/Screens/SelectV2/BeatmapPanel.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -23,11 +23,11 @@ using osuTK; namespace osu.Game.Screens.SelectV2 { - public partial class BeatmapPanel : PoolableDrawable, ICarouselPanel + public partial class PanelBeatmap : PoolableDrawable, ICarouselPanel { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; - // todo: this should be replaced with information from CarouselItem about how deep is BeatmapPanel in the carousel + // todo: this should be replaced with information from CarouselItem about how deep is PanelBeatmap in the carousel // (i.e. whether it's under a beatmap set that's under a group, or just under a top-level beatmap set). private const float difficulty_x_offset = 100f; // constant X offset for beatmap difficulty panels specifically. diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs similarity index 98% rename from osu.Game/Screens/SelectV2/BeatmapSetPanel.cs rename to osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index 6caabb79c3..2904cda9de 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -19,11 +19,11 @@ using osuTK; namespace osu.Game.Screens.SelectV2 { - public partial class BeatmapSetPanel : PoolableDrawable, ICarouselPanel + public partial class PanelBeatmapSet : PoolableDrawable, ICarouselPanel { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; - // todo: this should be replaced with information from CarouselItem about how deep is BeatmapPanel in the carousel + // todo: this should be replaced with information from CarouselItem about how deep is PanelBeatmap in the carousel // (i.e. whether it's under a beatmap set that's under a group, or just under a top-level beatmap set). private const float set_x_offset = 20f; // constant X offset for beatmap set/standalone panels specifically. diff --git a/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs similarity index 99% rename from osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs rename to osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs index e8628d5b78..c858e039ec 100644 --- a/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs @@ -25,11 +25,11 @@ using osuTK; namespace osu.Game.Screens.SelectV2 { - public partial class BeatmapStandalonePanel : PoolableDrawable, ICarouselPanel + public partial class PanelBeatmapStandalone : PoolableDrawable, ICarouselPanel { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; - // todo: this should be replaced with information from CarouselItem about how deep is BeatmapPanel in the carousel + // todo: this should be replaced with information from CarouselItem about how deep is PanelBeatmap in the carousel // (i.e. whether it's under a beatmap set that's under a group, or just under a top-level beatmap set). private const float standalone_x_offset = 20f; // constant X offset for beatmap set/standalone panels specifically. diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/PanelGroup.cs similarity index 98% rename from osu.Game/Screens/SelectV2/GroupPanel.cs rename to osu.Game/Screens/SelectV2/PanelGroup.cs index 506a230cb4..cdd0695147 100644 --- a/osu.Game/Screens/SelectV2/GroupPanel.cs +++ b/osu.Game/Screens/SelectV2/PanelGroup.cs @@ -18,7 +18,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public partial class GroupPanel : PoolableDrawable, ICarouselPanel + public partial class PanelGroup : PoolableDrawable, ICarouselPanel { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; diff --git a/osu.Game/Screens/SelectV2/StarsGroupPanel.cs b/osu.Game/Screens/SelectV2/PanelGroupStarDificulty.cs similarity index 98% rename from osu.Game/Screens/SelectV2/StarsGroupPanel.cs rename to osu.Game/Screens/SelectV2/PanelGroupStarDificulty.cs index 7e2647ccbf..2215e643bd 100644 --- a/osu.Game/Screens/SelectV2/StarsGroupPanel.cs +++ b/osu.Game/Screens/SelectV2/PanelGroupStarDificulty.cs @@ -22,7 +22,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public partial class StarsGroupPanel : PoolableDrawable, ICarouselPanel + public partial class PanelGroupStarDificulty : PoolableDrawable, ICarouselPanel { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; From 068a66e7d4c9bef92ea39e5237b37bc628e9e14f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 12 Feb 2025 18:35:35 +0900 Subject: [PATCH 456/801] Move room tracking to lounge subscreen --- .../TestSceneLoungeRoomsContainer.cs | 28 ++++----- .../TestScenePlaylistsLoungeSubScreen.cs | 30 ++++----- .../Components/ListingPollingComponent.cs | 38 ++---------- .../Components/SelectionPollingComponent.cs | 5 +- .../Lounge/Components/RoomsContainer.cs | 45 ++++---------- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 62 ++++++++++++++----- .../Multiplayer/MultiplayerLoungeSubScreen.cs | 34 ---------- .../Playlists/PlaylistsLoungeSubScreen.cs | 3 - 8 files changed, 95 insertions(+), 150 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index 797b69ec72..10df77f88c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -50,17 +50,17 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add rooms", () => RoomManager.AddRooms(5, withSpotlightRooms: true)); - AddAssert("has 5 rooms", () => container.Rooms.Count == 5); + AddAssert("has 5 rooms", () => container.DrawableRooms.Count == 5); - AddAssert("all spotlights at top", () => container.Rooms + AddAssert("all spotlights at top", () => container.DrawableRooms .SkipWhile(r => r.Room.Category == RoomCategory.Spotlight) .All(r => r.Room.Category == RoomCategory.Normal)); AddStep("remove first room", () => RoomManager.RemoveRoom(RoomManager.Rooms.First(r => r.RoomID == 0))); - AddAssert("has 4 rooms", () => container.Rooms.Count == 4); - AddAssert("first room removed", () => container.Rooms.All(r => r.Room.RoomID != 0)); + AddAssert("has 4 rooms", () => container.DrawableRooms.Count == 4); + AddAssert("first room removed", () => container.DrawableRooms.All(r => r.Room.RoomID != 0)); - AddStep("select first room", () => container.Rooms.First().TriggerClick()); + AddStep("select first room", () => container.DrawableRooms.First().TriggerClick()); AddAssert("first spotlight selected", () => checkRoomSelected(RoomManager.Rooms.First(r => r.Category == RoomCategory.Spotlight))); AddStep("remove last room", () => RoomManager.RemoveRoom(RoomManager.Rooms.MinBy(r => r.RoomID)!)); @@ -137,15 +137,15 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add rooms", () => RoomManager.AddRooms(4)); - AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4); + AddUntilStep("4 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 4); AddStep("filter one room", () => container.Filter.Value = new FilterCriteria { SearchString = "1" }); - AddUntilStep("1 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 1); + AddUntilStep("1 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 1); AddStep("remove filter", () => container.Filter.Value = null); - AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4); + AddUntilStep("4 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 4); } [Test] @@ -156,13 +156,13 @@ namespace osu.Game.Tests.Visual.Multiplayer // Todo: What even is this case...? AddStep("set empty filter criteria", () => container.Filter.Value = new FilterCriteria()); - AddUntilStep("5 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 5); + AddUntilStep("5 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 5); AddStep("filter osu! rooms", () => container.Filter.Value = new FilterCriteria { Ruleset = new OsuRuleset().RulesetInfo }); - AddUntilStep("2 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 2); + AddUntilStep("2 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 2); AddStep("filter catch rooms", () => container.Filter.Value = new FilterCriteria { Ruleset = new CatchRuleset().RulesetInfo }); - AddUntilStep("3 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 3); + AddUntilStep("3 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 3); } [Test] @@ -176,15 +176,15 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("apply default filter", () => container.Filter.SetDefault()); - AddUntilStep("both rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 2); + AddUntilStep("both rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 2); AddStep("filter public rooms", () => container.Filter.Value = new FilterCriteria { Permissions = RoomPermissionsFilter.Public }); - AddUntilStep("private room hidden", () => container.Rooms.All(r => !r.Room.HasPassword)); + AddUntilStep("private room hidden", () => container.DrawableRooms.All(r => !r.Room.HasPassword)); AddStep("filter private rooms", () => container.Filter.Value = new FilterCriteria { Permissions = RoomPermissionsFilter.Private }); - AddUntilStep("public room hidden", () => container.Rooms.All(r => r.Room.HasPassword)); + AddUntilStep("public room hidden", () => container.DrawableRooms.All(r => r.Room.HasPassword)); } [Test] diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs index 53c7873de5..9d65be2a19 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs @@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Playlists public void TestManyRooms() { AddStep("add rooms", () => RoomManager.AddRooms(500)); - AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 500); + AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 500); } [Test] @@ -45,45 +45,45 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("reset mouse", () => InputManager.ReleaseButton(MouseButton.Left)); AddStep("add rooms", () => RoomManager.AddRooms(30)); - AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30); + AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 30); - AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0])); + AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.DrawableRooms[0])); - AddStep("move mouse to third room", () => InputManager.MoveMouseTo(roomsContainer.Rooms[2])); + AddStep("move mouse to third room", () => InputManager.MoveMouseTo(roomsContainer.DrawableRooms[2])); AddStep("hold down", () => InputManager.PressButton(MouseButton.Left)); - AddStep("drag to top", () => InputManager.MoveMouseTo(roomsContainer.Rooms[0])); + AddStep("drag to top", () => InputManager.MoveMouseTo(roomsContainer.DrawableRooms[0])); AddAssert("first and second room masked", () - => !checkRoomVisible(roomsContainer.Rooms[0]) && - !checkRoomVisible(roomsContainer.Rooms[1])); + => !checkRoomVisible(roomsContainer.DrawableRooms[0]) && + !checkRoomVisible(roomsContainer.DrawableRooms[1])); } [Test] public void TestScrollSelectedIntoView() { AddStep("add rooms", () => RoomManager.AddRooms(30)); - AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30); + AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 30); - AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0])); + AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.DrawableRooms[0])); - AddStep("select last room", () => roomsContainer.Rooms[^1].TriggerClick()); + AddStep("select last room", () => roomsContainer.DrawableRooms[^1].TriggerClick()); - AddUntilStep("first room is masked", () => !checkRoomVisible(roomsContainer.Rooms[0])); - AddUntilStep("last room is not masked", () => checkRoomVisible(roomsContainer.Rooms[^1])); + AddUntilStep("first room is masked", () => !checkRoomVisible(roomsContainer.DrawableRooms[0])); + AddUntilStep("last room is not masked", () => checkRoomVisible(roomsContainer.DrawableRooms[^1])); } [Test] public void TestEnteringRoomTakesLeaseOnSelection() { AddStep("add rooms", () => RoomManager.AddRooms(1)); - AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 1); + AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 1); AddAssert("selected room is not disabled", () => !loungeScreen.SelectedRoom.Disabled); - AddStep("select room", () => roomsContainer.Rooms[0].TriggerClick()); + AddStep("select room", () => roomsContainer.DrawableRooms[0].TriggerClick()); AddAssert("selected room is non-null", () => loungeScreen.SelectedRoom.Value != null); - AddStep("enter room", () => roomsContainer.Rooms[0].TriggerClick()); + AddStep("enter room", () => roomsContainer.DrawableRooms[0].TriggerClick()); AddUntilStep("wait for match load", () => Stack.CurrentScreen is PlaylistsRoomSubScreen); diff --git a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs index 21452727b8..5cb4c9420a 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs @@ -1,9 +1,9 @@ // 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 System.Threading.Tasks; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Lounge.Components; @@ -15,23 +15,8 @@ namespace osu.Game.Screens.OnlinePlay.Components /// public partial class ListingPollingComponent : RoomPollingComponent { - public IBindable InitialRoomsReceived => initialRoomsReceived; - private readonly Bindable initialRoomsReceived = new Bindable(); - - public readonly Bindable Filter = new Bindable(); - - [BackgroundDependencyLoader] - private void load() - { - Filter.BindValueChanged(_ => - { - RoomManager.ClearRooms(); - initialRoomsReceived.Value = false; - - if (IsLoaded) - PollImmediately(); - }); - } + public required Action RoomsReceived { get; init; } + public readonly IBindable Filter = new Bindable(); private GetRoomsRequest? lastPollRequest; @@ -43,26 +28,14 @@ namespace osu.Game.Screens.OnlinePlay.Components if (Filter.Value == null) return base.Poll(); - var tcs = new TaskCompletionSource(); - lastPollRequest?.Cancel(); + var tcs = new TaskCompletionSource(); var req = new GetRoomsRequest(Filter.Value); req.Success += result => { - result = result.Where(r => r.Category != RoomCategory.DailyChallenge).ToList(); - - foreach (var existing in RoomManager.Rooms.ToArray()) - { - if (result.All(r => r.RoomID != existing.RoomID)) - RoomManager.RemoveRoom(existing); - } - - foreach (var incoming in result) - RoomManager.AddOrUpdateRoom(incoming); - - initialRoomsReceived.Value = true; + RoomsReceived(result.Where(r => r.Category != RoomCategory.DailyChallenge).ToArray()); tcs.SetResult(true); }; @@ -71,6 +44,7 @@ namespace osu.Game.Screens.OnlinePlay.Components API.Queue(req); lastPollRequest = req; + return tcs.Task; } } diff --git a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs index 7cee8b3546..f04fd6a096 100644 --- a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs @@ -28,15 +28,14 @@ namespace osu.Game.Screens.OnlinePlay.Components if (room.RoomID == null) return base.Poll(); - var tcs = new TaskCompletionSource(); - lastPollRequest?.Cancel(); + var tcs = new TaskCompletionSource(); var req = new GetRoomRequest(room.RoomID.Value); req.Success += result => { - RoomManager.AddOrUpdateRoom(result); + room.CopyFrom(result); tcs.SetResult(true); }; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index 6eda993f94..6681cbe720 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -7,10 +7,8 @@ using System.Collections.Specialized; using System.Diagnostics; using System.Globalization; using System.Linq; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; @@ -24,17 +22,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { public partial class RoomsContainer : CompositeDrawable, IKeyBindingHandler { + public readonly BindableList Rooms = new BindableList(); public readonly Bindable SelectedRoom = new Bindable(); public readonly Bindable Filter = new Bindable(); - public IReadOnlyList Rooms => roomFlow.FlowingChildren.Cast().ToArray(); + public IReadOnlyList DrawableRooms => roomFlow.FlowingChildren.Cast().ToArray(); - private readonly IBindableList rooms = new BindableList(); private readonly FillFlowContainer roomFlow; - [Resolved] - private IRoomManager roomManager { get; set; } = null!; - // handle deselection public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; @@ -62,11 +57,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components protected override void LoadComplete() { - rooms.CollectionChanged += roomsChanged; - roomManager.RoomsUpdated += updateSorting; - - rooms.BindTo(roomManager.Rooms); - + Rooms.BindCollectionChanged(roomsChanged, true); Filter.BindValueChanged(criteria => applyFilterCriteria(criteria.NewValue), true); } @@ -155,7 +146,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private void addRooms(IEnumerable rooms) { foreach (var room in rooms) - roomFlow.Add(new DrawableLoungeRoom(room) { SelectedRoom = SelectedRoom }); + { + var drawableRoom = new DrawableLoungeRoom(room) { SelectedRoom = SelectedRoom }; + + roomFlow.Add(drawableRoom); + + // Always show spotlight playlists at the top of the listing. + roomFlow.SetLayoutPosition(drawableRoom, room.Category > RoomCategory.Normal ? float.MinValue : -(room.RoomID ?? 0)); + } applyFilterCriteria(Filter.Value); } @@ -181,17 +179,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components SelectedRoom.Value = null; } - private void updateSorting() - { - foreach (var room in roomFlow) - { - roomFlow.SetLayoutPosition(room, room.Room.Category > RoomCategory.Normal - // Always show spotlight playlists at the top of the listing. - ? float.MinValue - : -(room.Room.RoomID ?? 0)); - } - } - protected override bool OnClick(ClickEvent e) { if (!SelectedRoom.Disabled) @@ -226,7 +213,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components if (SelectedRoom.Disabled) return; - var visibleRooms = Rooms.AsEnumerable().Where(r => r.IsPresent); + var visibleRooms = DrawableRooms.AsEnumerable().Where(r => r.IsPresent); Room? room; @@ -246,13 +233,5 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } #endregion - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (roomManager.IsNotNull()) - roomManager.RoomsUpdated -= updateSorting; - } } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 0e08e398a4..78501a56d7 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -53,8 +53,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge AutoSizeAxes = Axes.Both }; - protected ListingPollingComponent ListingPollingComponent { get; private set; } = null!; - protected readonly Bindable SelectedRoom = new Bindable(); [Resolved] @@ -75,12 +73,15 @@ namespace osu.Game.Screens.OnlinePlay.Lounge [Resolved] protected OsuConfigManager Config { get; private set; } = null!; - private IDisposable? joiningRoomOperation { get; set; } + private IDisposable? joiningRoomOperation; private LeasedBindable? selectionLease; + private readonly BindableList rooms = new BindableList(); private readonly Bindable filter = new Bindable(); + private readonly Bindable hasListingResults = new Bindable(); private readonly IBindable operationInProgress = new Bindable(); private readonly IBindable isIdle = new BindableBool(); + private ListingPollingComponent listingPollingComponent = null!; private PopoverContainer popoverContainer = null!; private LoadingLayer loadingLayer = null!; private RoomsContainer roomsContainer = null!; @@ -100,7 +101,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge InternalChildren = new Drawable[] { - ListingPollingComponent = CreatePollingComponent().With(c => c.Filter.BindTarget = filter), + listingPollingComponent = new ListingPollingComponent + { + RoomsReceived = onListingReceived, + Filter = { BindTarget = filter } + }, popoverContainer = new PopoverContainer { Name = @"Rooms area", @@ -116,8 +121,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge ScrollbarOverlapsContent = false, Child = roomsContainer = new RoomsContainer { + Rooms = { BindTarget = rooms }, + SelectedRoom = { BindTarget = SelectedRoom }, Filter = { BindTarget = filter }, - SelectedRoom = { BindTarget = SelectedRoom } } }, }, @@ -178,7 +184,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge // scroll selected room into view on selection. SelectedRoom.BindValueChanged(val => { - var drawable = roomsContainer.Rooms.FirstOrDefault(r => r.Room == val.NewValue); + var drawable = roomsContainer.DrawableRooms.FirstOrDefault(r => r.Room == val.NewValue); if (drawable != null) scrollContainer.ScrollIntoView(drawable); }); @@ -190,7 +196,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge searchTextBox.Current.BindValueChanged(_ => updateFilterDebounced()); ruleset.BindValueChanged(_ => UpdateFilter()); - isIdle.BindValueChanged(_ => updatePollingRate(this.IsCurrentScreen()), true); if (ongoingOperationTracker != null) @@ -199,11 +204,38 @@ namespace osu.Game.Screens.OnlinePlay.Lounge operationInProgress.BindValueChanged(_ => updateLoadingLayer()); } - ListingPollingComponent.InitialRoomsReceived.BindValueChanged(_ => updateLoadingLayer(), true); + hasListingResults.BindValueChanged(_ => updateLoadingLayer()); + + filter.BindValueChanged(_ => + { + rooms.Clear(); + hasListingResults.Value = false; + listingPollingComponent.PollImmediately(); + }); updateFilter(); } + private void onListingReceived(Room[] result) + { + Dictionary localRoomsById = rooms.ToDictionary(r => r.RoomID!.Value); + Dictionary resultRoomsById = result.ToDictionary(r => r.RoomID!.Value); + + // Remove all local rooms no longer in the result set. + rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value)); + + // Add or update local rooms with the result set. + foreach (var r in result) + { + if (localRoomsById.TryGetValue(r.RoomID!.Value, out Room? existingRoom)) + existingRoom.CopyFrom(r); + else + rooms.Add(r); + } + + hasListingResults.Value = true; + } + #region Filtering public void UpdateFilter() => Scheduler.AddOnce(updateFilter); @@ -267,7 +299,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge onReturning(); // Poll for any newly-created rooms (including potentially the user's own). - ListingPollingComponent.PollImmediately(); + listingPollingComponent.PollImmediately(); } public override bool OnExiting(ScreenExitEvent e) @@ -392,11 +424,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge this.Push(CreateRoomSubScreen(room)); } - public void RefreshRooms() => ListingPollingComponent.PollImmediately(); + public void RefreshRooms() => listingPollingComponent.PollImmediately(); private void updateLoadingLayer() { - if (operationInProgress.Value || !ListingPollingComponent.InitialRoomsReceived.Value) + if (operationInProgress.Value || !hasListingResults.Value) loadingLayer.Show(); else loadingLayer.Hide(); @@ -405,11 +437,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private void updatePollingRate(bool isCurrentScreen) { if (!isCurrentScreen) - ListingPollingComponent.TimeBetweenPolls.Value = 0; + listingPollingComponent.TimeBetweenPolls.Value = 0; else - ListingPollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 120000 : 15000; + listingPollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 120000 : 15000; - Logger.Log($"Polling adjusted (listing: {ListingPollingComponent.TimeBetweenPolls.Value})"); + Logger.Log($"Polling adjusted (listing: {listingPollingComponent.TimeBetweenPolls.Value})"); } protected abstract OsuButton CreateNewRoomButton(); @@ -421,7 +453,5 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected abstract Room CreateNewRoom(); protected abstract RoomSubScreen CreateRoomSubScreen(Room room); - - protected abstract ListingPollingComponent CreatePollingComponent(); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index 873a9cde88..3cf873ec78 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -79,8 +79,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override RoomSubScreen CreateRoomSubScreen(Room room) => new MultiplayerMatchSubScreen(room); - protected override ListingPollingComponent CreatePollingComponent() => new MultiplayerListingPollingComponent(); - protected override void JoinInternal(Room room, string? password, Action onSuccess, Action onFailure) { client.JoinRoom(room, password).ContinueWith(result => @@ -109,37 +107,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer base.OpenNewRoom(room); } - - private partial class MultiplayerListingPollingComponent : ListingPollingComponent - { - [Resolved] - private MultiplayerClient client { get; set; } = null!; - - private readonly IBindable isConnected = new Bindable(); - - [BackgroundDependencyLoader] - private void load() - { - isConnected.BindTo(client.IsConnected); - isConnected.BindValueChanged(_ => Scheduler.AddOnce(poll), true); - } - - private void poll() - { - if (isConnected.Value && IsLoaded) - PollImmediately(); - } - - protected override Task Poll() - { - if (!isConnected.Value) - return Task.CompletedTask; - - if (client.Room != null) - return Task.CompletedTask; - - return base.Poll(); - } - } } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs index 6ed367328c..26eae50797 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Match; @@ -87,8 +86,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override RoomSubScreen CreateRoomSubScreen(Room room) => new PlaylistsRoomSubScreen(room); - protected override ListingPollingComponent CreatePollingComponent() => new ListingPollingComponent(); - private enum PlaylistsCategory { Any, From 96db6964df2e1045eacedebae3bfdf95eb250983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Feb 2025 10:55:57 +0100 Subject: [PATCH 457/801] Adjust things further --- .../Skinning/Legacy/LegacySwell.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index d3b5d54828..5d65ac6058 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -15,11 +15,14 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Extensions.ObjectExtensions; using System; using System.Globalization; +using osu.Framework.Utils; namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { public partial class LegacySwell : Container { + private const float scale_adjust = 768f / 480; + private DrawableSwell drawableSwell = null!; private Container bodyContainer = null!; @@ -80,12 +83,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(1.86f * 0.8f), + Alpha = 0.8f, }, - remainingHitsText = new LegacySpriteText(LegacyFont.Combo) + remainingHitsText = new LegacySpriteText(LegacyFont.Score) { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Position = new Vector2(0f, 165f), + Position = new Vector2(0f, 130f), Scale = Vector2.One, }, } @@ -96,6 +100,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy Anchor = Anchor.Centre, Origin = Anchor.Centre, Alpha = 0, + Y = -40, }, }, }, @@ -159,11 +164,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } clearAnimation - .MoveTo(new Vector2(0, 0)) - .MoveTo(new Vector2(0, -90), clear_fade_in * 2, Easing.Out) + .MoveToOffset(new Vector2(0, -90 * scale_adjust), clear_fade_in * 2, Easing.Out) .ScaleTo(0.4f) .ScaleTo(1f, clear_fade_in * 2, Easing.Out) - .FadeIn(clear_fade_in) + .FadeIn() .Delay(clear_fade_in * 3) .FadeOut(clear_fade_in * 2.5); } From 0ac08158e33867092f76f94b1534ba3bc1ce962c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Feb 2025 11:20:27 +0100 Subject: [PATCH 458/801] Fix transforms from swell progress being cleared on completion by not using transforms --- .../Objects/Drawables/DrawableSwell.cs | 4 +-- .../Skinning/Default/DefaultSwell.cs | 26 +++++++++++------ .../Skinning/Legacy/LegacySwell.cs | 28 +++++++++++++------ 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 1dde4b6f9c..6ad14c87d1 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// public bool MustAlternate { get; internal set; } = true; - public event Action UpdateHitProgress; + public event Action UpdateHitProgress; public DrawableSwell() : this(null) @@ -125,7 +125,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables int numHits = ticks.Count(r => r.IsHit); - UpdateHitProgress?.Invoke(numHits, HitObject.RequiredHits); + UpdateHitProgress?.Invoke(numHits); if (numHits == HitObject.RequiredHits) ApplyMaxResult(); diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs index a588f866c6..ac72ba73b8 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs @@ -8,10 +8,12 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Skinning.Default @@ -29,6 +31,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default private readonly CircularContainer targetRing; private readonly CircularContainer expandingRing; private readonly Drawable centreCircle; + private int numHits; public DefaultSwell() { @@ -125,18 +128,25 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default }; } - private void animateSwellProgress(int numHits, int requiredHits) + private void animateSwellProgress(int numHits) { - float completion = (float)numHits / requiredHits; + this.numHits = numHits; - centreCircle.RotateTo((float)(completion * drawableSwell.HitObject.Duration / 8), 4000, Easing.OutQuint); + float completion = (float)numHits / drawableSwell.HitObject.RequiredHits; + expandingRing.Alpha += Math.Clamp(completion / 16, 0.1f, 0.6f); + } - expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); + protected override void Update() + { + base.Update(); - expandingRing - .FadeTo(expandingRing.Alpha + Math.Clamp(completion / 16, 0.1f, 0.6f), 50) - .Then() - .FadeTo(completion / 8, 2000, Easing.OutQuint); + float completion = (float)numHits / drawableSwell.HitObject.RequiredHits; + + centreCircle.Rotation = (float)Interpolation.DampContinuously(centreCircle.Rotation, + (float)(completion * drawableSwell.HitObject.Duration / 8), 500, Math.Abs(Time.Elapsed)); + expandingRing.Scale = new Vector2((float)Interpolation.DampContinuously(expandingRing.Scale.X, + 1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 35, Math.Abs(Time.Elapsed))); + expandingRing.Alpha = (float)Interpolation.DampContinuously(expandingRing.Alpha, completion / 16, 250, Math.Abs(Time.Elapsed)); } private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 5d65ac6058..62ccd05a06 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -35,6 +35,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private bool samplePlayed; + private int numHits; + public LegacySwell() { Anchor = Anchor.Centre; @@ -112,17 +114,25 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy clearSample = skin.GetSample(new SampleInfo("spinner-osu")); } - private void animateSwellProgress(int numHits, int requiredHits) + private void animateSwellProgress(int numHits) { - int remainingHits = requiredHits - numHits; - remainingHitsText.Text = remainingHits.ToString(CultureInfo.InvariantCulture); - remainingHitsText.ScaleTo(1.6f - (0.6f * ((float)remainingHits / requiredHits)), 60, Easing.Out); + this.numHits = numHits; + remainingHitsText.Text = (drawableSwell.HitObject.RequiredHits - numHits).ToString(CultureInfo.InvariantCulture); + spinnerCircle.Scale = new Vector2(Math.Min(0.94f, spinnerCircle.Scale.X + 0.02f)); + } - spinnerCircle.ClearTransforms(); - spinnerCircle - .RotateTo(180f * numHits, 1000, Easing.OutQuint) - .ScaleTo(Math.Min(0.94f, spinnerCircle.Scale.X + 0.02f)) - .ScaleTo(0.8f, 400, Easing.Out); + protected override void Update() + { + base.Update(); + + int requiredHits = drawableSwell.HitObject.RequiredHits; + int remainingHits = requiredHits - numHits; + remainingHitsText.Scale = new Vector2((float)Interpolation.DampContinuously( + remainingHitsText.Scale.X, 1.6f - (0.6f * ((float)remainingHits / requiredHits)), 17.5, Math.Abs(Time.Elapsed))); + + spinnerCircle.Rotation = (float)Interpolation.DampContinuously(spinnerCircle.Rotation, 180f * numHits, 130, Math.Abs(Time.Elapsed)); + spinnerCircle.Scale = new Vector2((float)Interpolation.DampContinuously( + spinnerCircle.Scale.X, 0.8f, 120, Math.Abs(Time.Elapsed))); } private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) From e385848edcbfbab7eaf0618a01ffb98aeed209d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Feb 2025 11:30:30 +0100 Subject: [PATCH 459/801] Add missing animation of warning sprite --- osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 62ccd05a06..c9e03d3508 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy public partial class LegacySwell : Container { private const float scale_adjust = 768f / 480; + private static readonly Vector2 swell_display_position = new Vector2(250f, 100f); private DrawableSwell drawableSwell = null!; @@ -60,7 +61,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Position = new Vector2(250f, 100f), // ballparked to be horizontally centred on 4:3 resolution + Position = swell_display_position, // ballparked to be horizontally centred on 4:3 resolution Children = new Drawable[] { @@ -152,7 +153,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy const double body_transition_duration = 200; - warning.FadeOut(body_transition_duration); + warning.MoveTo(swell_display_position, body_transition_duration) + .ScaleTo(3, body_transition_duration, Easing.Out) + .FadeOut(body_transition_duration); + bodyContainer.FadeIn(body_transition_duration); approachCircle.ResizeTo(0.1f * 0.8f, swell.Duration); } From d87a775e716801705b1de47cc4d2776770c348ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Feb 2025 13:19:55 +0100 Subject: [PATCH 460/801] Fix clear sample potentially playing multiple times simultaneously --- osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index c9e03d3508..9f1b692984 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -8,7 +8,6 @@ using osu.Game.Skinning; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects; -using osu.Framework.Audio.Sample; using osu.Game.Audio; using osuTK; using osu.Game.Rulesets.Objects.Drawables; @@ -31,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private Sprite spinnerCircle = null!; private Sprite approachCircle = null!; private Sprite clearAnimation = null!; - private ISample? clearSample; + private SkinnableSound clearSample = null!; private LegacySpriteText remainingHitsText = null!; private bool samplePlayed; @@ -107,12 +106,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy }, }, }, + clearSample = new SkinnableSound(new SampleInfo("spinner-osu")), }; drawableSwell = (DrawableSwell)hitObject; drawableSwell.UpdateHitProgress += animateSwellProgress; drawableSwell.ApplyCustomUpdateState += updateStateTransforms; - clearSample = skin.GetSample(new SampleInfo("spinner-osu")); } private void animateSwellProgress(int numHits) @@ -173,7 +172,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { if (!samplePlayed) { - clearSample?.Play(); + clearSample.Play(); samplePlayed = true; } From f146a7d116bbebec4880c8a3dd7124d20dc58022 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 12 Feb 2025 21:09:58 +0900 Subject: [PATCH 461/801] Remove `RoomManager` and related components --- .../TestSceneLoungeRoomsContainer.cs | 54 ++++++------ .../TestSceneMultiplayerLoungeSubScreen.cs | 33 +++++--- .../TestScenePlaylistsLoungeSubScreen.cs | 30 +++---- .../TestScenePlaylistsMatchSettingsOverlay.cs | 2 - .../Components/ListingPollingComponent.cs | 14 +++- .../OnlinePlay/Components/RoomManager.cs | 82 ------------------- .../Components/RoomPollingComponent.cs | 18 ---- .../Components/SelectionPollingComponent.cs | 14 +++- osu.Game/Screens/OnlinePlay/IRoomManager.cs | 42 ---------- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 12 +-- .../Screens/OnlinePlay/OnlinePlayScreen.cs | 5 -- .../IOnlinePlayTestSceneDependencies.cs | 5 -- .../Visual/OnlinePlay/OnlinePlayTestScene.cs | 32 +++++++- .../OnlinePlayTestSceneDependencies.cs | 3 - .../Visual/OnlinePlay/TestRoomManager.cs | 59 ------------- .../OnlinePlay/TestRoomRequestsHandler.cs | 3 +- 16 files changed, 121 insertions(+), 287 deletions(-) delete mode 100644 osu.Game/Screens/OnlinePlay/Components/RoomManager.cs delete mode 100644 osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs delete mode 100644 osu.Game/Screens/OnlinePlay/IRoomManager.cs delete mode 100644 osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index 10df77f88c..9daad960c7 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -3,6 +3,7 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -19,8 +20,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { public partial class TestSceneLoungeRoomsContainer : OnlinePlayTestScene { - protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; - + private BindableList rooms = null!; private RoomsContainer container = null!; public override void SetUpSteps() @@ -29,6 +29,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create container", () => { + rooms = new BindableList(); Child = new PopoverContainer { RelativeSizeAxes = Axes.X, @@ -36,9 +37,9 @@ namespace osu.Game.Tests.Visual.Multiplayer Anchor = Anchor.Centre, Origin = Anchor.Centre, Width = 0.5f, - Child = container = new RoomsContainer { + Rooms = { BindTarget = rooms }, SelectedRoom = { BindTarget = SelectedRoom } } }; @@ -48,7 +49,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestBasicListChanges() { - AddStep("add rooms", () => RoomManager.AddRooms(5, withSpotlightRooms: true)); + AddStep("add rooms", () => rooms.AddRange(GenerateRooms(5, withSpotlightRooms: true))); AddAssert("has 5 rooms", () => container.DrawableRooms.Count == 5); @@ -56,49 +57,50 @@ namespace osu.Game.Tests.Visual.Multiplayer .SkipWhile(r => r.Room.Category == RoomCategory.Spotlight) .All(r => r.Room.Category == RoomCategory.Normal)); - AddStep("remove first room", () => RoomManager.RemoveRoom(RoomManager.Rooms.First(r => r.RoomID == 0))); + AddStep("remove first room", () => rooms.RemoveAt(0)); AddAssert("has 4 rooms", () => container.DrawableRooms.Count == 4); AddAssert("first room removed", () => container.DrawableRooms.All(r => r.Room.RoomID != 0)); AddStep("select first room", () => container.DrawableRooms.First().TriggerClick()); - AddAssert("first spotlight selected", () => checkRoomSelected(RoomManager.Rooms.First(r => r.Category == RoomCategory.Spotlight))); + AddAssert("first spotlight selected", () => checkRoomSelected(rooms.First(r => r.Category == RoomCategory.Spotlight))); - AddStep("remove last room", () => RoomManager.RemoveRoom(RoomManager.Rooms.MinBy(r => r.RoomID)!)); - AddAssert("first spotlight still selected", () => checkRoomSelected(RoomManager.Rooms.First(r => r.Category == RoomCategory.Spotlight))); + AddStep("remove last room", () => rooms.RemoveAt(rooms.Count - 1)); + AddAssert("first spotlight still selected", () => checkRoomSelected(rooms.First(r => r.Category == RoomCategory.Spotlight))); - AddStep("remove spotlight room", () => RoomManager.RemoveRoom(RoomManager.Rooms.Single(r => r.Category == RoomCategory.Spotlight))); + AddStep("remove spotlight room", () => rooms.RemoveAll(r => r.Category == RoomCategory.Spotlight)); AddAssert("selection vacated", () => checkRoomSelected(null)); } [Test] public void TestKeyboardNavigation() { - AddStep("add rooms", () => RoomManager.AddRooms(3)); + AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3))); AddAssert("no selection", () => checkRoomSelected(null)); press(Key.Down); - AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First())); + AddAssert("first room selected", () => checkRoomSelected(container.DrawableRooms.First().Room)); press(Key.Up); - AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First())); + AddAssert("first room selected", () => checkRoomSelected(container.DrawableRooms.First().Room)); press(Key.Down); press(Key.Down); - AddAssert("last room selected", () => checkRoomSelected(RoomManager.Rooms.Last())); + AddAssert("last room selected", () => checkRoomSelected(container.DrawableRooms.Last().Room)); } [Test] public void TestKeyboardNavigationAfterOrderChange() { - AddStep("add rooms", () => RoomManager.AddRooms(3)); + AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3))); AddStep("reorder rooms", () => { - var room = RoomManager.Rooms[1]; + var room = rooms[1]; + rooms.Remove(room); - RoomManager.RemoveRoom(room); - RoomManager.AddOrUpdateRoom(room); + room.RoomID += 3; + rooms.Add(room); }); AddAssert("no selection", () => checkRoomSelected(null)); @@ -116,12 +118,12 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestClickDeselection() { - AddStep("add room", () => RoomManager.AddRooms(1)); + AddStep("add room", () => rooms.AddRange(GenerateRooms(1))); AddAssert("no selection", () => checkRoomSelected(null)); press(Key.Down); - AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First())); + AddAssert("first room selected", () => checkRoomSelected(container.DrawableRooms.First().Room)); AddStep("click away", () => InputManager.Click(MouseButton.Left)); AddAssert("no selection", () => checkRoomSelected(null)); @@ -135,11 +137,11 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestStringFiltering() { - AddStep("add rooms", () => RoomManager.AddRooms(4)); + AddStep("add rooms", () => rooms.AddRange(GenerateRooms(4))); AddUntilStep("4 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 4); - AddStep("filter one room", () => container.Filter.Value = new FilterCriteria { SearchString = "1" }); + AddStep("filter one room", () => container.Filter.Value = new FilterCriteria { SearchString = rooms.First().Name }); AddUntilStep("1 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 1); @@ -151,8 +153,8 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestRulesetFiltering() { - AddStep("add rooms", () => RoomManager.AddRooms(2, new OsuRuleset().RulesetInfo)); - AddStep("add rooms", () => RoomManager.AddRooms(3, new CatchRuleset().RulesetInfo)); + AddStep("add rooms", () => rooms.AddRange(GenerateRooms(2, new OsuRuleset().RulesetInfo))); + AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3, new CatchRuleset().RulesetInfo))); // Todo: What even is this case...? AddStep("set empty filter criteria", () => container.Filter.Value = new FilterCriteria()); @@ -170,8 +172,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add rooms", () => { - RoomManager.AddRooms(1, withPassword: true); - RoomManager.AddRooms(1, withPassword: false); + rooms.AddRange(GenerateRooms(1, withPassword: true)); + rooms.AddRange(GenerateRooms(1, withPassword: false)); }); AddStep("apply default filter", () => container.Filter.SetDefault()); @@ -190,7 +192,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestPasswordProtectedRooms() { - AddStep("add rooms", () => RoomManager.AddRooms(3, withPassword: true)); + AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3, withPassword: true))); } private bool checkRoomSelected(Room? room) => SelectedRoom.Value == room; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs index eb649acd2d..b4ec9d5858 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs @@ -8,8 +8,8 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Lounge; -using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Tests.Visual.OnlinePlay; using osuTK.Input; @@ -18,11 +18,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { public partial class TestSceneMultiplayerLoungeSubScreen : MultiplayerTestScene { - protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; - - private LoungeSubScreen loungeScreen = null!; - - private RoomsContainer roomsContainer => loungeScreen.ChildrenOfType().First(); + private MultiplayerLoungeSubScreen loungeScreen = null!; public TestSceneMultiplayerLoungeSubScreen() : base(false) @@ -40,7 +36,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestJoinRoomWithoutPassword() { - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: false)); + createRooms(GenerateRooms(1, withPassword: false)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("join room", () => InputManager.Key(Key.Enter)); @@ -50,7 +46,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestPopoverHidesOnBackButton() { - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + createRooms(GenerateRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); @@ -70,7 +66,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestPopoverHidesOnLeavingScreen() { - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + createRooms(GenerateRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); @@ -86,7 +82,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + createRooms(GenerateRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); @@ -105,7 +101,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + createRooms(GenerateRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); @@ -124,7 +120,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + createRooms(GenerateRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); @@ -139,7 +135,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + createRooms(GenerateRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); @@ -149,6 +145,17 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("room joined", () => MultiplayerClient.RoomJoined); } + private void createRooms(params Room[] rooms) + { + AddStep("create rooms", () => + { + foreach (var room in rooms) + API.Queue(new CreateRoomRequest(room)); + }); + + AddStep("refresh lounge", () => loungeScreen.RefreshRooms()); + } + protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies(); } } diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs index 9d65be2a19..94a81ecdc7 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs @@ -17,8 +17,6 @@ namespace osu.Game.Tests.Visual.Playlists { public partial class TestScenePlaylistsLoungeSubScreen : OnlinePlayTestScene { - protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; - private TestLoungeSubScreen loungeScreen = null!; public override void SetUpSteps() @@ -26,7 +24,6 @@ namespace osu.Game.Tests.Visual.Playlists base.SetUpSteps(); AddStep("push screen", () => LoadScreen(loungeScreen = new TestLoungeSubScreen())); - AddUntilStep("wait for present", () => loungeScreen.IsCurrentScreen()); } @@ -35,8 +32,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestManyRooms() { - AddStep("add rooms", () => RoomManager.AddRooms(500)); - AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 500); + createRooms(GenerateRooms(500)); } [Test] @@ -44,10 +40,7 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("reset mouse", () => InputManager.ReleaseButton(MouseButton.Left)); - AddStep("add rooms", () => RoomManager.AddRooms(30)); - AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 30); - - AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.DrawableRooms[0])); + createRooms(GenerateRooms(30)); AddStep("move mouse to third room", () => InputManager.MoveMouseTo(roomsContainer.DrawableRooms[2])); AddStep("hold down", () => InputManager.PressButton(MouseButton.Left)); @@ -61,10 +54,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestScrollSelectedIntoView() { - AddStep("add rooms", () => RoomManager.AddRooms(30)); - AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 30); - - AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.DrawableRooms[0])); + createRooms(GenerateRooms(30)); AddStep("select last room", () => roomsContainer.DrawableRooms[^1].TriggerClick()); @@ -75,8 +65,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestEnteringRoomTakesLeaseOnSelection() { - AddStep("add rooms", () => RoomManager.AddRooms(1)); - AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 1); + createRooms(GenerateRooms(1)); AddAssert("selected room is not disabled", () => !loungeScreen.SelectedRoom.Disabled); @@ -95,6 +84,17 @@ namespace osu.Game.Tests.Visual.Playlists loungeScreen.ChildrenOfType().First().ScreenSpaceDrawQuad .Contains(room.ScreenSpaceDrawQuad.Centre); + private void createRooms(params Room[] rooms) + { + AddStep("create rooms", () => + { + foreach (var room in rooms) + API.Queue(new CreateRoomRequest(room)); + }); + + AddStep("refresh lounge", () => loungeScreen.RefreshRooms()); + } + private partial class TestLoungeSubScreen : PlaylistsLoungeSubScreen { public new Bindable SelectedRoom => base.SelectedRoom; diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs index 51e39e1b7f..f7b0bc0d58 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs @@ -17,8 +17,6 @@ namespace osu.Game.Tests.Visual.Playlists { public partial class TestScenePlaylistsMatchSettingsOverlay : OnlinePlayTestScene { - protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; - private TestRoomSettings settings = null!; private Func? handleRequest; diff --git a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs index 5cb4c9420a..1495f97de4 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs @@ -4,17 +4,23 @@ using System; using System.Linq; using System.Threading.Tasks; +using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Game.Online; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Lounge.Components; namespace osu.Game.Screens.OnlinePlay.Components { /// - /// A that polls for the lounge listing. + /// A that polls for the lounge listing. /// - public partial class ListingPollingComponent : RoomPollingComponent + public partial class ListingPollingComponent : PollingComponent { + [Resolved] + private IAPIProvider api { get; set; } = null!; + public required Action RoomsReceived { get; init; } public readonly IBindable Filter = new Bindable(); @@ -22,7 +28,7 @@ namespace osu.Game.Screens.OnlinePlay.Components protected override Task Poll() { - if (!API.IsLoggedIn) + if (!api.IsLoggedIn) return base.Poll(); if (Filter.Value == null) @@ -41,7 +47,7 @@ namespace osu.Game.Screens.OnlinePlay.Components req.Failure += _ => tcs.SetResult(false); - API.Queue(req); + api.Queue(req); lastPollRequest = req; diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs deleted file mode 100644 index a1b61ea7a3..0000000000 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ /dev/null @@ -1,82 +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; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using osu.Framework.Bindables; -using osu.Framework.Development; -using osu.Framework.Graphics; -using osu.Framework.Logging; -using osu.Game.Online.Rooms; - -namespace osu.Game.Screens.OnlinePlay.Components -{ - // Todo: This class should be inlined into the lounge. - public partial class RoomManager : Component, IRoomManager - { - public event Action? RoomsUpdated; - - private readonly BindableList rooms = new BindableList(); - - public IBindableList Rooms => rooms; - - public RoomManager() - { - RelativeSizeAxes = Axes.Both; - } - - private readonly HashSet ignoredRooms = new HashSet(); - - public void AddOrUpdateRoom(Room room) - { - Debug.Assert(ThreadSafety.IsUpdateThread); - Debug.Assert(room.RoomID != null); - - if (ignoredRooms.Contains(room.RoomID.Value)) - return; - - try - { - var existing = rooms.FirstOrDefault(e => e.RoomID == room.RoomID); - if (existing == null) - rooms.Add(room); - else - existing.CopyFrom(room); - } - catch (Exception ex) - { - Logger.Error(ex, $"Failed to update room: {room.Name}."); - - ignoredRooms.Add(room.RoomID.Value); - rooms.Remove(room); - } - - notifyRoomsUpdated(); - } - - public void RemoveRoom(Room room) - { - Debug.Assert(ThreadSafety.IsUpdateThread); - - rooms.Remove(room); - notifyRoomsUpdated(); - } - - public void ClearRooms() - { - Debug.Assert(ThreadSafety.IsUpdateThread); - - rooms.Clear(); - notifyRoomsUpdated(); - } - - private void notifyRoomsUpdated() - { - Scheduler.AddOnce(invokeRoomsUpdated); - - void invokeRoomsUpdated() => RoomsUpdated?.Invoke(); - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs deleted file mode 100644 index 0ba7f20f1c..0000000000 --- a/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Game.Online; -using osu.Game.Online.API; - -namespace osu.Game.Screens.OnlinePlay.Components -{ - public abstract partial class RoomPollingComponent : PollingComponent - { - [Resolved] - protected IAPIProvider API { get; private set; } = null!; - - [Resolved] - protected IRoomManager RoomManager { get; private set; } = null!; - } -} diff --git a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs index f04fd6a096..bfa059f72e 100644 --- a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs @@ -2,15 +2,21 @@ // See the LICENCE file in the repository root for full licence text. using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Game.Online; +using osu.Game.Online.API; using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Components { /// - /// A that polls for the currently-selected room. + /// A that polls for and updates a room. /// - public partial class SelectionPollingComponent : RoomPollingComponent + public partial class SelectionPollingComponent : PollingComponent { + [Resolved] + private IAPIProvider api { get; set; } = null!; + private readonly Room room; public SelectionPollingComponent(Room room) @@ -22,7 +28,7 @@ namespace osu.Game.Screens.OnlinePlay.Components protected override Task Poll() { - if (!API.IsLoggedIn) + if (!api.IsLoggedIn) return base.Poll(); if (room.RoomID == null) @@ -41,7 +47,7 @@ namespace osu.Game.Screens.OnlinePlay.Components req.Failure += _ => tcs.SetResult(false); - API.Queue(req); + api.Queue(req); lastPollRequest = req; diff --git a/osu.Game/Screens/OnlinePlay/IRoomManager.cs b/osu.Game/Screens/OnlinePlay/IRoomManager.cs deleted file mode 100644 index 8ecb1dd7e0..0000000000 --- a/osu.Game/Screens/OnlinePlay/IRoomManager.cs +++ /dev/null @@ -1,42 +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; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Game.Online.Rooms; - -namespace osu.Game.Screens.OnlinePlay -{ - [Cached(typeof(IRoomManager))] - public interface IRoomManager - { - /// - /// Invoked when the s have been updated. - /// - event Action RoomsUpdated; - - /// - /// All the active s. - /// - IBindableList Rooms { get; } - - /// - /// Adds a to this . - /// If already existing, the local room will be updated with the given one. - /// - /// The incoming . - void AddOrUpdateRoom(Room room); - - /// - /// Removes a from this . - /// - /// The to remove. - void RemoveRoom(Room room); - - /// - /// Removes all s from this . - /// - void ClearRooms(); - } -} diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 78501a56d7..6c383f1bf6 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -54,6 +54,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge }; protected readonly Bindable SelectedRoom = new Bindable(); + protected readonly BindableList Rooms = new BindableList(); [Resolved] private MusicController music { get; set; } = null!; @@ -76,7 +77,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private IDisposable? joiningRoomOperation; private LeasedBindable? selectionLease; - private readonly BindableList rooms = new BindableList(); private readonly Bindable filter = new Bindable(); private readonly Bindable hasListingResults = new Bindable(); private readonly IBindable operationInProgress = new Bindable(); @@ -121,7 +121,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge ScrollbarOverlapsContent = false, Child = roomsContainer = new RoomsContainer { - Rooms = { BindTarget = rooms }, + Rooms = { BindTarget = Rooms }, SelectedRoom = { BindTarget = SelectedRoom }, Filter = { BindTarget = filter }, } @@ -208,7 +208,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge filter.BindValueChanged(_ => { - rooms.Clear(); + Rooms.Clear(); hasListingResults.Value = false; listingPollingComponent.PollImmediately(); }); @@ -218,11 +218,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private void onListingReceived(Room[] result) { - Dictionary localRoomsById = rooms.ToDictionary(r => r.RoomID!.Value); + Dictionary localRoomsById = Rooms.ToDictionary(r => r.RoomID!.Value); Dictionary resultRoomsById = result.ToDictionary(r => r.RoomID!.Value); // Remove all local rooms no longer in the result set. - rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value)); + Rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value)); // Add or update local rooms with the result set. foreach (var r in result) @@ -230,7 +230,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge if (localRoomsById.TryGetValue(r.RoomID!.Value, out Room? existingRoom)) existingRoom.CopyFrom(r); else - rooms.Add(r); + Rooms.Add(r); } hasListingResults.Value = true; diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 8988c82dee..812e42479b 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -11,7 +11,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Screens.Menu; -using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Users; @@ -39,9 +38,6 @@ namespace osu.Game.Screens.OnlinePlay [Cached] private readonly OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker(); - [Cached(Type = typeof(IRoomManager))] - private readonly RoomManager roomManager = new RoomManager(); - [Resolved] protected IAPIProvider API { get; private set; } = null!; @@ -65,7 +61,6 @@ namespace osu.Game.Screens.OnlinePlay { screenStack, new Header(ScreenTitle, screenStack), - roomManager, ongoingOperationTracker, } }; diff --git a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs index 8ddc5325db..5780cf6eff 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs @@ -18,11 +18,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// Bindable SelectedRoom { get; } - /// - /// The cached - /// - IRoomManager RoomManager { get; } - /// /// The cached . /// diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs index 3f6c175fbd..c3a5e1c3ec 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs @@ -10,7 +10,9 @@ using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; +using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay; namespace osu.Game.Tests.Visual.OnlinePlay @@ -21,7 +23,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay public abstract partial class OnlinePlayTestScene : ScreenTestScene, IOnlinePlayTestSceneDependencies { public Bindable SelectedRoom => OnlinePlayDependencies.SelectedRoom; - public IRoomManager RoomManager => OnlinePlayDependencies.RoomManager; public OngoingOperationTracker OngoingOperationTracker => OnlinePlayDependencies.OngoingOperationTracker; public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker => OnlinePlayDependencies.AvailabilityTracker; public TestUserLookupCache UserLookupCache => OnlinePlayDependencies.UserLookupCache; @@ -34,9 +35,13 @@ namespace osu.Game.Tests.Visual.OnlinePlay protected override Container Content => content; + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + private readonly Container content; private readonly Container drawableDependenciesContainer; private DelegatedDependencyContainer dependencies = null!; + private int currentRoomId; protected OnlinePlayTestScene() { @@ -93,6 +98,31 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// protected virtual OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new OnlinePlayTestSceneDependencies(); + protected Room[] GenerateRooms(int count, RulesetInfo? ruleset = null, bool withPassword = false, bool withSpotlightRooms = false) + { + Room[] rooms = new Room[count]; + + // Can't reference Osu ruleset project here. + ruleset ??= rulesets.GetRuleset(0)!; + + for (int i = 0; i < count; i++) + { + rooms[i] = new Room + { + RoomID = currentRoomId++, + Name = $@"Room {currentRoomId}", + Host = new APIUser { Username = @"Host" }, + Duration = TimeSpan.FromSeconds(10), + Category = withSpotlightRooms && i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal, + Password = withPassword ? @"password" : null, + PlaylistItemStats = new Room.RoomPlaylistItemStats { RulesetIDs = [ruleset.OnlineID] }, + Playlist = [new PlaylistItem(new BeatmapInfo { Metadata = new BeatmapMetadata() }) { RulesetID = ruleset.OnlineID }] + }; + } + + return rooms; + } + /// /// A providing a mutable lookup source for online play dependencies. /// diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs index 203922c057..cc448beea0 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs @@ -19,7 +19,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay public class OnlinePlayTestSceneDependencies : IReadOnlyDependencyContainer, IOnlinePlayTestSceneDependencies { public Bindable SelectedRoom { get; } - public IRoomManager RoomManager { get; } public OngoingOperationTracker OngoingOperationTracker { get; } public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker { get; } public TestRoomRequestsHandler RequestsHandler { get; } @@ -40,7 +39,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay RequestsHandler = new TestRoomRequestsHandler(); OngoingOperationTracker = new OngoingOperationTracker(); AvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); - RoomManager = new TestRoomManager(); UserLookupCache = new TestUserLookupCache(); BeatmapLookupCache = new BeatmapLookupCache(); @@ -48,7 +46,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay CacheAs(RequestsHandler); CacheAs(SelectedRoom); - CacheAs(RoomManager); CacheAs(OngoingOperationTracker); CacheAs(AvailabilityTracker); CacheAs(new OverlayColourProvider(OverlayColourScheme.Plum)); diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs deleted file mode 100644 index bff2753929..0000000000 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs +++ /dev/null @@ -1,59 +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; -using osu.Framework.Allocation; -using osu.Game.Beatmaps; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Rooms; -using osu.Game.Rulesets; -using osu.Game.Screens.OnlinePlay.Components; - -namespace osu.Game.Tests.Visual.OnlinePlay -{ - /// - /// A very simple for use in online play test scenes. - /// - public partial class TestRoomManager : RoomManager - { - private int currentRoomId; - - [Resolved] - private IAPIProvider api { get; set; } = null!; - - [Resolved] - private RulesetStore rulesets { get; set; } = null!; - - public void AddRooms(int count, RulesetInfo? ruleset = null, bool withPassword = false, bool withSpotlightRooms = false) - { - // Can't reference Osu ruleset project here. - ruleset ??= rulesets.GetRuleset(0)!; - - for (int i = 0; i < count; i++) - { - AddRoom(new Room - { - Name = $@"Room {currentRoomId}", - Host = new APIUser { Username = @"Host" }, - Duration = TimeSpan.FromSeconds(10), - Category = withSpotlightRooms && i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal, - Password = withPassword ? @"password" : null, - PlaylistItemStats = new Room.RoomPlaylistItemStats { RulesetIDs = [ruleset.OnlineID] }, - Playlist = [new PlaylistItem(new BeatmapInfo { Metadata = new BeatmapMetadata() }) { RulesetID = ruleset.OnlineID }] - }); - } - } - - public void AddRoom(Room room) - { - room.RoomID = -currentRoomId; - - var req = new CreateRoomRequest(room); - req.Success += AddOrUpdateRoom; - api.Queue(req); - - currentRoomId++; - } - } -} diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index c9149bda22..63bc9325fa 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -36,8 +36,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay private int currentScoreId = 1; /// - /// Handles an API request, while also updating the local state to match - /// how the server would eventually respond and update an . + /// Handles an API request, while also updating the local state to match how the server would eventually respond. /// /// The API request to handle. /// The local user to store in responses where required. From 1b07b6d16f49fd06572c3366685a08f2a2641669 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 12 Feb 2025 21:48:59 +0900 Subject: [PATCH 462/801] Remove selected room leasing, make bindables private I believe once upon a time the `SelectedRoom` bindable used to be bound to `RoomManager.JoinedRoom` or similar. But now it's effectively private to the lounge subscreen and so a lease is unnecessary. --- .../TestScenePlaylistsLoungeSubScreen.cs | 28 +------------ .../OnlinePlay/Lounge/LoungeSubScreen.cs | 39 ++++++------------- 2 files changed, 13 insertions(+), 54 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs index 94a81ecdc7..35bf6dc28a 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs @@ -3,7 +3,6 @@ using System.Linq; using NUnit.Framework; -using osu.Framework.Bindables; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Graphics.Containers; @@ -17,13 +16,13 @@ namespace osu.Game.Tests.Visual.Playlists { public partial class TestScenePlaylistsLoungeSubScreen : OnlinePlayTestScene { - private TestLoungeSubScreen loungeScreen = null!; + private PlaylistsLoungeSubScreen loungeScreen = null!; public override void SetUpSteps() { base.SetUpSteps(); - AddStep("push screen", () => LoadScreen(loungeScreen = new TestLoungeSubScreen())); + AddStep("push screen", () => LoadScreen(loungeScreen = new PlaylistsLoungeSubScreen())); AddUntilStep("wait for present", () => loungeScreen.IsCurrentScreen()); } @@ -62,24 +61,6 @@ namespace osu.Game.Tests.Visual.Playlists AddUntilStep("last room is not masked", () => checkRoomVisible(roomsContainer.DrawableRooms[^1])); } - [Test] - public void TestEnteringRoomTakesLeaseOnSelection() - { - createRooms(GenerateRooms(1)); - - AddAssert("selected room is not disabled", () => !loungeScreen.SelectedRoom.Disabled); - - AddStep("select room", () => roomsContainer.DrawableRooms[0].TriggerClick()); - AddAssert("selected room is non-null", () => loungeScreen.SelectedRoom.Value != null); - - AddStep("enter room", () => roomsContainer.DrawableRooms[0].TriggerClick()); - - AddUntilStep("wait for match load", () => Stack.CurrentScreen is PlaylistsRoomSubScreen); - - AddAssert("selected room is non-null", () => loungeScreen.SelectedRoom.Value != null); - AddAssert("selected room is disabled", () => loungeScreen.SelectedRoom.Disabled); - } - private bool checkRoomVisible(DrawableRoom room) => loungeScreen.ChildrenOfType().First().ScreenSpaceDrawQuad .Contains(room.ScreenSpaceDrawQuad.Centre); @@ -94,10 +75,5 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("refresh lounge", () => loungeScreen.RefreshRooms()); } - - private partial class TestLoungeSubScreen : PlaylistsLoungeSubScreen - { - public new Bindable SelectedRoom => base.SelectedRoom; - } } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 6c383f1bf6..7bb0c67990 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected override BackgroundScreen CreateBackground() => new LoungeBackgroundScreen { - SelectedRoom = { BindTarget = SelectedRoom } + SelectedRoom = { BindTarget = selectedRoom } }; protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby(); @@ -53,9 +53,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge AutoSizeAxes = Axes.Both }; - protected readonly Bindable SelectedRoom = new Bindable(); - protected readonly BindableList Rooms = new BindableList(); - [Resolved] private MusicController music { get; set; } = null!; @@ -75,8 +72,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected OsuConfigManager Config { get; private set; } = null!; private IDisposable? joiningRoomOperation; - private LeasedBindable? selectionLease; + private readonly Bindable selectedRoom = new Bindable(); + private readonly BindableList rooms = new BindableList(); private readonly Bindable filter = new Bindable(); private readonly Bindable hasListingResults = new Bindable(); private readonly IBindable operationInProgress = new Bindable(); @@ -121,8 +119,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge ScrollbarOverlapsContent = false, Child = roomsContainer = new RoomsContainer { - Rooms = { BindTarget = Rooms }, - SelectedRoom = { BindTarget = SelectedRoom }, + Rooms = { BindTarget = rooms }, + SelectedRoom = { BindTarget = selectedRoom }, Filter = { BindTarget = filter }, } }, @@ -182,7 +180,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge }; // scroll selected room into view on selection. - SelectedRoom.BindValueChanged(val => + selectedRoom.BindValueChanged(val => { var drawable = roomsContainer.DrawableRooms.FirstOrDefault(r => r.Room == val.NewValue); if (drawable != null) @@ -208,7 +206,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge filter.BindValueChanged(_ => { - Rooms.Clear(); + rooms.Clear(); hasListingResults.Value = false; listingPollingComponent.PollImmediately(); }); @@ -218,11 +216,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private void onListingReceived(Room[] result) { - Dictionary localRoomsById = Rooms.ToDictionary(r => r.RoomID!.Value); + Dictionary localRoomsById = rooms.ToDictionary(r => r.RoomID!.Value); Dictionary resultRoomsById = result.ToDictionary(r => r.RoomID!.Value); // Remove all local rooms no longer in the result set. - Rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value)); + rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value)); // Add or update local rooms with the result set. foreach (var r in result) @@ -230,7 +228,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge if (localRoomsById.TryGetValue(r.RoomID!.Value, out Room? existingRoom)) existingRoom.CopyFrom(r); else - Rooms.Add(r); + rooms.Add(r); } hasListingResults.Value = true; @@ -286,14 +284,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { base.OnResuming(e); - Debug.Assert(selectionLease != null); - - selectionLease.Return(); - selectionLease = null; - - if (SelectedRoom.Value?.RoomID == null) - SelectedRoom.Value = new Room(); - music.EnsurePlayingSomething(); onReturning(); @@ -415,14 +405,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge OpenNewRoom(room ?? CreateNewRoom()); }); - protected virtual void OpenNewRoom(Room room) - { - selectionLease = SelectedRoom.BeginLease(false); - Debug.Assert(selectionLease != null); - selectionLease.Value = room; - - this.Push(CreateRoomSubScreen(room)); - } + protected virtual void OpenNewRoom(Room room) => this.Push(CreateRoomSubScreen(room)); public void RefreshRooms() => listingPollingComponent.PollImmediately(); From 74ccac37ae665ea2a9a603316077453520a8b9de Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 12 Feb 2025 21:57:18 +0900 Subject: [PATCH 463/801] Encapsulate RoomsContainer scroll a bit better --- .../TestSceneLoungeRoomsContainer.cs | 4 +-- .../Lounge/Components/RoomsContainer.cs | 35 ++++++++++++------- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 26 +++----------- 3 files changed, 30 insertions(+), 35 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index 9daad960c7..772eb91174 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -32,13 +32,13 @@ namespace osu.Game.Tests.Visual.Multiplayer rooms = new BindableList(); Child = new PopoverContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, Width = 0.5f, Child = container = new RoomsContainer { + RelativeSizeAxes = Axes.Both, Rooms = { BindTarget = rooms }, SelectedRoom = { BindTarget = SelectedRoom } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index 6681cbe720..65f969bc7b 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Input.Bindings; using osu.Game.Online.Rooms; @@ -28,6 +29,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components public IReadOnlyList DrawableRooms => roomFlow.FlowingChildren.Cast().ToArray(); + private readonly ScrollContainer scroll; private readonly FillFlowContainer roomFlow; // handle deselection @@ -35,28 +37,29 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components public RoomsContainer() { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - - // account for the fact we are in a scroll container and want a bit of spacing from the scroll bar. - Padding = new MarginPadding { Right = 5 }; - - InternalChild = new OsuContextMenuContainer + InternalChild = scroll = new OsuScrollContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Child = roomFlow = new FillFlowContainer + RelativeSizeAxes = Axes.Both, + ScrollbarOverlapsContent = false, + Padding = new MarginPadding { Right = 5 }, + Child = new OsuContextMenuContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(10), + Child = roomFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(10), + } } }; } protected override void LoadComplete() { + SelectedRoom.BindValueChanged(onSelectedRoomChanged, true); Rooms.BindCollectionChanged(roomsChanged, true); Filter.BindValueChanged(criteria => applyFilterCriteria(criteria.NewValue), true); } @@ -119,6 +122,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } + private void onSelectedRoomChanged(ValueChangedEvent room) + { + // scroll selected room into view on selection. + var drawable = DrawableRooms.FirstOrDefault(r => r.Room == room.NewValue); + if (drawable != null) + scroll.ScrollIntoView(drawable); + } + private void roomsChanged(object? sender, NotifyCollectionChangedEventArgs args) { switch (args.Action) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 7bb0c67990..1877244c03 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -17,7 +17,6 @@ using osu.Framework.Logging; using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Configuration; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input; using osu.Game.Online.API; @@ -82,7 +81,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private ListingPollingComponent listingPollingComponent = null!; private PopoverContainer popoverContainer = null!; private LoadingLayer loadingLayer = null!; - private RoomsContainer roomsContainer = null!; private SearchTextBox searchTextBox = null!; protected Dropdown StatusDropdown { get; private set; } = null!; @@ -95,8 +93,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge if (idleTracker != null) isIdle.BindTo(idleTracker.IsIdle); - OsuScrollContainer scrollContainer; - InternalChildren = new Drawable[] { listingPollingComponent = new ListingPollingComponent @@ -113,17 +109,13 @@ namespace osu.Game.Screens.OnlinePlay.Lounge Horizontal = WaveOverlayContainer.WIDTH_PADDING, Top = Header.HEIGHT + controls_area_height + 20, }, - Child = scrollContainer = new OsuScrollContainer + Child = new RoomsContainer { RelativeSizeAxes = Axes.Both, - ScrollbarOverlapsContent = false, - Child = roomsContainer = new RoomsContainer - { - Rooms = { BindTarget = rooms }, - SelectedRoom = { BindTarget = selectedRoom }, - Filter = { BindTarget = filter }, - } - }, + Rooms = { BindTarget = rooms }, + SelectedRoom = { BindTarget = selectedRoom }, + Filter = { BindTarget = filter }, + } }, loadingLayer = new LoadingLayer(true), new FillFlowContainer @@ -178,14 +170,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge }, }, }; - - // scroll selected room into view on selection. - selectedRoom.BindValueChanged(val => - { - var drawable = roomsContainer.DrawableRooms.FirstOrDefault(r => r.Room == val.NewValue); - if (drawable != null) - scrollContainer.ScrollIntoView(drawable); - }); } protected override void LoadComplete() From 43928c94db5b4695b2baab8acfb41d58198322aa Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 12 Feb 2025 22:03:22 +0900 Subject: [PATCH 464/801] Remove remaining bindables --- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 17 +++++++---------- .../Multiplayer/MultiplayerLoungeSubScreen.cs | 3 --- .../OnlinePlay/TestRoomRequestsHandler.cs | 2 -- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 1877244c03..2e78e88ccf 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected override BackgroundScreen CreateBackground() => new LoungeBackgroundScreen { - SelectedRoom = { BindTarget = selectedRoom } + SelectedRoom = { BindTarget = roomsContainer.SelectedRoom } }; protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby(); @@ -72,12 +72,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private IDisposable? joiningRoomOperation; - private readonly Bindable selectedRoom = new Bindable(); - private readonly BindableList rooms = new BindableList(); private readonly Bindable filter = new Bindable(); private readonly Bindable hasListingResults = new Bindable(); private readonly IBindable operationInProgress = new Bindable(); private readonly IBindable isIdle = new BindableBool(); + private RoomsContainer roomsContainer = null!; private ListingPollingComponent listingPollingComponent = null!; private PopoverContainer popoverContainer = null!; private LoadingLayer loadingLayer = null!; @@ -109,11 +108,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge Horizontal = WaveOverlayContainer.WIDTH_PADDING, Top = Header.HEIGHT + controls_area_height + 20, }, - Child = new RoomsContainer + Child = roomsContainer = new RoomsContainer { RelativeSizeAxes = Axes.Both, - Rooms = { BindTarget = rooms }, - SelectedRoom = { BindTarget = selectedRoom }, Filter = { BindTarget = filter }, } }, @@ -190,7 +187,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge filter.BindValueChanged(_ => { - rooms.Clear(); + roomsContainer.Rooms.Clear(); hasListingResults.Value = false; listingPollingComponent.PollImmediately(); }); @@ -200,11 +197,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private void onListingReceived(Room[] result) { - Dictionary localRoomsById = rooms.ToDictionary(r => r.RoomID!.Value); + Dictionary localRoomsById = roomsContainer.Rooms.ToDictionary(r => r.RoomID!.Value); Dictionary resultRoomsById = result.ToDictionary(r => r.RoomID!.Value); // Remove all local rooms no longer in the result set. - rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value)); + roomsContainer.Rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value)); // Add or update local rooms with the result set. foreach (var r in result) @@ -212,7 +209,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge if (localRoomsById.TryGetValue(r.RoomID!.Value, out Room? existingRoom)) existingRoom.CopyFrom(r); else - rooms.Add(r); + roomsContainer.Rooms.Add(r); } hasListingResults.Value = true; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index 3cf873ec78..6191cfd975 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -3,9 +3,7 @@ using System; using System.Collections.Generic; -using System.Threading.Tasks; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Logging; using osu.Framework.Graphics; @@ -15,7 +13,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Match; diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index 63bc9325fa..617a4cff79 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -15,7 +15,6 @@ using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; -using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Tests.Beatmaps; using osu.Game.Utils; @@ -28,7 +27,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay public class TestRoomRequestsHandler { public IReadOnlyList ServerSideRooms => serverSideRooms; - private readonly List serverSideRooms = new List(); private int currentRoomId = 1; From ee6dcbd80899c3865803311b372c8f8623092ade Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Feb 2025 14:12:43 +0100 Subject: [PATCH 465/801] Fix android build again Another month, another freak android build failure. From what I can tell, this time the build is broken because the second- -to-last workaround applied to fix it, namely explicitly specifying the version of workloads to install, stopped working, presumably because github pushed a new .NET SDK version to runners, and microsoft didn't actually put up a set of workload packages whose version matches the .NET SDK version 1:1. Thankfully, the fix to the *last* android build breakage (which caused the workload installation to completely and irrecoverably fail), appears to be rolling out this week, and *also* fix that same second-last issue, so both workarounds of specifying the workload version and pinning the image to `windows-2019` appear to no longer be required. Note that the newest image version, 20250209.1.0, is still not fully rolled out yet, thus rather than just fix all builds, this will fix like 20% of builds until the newest image is fully rolled out. But I guess a 20% passing build is better than a 0% passing build, in a sense...? --- .github/workflows/ci.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a88f1320cd..d75f09f184 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,7 +96,7 @@ jobs: build-only-android: name: Build only (Android) - runs-on: windows-2019 + runs-on: windows-latest timeout-minutes: 60 steps: - name: Checkout @@ -114,10 +114,7 @@ jobs: dotnet-version: "8.0.x" - name: Install .NET workloads - # since windows image 20241113.3.0, not specifying a version here - # installs the .NET 7 version of android workload for very unknown reasons. - # revisit once we upgrade to .NET 9, it's probably fixed there. - run: dotnet workload install android --version (dotnet --version) + run: dotnet workload install android - name: Compile run: dotnet build -c Debug osu.Android.slnf From 24cc77287e5e715a0fc684999f0a9aadd1355380 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 12 Feb 2025 22:21:04 +0900 Subject: [PATCH 466/801] Refactor polling components (namespace/namings) --- .../Visual/Multiplayer/TestSceneMultiplayer.cs | 3 +-- .../LoungePollingComponent.cs} | 4 ++-- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 17 ++++++++--------- .../Playlists/PlaylistsRoomSubScreen.cs | 8 ++++---- .../PlaylistsRoomUpdater.cs} | 6 +++--- 5 files changed, 18 insertions(+), 20 deletions(-) rename osu.Game/Screens/OnlinePlay/{Components/ListingPollingComponent.cs => Lounge/LoungePollingComponent.cs} (92%) rename osu.Game/Screens/OnlinePlay/{Components/SelectionPollingComponent.cs => Playlists/PlaylistsRoomUpdater.cs} (88%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 0966c61a3a..a87216287d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -33,7 +33,6 @@ using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Taiko; using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay; -using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Match; @@ -806,7 +805,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for room", () => this.ChildrenOfType().Any()); AddStep("select room", () => InputManager.Key(Key.Down)); - AddStep("disable polling", () => this.ChildrenOfType().Single().TimeBetweenPolls.Value = 0); + AddStep("disable polling", () => this.ChildrenOfType().Single().TimeBetweenPolls.Value = 0); AddStep("change server-side settings", () => { multiplayerClient.ServerSideRooms[0].Name = "New name"; diff --git a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungePollingComponent.cs similarity index 92% rename from osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs rename to osu.Game/Screens/OnlinePlay/Lounge/LoungePollingComponent.cs index 1495f97de4..420a96cf8a 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungePollingComponent.cs @@ -11,12 +11,12 @@ using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Lounge.Components; -namespace osu.Game.Screens.OnlinePlay.Components +namespace osu.Game.Screens.OnlinePlay.Lounge { /// /// A that polls for the lounge listing. /// - public partial class ListingPollingComponent : PollingComponent + public partial class LoungePollingComponent : PollingComponent { [Resolved] private IAPIProvider api { get; set; } = null!; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 2e78e88ccf..3a4da96ba1 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -24,7 +24,6 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Rulesets; -using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Users; @@ -77,7 +76,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private readonly IBindable operationInProgress = new Bindable(); private readonly IBindable isIdle = new BindableBool(); private RoomsContainer roomsContainer = null!; - private ListingPollingComponent listingPollingComponent = null!; + private LoungePollingComponent pollingComponent = null!; private PopoverContainer popoverContainer = null!; private LoadingLayer loadingLayer = null!; private SearchTextBox searchTextBox = null!; @@ -94,7 +93,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge InternalChildren = new Drawable[] { - listingPollingComponent = new ListingPollingComponent + pollingComponent = new LoungePollingComponent { RoomsReceived = onListingReceived, Filter = { BindTarget = filter } @@ -189,7 +188,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { roomsContainer.Rooms.Clear(); hasListingResults.Value = false; - listingPollingComponent.PollImmediately(); + pollingComponent.PollImmediately(); }); updateFilter(); @@ -270,7 +269,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge onReturning(); // Poll for any newly-created rooms (including potentially the user's own). - listingPollingComponent.PollImmediately(); + pollingComponent.PollImmediately(); } public override bool OnExiting(ScreenExitEvent e) @@ -388,7 +387,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected virtual void OpenNewRoom(Room room) => this.Push(CreateRoomSubScreen(room)); - public void RefreshRooms() => listingPollingComponent.PollImmediately(); + public void RefreshRooms() => pollingComponent.PollImmediately(); private void updateLoadingLayer() { @@ -401,11 +400,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private void updatePollingRate(bool isCurrentScreen) { if (!isCurrentScreen) - listingPollingComponent.TimeBetweenPolls.Value = 0; + pollingComponent.TimeBetweenPolls.Value = 0; else - listingPollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 120000 : 15000; + pollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 120000 : 15000; - Logger.Log($"Polling adjusted (listing: {listingPollingComponent.TimeBetweenPolls.Value})"); + Logger.Log($"Polling adjusted (listing: {pollingComponent.TimeBetweenPolls.Value})"); } protected abstract OsuButton CreateNewRoomButton(); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index bf0e428483..a74ae642fb 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -44,7 +44,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private IdleTracker? idleTracker { get; set; } private MatchLeaderboard leaderboard = null!; - private SelectionPollingComponent selectionPollingComponent = null!; + private PlaylistsRoomUpdater roomUpdater = null!; private FillFlowContainer progressSection = null!; private DrawableRoomPlaylist drawablePlaylist = null!; @@ -64,7 +64,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (idleTracker != null) isIdle.BindTo(idleTracker.IsIdle); - AddInternal(selectionPollingComponent = new SelectionPollingComponent(Room)); + AddInternal(roomUpdater = new PlaylistsRoomUpdater(Room)); } protected override void LoadComplete() @@ -328,8 +328,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private void updatePollingRate() { - selectionPollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 30000 : 5000; - Logger.Log($"Polling adjusted (selection: {selectionPollingComponent.TimeBetweenPolls.Value})"); + roomUpdater.TimeBetweenPolls.Value = isIdle.Value ? 30000 : 5000; + Logger.Log($"Polling adjusted (selection: {roomUpdater.TimeBetweenPolls.Value})"); } private void closePlaylist() diff --git a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomUpdater.cs similarity index 88% rename from osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs rename to osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomUpdater.cs index bfa059f72e..f68703750a 100644 --- a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomUpdater.cs @@ -7,19 +7,19 @@ using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.Rooms; -namespace osu.Game.Screens.OnlinePlay.Components +namespace osu.Game.Screens.OnlinePlay.Playlists { /// /// A that polls for and updates a room. /// - public partial class SelectionPollingComponent : PollingComponent + public partial class PlaylistsRoomUpdater : PollingComponent { [Resolved] private IAPIProvider api { get; set; } = null!; private readonly Room room; - public SelectionPollingComponent(Room room) + public PlaylistsRoomUpdater(Room room) { this.room = room; } From 205d6ecffbc989d75c1a32e53a29a9342b88c175 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 12 Feb 2025 22:51:25 +0900 Subject: [PATCH 467/801] Remove `SelectedRoom` abstraction from `OnlinePlayTestScene` --- .../StatefulMultiplayerClientTest.cs | 6 ++ .../TestSceneDrawableRoomParticipantsList.cs | 15 +++-- .../TestSceneLoungeRoomsContainer.cs | 7 +- .../TestSceneMatchBeatmapDetailArea.cs | 10 +-- .../Multiplayer/TestSceneMatchLeaderboard.cs | 4 +- .../TestSceneMultiSpectatorLeaderboard.cs | 2 + .../TestSceneMultiSpectatorScreen.cs | 6 +- .../TestSceneMultiplayerLoungeSubScreen.cs | 5 -- .../TestSceneMultiplayerMatchSongSelect.cs | 13 +++- .../TestSceneMultiplayerMatchSubScreen.cs | 28 ++++---- .../TestSceneMultiplayerParticipantsList.cs | 6 +- .../Multiplayer/TestSceneMultiplayerPlayer.cs | 6 ++ .../TestSceneMultiplayerPlaylist.cs | 5 +- .../TestSceneMultiplayerQueueList.cs | 5 +- .../TestSceneMultiplayerSpectateButton.cs | 11 +-- .../TestScenePlaylistsSongSelect.cs | 23 ++++--- .../TestScenePlaylistsMatchSettingsOverlay.cs | 29 ++++---- .../TestScenePlaylistsParticipantsList.cs | 10 +-- .../TestScenePlaylistsRoomCreation.cs | 12 ++-- .../Multiplayer/MultiplayerTestScene.cs | 67 ++++++++++--------- .../IOnlinePlayTestSceneDependencies.cs | 6 -- .../Visual/OnlinePlay/OnlinePlayTestScene.cs | 2 - .../OnlinePlayTestSceneDependencies.cs | 4 -- 23 files changed, 149 insertions(+), 133 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs index be30e06ed4..c0ca387260 100644 --- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs +++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs @@ -15,6 +15,12 @@ namespace osu.Game.Tests.NonVisual.Multiplayer [HeadlessTest] public partial class StatefulMultiplayerClientTest : MultiplayerTestScene { + public override void SetUpSteps() + { + base.SetUpSteps(); + JoinDefaultRoom(); + } + [Test] public void TestUserAddedOnJoin() { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs index c1662bf944..2fd1268c8a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs @@ -15,6 +15,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { public partial class TestSceneDrawableRoomParticipantsList : OnlinePlayTestScene { + private Room room = null!; private DrawableRoomParticipantsList list = null!; public override void SetUpSteps() @@ -23,7 +24,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create list", () => { - SelectedRoom.Value = new Room + room = new Room { Name = "test room", Host = new APIUser @@ -33,7 +34,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } }; - Child = list = new DrawableRoomParticipantsList(SelectedRoom.Value) + Child = list = new DrawableRoomParticipantsList(room) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -119,7 +120,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("4 circles displayed", () => list.ChildrenOfType().Count() == 4); AddAssert("46 hidden users", () => list.ChildrenOfType().Single().Count == 46); - AddStep("remove from end", () => removeUserAt(SelectedRoom.Value!.RecentParticipants.Count - 1)); + AddStep("remove from end", () => removeUserAt(room.RecentParticipants.Count - 1)); AddAssert("4 circles displayed", () => list.ChildrenOfType().Count() == 4); AddAssert("45 hidden users", () => list.ChildrenOfType().Single().Count == 45); @@ -138,18 +139,18 @@ namespace osu.Game.Tests.Visual.Multiplayer private void addUser(int id) { - SelectedRoom.Value!.RecentParticipants = SelectedRoom.Value!.RecentParticipants.Append(new APIUser + room.RecentParticipants = room.RecentParticipants.Append(new APIUser { Id = id, Username = $"User {id}" }).ToArray(); - SelectedRoom.Value!.ParticipantCount++; + room.ParticipantCount++; } private void removeUserAt(int index) { - SelectedRoom.Value!.RecentParticipants = SelectedRoom.Value!.RecentParticipants.Where(u => !u.Equals(SelectedRoom.Value!.RecentParticipants[index])).ToArray(); - SelectedRoom.Value!.ParticipantCount--; + room.RecentParticipants = room.RecentParticipants.Where(u => !u.Equals(room.RecentParticipants[index])).ToArray(); + room.ParticipantCount--; } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index 772eb91174..e83a966144 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -21,6 +21,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public partial class TestSceneLoungeRoomsContainer : OnlinePlayTestScene { private BindableList rooms = null!; + private Bindable selectedRoom = null!; private RoomsContainer container = null!; public override void SetUpSteps() @@ -30,6 +31,8 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create container", () => { rooms = new BindableList(); + selectedRoom = new Bindable(); + Child = new PopoverContainer { RelativeSizeAxes = Axes.Both, @@ -40,7 +43,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { RelativeSizeAxes = Axes.Both, Rooms = { BindTarget = rooms }, - SelectedRoom = { BindTarget = SelectedRoom } + SelectedRoom = { BindTarget = selectedRoom } } }; }); @@ -195,7 +198,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3, withPassword: true))); } - private bool checkRoomSelected(Room? room) => SelectedRoom.Value == room; + private bool checkRoomSelected(Room? room) => selectedRoom.Value == room; private Room? getRoomInFlow(int index) => (container.ChildrenOfType>().First().FlowingChildren.ElementAt(index) as DrawableRoom)?.Room; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs index 813a420cbd..e372d63fde 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs @@ -16,15 +16,15 @@ namespace osu.Game.Tests.Visual.Multiplayer { public partial class TestSceneMatchBeatmapDetailArea : OnlinePlayTestScene { + private Room room = null!; + public override void SetUpSteps() { base.SetUpSteps(); AddStep("create area", () => { - SelectedRoom.Value = new Room(); - - Child = new MatchBeatmapDetailArea(SelectedRoom.Value) + Child = new MatchBeatmapDetailArea(room = new Room()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -36,9 +36,9 @@ namespace osu.Game.Tests.Visual.Multiplayer private void createNewItem() { - SelectedRoom.Value!.Playlist = SelectedRoom.Value.Playlist.Append(new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) + room.Playlist = room.Playlist.Append(new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { - ID = SelectedRoom.Value.Playlist.Count, + ID = room.Playlist.Count, RulesetID = new OsuRuleset().RulesetInfo.OnlineID, RequiredMods = new[] { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs index 38522db4d4..39ad21d0b0 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs @@ -61,9 +61,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create leaderboard", () => { - SelectedRoom.Value = new Room { RoomID = 3 }; - - Child = new MatchLeaderboard(SelectedRoom.Value) + Child = new MatchLeaderboard(new Room { RoomID = 3 }) { Origin = Anchor.Centre, Anchor = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs index 3245b3c6a9..1821c2f3bc 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs @@ -24,6 +24,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); + JoinDefaultRoom(); + AddStep("reset", () => { leaderboard?.RemoveAndDisposeImmediately(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index 0a3d48828e..6cbd8a3fed 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -17,6 +17,7 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; +using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.UI; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; @@ -42,6 +43,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private BeatmapManager beatmapManager { get; set; } = null!; private MultiSpectatorScreen spectatorScreen = null!; + private Room room = null!; private readonly List playingUsers = new List(); @@ -63,6 +65,8 @@ namespace osu.Game.Tests.Visual.Multiplayer base.SetUpSteps(); AddStep("clear playing users", () => playingUsers.Clear()); + + JoinDefaultRoom(r => room = r); } [TestCase(1)] @@ -455,7 +459,7 @@ namespace osu.Game.Tests.Visual.Multiplayer applyToBeatmap?.Invoke(Beatmap.Value); - LoadScreen(spectatorScreen = new MultiSpectatorScreen(SelectedRoom.Value!, playingUsers.ToArray())); + LoadScreen(spectatorScreen = new MultiSpectatorScreen(room, playingUsers.ToArray())); }); AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded && (!waitForPlayerLoad || spectatorScreen.AllPlayersLoaded)); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs index b4ec9d5858..56187f8778 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs @@ -20,11 +20,6 @@ namespace osu.Game.Tests.Visual.Multiplayer { private MultiplayerLoungeSubScreen loungeScreen = null!; - public TestSceneMultiplayerLoungeSubScreen() - : base(false) - { - } - public override void SetUpSteps() { base.SetUpSteps(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 298e6e1b3c..287d7f5816 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -39,6 +39,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private TestMultiplayerMatchSongSelect songSelect = null!; private Live importedBeatmapSet = null!; + private Room room = null!; [Resolved] private OsuConfigManager configManager { get; set; } = null!; @@ -58,6 +59,12 @@ namespace osu.Game.Tests.Visual.Multiplayer Add(beatmapStore); } + public override void SetUpSteps() + { + base.SetUpSteps(); + JoinDefaultRoom(r => room = r); + } + private void setUp() { AddStep("create song select", () => @@ -66,7 +73,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Beatmap.SetDefault(); SelectedMods.SetDefault(); - LoadScreen(songSelect = new TestMultiplayerMatchSongSelect(SelectedRoom.Value!)); + LoadScreen(songSelect = new TestMultiplayerMatchSongSelect(room)); }); AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded); @@ -138,8 +145,8 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create song select", () => { - SelectedRoom.Value!.Playlist.Single().RulesetID = 2; - songSelect = new TestMultiplayerMatchSongSelect(SelectedRoom.Value, SelectedRoom.Value.Playlist.Single()); + room.Playlist.Single().RulesetID = 2; + songSelect = new TestMultiplayerMatchSongSelect(room, room.Playlist.Single()); songSelect.OnLoadComplete += _ => Ruleset.Value = new TaikoRuleset().RulesetInfo; LoadScreen(songSelect); }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index e95209f993..18e926ca5d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -43,11 +43,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private MultiplayerMatchSubScreen screen = null!; private BeatmapManager beatmaps = null!; private BeatmapSetInfo importedSet = null!; - - public TestSceneMultiplayerMatchSubScreen() - : base(false) - { - } + private Room room = null!; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -66,8 +62,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("load match", () => { - SelectedRoom.Value = new Room { Name = "Test Room" }; - LoadScreen(screen = new TestMultiplayerMatchSubScreen(SelectedRoom.Value!)); + room = new Room { Name = "Test Room" }; + LoadScreen(screen = new TestMultiplayerMatchSubScreen(room)); }); AddUntilStep("wait for load", () => screen.IsCurrentScreen()); @@ -78,7 +74,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add playlist item", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { @@ -97,7 +93,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add playlist item", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(new TestBeatmap(new TaikoRuleset().RulesetInfo).BeatmapInfo) { @@ -122,7 +118,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("set playlist", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { @@ -139,7 +135,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("set playlist", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo) { @@ -170,7 +166,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add playlist item with allowed mod", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { @@ -199,7 +195,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add playlist item with allowed mod", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { @@ -223,7 +219,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add playlist item with no allowed mods", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { @@ -246,7 +242,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add two playlist items", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo) { @@ -285,7 +281,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add playlist item", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 238a716f91..e7e6112297 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -25,9 +25,11 @@ namespace osu.Game.Tests.Visual.Multiplayer { public partial class TestSceneMultiplayerParticipantsList : MultiplayerTestScene { - [SetUpSteps] - public void SetupSteps() + public override void SetUpSteps() { + base.SetUpSteps(); + + JoinDefaultRoom(); createNewParticipantsList(); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs index 94dd114c32..1a5be48cad 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs @@ -22,6 +22,12 @@ namespace osu.Game.Tests.Visual.Multiplayer { private MultiplayerPlayer player = null!; + public override void SetUpSteps() + { + base.SetUpSteps(); + JoinDefaultRoom(); + } + [Test] public void TestGameplay() { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 77b75f407b..406c6cacae 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -32,6 +32,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private BeatmapManager beatmaps = null!; private BeatmapSetInfo importedSet = null!; private BeatmapInfo importedBeatmap = null!; + private Room room = null!; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -46,9 +47,11 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); + JoinDefaultRoom(r => room = r); + AddStep("create list", () => { - Child = list = new MultiplayerPlaylist(SelectedRoom.Value!) + Child = list = new MultiplayerPlaylist(room) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index 3ef2e4ecf4..5eba67bab5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -29,6 +29,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private BeatmapManager beatmaps = null!; private BeatmapSetInfo importedSet = null!; private BeatmapInfo importedBeatmap = null!; + private Room room = null!; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -42,9 +43,11 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); + JoinDefaultRoom(r => room = r); + AddStep("create playlist", () => { - Child = playlist = new MultiplayerQueueList(SelectedRoom.Value!) + Child = playlist = new MultiplayerQueueList(room) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index 1429f86164..f92721b04b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -28,6 +28,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { private MultiplayerSpectateButton spectateButton = null!; private MatchStartControl startControl = null!; + private Room room = null!; private BeatmapSetInfo importedSet = null!; private BeatmapManager beatmaps = null!; @@ -46,11 +47,11 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); + JoinDefaultRoom(r => room = r); + AddStep("create button", () => { - PlaylistItem item = SelectedRoom.Value!.Playlist.First(); - - AvailabilityTracker.SelectedItem.Value = item; + AvailabilityTracker.SelectedItem.Value = room.Playlist.First(); importedSet = beatmaps.GetAllUsableBeatmapSets().First(); Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()); @@ -69,14 +70,14 @@ namespace osu.Game.Tests.Visual.Multiplayer Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(200, 50), - SelectedItem = new Bindable(item) + SelectedItem = new Bindable(room.Playlist.First()) }, startControl = new MatchStartControl { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(200, 50), - SelectedItem = new Bindable(item) + SelectedItem = new Bindable(room.Playlist.First()) } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs index 726d0ac9f9..7c73fb8321 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs @@ -27,6 +27,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { private BeatmapManager manager = null!; private TestPlaylistsSongSelect songSelect = null!; + private Room room = null!; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -51,13 +52,13 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("reset", () => { - SelectedRoom.Value = new Room(); + room = new Room(); Ruleset.Value = new OsuRuleset().RulesetInfo; Beatmap.SetDefault(); SelectedMods.Value = Array.Empty(); }); - AddStep("create song select", () => LoadScreen(songSelect = new TestPlaylistsSongSelect(SelectedRoom.Value!))); + AddStep("create song select", () => LoadScreen(songSelect = new TestPlaylistsSongSelect(room))); AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded); } @@ -65,14 +66,14 @@ namespace osu.Game.Tests.Visual.Multiplayer public void TestItemAddedIfEmptyOnStart() { AddStep("finalise selection", () => songSelect.FinaliseSelection()); - AddAssert("playlist has 1 item", () => SelectedRoom.Value!.Playlist.Count == 1); + AddAssert("playlist has 1 item", () => room.Playlist.Count == 1); } [Test] public void TestItemAddedWhenCreateNewItemClicked() { AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!()); - AddAssert("playlist has 1 item", () => SelectedRoom.Value!.Playlist.Count == 1); + AddAssert("playlist has 1 item", () => room.Playlist.Count == 1); } [Test] @@ -80,7 +81,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!()); AddStep("finalise selection", () => songSelect.FinaliseSelection()); - AddAssert("playlist has 1 item", () => SelectedRoom.Value!.Playlist.Count == 1); + AddAssert("playlist has 1 item", () => room.Playlist.Count == 1); } [Test] @@ -88,7 +89,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!()); AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!()); - AddAssert("playlist has 2 items", () => SelectedRoom.Value!.Playlist.Count == 2); + AddAssert("playlist has 2 items", () => room.Playlist.Count == 2); } [Test] @@ -96,10 +97,10 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!()); AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!()); - AddStep("rearrange", () => SelectedRoom.Value!.Playlist = SelectedRoom.Value!.Playlist.Skip(1).Append(SelectedRoom.Value!.Playlist[0]).ToArray()); + AddStep("rearrange", () => room.Playlist = room.Playlist.Skip(1).Append(room.Playlist[0]).ToArray()); AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!()); - AddAssert("new item has id 2", () => SelectedRoom.Value!.Playlist.Last().ID == 2); + AddAssert("new item has id 2", () => room.Playlist.Last().ID == 2); } /// @@ -115,13 +116,13 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("item 1 has rate 1.5", () => { - var mod = (OsuModDoubleTime)SelectedRoom.Value!.Playlist.First().RequiredMods[0].ToMod(new OsuRuleset()); + var mod = (OsuModDoubleTime)room.Playlist.First().RequiredMods[0].ToMod(new OsuRuleset()); return Precision.AlmostEquals(1.5, mod.SpeedChange.Value); }); AddAssert("item 2 has rate 2", () => { - var mod = (OsuModDoubleTime)SelectedRoom.Value!.Playlist.Last().RequiredMods[0].ToMod(new OsuRuleset()); + var mod = (OsuModDoubleTime)room.Playlist.Last().RequiredMods[0].ToMod(new OsuRuleset()); return Precision.AlmostEquals(2, mod.SpeedChange.Value); }); } @@ -147,7 +148,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("change stored mod rate", () => mod.SpeedChange.Value = 2); AddAssert("item has rate 1.5", () => { - var m = (OsuModDoubleTime)SelectedRoom.Value!.Playlist.First().RequiredMods[0].ToMod(new OsuRuleset()); + var m = (OsuModDoubleTime)room.Playlist.First().RequiredMods[0].ToMod(new OsuRuleset()); return Precision.AlmostEquals(1.5, m.SpeedChange.Value); }); } diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs index f7b0bc0d58..c714c39e22 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs @@ -18,6 +18,7 @@ namespace osu.Game.Tests.Visual.Playlists public partial class TestScenePlaylistsMatchSettingsOverlay : OnlinePlayTestScene { private TestRoomSettings settings = null!; + private Room room = null!; private Func? handleRequest; public override void SetUpSteps() @@ -47,9 +48,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("create overlay", () => { - SelectedRoom.Value = new Room(); - - Child = settings = new TestRoomSettings(SelectedRoom.Value!) + Child = settings = new TestRoomSettings(room = new Room()) { RelativeSizeAxes = Axes.Both, State = { Value = Visibility.Visible } @@ -62,19 +61,19 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("clear name and beatmap", () => { - SelectedRoom.Value!.Name = ""; - SelectedRoom.Value!.Playlist = []; + room.Name = ""; + room.Playlist = []; }); AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value); - AddStep("set name", () => SelectedRoom.Value!.Name = "Room name"); + AddStep("set name", () => room.Name = "Room name"); AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value); - AddStep("set beatmap", () => SelectedRoom.Value!.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)]); + AddStep("set beatmap", () => room.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)]); AddAssert("button enabled", () => settings.ApplyButton.Enabled.Value); - AddStep("clear name", () => SelectedRoom.Value!.Name = ""); + AddStep("clear name", () => room.Name = ""); AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value); } @@ -90,7 +89,7 @@ namespace osu.Game.Tests.Visual.Playlists { settings.NameField.Current.Value = expected_name; settings.DurationField.Current.Value = expectedDuration; - SelectedRoom.Value!.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)]; + room.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)]; handleRequest = r => { @@ -115,8 +114,8 @@ namespace osu.Game.Tests.Visual.Playlists { var beatmap = CreateBeatmap(Ruleset.Value).BeatmapInfo; - SelectedRoom.Value!.Name = "Test Room"; - SelectedRoom.Value!.Playlist = [new PlaylistItem(beatmap)]; + room.Name = "Test Room"; + room.Playlist = [new PlaylistItem(beatmap)]; errorMessage = $"{not_found_prefix} {beatmap.OnlineID}"; @@ -124,13 +123,13 @@ namespace osu.Game.Tests.Visual.Playlists }); AddAssert("error not displayed", () => !settings.ErrorText.IsPresent); - AddAssert("playlist item valid", () => SelectedRoom.Value!.Playlist[0].Valid.Value); + AddAssert("playlist item valid", () => room.Playlist[0].Valid.Value); AddStep("create room", () => settings.ApplyButton.Action.Invoke()); AddAssert("error displayed", () => settings.ErrorText.IsPresent); AddAssert("error has custom text", () => settings.ErrorText.Text != errorMessage); - AddAssert("playlist item marked invalid", () => !SelectedRoom.Value!.Playlist[0].Valid.Value); + AddAssert("playlist item marked invalid", () => !room.Playlist[0].Valid.Value); } [Test] @@ -142,8 +141,8 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("setup", () => { - SelectedRoom.Value!.Name = "Test Room"; - SelectedRoom.Value!.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)]; + room.Name = "Test Room"; + room.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)]; handleRequest = _ => failText; }); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs index c60b208ffc..e1ec30d02a 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs @@ -14,13 +14,15 @@ namespace osu.Game.Tests.Visual.Playlists { public partial class TestScenePlaylistsParticipantsList : OnlinePlayTestScene { + private Room room = null!; + public override void SetUpSteps() { base.SetUpSteps(); - AddStep("create list", () => + AddStep("create room", () => { - SelectedRoom.Value = new Room + room = new Room { RoomID = 7, RecentParticipants = Enumerable.Range(0, 50).Select(_ => new APIUser @@ -38,7 +40,7 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("create component", () => { - Child = new ParticipantsDisplay(SelectedRoom.Value!, Direction.Horizontal) + Child = new ParticipantsDisplay(room, Direction.Horizontal) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -52,7 +54,7 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("create component", () => { - Child = new ParticipantsDisplay(SelectedRoom.Value!, Direction.Vertical) + Child = new ParticipantsDisplay(room, Direction.Vertical) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs index 0270840597..a748d61d44 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs @@ -35,6 +35,7 @@ namespace osu.Game.Tests.Visual.Playlists private BeatmapManager manager = null!; private TestPlaylistsRoomSubScreen match = null!; private BeatmapSetInfo importedBeatmap = null!; + private Room room = null!; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -47,11 +48,9 @@ namespace osu.Game.Tests.Visual.Playlists [SetUpSteps] public void SetupSteps() { - AddStep("set room", () => SelectedRoom.Value = new Room()); - importBeatmap(); - AddStep("load match", () => LoadScreen(match = new TestPlaylistsRoomSubScreen(SelectedRoom.Value!))); + AddStep("load match", () => LoadScreen(match = new TestPlaylistsRoomSubScreen(room = new Room()))); AddUntilStep("wait for load", () => match.IsCurrentScreen()); } @@ -119,7 +118,7 @@ namespace osu.Game.Tests.Visual.Playlists ]; }); - AddAssert("first playlist item selected", () => match.SelectedItem.Value == SelectedRoom.Value!.Playlist[0]); + AddAssert("first playlist item selected", () => match.SelectedItem.Value == room.Playlist[0]); } [Test] @@ -197,10 +196,9 @@ namespace osu.Game.Tests.Visual.Playlists AddUntilStep("match has correct beatmap", () => realHash == match.Beatmap.Value.BeatmapInfo.MD5Hash); } - private void setupAndCreateRoom(Action room) + private void setupAndCreateRoom(Action setupFunc) { - AddStep("setup room", () => room(SelectedRoom.Value!)); - + AddStep("setup room", () => setupFunc(room)); AddStep("click create button", () => { InputManager.MoveMouseTo(this.ChildrenOfType().Single()); diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index d1497d5142..97c213c7b1 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Game.Online.Rooms; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Visual.OnlinePlay; @@ -23,43 +24,43 @@ namespace osu.Game.Tests.Visual.Multiplayer public bool RoomJoined => MultiplayerClient.RoomJoined; - private readonly bool joinRoom; - - protected MultiplayerTestScene(bool joinRoom = true) + /// + /// Creates and joins a basic multiplayer room. + /// + /// A callback that may be used to further set up the room. + protected void JoinDefaultRoom(Action? setupFunc = null) { - this.joinRoom = joinRoom; - } - - protected virtual Room CreateRoom() - { - return new Room + AddStep("join room", () => { - Name = "test name", - Type = MatchType.HeadToHead, - Playlist = - [ - new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo) - { - RulesetID = Ruleset.Value.OnlineID - } - ] - }; - } - - public override void SetUpSteps() - { - base.SetUpSteps(); - - if (joinRoom) - { - AddStep("join room", () => + Room room = new Room { - SelectedRoom.Value = CreateRoom(); - MultiplayerClient.CreateRoom(SelectedRoom.Value).ConfigureAwait(false); - }); + Name = "test name", + Type = MatchType.HeadToHead, + Playlist = + [ + new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo) + { + RulesetID = Ruleset.Value.OnlineID + } + ] + }; - AddUntilStep("wait for room join", () => RoomJoined); - } + setupFunc?.Invoke(room); + + MultiplayerClient.CreateRoom(room).ConfigureAwait(false); + }); + + AddUntilStep("wait for room join", () => RoomJoined); + } + + /// + /// Creates and joins the given room. + /// + /// The room to create. If null, a default room will be created. + protected void JoinRoom(Room room) + { + AddStep("join room", () => MultiplayerClient.CreateRoom(room).ConfigureAwait(false)); + AddUntilStep("wait for room join", () => RoomJoined); } protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies(); diff --git a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs index 5780cf6eff..60730ee9a4 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Bindables; using osu.Game.Database; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay; @@ -13,11 +12,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// public interface IOnlinePlayTestSceneDependencies { - /// - /// The cached . - /// - Bindable SelectedRoom { get; } - /// /// The cached . /// diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs index c3a5e1c3ec..ce8df36590 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs @@ -3,7 +3,6 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; @@ -22,7 +21,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// public abstract partial class OnlinePlayTestScene : ScreenTestScene, IOnlinePlayTestSceneDependencies { - public Bindable SelectedRoom => OnlinePlayDependencies.SelectedRoom; public OngoingOperationTracker OngoingOperationTracker => OnlinePlayDependencies.OngoingOperationTracker; public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker => OnlinePlayDependencies.AvailabilityTracker; public TestUserLookupCache UserLookupCache => OnlinePlayDependencies.UserLookupCache; diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs index cc448beea0..9537c7958c 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Database; using osu.Game.Online.Rooms; @@ -18,7 +17,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// public class OnlinePlayTestSceneDependencies : IReadOnlyDependencyContainer, IOnlinePlayTestSceneDependencies { - public Bindable SelectedRoom { get; } public OngoingOperationTracker OngoingOperationTracker { get; } public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker { get; } public TestRoomRequestsHandler RequestsHandler { get; } @@ -35,7 +33,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay public OnlinePlayTestSceneDependencies() { - SelectedRoom = new Bindable(); RequestsHandler = new TestRoomRequestsHandler(); OngoingOperationTracker = new OngoingOperationTracker(); AvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); @@ -45,7 +42,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay dependencies = new DependencyContainer(); CacheAs(RequestsHandler); - CacheAs(SelectedRoom); CacheAs(OngoingOperationTracker); CacheAs(AvailabilityTracker); CacheAs(new OverlayColourProvider(OverlayColourScheme.Plum)); From d923a478e9a044432cd611424ff57b5862d69865 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Feb 2025 00:04:33 +0900 Subject: [PATCH 468/801] Remove unused method --- .../Tests/Visual/Multiplayer/MultiplayerTestScene.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index 97c213c7b1..8150807f4f 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -53,16 +53,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for room join", () => RoomJoined); } - /// - /// Creates and joins the given room. - /// - /// The room to create. If null, a default room will be created. - protected void JoinRoom(Room room) - { - AddStep("join room", () => MultiplayerClient.CreateRoom(room).ConfigureAwait(false)); - AddUntilStep("wait for room join", () => RoomJoined); - } - protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies(); } } From 550d21df42a11202b932194e6e40bd90e384b2e3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Feb 2025 00:21:08 +0900 Subject: [PATCH 469/801] Fix failing tests due to text change --- .../Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 36f5bba384..37a3cc2faf 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -266,7 +266,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private void assertQueueTabCount(int count) { - string queueTabText = count > 0 ? $"Queue ({count})" : "Queue"; + string queueTabText = count > 0 ? $"Up next ({count})" : "Up next"; AddUntilStep($"Queue tab shows \"{queueTabText}\"", () => { return this.ChildrenOfType.OsuTabItem>() From 7d6701f8e9383f1a1790103f8b29d598fdc13bb7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Feb 2025 01:20:42 +0900 Subject: [PATCH 470/801] Attempt to fix intermittent collections test --- .../Visual/Collections/TestSceneManageCollectionsDialog.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index 0f2f716a07..60675018e9 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -376,6 +376,6 @@ namespace osu.Game.Tests.Visual.Collections private void assertCollectionName(int index, string name) => AddUntilStep($"item {index + 1} has correct name", - () => dialog.ChildrenOfType().Single().OrderedItems.ElementAt(index).ChildrenOfType().First().Text == name); + () => dialog.ChildrenOfType().Single().OrderedItems.ElementAtOrDefault(index)?.ChildrenOfType().First().Text == name); } } From 315a480931e256c8e79a7193c54dad451e75cd94 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 00:03:30 +0900 Subject: [PATCH 471/801] Disallow focus on difficulty range slider Alternative to https://github.com/ppy/osu/pull/31749. Closes https://github.com/ppy/osu/issues/31559. --- osu.Game/Graphics/UserInterface/RangeSlider.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/RangeSlider.cs b/osu.Game/Graphics/UserInterface/RangeSlider.cs index 422c2ca4a3..acf10ce827 100644 --- a/osu.Game/Graphics/UserInterface/RangeSlider.cs +++ b/osu.Game/Graphics/UserInterface/RangeSlider.cs @@ -162,6 +162,8 @@ namespace osu.Game.Graphics.UserInterface protected partial class BoundSlider : RoundedSliderBar { + public override bool AcceptsFocus => false; + public new Nub Nub => base.Nub; public string? DefaultString; From 965038598975043dc148bf14b14a3adf6b688eb6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 00:06:20 +0900 Subject: [PATCH 472/801] Also disable sliderbar focus when disabled --- osu.Game/Graphics/UserInterface/OsuSliderBar.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs index 334fe343ae..4b52ac4a3a 100644 --- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs @@ -18,6 +18,8 @@ namespace osu.Game.Graphics.UserInterface public abstract partial class OsuSliderBar : SliderBar, IHasTooltip where T : struct, INumber, IMinMaxValue { + public override bool AcceptsFocus => !Current.Disabled; + public bool PlaySamplesOnAdjust { get; set; } = true; /// From 601e6d8a70e953b59f0066fbe6de75ed16091c09 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 13:53:42 +0900 Subject: [PATCH 473/801] Refactor pass for code quality --- .../AddPlaylistToCollectionButton.cs | 57 ++++++++++++------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index 8b5d5c752c..d4b89a5b28 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -2,10 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Database; @@ -17,7 +18,7 @@ using Realms; namespace osu.Game.Screens.OnlinePlay.Playlists { - public partial class AddPlaylistToCollectionButton : RoundedButton + public partial class AddPlaylistToCollectionButton : RoundedButton, IHasTooltip { private readonly Room room; private readonly Bindable downloadedBeatmapsCount = new Bindable(0); @@ -34,7 +35,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public AddPlaylistToCollectionButton(Room room) { this.room = room; - Text = formatButtonText(downloadedBeatmapsCount.Value, collectionExists.Value); } [BackgroundDependencyLoader] @@ -43,31 +43,31 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Action = () => { if (room.Playlist.Count == 0) - { - notifications?.Post(new SimpleErrorNotification { Text = "Cannot add local beatmaps" }); return; - } - var beatmaps = realmAccess.Realm.All().Filter(formatFilterQuery(room.Playlist)).ToList(); + var beatmaps = getBeatmapsForPlaylist(realmAccess.Realm).ToArray(); var collection = realmAccess.Realm.All().FirstOrDefault(c => c.Name == room.Name); if (collection == null) { - collection = new BeatmapCollection(room.Name, beatmaps.Select(i => i.MD5Hash).Distinct().ToList()); + collection = new BeatmapCollection(room.Name); realmAccess.Realm.Write(() => realmAccess.Realm.Add(collection)); notifications?.Post(new SimpleNotification { Text = $"Created new collection: {room.Name}" }); } else { - collection.ToLive(realmAccess).PerformWrite(c => - { - beatmaps = beatmaps.Where(i => !c.BeatmapMD5Hashes.Contains(i.MD5Hash)).ToList(); - foreach (var item in beatmaps) - c.BeatmapMD5Hashes.Add(item.MD5Hash); - notifications?.Post(new SimpleNotification { Text = $"Updated collection: {room.Name}" }); - }); + notifications?.Post(new SimpleNotification { Text = $"Updated collection: {room.Name}" }); } + + collection.ToLive(realmAccess).PerformWrite(c => + { + foreach (var item in beatmaps) + { + if (!c.BeatmapMD5Hashes.Contains(item.MD5Hash)) + c.BeatmapMD5Hashes.Add(item.MD5Hash); + } + }); }; } @@ -76,13 +76,28 @@ namespace osu.Game.Screens.OnlinePlay.Playlists base.LoadComplete(); if (room.Playlist.Count > 0) - beatmapSubscription = realmAccess.RegisterForNotifications(r => r.All().Filter(formatFilterQuery(room.Playlist)), (sender, _) => downloadedBeatmapsCount.Value = sender.Count); + { + beatmapSubscription = + realmAccess.RegisterForNotifications(getBeatmapsForPlaylist, (sender, _) => downloadedBeatmapsCount.Value = sender.Count); + } - collectionSubscription = realmAccess.RegisterForNotifications(r => r.All().Where(c => c.Name == room.Name), (sender, _) => collectionExists.Value = sender.Count > 0); + collectionSubscription = realmAccess.RegisterForNotifications(r => r.All().Where(c => c.Name == room.Name), (sender, _) => collectionExists.Value = sender.Any()); - downloadedBeatmapsCount.BindValueChanged(_ => Text = formatButtonText(downloadedBeatmapsCount.Value, collectionExists.Value)); + downloadedBeatmapsCount.BindValueChanged(_ => updateButtonText()); + collectionExists.BindValueChanged(_ => updateButtonText(), true); + } - collectionExists.BindValueChanged(_ => Text = formatButtonText(downloadedBeatmapsCount.Value, collectionExists.Value), true); + private IQueryable getBeatmapsForPlaylist(Realm r) + { + return r.All().Filter(string.Join(" OR ", room.Playlist.Select(item => $"(OnlineID == {item.Beatmap.OnlineID})").Distinct())); + } + + private void updateButtonText() + { + if (!collectionExists.Value) + Text = $"Create new collection with {downloadedBeatmapsCount.Value} beatmaps"; + else + Text = $"Update collection with {downloadedBeatmapsCount.Value} beatmaps"; } protected override void Dispose(bool isDisposing) @@ -93,8 +108,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists collectionSubscription?.Dispose(); } - private string formatFilterQuery(IReadOnlyList playlistItems) => string.Join(" OR ", playlistItems.Select(item => $"(OnlineID == {item.Beatmap.OnlineID})").Distinct()); - - private string formatButtonText(int count, bool collectionExists) => $"Add {count} {(count == 1 ? "beatmap" : "beatmaps")} to {(collectionExists ? "collection" : "new collection")}"; + public LocalisableString TooltipText => "Only downloaded beatmaps will be added to the collection"; } } From 8561df40c52bc60a16335e77b6024ae6d50c6984 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 14:30:33 +0900 Subject: [PATCH 474/801] Add better messaging and handling of edge cases --- .../AddPlaylistToCollectionButton.cs | 110 ++++++++++++------ 1 file changed, 77 insertions(+), 33 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index d4b89a5b28..595e9ad15c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -2,9 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics.Cursor; using osu.Framework.Localisation; using osu.Game.Beatmaps; @@ -21,13 +21,15 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public partial class AddPlaylistToCollectionButton : RoundedButton, IHasTooltip { private readonly Room room; - private readonly Bindable downloadedBeatmapsCount = new Bindable(0); - private readonly Bindable collectionExists = new Bindable(false); + private IDisposable? beatmapSubscription; private IDisposable? collectionSubscription; + private Live? collection; + private HashSet localBeatmapHashes = new HashSet(); + [Resolved] - private RealmAccess realmAccess { get; set; } = null!; + private RealmAccess realm { get; set; } = null!; [Resolved(canBeNull: true)] private INotificationOverlay? notifications { get; set; } @@ -45,29 +47,29 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (room.Playlist.Count == 0) return; - var beatmaps = getBeatmapsForPlaylist(realmAccess.Realm).ToArray(); + var beatmaps = getBeatmapsForPlaylist(realm.Realm).ToArray(); - var collection = realmAccess.Realm.All().FirstOrDefault(c => c.Name == room.Name); + int countBefore = 0; + int countAfter = 0; - if (collection == null) + collection ??= realm.Realm.Write(() => realm.Realm.Add(new BeatmapCollection(room.Name)).ToLive(realm)); + collection.PerformWrite(c => { - collection = new BeatmapCollection(room.Name); - realmAccess.Realm.Write(() => realmAccess.Realm.Add(collection)); - notifications?.Post(new SimpleNotification { Text = $"Created new collection: {room.Name}" }); - } - else - { - notifications?.Post(new SimpleNotification { Text = $"Updated collection: {room.Name}" }); - } + countBefore = c.BeatmapMD5Hashes.Count; - collection.ToLive(realmAccess).PerformWrite(c => - { foreach (var item in beatmaps) { if (!c.BeatmapMD5Hashes.Contains(item.MD5Hash)) c.BeatmapMD5Hashes.Add(item.MD5Hash); } + + countAfter = c.BeatmapMD5Hashes.Count; }); + + if (countBefore == 0) + notifications?.Post(new SimpleNotification { Text = $"Created new collection \"{room.Name}\" with {countAfter} beatmaps." }); + else + notifications?.Post(new SimpleNotification { Text = $"Added {countAfter - countBefore} beatmaps to collection \"{room.Name}\"." }); }; } @@ -75,16 +77,53 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { base.LoadComplete(); - if (room.Playlist.Count > 0) + Enabled.Value = false; + + if (room.Playlist.Count == 0) + return; + + beatmapSubscription = realm.RegisterForNotifications(getBeatmapsForPlaylist, (sender, _) => { - beatmapSubscription = - realmAccess.RegisterForNotifications(getBeatmapsForPlaylist, (sender, _) => downloadedBeatmapsCount.Value = sender.Count); - } + localBeatmapHashes = sender.Select(b => b.MD5Hash).ToHashSet(); + Schedule(updateButtonState); + }); - collectionSubscription = realmAccess.RegisterForNotifications(r => r.All().Where(c => c.Name == room.Name), (sender, _) => collectionExists.Value = sender.Any()); + collectionSubscription = realm.RegisterForNotifications(r => r.All().Where(c => c.Name == room.Name), (sender, _) => + { + collection = sender.FirstOrDefault()?.ToLive(realm); + Schedule(updateButtonState); + }); + } - downloadedBeatmapsCount.BindValueChanged(_ => updateButtonText()); - collectionExists.BindValueChanged(_ => updateButtonText(), true); + private void updateButtonState() + { + int countToAdd = getCountToBeAdded(); + + if (collection == null) + Text = $"Create new collection with {countToAdd} beatmaps"; + else + Text = $"Update collection with {countToAdd} beatmaps"; + + Enabled.Value = countToAdd > 0; + } + + private int getCountToBeAdded() + { + if (collection == null) + return localBeatmapHashes.Count; + + return collection.PerformRead(c => + { + int count = localBeatmapHashes.Count; + + foreach (string hash in localBeatmapHashes) + { + if (c.BeatmapMD5Hashes.Contains(hash)) + count--; + } + + return count; + }); } private IQueryable getBeatmapsForPlaylist(Realm r) @@ -92,14 +131,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return r.All().Filter(string.Join(" OR ", room.Playlist.Select(item => $"(OnlineID == {item.Beatmap.OnlineID})").Distinct())); } - private void updateButtonText() - { - if (!collectionExists.Value) - Text = $"Create new collection with {downloadedBeatmapsCount.Value} beatmaps"; - else - Text = $"Update collection with {downloadedBeatmapsCount.Value} beatmaps"; - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -108,6 +139,19 @@ namespace osu.Game.Screens.OnlinePlay.Playlists collectionSubscription?.Dispose(); } - public LocalisableString TooltipText => "Only downloaded beatmaps will be added to the collection"; + public LocalisableString TooltipText + { + get + { + if (Enabled.Value) + return string.Empty; + + int currentCollectionCount = collection?.PerformRead(c => c.BeatmapMD5Hashes.Count) ?? 0; + if (room.Playlist.DistinctBy(i => i.Beatmap.OnlineID).Count() == currentCollectionCount) + return "All beatmaps have been added!"; + + return "Download some beatmaps first."; + } + } } } From f9b7a8ed103e39fbd5a791699e5c99b366736766 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 14:49:25 +0900 Subject: [PATCH 475/801] Make realm operation asynchronous for good measure --- .../AddPlaylistToCollectionButton.cs | 49 +++++++++++++------ 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index 595e9ad15c..741173f9a3 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -47,14 +47,18 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (room.Playlist.Count == 0) return; - var beatmaps = getBeatmapsForPlaylist(realm.Realm).ToArray(); - int countBefore = 0; int countAfter = 0; - collection ??= realm.Realm.Write(() => realm.Realm.Add(new BeatmapCollection(room.Name)).ToLive(realm)); - collection.PerformWrite(c => + Text = "Updating collection..."; + Enabled.Value = false; + + realm.WriteAsync(r => { + var beatmaps = getBeatmapsForPlaylist(r).ToArray(); + var c = getCollectionsForPlaylist(r).FirstOrDefault() + ?? r.Add(new BeatmapCollection(room.Name)); + countBefore = c.BeatmapMD5Hashes.Count; foreach (var item in beatmaps) @@ -64,12 +68,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } countAfter = c.BeatmapMD5Hashes.Count; - }); - - if (countBefore == 0) - notifications?.Post(new SimpleNotification { Text = $"Created new collection \"{room.Name}\" with {countAfter} beatmaps." }); - else - notifications?.Post(new SimpleNotification { Text = $"Added {countAfter - countBefore} beatmaps to collection \"{room.Name}\"." }); + }).ContinueWith(_ => Schedule(() => + { + if (countBefore == 0) + notifications?.Post(new SimpleNotification { Text = $"Created new collection \"{room.Name}\" with {countAfter} beatmaps." }); + else + notifications?.Post(new SimpleNotification { Text = $"Added {countAfter - countBefore} beatmaps to collection \"{room.Name}\"." }); + })); }; } @@ -77,6 +82,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { base.LoadComplete(); + // will be updated via updateButtonState() when ready. Enabled.Value = false; if (room.Playlist.Count == 0) @@ -88,7 +94,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Schedule(updateButtonState); }); - collectionSubscription = realm.RegisterForNotifications(r => r.All().Where(c => c.Name == room.Name), (sender, _) => + collectionSubscription = realm.RegisterForNotifications(getCollectionsForPlaylist, (sender, _) => { collection = sender.FirstOrDefault()?.ToLive(realm); Schedule(updateButtonState); @@ -101,8 +107,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (collection == null) Text = $"Create new collection with {countToAdd} beatmaps"; + else if (hasAllItemsInCollection) + Text = "Collection complete!"; else - Text = $"Update collection with {countToAdd} beatmaps"; + Text = $"Add {countToAdd} beatmaps to collection"; Enabled.Value = countToAdd > 0; } @@ -126,11 +134,25 @@ namespace osu.Game.Screens.OnlinePlay.Playlists }); } + private IQueryable getCollectionsForPlaylist(Realm r) => r.All().Where(c => c.Name == room.Name); + private IQueryable getBeatmapsForPlaylist(Realm r) { return r.All().Filter(string.Join(" OR ", room.Playlist.Select(item => $"(OnlineID == {item.Beatmap.OnlineID})").Distinct())); } + private bool hasAllItemsInCollection + { + get + { + if (collection == null) + return false; + + return room.Playlist.DistinctBy(i => i.Beatmap.OnlineID).Count() == + collection.PerformRead(c => c.BeatmapMD5Hashes.Count); + } + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -146,8 +168,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (Enabled.Value) return string.Empty; - int currentCollectionCount = collection?.PerformRead(c => c.BeatmapMD5Hashes.Count) ?? 0; - if (room.Playlist.DistinctBy(i => i.Beatmap.OnlineID).Count() == currentCollectionCount) + if (hasAllItemsInCollection) return "All beatmaps have been added!"; return "Download some beatmaps first."; From 8ce28d56bbe245eed781e0055ea0befd72533f8e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 14:58:04 +0900 Subject: [PATCH 476/801] Fix tests not waiting enough --- .../Playlists/TestSceneAddPlaylistToCollectionButton.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs index f18488170d..46c93d9ae2 100644 --- a/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs +++ b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs @@ -77,9 +77,9 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("click button", () => InputManager.Click(MouseButton.Left)); - AddAssert("notification shown", () => notificationOverlay.AllNotifications.FirstOrDefault(n => n.Text.ToString().StartsWith("Created", StringComparison.Ordinal)) != null); + AddUntilStep("notification shown", () => notificationOverlay.AllNotifications.Any(n => n.Text.ToString().StartsWith("Created new collection", StringComparison.Ordinal))); - AddAssert("realm is updated", () => Realm.Realm.All().FirstOrDefault(c => c.Name == room.Name) != null); + AddUntilStep("realm is updated", () => Realm.Realm.All().FirstOrDefault(c => c.Name == room.Name) != null); } private void importBeatmap() => AddStep("import beatmap", () => From 3f3cb3df2a5b12ae2fb9cfa8b3db1daa076f9c44 Mon Sep 17 00:00:00 2001 From: Rudi Herouard Date: Mon, 20 Jan 2025 16:35:21 +0100 Subject: [PATCH 477/801] Fix toolbox settings hiding when dragging a slider --- osu.Game/Overlays/SettingsToolboxGroup.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs index f8cf218564..cf72125007 100644 --- a/osu.Game/Overlays/SettingsToolboxGroup.cs +++ b/osu.Game/Overlays/SettingsToolboxGroup.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Layout; using osu.Game.Graphics; @@ -54,6 +55,8 @@ namespace osu.Game.Overlays private IconButton expandButton = null!; + private InputManager inputManager = null!; + /// /// Create a new instance. /// @@ -125,6 +128,8 @@ namespace osu.Game.Overlays { base.LoadComplete(); + inputManager = GetContainingInputManager()!; + Expanded.BindValueChanged(_ => updateExpandedState(true)); updateExpandedState(false); @@ -172,7 +177,9 @@ namespace osu.Game.Overlays // potentially continuing to get processed while content has changed to autosize. content.ClearTransforms(); - if (Expanded.Value || IsHovered) + bool sliderDraggedInHimself = inputManager.DraggedDrawable.IsRootedAt(this); + + if (Expanded.Value || IsHovered || sliderDraggedInHimself) { content.AutoSizeAxes = Axes.Y; content.AutoSizeDuration = animate ? transition_duration : 0; From 9456e376f370b2ea0260a781fd6f90e1e87ad106 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 15:15:05 +0900 Subject: [PATCH 478/801] Fix expanded state not updating on drag end --- osu.Game/Overlays/SettingsToolboxGroup.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs index cf72125007..dd41f156f3 100644 --- a/osu.Game/Overlays/SettingsToolboxGroup.cs +++ b/osu.Game/Overlays/SettingsToolboxGroup.cs @@ -57,6 +57,8 @@ namespace osu.Game.Overlays private InputManager inputManager = null!; + private Drawable? draggedChild; + /// /// Create a new instance. /// @@ -161,6 +163,13 @@ namespace osu.Game.Overlays headerText.FadeTo(headerText.DrawWidth < DrawWidth ? 1 : 0, 150, Easing.OutQuint); headerTextVisibilityCache.Validate(); } + + // Dragged child finished its drag operation. + if (draggedChild != null && inputManager.DraggedDrawable != draggedChild) + { + draggedChild = null; + updateExpandedState(true); + } } protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) @@ -173,13 +182,17 @@ namespace osu.Game.Overlays private void updateExpandedState(bool animate) { + // before we collapse down, let's double check the user is not dragging a UI control contained within us. + if (inputManager.DraggedDrawable.IsRootedAt(this)) + { + draggedChild = inputManager.DraggedDrawable; + } + // clearing transforms is necessary to avoid a previous height transform // potentially continuing to get processed while content has changed to autosize. content.ClearTransforms(); - bool sliderDraggedInHimself = inputManager.DraggedDrawable.IsRootedAt(this); - - if (Expanded.Value || IsHovered || sliderDraggedInHimself) + if (Expanded.Value || IsHovered || draggedChild != null) { content.AutoSizeAxes = Axes.Y; content.AutoSizeDuration = animate ? transition_duration : 0; From 88188e8fcb4b15d0214d7106810f10b1f5c66fbe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 16:00:19 +0900 Subject: [PATCH 479/801] Add API models for teams --- .../Online/API/Requests/Responses/APITeam.cs | 23 +++++++++++++++++++ .../Online/API/Requests/Responses/APIUser.cs | 4 ++++ 2 files changed, 27 insertions(+) create mode 100644 osu.Game/Online/API/Requests/Responses/APITeam.cs diff --git a/osu.Game/Online/API/Requests/Responses/APITeam.cs b/osu.Game/Online/API/Requests/Responses/APITeam.cs new file mode 100644 index 0000000000..b4fcc2d26e --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APITeam.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + [JsonObject(MemberSerialization.OptIn)] + public class APITeam + { + [JsonProperty(@"id")] + public int Id { get; set; } = 1; + + [JsonProperty(@"name")] + public string Name { get; set; } = string.Empty; + + [JsonProperty(@"short_name")] + public string ShortName { get; set; } = string.Empty; + + [JsonProperty(@"flag_url")] + public string FlagUrl = string.Empty; + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index 30fceab852..92b7d9d874 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -55,6 +55,10 @@ namespace osu.Game.Online.API.Requests.Responses set => countryCodeString = value.ToString(); } + [JsonProperty(@"team")] + [CanBeNull] + public APITeam Team { get; set; } + [JsonProperty(@"profile_colour")] public string Colour; From 303961d1015f2e32549680a76fa2b68112236166 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 16:19:55 +0900 Subject: [PATCH 480/801] Add drawable implementations of team logo --- .../Online/Leaderboards/LeaderboardScore.cs | 6 ++ .../Overlays/BeatmapSet/Scores/ScoreTable.cs | 15 +++- .../BeatmapSet/Scores/TopScoreUserSection.cs | 27 +++++- .../Profile/Header/TopHeaderContainer.cs | 6 ++ .../Participants/ParticipantPanel.cs | 6 ++ .../Leaderboards/LeaderboardScoreV2.cs | 6 ++ .../OnlinePlay/TestRoomRequestsHandler.cs | 11 ++- .../Users/Drawables/UpdateableTeamFlag.cs | 86 +++++++++++++++++++ osu.Game/Users/UserGridPanel.cs | 3 +- osu.Game/Users/UserPanel.cs | 5 ++ osu.Game/Users/UserRankPanel.cs | 3 +- 11 files changed, 167 insertions(+), 7 deletions(-) create mode 100644 osu.Game/Users/Drawables/UpdateableTeamFlag.cs diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 52074119b8..11e1710e75 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -199,6 +199,12 @@ namespace osu.Game.Online.Leaderboards Origin = Anchor.CentreLeft, Size = new Vector2(28, 20), }, + new UpdateableTeamFlag(user.Team) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(40, 20), + }, new DateLabel(Score.Date) { Anchor = Anchor.CentreLeft, diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index c70c41feed..be6ad49150 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -160,7 +160,20 @@ namespace osu.Game.Overlays.BeatmapSet.Scores { Size = new Vector2(19, 14), }, - username, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4), + Children = new Drawable[] + { + new UpdateableTeamFlag(score.User.Team) + { + Size = new Vector2(28, 14), + }, + username, + } + }, #pragma warning disable 618 new StatisticText(score.MaxCombo, score.BeatmapInfo!.MaxCombo, @"0\x"), #pragma warning restore 618 diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs index 13ba9fb74b..14c9bedc67 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs @@ -27,7 +27,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores private readonly UpdateableAvatar avatar; private readonly LinkFlowContainer usernameText; private readonly DrawableDate achievedOn; + private readonly UpdateableFlag flag; + private readonly UpdateableTeamFlag teamFlag; public TopScoreUserSection() { @@ -112,12 +114,30 @@ namespace osu.Game.Overlays.BeatmapSet.Scores }, } }, - flag = new UpdateableFlag + new FillFlowContainer { + AutoSizeAxes = Axes.Both, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Size = new Vector2(19, 14), - Margin = new MarginPadding { Top = 3 }, // makes spacing look more even + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4), + Children = new Drawable[] + { + flag = new UpdateableFlag + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(19, 14), + Margin = new MarginPadding { Top = 3 }, // makes spacing look more even + }, + teamFlag = new UpdateableTeamFlag + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(28, 14), + Margin = new MarginPadding { Top = 3 }, // makes spacing look more even + }, + } }, } } @@ -139,6 +159,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores { avatar.User = value.User; flag.CountryCode = value.User.CountryCode; + teamFlag.Team = value.User.Team; achievedOn.Date = value.Date; usernameText.Clear(); diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index ba2cd5b705..5f404375e6 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -42,6 +42,7 @@ namespace osu.Game.Overlays.Profile.Header private ExternalLinkButton openUserExternally = null!; private OsuSpriteText titleText = null!; private UpdateableFlag userFlag = null!; + private UpdateableTeamFlag teamFlag = null!; private OsuHoverContainer userCountryContainer = null!; private OsuSpriteText userCountryText = null!; private GroupBadgeFlow groupBadgeFlow = null!; @@ -166,6 +167,10 @@ namespace osu.Game.Overlays.Profile.Header { Size = new Vector2(28, 20), }, + teamFlag = new UpdateableTeamFlag + { + Size = new Vector2(40, 20), + }, userCountryContainer = new OsuHoverContainer { AutoSizeAxes = Axes.Both, @@ -215,6 +220,7 @@ namespace osu.Game.Overlays.Profile.Header usernameText.Text = user?.Username ?? string.Empty; openUserExternally.Link = $@"{api.Endpoints.WebsiteUrl}/users/{user?.Id ?? 0}"; userFlag.CountryCode = user?.CountryCode ?? default; + teamFlag.Team = user?.Team; userCountryText.Text = (user?.CountryCode ?? default).GetDescription(); userCountryContainer.Action = () => rankingsOverlay?.ShowCountry(user?.CountryCode ?? default); supporterTag.SupportLevel = user?.SupportLevel ?? 0; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 0fa2be44f3..0cedfb9909 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -140,6 +140,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants Size = new Vector2(28, 20), CountryCode = user?.CountryCode ?? default }, + new UpdateableTeamFlag(user?.Team) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(40, 20), + }, new OsuSpriteText { Anchor = Anchor.CentreLeft, diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs index a2253b413c..978d6eca32 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs @@ -339,6 +339,12 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Origin = Anchor.CentreLeft, Size = new Vector2(24, 16), }, + new UpdateableTeamFlag(user.Team) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(40, 20), + }, new DateLabel(score.Date) { Anchor = Anchor.CentreLeft, diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index c9149bda22..d73fd5ab22 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Newtonsoft.Json; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Online.API; @@ -221,7 +222,15 @@ namespace osu.Game.Tests.Visual.OnlinePlay : new APIUser { Id = id, - Username = $"User {id}" + Username = $"User {id}", + Team = RNG.NextBool() + ? new APITeam + { + Name = "Collective Wangs", + ShortName = "WANG", + FlagUrl = "https://assets.ppy.sh/teams/logo/1/wanglogo.jpg", + } + : null, }) .Where(u => u != null).ToList(), }); diff --git a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs new file mode 100644 index 0000000000..486cb697a1 --- /dev/null +++ b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs @@ -0,0 +1,86 @@ +// 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.Cursor; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Localisation; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Users.Drawables +{ + /// + /// A team logo which can update to a new team when needed. + /// + public partial class UpdateableTeamFlag : ModelBackedDrawable + { + public APITeam? Team + { + get => Model; + set => Model = value; + } + + protected override double LoadDelay => 200; + + public UpdateableTeamFlag(APITeam? team = null) + { + Team = team; + + Masking = true; + } + + protected override Drawable? CreateDrawable(APITeam? team) + { + if (team == null) + return Empty(); + + return new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new TeamFlag(team) + { + RelativeSizeAxes = Axes.Both + }, + new HoverClickSounds() + } + }; + } + + // Generally we just want team flags to disappear if the user doesn't have one. + // This also handles fill flow cases and avoids spacing being added for non-displaying flags. + public override bool IsPresent => base.IsPresent && Team != null; + + protected override void Update() + { + base.Update(); + + CornerRadius = DrawHeight / 8; + } + + public partial class TeamFlag : Sprite, IHasTooltip + { + private readonly APITeam team; + + public LocalisableString TooltipText { get; } + + public TeamFlag(APITeam team) + { + this.team = team; + TooltipText = team.Name; + } + + [BackgroundDependencyLoader] + private void load(LargeTextureStore textures) + { + if (!string.IsNullOrEmpty(team.Name)) + Texture = textures.Get(team.FlagUrl); + } + } + } +} diff --git a/osu.Game/Users/UserGridPanel.cs b/osu.Game/Users/UserGridPanel.cs index fce543415d..f62c9ab4e7 100644 --- a/osu.Game/Users/UserGridPanel.cs +++ b/osu.Game/Users/UserGridPanel.cs @@ -82,9 +82,10 @@ namespace osu.Game.Users AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(6), - Children = new Drawable[] + Children = new[] { CreateFlag(), + CreateTeamLogo(), // supporter icon is being added later } } diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 0d3ea52611..09a5cb414f 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -130,6 +130,11 @@ namespace osu.Game.Users Action = Action, }; + protected Drawable CreateTeamLogo() => new UpdateableTeamFlag(User.Team) + { + Size = new Vector2(52, 26), + }; + public MenuItem[] ContextMenuItems { get diff --git a/osu.Game/Users/UserRankPanel.cs b/osu.Game/Users/UserRankPanel.cs index 5e3ae172be..ff8adf055c 100644 --- a/osu.Game/Users/UserRankPanel.cs +++ b/osu.Game/Users/UserRankPanel.cs @@ -147,9 +147,10 @@ namespace osu.Game.Users AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(6), - Children = new Drawable[] + Children = new[] { CreateFlag(), + CreateTeamLogo(), // supporter icon is being added later } } From 44faabddcd79b0ada819d03cb10044b377e5fe89 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 16:41:59 +0900 Subject: [PATCH 481/801] Add skinnable team flag --- osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs | 56 +++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs diff --git a/osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs b/osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs new file mode 100644 index 0000000000..f8ef03c58c --- /dev/null +++ b/osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs @@ -0,0 +1,56 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Skinning; +using osu.Game.Users.Drawables; +using osuTK; + +namespace osu.Game.Screens.Play.HUD +{ + public partial class PlayerTeamFlag : CompositeDrawable, ISerialisableDrawable + { + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => false; + + private readonly UpdateableTeamFlag flag; + + private const float default_size = 40f; + + [Resolved] + private GameplayState? gameplayState { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private IBindable? apiUser; + + public PlayerTeamFlag() + { + Size = new Vector2(default_size, default_size / 2f); + + InternalChild = flag = new UpdateableTeamFlag + { + RelativeSizeAxes = Axes.Both, + }; + } + + [BackgroundDependencyLoader] + private void load() + { + if (gameplayState != null) + flag.Team = gameplayState.Score.ScoreInfo.User.Team; + else + { + apiUser = api.LocalUser.GetBoundCopy(); + apiUser.BindValueChanged(u => flag.Team = u.NewValue.Team, true); + } + } + + public bool UsesFixedAnchor { get; set; } + } +} From 4184dd27180b3ae8407c8d06d86894950e8b1b67 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 17:18:25 +0900 Subject: [PATCH 482/801] Give more breathing room in leaderboard scores --- .../Online/Leaderboards/LeaderboardScore.cs | 46 ++++++++++++------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 11e1710e75..0181c28218 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -189,7 +189,7 @@ namespace osu.Game.Online.Leaderboards RelativeSizeAxes = Axes.Y, Direction = FillDirection.Horizontal, Spacing = new Vector2(5f, 0f), - Width = 87f, + Width = 114f, Masking = true, Children = new Drawable[] { @@ -212,15 +212,6 @@ namespace osu.Game.Online.Leaderboards }, }, }, - new FillFlowContainer - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Left = edge_margin }, - Children = statisticsLabels - }, }, }, }, @@ -240,6 +231,7 @@ namespace osu.Game.Online.Leaderboards GlowColour = Color4Extensions.FromHex(@"83ccfa"), Current = scoreManager.GetBindableTotalScoreString(Score), Font = OsuFont.Numeric.With(size: 23), + Margin = new MarginPadding { Top = 1 }, }, RankContainer = new Container { @@ -256,13 +248,32 @@ namespace osu.Game.Online.Leaderboards }, }, }, - modsContainer = new FillFlowContainer + new FillFlowContainer { + AutoSizeAxes = Axes.Both, Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - ChildrenEnumerable = Score.Mods.AsOrdered().Select(mod => new ModIcon(mod) { Scale = new Vector2(0.375f) }) + Children = new Drawable[] + { + new FillFlowContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Margin = new MarginPadding { Left = edge_margin }, + Children = statisticsLabels + }, + modsContainer = new FillFlowContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + ChildrenEnumerable = Score.Mods.AsOrdered().Select(mod => new ModIcon(mod) { Scale = new Vector2(0.34f) }) + }, + } }, }, }, @@ -330,7 +341,7 @@ namespace osu.Game.Online.Leaderboards private partial class ScoreComponentLabel : Container, IHasTooltip { - private const float icon_size = 20; + private const float icon_size = 16; private readonly FillFlowContainer content; public override bool Contains(Vector2 screenSpacePos) => content.Contains(screenSpacePos); @@ -346,7 +357,7 @@ namespace osu.Game.Online.Leaderboards { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Padding = new MarginPadding { Right = 10 }, + Padding = new MarginPadding { Right = 5 }, Children = new Drawable[] { new Container @@ -381,7 +392,8 @@ namespace osu.Game.Online.Leaderboards Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Text = statistic.Value, - Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold, fixedWidth: true) + Spacing = new Vector2(-1, 0), + Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, fixedWidth: true) }, }, }; @@ -412,7 +424,7 @@ namespace osu.Game.Online.Leaderboards public DateLabel(DateTimeOffset date) : base(date) { - Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold, italics: true); + Font = OsuFont.GetFont(size: 13, weight: FontWeight.Bold, italics: true); } protected override string Format() => Date.ToShortRelativeTime(TimeSpan.FromSeconds(30)); From 4e043e7cabc242b051275e84b17b88553c28844b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 18:35:27 +0900 Subject: [PATCH 483/801] Change how values are applied to (hopefully) simplify things --- osu.Game/Graphics/Containers/ScalingContainer.cs | 3 ++- osu.Game/OsuGame.cs | 6 ++++-- osu.iOS/OsuGameIOS.cs | 3 ++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index ac76c0546b..2a5ce23b64 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -116,7 +116,8 @@ namespace osu.Game.Graphics.Containers protected override void Update() { - TargetDrawSize = new Vector2(1024, 1024 / (game?.BaseAspectRatio ?? 1f)); + if (game != null) + TargetDrawSize = game.ScalingContainerTargetDrawSize; Scale = new Vector2(CurrentScale); Size = new Vector2(1 / CurrentScale); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index ecc71822af..d379392a7d 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -72,6 +72,7 @@ using osu.Game.Skinning; using osu.Game.Updater; using osu.Game.Users; using osu.Game.Utils; +using osuTK; using osuTK.Graphics; using Sentry; @@ -814,9 +815,10 @@ namespace osu.Game protected virtual UpdateManager CreateUpdateManager() => new UpdateManager(); /// - /// The base aspect ratio to use in all s. + /// Adjust the globally applied in every . + /// Useful for changing how the game handles different aspect ratios. /// - protected internal virtual float BaseAspectRatio => 4f / 3f; + protected internal virtual Vector2 ScalingContainerTargetDrawSize { get; } = new Vector2(1024, 768); protected override Container CreateScalingContainer() => new ScalingContainer(ScalingMode.Everything); diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index 64b2292d62..883e89e38a 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -11,6 +11,7 @@ using osu.Game; using osu.Game.Screens; using osu.Game.Updater; using osu.Game.Utils; +using osuTK; using UIKit; namespace osu.iOS @@ -22,7 +23,7 @@ namespace osu.iOS public override bool HideUnlicensedContent => true; - protected override float BaseAspectRatio => (float)(UIScreen.MainScreen.Bounds.Width / UIScreen.MainScreen.Bounds.Height); + protected override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth); public OsuGameIOS(AppDelegate appDelegate) { From 248bf43ec9c84d2ea31eb0c51cf814760d79e035 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 18:35:43 +0900 Subject: [PATCH 484/801] Apply nullability to `ScalingContainer` --- .../Graphics/Containers/ScalingContainer.cs | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index 2a5ce23b64..9d2a1c16af 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -1,9 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -27,17 +24,17 @@ namespace osu.Game.Graphics.Containers { internal const float TRANSITION_DURATION = 500; - private Bindable sizeX; - private Bindable sizeY; - private Bindable posX; - private Bindable posY; - private Bindable applySafeAreaPadding; + private Bindable sizeX = null!; + private Bindable sizeY = null!; + private Bindable posX = null!; + private Bindable posY = null!; + private Bindable applySafeAreaPadding = null!; - private Bindable safeAreaPadding; + private Bindable safeAreaPadding = null!; private readonly ScalingMode? targetMode; - private Bindable scalingMode; + private Bindable scalingMode = null!; private readonly Container content; protected override Container Content => content; @@ -46,9 +43,9 @@ namespace osu.Game.Graphics.Containers private readonly Container sizableContainer; - private BackgroundScreenStack backgroundStack; + private BackgroundScreenStack? backgroundStack; - private Bindable scalingMenuBackgroundDim; + private Bindable scalingMenuBackgroundDim = null!; private RectangleF? customRect; private bool customRectIsRelativePosition; @@ -89,7 +86,8 @@ namespace osu.Game.Graphics.Containers public partial class ScalingDrawSizePreservingFillContainer : DrawSizePreservingFillContainer { private readonly bool applyUIScale; - private Bindable uiScale; + + private Bindable? uiScale; protected float CurrentScale { get; private set; } = 1; @@ -101,8 +99,7 @@ namespace osu.Game.Graphics.Containers } [Resolved(canBeNull: true)] - [CanBeNull] - private OsuGame game { get; set; } + private OsuGame? game { get; set; } [BackgroundDependencyLoader] private void load(OsuConfigManager osuConfig) @@ -240,13 +237,13 @@ namespace osu.Game.Graphics.Containers private partial class SizeableAlwaysInputContainer : Container { [Resolved] - private GameHost host { get; set; } + private GameHost host { get; set; } = null!; [Resolved] - private ISafeArea safeArea { get; set; } + private ISafeArea safeArea { get; set; } = null!; [Resolved] - private OsuConfigManager config { get; set; } + private OsuConfigManager config { get; set; } = null!; private readonly bool confineHostCursor; private readonly LayoutValue cursorRectCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); From 26a2d0394e5d39de630524166691d86a929a501f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 19:04:26 +0900 Subject: [PATCH 485/801] Invalidate drawable on potential presence change --- osu.Game/Users/Drawables/UpdateableTeamFlag.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs index 486cb697a1..1efde2af68 100644 --- a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs +++ b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs @@ -21,7 +21,11 @@ namespace osu.Game.Users.Drawables public APITeam? Team { get => Model; - set => Model = value; + set + { + Model = value; + Invalidate(Invalidation.Presence); + } } protected override double LoadDelay => 200; From 82c16dee60e1e8702d95657d654d36934c083ac2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 19:05:13 +0900 Subject: [PATCH 486/801] Add missing `LongRunningLoad` attribute --- .../Users/Drawables/UpdateableTeamFlag.cs | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs index 1efde2af68..9c2bbb7e3e 100644 --- a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs +++ b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs @@ -42,18 +42,7 @@ namespace osu.Game.Users.Drawables if (team == null) return Empty(); - return new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new TeamFlag(team) - { - RelativeSizeAxes = Axes.Both - }, - new HoverClickSounds() - } - }; + return new TeamFlag(team) { RelativeSizeAxes = Axes.Both }; } // Generally we just want team flags to disappear if the user doesn't have one. @@ -67,7 +56,8 @@ namespace osu.Game.Users.Drawables CornerRadius = DrawHeight / 8; } - public partial class TeamFlag : Sprite, IHasTooltip + [LongRunningLoad] + public partial class TeamFlag : CompositeDrawable, IHasTooltip { private readonly APITeam team; @@ -82,8 +72,15 @@ namespace osu.Game.Users.Drawables [BackgroundDependencyLoader] private void load(LargeTextureStore textures) { - if (!string.IsNullOrEmpty(team.Name)) - Texture = textures.Get(team.FlagUrl); + InternalChildren = new Drawable[] + { + new HoverClickSounds(), + new Sprite + { + RelativeSizeAxes = Axes.Both, + Texture = textures.Get(team.FlagUrl) + } + }; } } } From b86eeabef08d8eb3d45848939f2ea36a44790cc7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 19:07:02 +0900 Subject: [PATCH 487/801] Fix one more misalignment on leaderboard scores --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 0181c28218..fc30f158f0 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -180,6 +180,7 @@ namespace osu.Game.Online.Leaderboards Height = 28, Direction = FillDirection.Horizontal, Spacing = new Vector2(10f, 0f), + Margin = new MarginPadding { Bottom = -2 }, Children = new Drawable[] { flagBadgeAndDateContainer = new FillFlowContainer From 1b5101ed5e155c19c0a37894ed3c5ea374ec55a5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 19:30:23 +0900 Subject: [PATCH 488/801] Add team flag display to rankings overlays --- osu.Game/Overlays/KudosuTable.cs | 4 ++-- .../Overlays/Rankings/Tables/CountriesTable.cs | 2 +- .../Overlays/Rankings/Tables/RankingsTable.cs | 17 +++++++---------- .../Overlays/Rankings/Tables/UserBasedTable.cs | 6 ++++-- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/KudosuTable.cs b/osu.Game/Overlays/KudosuTable.cs index 93884435a4..d6eaf586b9 100644 --- a/osu.Game/Overlays/KudosuTable.cs +++ b/osu.Game/Overlays/KudosuTable.cs @@ -80,7 +80,7 @@ namespace osu.Game.Overlays protected override CountryCode GetCountryCode(APIUser item) => item.CountryCode; - protected override Drawable CreateFlagContent(APIUser item) + protected override Drawable[] CreateFlagContent(APIUser item) { var username = new LinkFlowContainer(t => t.Font = OsuFont.GetFont(size: TEXT_SIZE, italics: true)) { @@ -89,7 +89,7 @@ namespace osu.Game.Overlays TextAnchor = Anchor.CentreLeft }; username.AddUserLink(item); - return username; + return [username]; } } } diff --git a/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs b/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs index fb3e58d2ac..733aa7ca54 100644 --- a/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs +++ b/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs @@ -34,7 +34,7 @@ namespace osu.Game.Overlays.Rankings.Tables protected override CountryCode GetCountryCode(CountryStatistics item) => item.Code; - protected override Drawable CreateFlagContent(CountryStatistics item) => new CountryName(item.Code); + protected override Drawable[] CreateFlagContent(CountryStatistics item) => [new CountryName(item.Code)]; protected override Drawable[] CreateAdditionalContent(CountryStatistics item) => new Drawable[] { diff --git a/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs b/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs index b9f7e443ca..f4ed41800a 100644 --- a/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs +++ b/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs @@ -80,7 +80,7 @@ namespace osu.Game.Overlays.Rankings.Tables protected abstract CountryCode GetCountryCode(TModel item); - protected abstract Drawable CreateFlagContent(TModel item); + protected abstract Drawable[] CreateFlagContent(TModel item); private OsuSpriteText createIndexDrawable(int index) => new RowText { @@ -92,16 +92,13 @@ namespace osu.Game.Overlays.Rankings.Tables { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), + Spacing = new Vector2(5, 0), Margin = new MarginPadding { Bottom = row_spacing }, - Children = new[] - { - new UpdateableFlag(GetCountryCode(item)) - { - Size = new Vector2(28, 20), - }, - CreateFlagContent(item) - } + Children = + [ + new UpdateableFlag(GetCountryCode(item)) { Size = new Vector2(28, 20) }, + ..CreateFlagContent(item) + ] }; protected class RankingsTableColumn : TableColumn diff --git a/osu.Game/Overlays/Rankings/Tables/UserBasedTable.cs b/osu.Game/Overlays/Rankings/Tables/UserBasedTable.cs index 4d25065578..c651108ec3 100644 --- a/osu.Game/Overlays/Rankings/Tables/UserBasedTable.cs +++ b/osu.Game/Overlays/Rankings/Tables/UserBasedTable.cs @@ -14,6 +14,8 @@ using osu.Game.Users; using osu.Game.Scoring; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; +using osu.Game.Users.Drawables; +using osuTK; namespace osu.Game.Overlays.Rankings.Tables { @@ -61,7 +63,7 @@ namespace osu.Game.Overlays.Rankings.Tables protected sealed override CountryCode GetCountryCode(UserStatistics item) => item.User.CountryCode; - protected sealed override Drawable CreateFlagContent(UserStatistics item) + protected sealed override Drawable[] CreateFlagContent(UserStatistics item) { var username = new LinkFlowContainer(t => t.Font = OsuFont.GetFont(size: TEXT_SIZE, italics: true)) { @@ -70,7 +72,7 @@ namespace osu.Game.Overlays.Rankings.Tables TextAnchor = Anchor.CentreLeft }; username.AddUserLink(item.User); - return username; + return [new UpdateableTeamFlag(item.User.Team) { Size = new Vector2(40, 20) }, username]; } protected sealed override Drawable[] CreateAdditionalContent(UserStatistics item) => new[] From d930da62104bb2c65fede2771d9a670524b80c60 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Feb 2025 18:10:19 +0900 Subject: [PATCH 489/801] Rewrite playlists to not inherit `RoomSubScreen` --- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 3 +- .../Multiplayer/MultiplayerLoungeSubScreen.cs | 3 +- .../Playlists/PlaylistsLoungeSubScreen.cs | 3 +- .../Playlists/PlaylistsRoomSubScreen.cs | 955 +++++++++++++----- 4 files changed, 716 insertions(+), 248 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index f00cf7427c..c3b3d63dcb 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -26,7 +26,6 @@ using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge.Components; -using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Users; using osuTK; @@ -408,7 +407,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge /// The created . protected abstract Room CreateNewRoom(); - protected abstract RoomSubScreen CreateRoomSubScreen(Room room); + protected abstract OnlinePlaySubScreen CreateRoomSubScreen(Room room); protected abstract ListingPollingComponent CreatePollingComponent(); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index dd61caa3db..1d728998b9 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -17,7 +17,6 @@ using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; -using osu.Game.Screens.OnlinePlay.Match; namespace osu.Game.Screens.OnlinePlay.Multiplayer { @@ -89,7 +88,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Type = MatchType.HeadToHead, }; - protected override RoomSubScreen CreateRoomSubScreen(Room room) => new MultiplayerMatchSubScreen(room); + protected override OnlinePlaySubScreen CreateRoomSubScreen(Room room) => new MultiplayerMatchSubScreen(room); protected override ListingPollingComponent CreatePollingComponent() => new MultiplayerListingPollingComponent(); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs index d66b4f844c..8670fcf78f 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs @@ -13,7 +13,6 @@ using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; -using osu.Game.Screens.OnlinePlay.Match; namespace osu.Game.Screens.OnlinePlay.Playlists { @@ -70,7 +69,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists }; } - protected override RoomSubScreen CreateRoomSubScreen(Room room) => new PlaylistsRoomSubScreen(room); + protected override OnlinePlaySubScreen CreateRoomSubScreen(Room room) => new PlaylistsRoomSubScreen(room); protected override ListingPollingComponent CreatePollingComponent() => new ListingPollingComponent(); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 7f2255e482..22b9006e47 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -2,60 +2,126 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; using osu.Framework.Logging; using osu.Framework.Screens; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Graphics.Cursor; using osu.Game.Input; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; +using osu.Game.Overlays; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Menu; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match.Components; +using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; using osu.Game.Users; +using osu.Game.Utils; using osuTK; using Container = osu.Framework.Graphics.Containers.Container; namespace osu.Game.Screens.OnlinePlay.Playlists { - public partial class PlaylistsRoomSubScreen : RoomSubScreen + public partial class PlaylistsRoomSubScreen : OnlinePlaySubScreen, IPreviewTrackOwner { public override string Title { get; } public override string ShortTitle => "playlist"; - private readonly IBindable isIdle = new BindableBool(); + public override bool? ApplyModTrackAdjustments => true; + + public override bool DisallowExternalBeatmapRulesetChanges => true; + + /// + /// Whether the user has confirmed they want to exit this screen in the presence of unsaved changes. + /// + protected bool ExitConfirmed { get; private set; } [Resolved] private IAPIProvider api { get; set; } = null!; - [Resolved(CanBeNull = true)] + [Resolved] + private AudioManager audio { get; set; } = null!; + + [Resolved] + private BeatmapManager beatmapManager { get; set; } = null!; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + [Resolved] + private PreviewTrackManager previewTrackManager { get; set; } = null!; + + [Resolved] + private MusicController music { get; set; } = null!; + + [Resolved] private IdleTracker? idleTracker { get; set; } - private MatchLeaderboard leaderboard = null!; + [Resolved] + private OnlinePlayScreen? parentScreen { get; set; } + + [Resolved] + private IOverlayManager? overlayManager { get; set; } + + [Resolved] + private IDialogOverlay? dialogOverlay { get; set; } + + [Cached] + private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); + + protected readonly Bindable SelectedItem = new Bindable(); + private readonly Bindable userBeatmap = new Bindable(); + private readonly Bindable userRuleset = new Bindable(); + private readonly Bindable> userMods = new Bindable>(Array.Empty()); + + private readonly IBindable isIdle = new BindableBool(); + private readonly Room room; + + private Drawable roomContent = null!; private SelectionPollingComponent selectionPollingComponent = null!; + private PlaylistsRoomSettingsOverlay settingsOverlay = null!; + + private MatchLeaderboard leaderboard = null!; private FillFlowContainer progressSection = null!; private DrawableRoomPlaylist drawablePlaylist = null!; - private readonly Bindable userBeatmap = new Bindable(); - private readonly Bindable userRuleset = new Bindable(); + private FillFlowContainer userModsSection = null!; + private RoomModSelectOverlay userModsSelectOverlay = null!; + + private FillFlowContainer userStyleSection = null!; + private Container userStyleDisplayContainer = null!; + + private Sample? sampleStart; + private IDisposable? userModsSelectOverlayRegistration; public PlaylistsRoomSubScreen(Room room) - : base(room, false) // Editing is temporarily not allowed. { + this.room = room; + Title = room.RoomID == null ? "New playlist" : room.Name; Activity.Value = new UserActivity.InLobby(room); + + Padding = new MarginPadding { Top = Header.HEIGHT }; } [BackgroundDependencyLoader] @@ -64,32 +130,350 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (idleTracker != null) isIdle.BindTo(idleTracker.IsIdle); - AddInternal(selectionPollingComponent = new SelectionPollingComponent(Room)); + sampleStart = audio.Samples.Get(@"SongSelect/confirm-selection"); + + InternalChild = new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + selectionPollingComponent = new SelectionPollingComponent(room), + beatmapAvailabilityTracker, + new MultiplayerRoomSounds(), + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 50) + }, + Content = new[] + { + // Padded main content (drawable room + main content) + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Horizontal = WaveOverlayContainer.WIDTH_PADDING, + Bottom = 30 + }, + Children = new[] + { + roomContent = new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, 10) + }, + Content = new[] + { + new Drawable[] + { + new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = new DrawableMatchRoom(room, false) + { + OnEdit = () => settingsOverlay.Show(), + SelectedItem = SelectedItem + } + } + }, + null, + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 10, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"3e3a44") // Temporary. + }, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(20), + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = 5, Vertical = 10 }, + Child = new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension(), + }, + Content = new[] + { + new Drawable?[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Right = 5 }, + Content = new[] + { + new Drawable[] { new OverlinedPlaylistHeader(room), }, + new Drawable[] + { + drawablePlaylist = new DrawableRoomPlaylist + { + RelativeSizeAxes = Axes.Both, + SelectedItem = { BindTarget = SelectedItem }, + AllowSelection = true, + AllowShowingResults = true, + RequestResults = item => + { + Debug.Assert(room.RoomID != null); + parentScreen?.Push(new PlaylistItemUserBestResultsScreen(room.RoomID.Value, item, + api.LocalUser.Value.Id)); + } + } + }, + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + } + }, + null, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + userModsSection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Bottom = 10 }, + Alpha = 0, + Children = new Drawable[] + { + new OverlinedHeader("Extra mods"), + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new Drawable[] + { + new UserModSelectButton + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 90, + Height = 30, + Text = "Select", + Action = showUserModSelect, + }, + new ModDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Current = userMods, + Scale = new Vector2(0.8f), + }, + } + } + } + }, + }, + new Drawable[] + { + userStyleSection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Bottom = 10 }, + Alpha = 0, + Children = new Drawable[] + { + new OverlinedHeader("Difficulty"), + userStyleDisplayContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } + } + }, + }, + new Drawable[] + { + progressSection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, + Margin = new MarginPadding { Bottom = 10 }, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new OverlinedHeader("Progress"), + new RoomLocalUserInfo(room), + } + }, + }, + new Drawable[] + { + new OverlinedHeader("Leaderboard") + }, + new Drawable[] { leaderboard = new MatchLeaderboard(room) { RelativeSizeAxes = Axes.Both }, }, + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + } + }, + null, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] { new OverlinedHeader("Chat") }, + new Drawable[] { new MatchChatDisplay(room) { RelativeSizeAxes = Axes.Both } } + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + } + }, + }, + }, + } + } + } + }, + new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, + } + } + } + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + // Resolves 1px masking errors between the settings overlay and the room panel. + Padding = new MarginPadding(-1), + Child = settingsOverlay = new PlaylistsRoomSettingsOverlay(room) + { + EditPlaylist = () => + { + if (this.IsCurrentScreen()) + this.Push(new PlaylistsSongSelect(room)); + }, + } + } + }, + }, + }, + // Footer + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"28242d") // Temporary. + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(5), + Child = new PlaylistsRoomFooter(room) + { + OnStart = startPlay, + OnClose = closePlaylist, + } + }, + } + } + } + } + } + } + }; + + LoadComponent(userModsSelectOverlay = new RoomModSelectOverlay + { + SelectedItem = { BindTarget = SelectedItem }, + SelectedMods = { BindTarget = userMods }, + IsValidMod = _ => false + }); } protected override void LoadComplete() { base.LoadComplete(); - SelectedItem.BindValueChanged(onSelectedItemChanged, true); + userModsSelectOverlayRegistration = overlayManager?.RegisterBlockingOverlay(userModsSelectOverlay); + + room.PropertyChanged += onRoomPropertyChanged; + isIdle.BindValueChanged(_ => updatePollingRate(), true); + SelectedItem.BindValueChanged(_ => onSelectedItemChanged()); + + beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); + beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateGameplayState()); + + userBeatmap.BindValueChanged(_ => updateGameplayState()); + userRuleset.BindValueChanged(_ => updateGameplayState()); + userMods.BindValueChanged(_ => updateGameplayState()); - Room.PropertyChanged += onRoomPropertyChanged; updateSetupState(); - updateRoomMaxAttempts(); - updateRoomPlaylist(); + updateGameplayState(); } - private void onSelectedItemChanged(ValueChangedEvent item) - { - // Simplest for now. - userBeatmap.Value = null; - userRuleset.Value = null; - } - - protected override IBeatmapInfo GetGameplayBeatmap() => userBeatmap.Value ?? base.GetGameplayBeatmap(); - protected override RulesetInfo GetGameplayRuleset() => userRuleset.Value ?? base.GetGameplayRuleset(); + #region Room/property updates + /// + /// Responds to changes of the 's properties. + /// + /// The that changed. + /// Describes the property that changed. private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e) { switch (e.PropertyName) @@ -97,255 +481,342 @@ namespace osu.Game.Screens.OnlinePlay.Playlists case nameof(Room.RoomID): updateSetupState(); break; - - case nameof(Room.MaxAttempts): - updateRoomMaxAttempts(); - break; - - case nameof(Room.Playlist): - updateRoomPlaylist(); - break; } } + /// + /// Adjusts the visibility of the settings and main content when changes. + /// Only the settings overlay is visible while the room isn't created, and only the main content is visible after creation. + /// private void updateSetupState() { - if (Room.RoomID != null) + if (room.RoomID == null) { - // Set the first playlist item. - // This is scheduled since updating the room and playlist may happen in an arbitrary order (via Room.CopyFrom()). - Schedule(() => SelectedItem.Value = Room.Playlist.FirstOrDefault()); + // A new room is being created. + // The main content should be hidden until the settings overlay is hidden, signaling the room is ready to be displayed. + roomContent.Hide(); + settingsOverlay.Show(); + } + else + { + roomContent.Show(); + settingsOverlay.Hide(); + + progressSection.Alpha = room.MaxAttempts != null ? 1 : 0; + drawablePlaylist.Items.ReplaceRange(0, drawablePlaylist.Items.Count, room.Playlist); + SelectedItem.Value = room.Playlist.FirstOrDefault(); } } - private void updateRoomMaxAttempts() - => progressSection.Alpha = Room.MaxAttempts != null ? 1 : 0; - - private void updateRoomPlaylist() - => drawablePlaylist.Items.ReplaceRange(0, drawablePlaylist.Items.Count, Room.Playlist); - - protected override Drawable CreateMainContent() => new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Horizontal = 5, Vertical = 10 }, - Child = new OsuContextMenuContainer - { - RelativeSizeAxes = Axes.Both, - Child = new GridContainer - { - RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.Absolute, 10), - new Dimension(), - new Dimension(GridSizeMode.Absolute, 10), - new Dimension(), - }, - Content = new[] - { - new Drawable?[] - { - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Right = 5 }, - Content = new[] - { - new Drawable[] { new OverlinedPlaylistHeader(Room), }, - new Drawable[] - { - drawablePlaylist = new DrawableRoomPlaylist - { - RelativeSizeAxes = Axes.Both, - SelectedItem = { BindTarget = SelectedItem }, - AllowSelection = true, - AllowShowingResults = true, - RequestResults = item => - { - Debug.Assert(Room.RoomID != null); - ParentScreen?.Push(new PlaylistItemUserBestResultsScreen(Room.RoomID.Value, item, api.LocalUser.Value.Id)); - } - } - }, - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - } - }, - null, - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new[] - { - UserModsSection = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Bottom = 10 }, - Alpha = 0, - Children = new Drawable[] - { - new OverlinedHeader("Extra mods"), - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Children = new Drawable[] - { - new UserModSelectButton - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Width = 90, - Height = 30, - Text = "Select", - Action = ShowUserModSelect, - }, - new ModDisplay - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Current = UserMods, - Scale = new Vector2(0.8f), - }, - } - } - } - }, - }, - new[] - { - UserStyleSection = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Bottom = 10 }, - Alpha = 0, - Children = new Drawable[] - { - new OverlinedHeader("Difficulty"), - UserStyleDisplayContainer = new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - } - } - }, - }, - new Drawable[] - { - progressSection = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Alpha = 0, - Margin = new MarginPadding { Bottom = 10 }, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new OverlinedHeader("Progress"), - new RoomLocalUserInfo(Room), - } - }, - }, - new Drawable[] - { - new OverlinedHeader("Leaderboard") - }, - new Drawable[] { leaderboard = new MatchLeaderboard(Room) { RelativeSizeAxes = Axes.Both }, }, - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - } - }, - null, - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] { new OverlinedHeader("Chat") }, - new Drawable[] { new MatchChatDisplay(Room) { RelativeSizeAxes = Axes.Both } } - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - } - }, - }, - }, - } - } - }; - - protected override Drawable CreateFooter() => new PlaylistsRoomFooter(Room) - { - OnStart = StartPlay, - OnClose = closePlaylist, - }; - - protected override RoomSettingsOverlay CreateRoomSettingsOverlay(Room room) => new PlaylistsRoomSettingsOverlay(room) - { - EditPlaylist = () => - { - if (this.IsCurrentScreen()) - this.Push(new PlaylistsSongSelect(Room)); - }, - }; - - protected override void OpenStyleSelection() - { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) - return; - - this.Push(new PlaylistsRoomFreestyleSelect(Room, item) - { - Beatmap = { BindTarget = userBeatmap }, - Ruleset = { BindTarget = userRuleset } - }); - } - + /// + /// Adjusts the rate at which the is updated. + /// private void updatePollingRate() { selectionPollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 30000 : 5000; Logger.Log($"Polling adjusted (selection: {selectionPollingComponent.TimeBetweenPolls.Value})"); } - private void closePlaylist() + /// + /// Responds to changes in the selected playlist item to validate the user's beatmap/ruleset/mod style and update UI components as necessary. + /// + private void onSelectedItemChanged() { - DialogOverlay?.Push(new ClosePlaylistDialog(Room, () => + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) + return; + + // Reset entire user style when disabled. + if (!item.Freestyle) { - var request = new ClosePlaylistRequest(Room.RoomID!.Value); - request.Success += () => Room.EndDate = DateTimeOffset.UtcNow; - API.Queue(request); + userBeatmap.Value = null; + userRuleset.Value = null; + } + + // Reset beatmap style when no longer from the same beatmap set. + if (userBeatmap.Value != null && userBeatmap.Value.BeatmapSet!.OnlineID != item.Beatmap.BeatmapSet!.OnlineID) + userBeatmap.Value = null; + + // Reset ruleset style when no longer valid for the beatmap. + if (userRuleset.Value != null) + { + IBeatmapInfo gameplayBeatmap = userBeatmap.Value ?? item.Beatmap; + int beatmapRuleset = gameplayBeatmap.Ruleset.OnlineID; + + if (beatmapRuleset > 0 && userRuleset.Value.OnlineID != beatmapRuleset) + userRuleset.Value = null; + } + + RulesetInfo gameplayRuleset = userRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; + Ruleset rulesetInstance = gameplayRuleset.CreateInstance(); + Mod[] allowedMods = item.Freestyle + ? rulesetInstance.AllMods.OfType().Where(m => ModUtils.IsValidFreeModForMatchType(m, room.Type)).ToArray() + : item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); + + // Remove any user mods that are no longer allowed. + Mod[] newUserMods = userMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); + if (!newUserMods.SequenceEqual(userMods.Value)) + userMods.Value = newUserMods; + + if (allowedMods.Length > 0) + { + userModsSection.Show(); + userModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); + } + else + { + userModsSection.Hide(); + userModsSelectOverlay.Hide(); + userModsSelectOverlay.IsValidMod = _ => false; + } + + if (item.Freestyle) + userStyleSection.Show(); + else + userStyleSection.Hide(); + + updateGameplayState(); + } + + /// + /// Adjusts the global beatmap/ruleset/mods values in preparation for a gameplay session. + /// + private void updateGameplayState() + { + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) + return; + + IBeatmapInfo gameplayBeatmap = userBeatmap.Value ?? item.Beatmap; + RulesetInfo gameplayRuleset = userRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; + Ruleset rulesetInstance = gameplayRuleset.CreateInstance(); + Mod[] gameplayMods = userMods.Value.Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToArray(); + + // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info + int beatmapId = gameplayBeatmap.OnlineID; + var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == beatmapId); + + Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); + Ruleset.Value = gameplayRuleset; + Mods.Value = gameplayMods; + + if (item.Freestyle) + { + PlaylistItem gameplayItem = item.With(ruleset: gameplayRuleset.OnlineID, beatmap: new Optional(gameplayBeatmap)); + PlaylistItem? currentItem = userStyleDisplayContainer.SingleOrDefault()?.Item; + + if (!gameplayItem.Equals(currentItem)) + { + userStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true) + { + AllowReordering = false, + AllowEditing = true, + RequestEdit = _ => showUserStyleSelect() + }; + } + } + } + + #endregion + + /// + /// Pushes a to start gameplay with the current selection. + /// + private void startPlay() + { + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) + return; + + // Required for validation inside the player. + RulesetInfo gameplayRuleset = userRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; + IBeatmapInfo gameplayBeatmap = userBeatmap.Value ?? item.Beatmap; + PlaylistItem gameplayItem = item.With(ruleset: gameplayRuleset.OnlineID, beatmap: new Optional(gameplayBeatmap)); + + sampleStart?.Play(); + + // fallback is to allow this class to operate when there is no parent OnlineScreen (testing purposes). + var targetScreen = (Screen?)parentScreen ?? this; + targetScreen.Push(new PlayerLoader(() => new PlaylistsPlayer(room, gameplayItem) + { + Exited = () => leaderboard.RefetchScores() })); } - protected override Screen CreateGameplayScreen(PlaylistItem selectedItem) + /// + /// Shows the user mod selection. + /// + private void showUserModSelect() { - return new PlayerLoader(() => new PlaylistsPlayer(Room, selectedItem) + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) + return; + + userModsSelectOverlay.Show(); + } + + /// + /// Shows the user style selection. + /// + private void showUserStyleSelect() + { + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) + return; + + this.Push(new PlaylistsRoomFreestyleSelect(room, item) { - Exited = () => leaderboard.RefetchScores() + Beatmap = { BindTarget = userBeatmap }, + Ruleset = { BindTarget = userRuleset } }); } + /// + /// May be invoked by the owner of the room to permanently close the room ahead of its intended end date. + /// + private void closePlaylist() + { + dialogOverlay?.Push(new ClosePlaylistDialog(room, () => + { + var request = new ClosePlaylistRequest(room.RoomID!.Value); + request.Success += () => room.EndDate = DateTimeOffset.UtcNow; + api.Queue(request); + })); + } + + #region Screen transition / track handling + + public override void OnEntering(ScreenTransitionEvent e) + { + base.OnEntering(e); + updateGameplayState(); + beginHandlingTrack(); + } + + public override void OnSuspending(ScreenTransitionEvent e) + { + endHandlingTrack(); + base.OnSuspending(e); + } + + public override void OnResuming(ScreenTransitionEvent e) + { + base.OnResuming(e); + updateGameplayState(); + beginHandlingTrack(); + } + + public override bool OnExiting(ScreenExitEvent e) + { + if (!ensureExitConfirmed()) + return true; + + RoomManager?.PartRoom(); + + endHandlingTrack(); + return base.OnExiting(e); + } + + public override bool OnBackButton() + { + if (room.RoomID == null) + { + if (!ensureExitConfirmed()) + return true; + + settingsOverlay.Hide(); + return base.OnBackButton(); + } + + if (userModsSelectOverlay.State.Value == Visibility.Visible) + { + userModsSelectOverlay.Hide(); + return true; + } + + if (settingsOverlay.State.Value == Visibility.Visible) + { + settingsOverlay.Hide(); + return true; + } + + return base.OnBackButton(); + } + + /// + /// Handles changes in the track to keep it looping while active. + /// + private void beginHandlingTrack() + { + Beatmap.BindValueChanged(applyLoopingToTrack, true); + } + + /// + /// Stops looping the current track and stops handling further changes to the track. + /// + private void endHandlingTrack() + { + Beatmap.ValueChanged -= applyLoopingToTrack; + Beatmap.Value.Track.Looping = false; + + previewTrackManager.StopAnyPlaying(this); + } + + /// + /// Invoked on changes to the beatmap to loop the track. See: . + /// + /// The beatmap change event. + private void applyLoopingToTrack(ValueChangedEvent beatmap) + { + if (!this.IsCurrentScreen()) + return; + + beatmap.NewValue.PrepareTrackForPreview(true); + music.EnsurePlayingSomething(); + } + + /// + /// Prompts the user to discard unsaved changes to the room before exiting. + /// + /// true if the user has confirmed they want to exit. + private bool ensureExitConfirmed() + { + if (ExitConfirmed) + return true; + + if (api.State.Value == APIState.Online) + return true; + + bool hasUnsavedChanges = room.RoomID == null && room.Playlist.Count > 0; + + if (dialogOverlay == null || !hasUnsavedChanges) + return true; + + // if the dialog is already displayed, block exiting until the user explicitly makes a decision. + if (dialogOverlay.CurrentDialog is ConfirmDiscardChangesDialog discardChangesDialog) + { + discardChangesDialog.Flash(); + return false; + } + + dialogOverlay.Push(new ConfirmDiscardChangesDialog(() => + { + ExitConfirmed = true; + settingsOverlay.Hide(); + this.Exit(); + })); + + return false; + } + + #endregion + + protected override BackgroundScreen CreateBackground() => new RoomBackgroundScreen(room.Playlist.FirstOrDefault()) + { + SelectedItem = { BindTarget = SelectedItem } + }; + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - Room.PropertyChanged -= onRoomPropertyChanged; + + userModsSelectOverlayRegistration?.Dispose(); + room.PropertyChanged -= onRoomPropertyChanged; } } } From 55809f5e0d7429dcaf8a59d6c1c82323bc8055de Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 14 Feb 2025 06:15:32 -0500 Subject: [PATCH 490/801] Apply changes to Android --- osu.Android/OsuGameAndroid.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 0f2451f0a0..e725f9245f 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -12,6 +12,7 @@ using osu.Game; using osu.Game.Screens; using osu.Game.Updater; using osu.Game.Utils; +using osuTK; namespace osu.Android { @@ -20,6 +21,8 @@ namespace osu.Android [Cached] private readonly OsuGameActivity gameActivity; + protected override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth); + public OsuGameAndroid(OsuGameActivity activity) : base(null) { From 27b9a6b7a386fb975df780dfd78d3ce3bcf114e9 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 14 Feb 2025 06:15:56 -0500 Subject: [PATCH 491/801] Reset UI scale for mobile platforms --- .../.idea/deploymentTargetSelector.xml | 10 ++++++++++ osu.Game/Configuration/OsuConfigManager.cs | 13 ++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 .idea/.idea.osu.Android/.idea/deploymentTargetSelector.xml diff --git a/.idea/.idea.osu.Android/.idea/deploymentTargetSelector.xml b/.idea/.idea.osu.Android/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000000..4432459b86 --- /dev/null +++ b/.idea/.idea.osu.Android/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 1244dd8cfc..76d06f3665 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -238,7 +238,7 @@ namespace osu.Game.Configuration public void Migrate() { - // arrives as 2020.123.0 + // arrives as 2020.123.0-lazer string rawVersion = Get(OsuSetting.Version); if (rawVersion.Length < 6) @@ -251,11 +251,14 @@ namespace osu.Game.Configuration if (!int.TryParse(pieces[0], out int year)) return; if (!int.TryParse(pieces[1], out int monthDay)) return; - // ReSharper disable once UnusedVariable - int combined = (year * 10000) + monthDay; + int combined = year * 10000 + monthDay; - // migrations can be added here using a condition like: - // if (combined < 20220103) { performMigration() } + if (combined < 20250214) + { + // UI scaling on mobile platforms has been internally adjusted such that 1x UI scale looks correctly zoomed in than before. + if (RuntimeInfo.IsMobile) + GetBindable(OsuSetting.UIScale).SetDefault(); + } } public override TrackedSettings CreateTrackedSettings() From 1cb4956cacd6e677af269419ba64e7fdadbf6f65 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 14 Feb 2025 20:30:54 +0900 Subject: [PATCH 492/801] Fix not properly selecting the first playlist item --- .../Playlists/PlaylistsRoomSubScreen.cs | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 22b9006e47..76f0c04295 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -467,8 +467,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists updateGameplayState(); } - #region Room/property updates - /// /// Responds to changes of the 's properties. /// @@ -485,7 +483,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } /// - /// Adjusts the visibility of the settings and main content when changes. + /// Responds to changes in to adjust the visibility of the settings and main content. /// Only the settings overlay is visible while the room isn't created, and only the main content is visible after creation. /// private void updateSetupState() @@ -502,9 +500,15 @@ namespace osu.Game.Screens.OnlinePlay.Playlists roomContent.Show(); settingsOverlay.Hide(); - progressSection.Alpha = room.MaxAttempts != null ? 1 : 0; - drawablePlaylist.Items.ReplaceRange(0, drawablePlaylist.Items.Count, room.Playlist); - SelectedItem.Value = room.Playlist.FirstOrDefault(); + // Scheduled because room properties are updated in arbitrary order. + Schedule(() => + { + progressSection.Alpha = room.MaxAttempts != null ? 1 : 0; + drawablePlaylist.Items.ReplaceRange(0, drawablePlaylist.Items.Count, room.Playlist); + + // Select an initial item for the user to help them get into a playable state quicker. + SelectedItem.Value = room.Playlist.FirstOrDefault(); + }); } } @@ -518,7 +522,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } /// - /// Responds to changes in the selected playlist item to validate the user's beatmap/ruleset/mod style and update UI components as necessary. + /// Responds to changes in the selected playlist item to validate the user's style selection. /// private void onSelectedItemChanged() { @@ -615,8 +619,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } } - #endregion - /// /// Pushes a to start gameplay with the current selection. /// @@ -679,8 +681,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists })); } - #region Screen transition / track handling - public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); @@ -804,8 +804,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return false; } - #endregion - protected override BackgroundScreen CreateBackground() => new RoomBackgroundScreen(room.Playlist.FirstOrDefault()) { SelectedItem = { BindTarget = SelectedItem } From 9458f0d01d2ae41d980a8a7a6a4bdc972e09457c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 14 Feb 2025 20:38:44 +0900 Subject: [PATCH 493/801] Remove unnecessary update, document other usage --- .../Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 76f0c04295..45e220cce9 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -684,7 +684,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); - updateGameplayState(); beginHandlingTrack(); } @@ -697,8 +696,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public override void OnResuming(ScreenTransitionEvent e) { base.OnResuming(e); - updateGameplayState(); beginHandlingTrack(); + + // Required when resuming from style selection. + updateGameplayState(); } public override bool OnExiting(ScreenExitEvent e) From ef2f482d041840bb4875a19b3f9a351b0415a63c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 14 Feb 2025 12:40:54 +0100 Subject: [PATCH 494/801] Fix skin deserialisation test --- .../Archives/modified-argon-20250214.osk | Bin 0 -> 1724 bytes osu.Game.Tests/Skins/SkinDeserialisationTest.cs | 2 ++ 2 files changed, 2 insertions(+) create mode 100644 osu.Game.Tests/Resources/Archives/modified-argon-20250214.osk diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-20250214.osk b/osu.Game.Tests/Resources/Archives/modified-argon-20250214.osk new file mode 100644 index 0000000000000000000000000000000000000000..74abef25caa81004c11911a9d223909871da3299 GIT binary patch literal 1724 zcmZ{kcTm%37{}kRks%4gNm2GDEGx7^gtCHxpTQugOo0$aLJ|bQE6SEIDk>ud2OBhk ztYARqDVq|3NCgB%L8eH6AmZrhwe;l9?_bY7>z?Pidp?jozkmz?Km?5WIGiGex*8P# z0NTEJ0H6jEh`IzKK_#VfM;l5?aC4J}*A%pStsNKY9OfLLBo|0LE4dUlMLGCBST5>n zv*#nCXTrwAaa_D*hrh9aeB&6q)F(3~J(7LJ7|WB0e`}`C-b;%xFrb ze8>?icyFh+%pMMD?(jj1stp}Y9j^?@57FN&EB#UsGk_^-87pVFdH%%Ncv_vq9$9M( zN*p8HAzyEpQJ;QPwytR$#M1YGzFP#|eY+_P0FVX%kob2I0@0788$cxy?@nVOh-=@A z!Bt_QZarPxiOa{)+UWlNAbb&z z`OsrmGWcm9gl%nT4$E~Xn^{n7ip9sdEOxuzNbx;UX{WZ;jS}v|YGzjOaF5_jX89G? z+Am!#^)&b;`@pu&f%=cgDqSS}qf##-8@ik~95FL8&nPjkyT=`!7pE3e;;51=i?+xc zc4odtQNlfeWd4u2Z&J=(aIAKWD-4)w89YMRY&^6Y>)nZ&uX<;Af5B#SI5qDxg2qU^ zb7r`aCdxl3kVbefUvRO|Fk>!kiDCL)g{_RMe>rsTZ@u%mJBE0z3Cl+Y;TBc{sDw^`3Z?w9O%RsQ6$7y6la z-h*bxTaL^T4NZMX1Pnv;QSI9$oebEi@rtXD;JUQs{funF&gE ze)n{l=~~kgaiY=bBpZE;eGmq`;WKPIn=NwZ+5nl2yPnTnd>{e$JB_c^s9D6TEzn{0 zg>w@5E#^o03pgpBk0}u5g6p#~MGs8!h&juBUCzq-k$R$Skx|pj1IbaJGf7%r$+Vc+ zOtfiHQ2Z$!w~SKXN$37u@WwbL(d>HJfFvnj0a6VEU8}87=``!aw>iY?4(i+dL|?2^ zEc>igvxsUKNs;=Me%lr@kB{{p3ICIy!xXweom;58ADYVI1Pcu!$^^O;pNr<^)8U4L z^@Atmm`yL#34=8ZtyX5Z;Rkn5iPbmSrPp>P=%JrBvRx1f6RG1Xg;|r}-Co;6e#!(L zw!6PX(9%#v)^wV63onM9dvg7cTD(cqd_tH@&TQpW4tYI%!55dM+t18eYd)@b(Yzv3 zmi+c}r$V4*I>u`BE~s!UvT)m0NKO%WWbNHy`|`{)_)M+UbUT(TZ-=5PR#A4+v&BX=m6M{ iShl^#_N#0u+F5Y>jUanLp|5cPAOMyD0JVZ&v;P5v*XV5k literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs index 55836302e6..5b343c80c5 100644 --- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs +++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs @@ -73,6 +73,8 @@ namespace osu.Game.Tests.Skins "Archives/modified-default-20241207.osk", // Covers skinnable spectator list "Archives/modified-argon-20250116.osk", + // Covers player team flag + "Archives/modified-argon-20250214.osk", }; /// From e17383edbdfc6bac0c14d8ab1cdc6eea2d7d8045 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 14 Feb 2025 21:47:23 +0900 Subject: [PATCH 495/801] Rewrite/optimise layout Reducing the amount of nesting to make things more readable. --- .../Playlists/PlaylistsRoomSubScreen.cs | 496 +++++++++--------- 1 file changed, 243 insertions(+), 253 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 45e220cce9..87bec5d8e1 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -13,7 +13,6 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Logging; using osu.Framework.Screens; @@ -43,6 +42,31 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { public partial class PlaylistsRoomSubScreen : OnlinePlaySubScreen, IPreviewTrackOwner { + /// + /// Footer height. + /// + private const float footer_height = 50; + + /// + /// Padding between content and footer. + /// + private const float footer_padding = 30; + + /// + /// Internal padding of the content. + /// + private const float content_padding = 20; + + /// + /// Padding between columns of the content. + /// + private const float column_padding = 10; + + /// + /// Padding between rows of the content. + /// + private const float row_padding = 10; + public override string Title { get; } public override string ShortTitle => "playlist"; @@ -132,7 +156,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists sampleStart = audio.Samples.Get(@"SongSelect/confirm-selection"); - InternalChild = new PopoverContainer + InternalChild = new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, Children = new Drawable[] @@ -140,297 +164,263 @@ namespace osu.Game.Screens.OnlinePlay.Playlists selectionPollingComponent = new SelectionPollingComponent(room), beatmapAvailabilityTracker, new MultiplayerRoomSounds(), - new GridContainer + new Container { RelativeSizeAxes = Axes.Both, - RowDimensions = new[] + Padding = new MarginPadding { - new Dimension(), - new Dimension(GridSizeMode.Absolute, 50) + Horizontal = WaveOverlayContainer.WIDTH_PADDING, + Bottom = footer_height + footer_padding }, - Content = new[] + Children = new[] { - // Padded main content (drawable room + main content) - new Drawable[] + roomContent = new GridContainer { - new Container + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, row_padding), + }, + Content = new[] + { + new Drawable[] { - Horizontal = WaveOverlayContainer.WIDTH_PADDING, - Bottom = 30 + new DrawableMatchRoom(room, false) + { + OnEdit = () => settingsOverlay.Show(), + SelectedItem = SelectedItem + } }, - Children = new[] + null, + new Drawable[] { - roomContent = new GridContainer + new Container { RelativeSizeAxes = Axes.Both, - RowDimensions = new[] + Masking = true, + CornerRadius = 10, + Children = new Drawable[] { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Absolute, 10) - }, - Content = new[] - { - new Drawable[] + new Box { - new OsuContextMenuContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Child = new DrawableMatchRoom(room, false) - { - OnEdit = () => settingsOverlay.Show(), - SelectedItem = SelectedItem - } - } + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"3e3a44") // Temporary. }, - null, - new Drawable[] + new GridContainer { - new Container + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(content_padding), + ColumnDimensions = new[] { - RelativeSizeAxes = Axes.Both, - Children = new[] + new Dimension(), + new Dimension(GridSizeMode.Absolute, column_padding), + new Dimension(), + new Dimension(GridSizeMode.Absolute, column_padding), + new Dimension(), + }, + Content = new[] + { + new Drawable?[] { - new Container + new GridContainer { RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = 10, - Child = new Box + RowDimensions = new[] { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"3e3a44") // Temporary. + new Dimension(GridSizeMode.AutoSize), + new Dimension(), }, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(20), - Child = new Container + Content = new[] { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Horizontal = 5, Vertical = 10 }, - Child = new OsuContextMenuContainer + new Drawable[] { - RelativeSizeAxes = Axes.Both, - Child = new GridContainer + new OverlinedPlaylistHeader(room), + }, + new Drawable[] + { + drawablePlaylist = new DrawableRoomPlaylist { RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] + SelectedItem = { BindTarget = SelectedItem }, + AllowSelection = true, + AllowShowingResults = true, + RequestResults = item => { - new Dimension(), - new Dimension(GridSizeMode.Absolute, 10), - new Dimension(), - new Dimension(GridSizeMode.Absolute, 10), - new Dimension(), - }, - Content = new[] - { - new Drawable?[] - { - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Right = 5 }, - Content = new[] - { - new Drawable[] { new OverlinedPlaylistHeader(room), }, - new Drawable[] - { - drawablePlaylist = new DrawableRoomPlaylist - { - RelativeSizeAxes = Axes.Both, - SelectedItem = { BindTarget = SelectedItem }, - AllowSelection = true, - AllowShowingResults = true, - RequestResults = item => - { - Debug.Assert(room.RoomID != null); - parentScreen?.Push(new PlaylistItemUserBestResultsScreen(room.RoomID.Value, item, - api.LocalUser.Value.Id)); - } - } - }, - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - } - }, - null, - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] - { - userModsSection = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Bottom = 10 }, - Alpha = 0, - Children = new Drawable[] - { - new OverlinedHeader("Extra mods"), - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Children = new Drawable[] - { - new UserModSelectButton - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Width = 90, - Height = 30, - Text = "Select", - Action = showUserModSelect, - }, - new ModDisplay - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Current = userMods, - Scale = new Vector2(0.8f), - }, - } - } - } - }, - }, - new Drawable[] - { - userStyleSection = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Bottom = 10 }, - Alpha = 0, - Children = new Drawable[] - { - new OverlinedHeader("Difficulty"), - userStyleDisplayContainer = new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - } - } - }, - }, - new Drawable[] - { - progressSection = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Alpha = 0, - Margin = new MarginPadding { Bottom = 10 }, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new OverlinedHeader("Progress"), - new RoomLocalUserInfo(room), - } - }, - }, - new Drawable[] - { - new OverlinedHeader("Leaderboard") - }, - new Drawable[] { leaderboard = new MatchLeaderboard(room) { RelativeSizeAxes = Axes.Both }, }, - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - } - }, - null, - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] { new OverlinedHeader("Chat") }, - new Drawable[] { new MatchChatDisplay(room) { RelativeSizeAxes = Axes.Both } } - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - } - }, - }, - }, + Debug.Assert(room.RoomID != null); + parentScreen?.Push(new PlaylistItemUserBestResultsScreen(room.RoomID.Value, item, + api.LocalUser.Value.Id)); + } } } } }, - new Container + null, + new GridContainer { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, row_padding), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, row_padding), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, row_padding), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + userModsSection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, + Children = new Drawable[] + { + new OverlinedHeader("Extra mods"), + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new Drawable[] + { + new UserModSelectButton + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 90, + Height = 30, + Text = "Select", + Action = showUserModSelect, + }, + new ModDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Current = userMods, + Scale = new Vector2(0.8f), + } + } + } + } + } + }, + null, + new Drawable[] + { + userStyleSection = 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 + } + } + } + }, + null, + new Drawable[] + { + progressSection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new OverlinedHeader("Progress"), + new RoomLocalUserInfo(room), + } + } + }, + null, + new Drawable[] + { + new OverlinedHeader("Leaderboard") + }, + new Drawable[] + { + leaderboard = new MatchLeaderboard(room) + { + RelativeSizeAxes = Axes.Both + }, + } + } }, + null, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + new OverlinedHeader("Chat") + }, + new Drawable[] + { + new MatchChatDisplay(room) + { + RelativeSizeAxes = Axes.Both + } + } + } + } } } } } - }, - new Container - { - RelativeSizeAxes = Axes.Both, - // Resolves 1px masking errors between the settings overlay and the room panel. - Padding = new MarginPadding(-1), - Child = settingsOverlay = new PlaylistsRoomSettingsOverlay(room) - { - EditPlaylist = () => - { - if (this.IsCurrentScreen()) - this.Push(new PlaylistsSongSelect(room)); - }, - } } - }, - }, - }, - // Footer - new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"28242d") // Temporary. - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(5), - Child = new PlaylistsRoomFooter(room) - { - OnStart = startPlay, - OnClose = closePlaylist, - } - }, } } + }, + settingsOverlay = new PlaylistsRoomSettingsOverlay(room) + { + EditPlaylist = () => + { + if (this.IsCurrentScreen()) + this.Push(new PlaylistsSongSelect(room)); + } + } + } + }, + new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = footer_height, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"28242d") // Temporary. + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(5), + Child = new PlaylistsRoomFooter(room) + { + OnStart = startPlay, + OnClose = closePlaylist + } } } } From f2c75ef593a816851c491fc5f2bdb51e83bcef60 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 14 Feb 2025 22:08:32 +0900 Subject: [PATCH 496/801] Always update selection when anything changes If style changes, then we also need to re-validate mods. Thus, we should just update the entire selection anyway, and merge the "gameplay state" into it for simplicity. --- .../Playlists/PlaylistsRoomSubScreen.cs | 77 ++++++++----------- 1 file changed, 32 insertions(+), 45 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 87bec5d8e1..38768097a4 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -444,17 +444,17 @@ namespace osu.Game.Screens.OnlinePlay.Playlists room.PropertyChanged += onRoomPropertyChanged; isIdle.BindValueChanged(_ => updatePollingRate(), true); - SelectedItem.BindValueChanged(_ => onSelectedItemChanged()); beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); - beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateGameplayState()); + beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateSelectionState()); - userBeatmap.BindValueChanged(_ => updateGameplayState()); - userRuleset.BindValueChanged(_ => updateGameplayState()); - userMods.BindValueChanged(_ => updateGameplayState()); + SelectedItem.BindValueChanged(_ => updateSelectionState()); + userBeatmap.BindValueChanged(_ => updateSelectionState()); + userRuleset.BindValueChanged(_ => updateSelectionState()); + userMods.BindValueChanged(_ => updateSelectionState()); updateSetupState(); - updateGameplayState(); + updateSelectionState(); } /// @@ -512,9 +512,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } /// - /// Responds to changes in the selected playlist item to validate the user's style selection. + /// Responds to changes in the selected playlist item or user style (beatmap/ruleset/mods) to validate and update global states in preparation for a gameplay session. /// - private void onSelectedItemChanged() + private void updateSelectionState() { if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) return; @@ -530,28 +530,40 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (userBeatmap.Value != null && userBeatmap.Value.BeatmapSet!.OnlineID != item.Beatmap.BeatmapSet!.OnlineID) userBeatmap.Value = null; + IBeatmapInfo gameplayBeatmap = userBeatmap.Value ?? item.Beatmap; + // Reset ruleset style when no longer valid for the beatmap. if (userRuleset.Value != null) { - IBeatmapInfo gameplayBeatmap = userBeatmap.Value ?? item.Beatmap; int beatmapRuleset = gameplayBeatmap.Ruleset.OnlineID; - if (beatmapRuleset > 0 && userRuleset.Value.OnlineID != beatmapRuleset) userRuleset.Value = null; } RulesetInfo gameplayRuleset = userRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; Ruleset rulesetInstance = gameplayRuleset.CreateInstance(); + + // Remove any user mods that are no longer allowed. Mod[] allowedMods = item.Freestyle ? rulesetInstance.AllMods.OfType().Where(m => ModUtils.IsValidFreeModForMatchType(m, room.Type)).ToArray() : item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); - - // Remove any user mods that are no longer allowed. Mod[] newUserMods = userMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); if (!newUserMods.SequenceEqual(userMods.Value)) userMods.Value = newUserMods; - if (allowedMods.Length > 0) + // Update global gameplay state to correspond to the new selection. + // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info + int beatmapId = gameplayBeatmap.OnlineID; + var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == beatmapId); + Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); + Ruleset.Value = gameplayRuleset; + Mods.Value = userMods.Value.Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToArray(); + + // Update UI elements to reflect the new selection. + bool freemods = allowedMods.Length > 0; + bool freestyle = item.Freestyle; + + if (freemods) { userModsSection.Show(); userModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); @@ -563,37 +575,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists userModsSelectOverlay.IsValidMod = _ => false; } - if (item.Freestyle) - userStyleSection.Show(); - else - userStyleSection.Hide(); - - updateGameplayState(); - } - - /// - /// Adjusts the global beatmap/ruleset/mods values in preparation for a gameplay session. - /// - private void updateGameplayState() - { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) - return; - - IBeatmapInfo gameplayBeatmap = userBeatmap.Value ?? item.Beatmap; - RulesetInfo gameplayRuleset = userRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; - Ruleset rulesetInstance = gameplayRuleset.CreateInstance(); - Mod[] gameplayMods = userMods.Value.Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToArray(); - - // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info - int beatmapId = gameplayBeatmap.OnlineID; - var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == beatmapId); - - Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); - Ruleset.Value = gameplayRuleset; - Mods.Value = gameplayMods; - - if (item.Freestyle) + if (freestyle) { + userStyleSection.Show(); + PlaylistItem gameplayItem = item.With(ruleset: gameplayRuleset.OnlineID, beatmap: new Optional(gameplayBeatmap)); PlaylistItem? currentItem = userStyleDisplayContainer.SingleOrDefault()?.Item; @@ -607,6 +592,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists }; } } + else + userStyleSection.Hide(); } /// @@ -688,8 +675,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists base.OnResuming(e); beginHandlingTrack(); - // Required when resuming from style selection. - updateGameplayState(); + // Required to update beatmap/ruleset when resuming from style selection. + updateSelectionState(); } public override bool OnExiting(ScreenExitEvent e) From 06f27277bfe1730e450c188af5a82b7720a4a172 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 14 Feb 2025 22:54:03 +0900 Subject: [PATCH 497/801] Use margins to remove padding from hidden elements If one of these elments is hidden, the following spacing element is expected to be hidden too. Simplest is to use margins. Old implementation already did this. --- .../OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index c0fe78134f..79728fc4b2 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -272,11 +272,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Absolute, row_padding), new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Absolute, row_padding), new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Absolute, row_padding), new Dimension(GridSizeMode.AutoSize), }, Content = new[] @@ -287,6 +284,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Bottom = row_padding }, Alpha = 0, Children = new Drawable[] { @@ -319,13 +317,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } } }, - null, new Drawable[] { userStyleSection = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Bottom = row_padding }, Alpha = 0, Children = new Drawable[] { @@ -338,13 +336,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } } }, - null, new Drawable[] { progressSection = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Bottom = row_padding }, Alpha = 0, Direction = FillDirection.Vertical, Children = new Drawable[] @@ -354,7 +352,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } } }, - null, new Drawable[] { new OverlinedHeader("Leaderboard") From 4e66536ae8a65219c971202addb6394c6744d1fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 14 Feb 2025 15:52:05 +0100 Subject: [PATCH 498/801] Fix failed scores with no hits on beatmaps with ridiculous mod combinations showing hundreds of pp points awarded (#31741) See https://discord.com/channels/188630481301012481/1097318920991559880/1334716356582572074. On `master` this is actually worse and shows thousands of pp points, so I guess `pp-dev` is a comparable improvement, but still flagrantly wrong. The reason why `pp-dev` is better is the `speedDeviation == null` guard at the start of `computeSpeedValue()` which turns off the rest of the calculation, therefore not exposing the bug where `relevantTotalDiff` can go negative. I still guarded it in this commit just for safety's sake given it is clear it can do very wrong stuff. --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 09ec890926..a667d12a44 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -273,7 +273,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= speedHighDeviationMultiplier; // Calculate accuracy assuming the worst case scenario - double relevantTotalDiff = totalHits - attributes.SpeedNoteCount; + double relevantTotalDiff = Math.Max(0, totalHits - attributes.SpeedNoteCount); double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff); double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat)); double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk)); @@ -297,7 +297,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty amountHitObjectsWithAccuracy += attributes.SliderCount; if (amountHitObjectsWithAccuracy > 0) - betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6); + betterAccuracyPercentage = ((countGreat - Math.Max(totalHits - amountHitObjectsWithAccuracy, 0)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6); else betterAccuracyPercentage = 0; From b21dd01de7263ecb6fa2817409b23e9eb16427c6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 15 Feb 2025 00:03:41 +0900 Subject: [PATCH 499/801] Use fixed width for digital clock display Supersedes and closes https://github.com/ppy/osu/pull/31093. --- .../Overlays/Toolbar/DigitalClockDisplay.cs | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/DigitalClockDisplay.cs b/osu.Game/Overlays/Toolbar/DigitalClockDisplay.cs index ada2f6ff86..bd1c944847 100644 --- a/osu.Game/Overlays/Toolbar/DigitalClockDisplay.cs +++ b/osu.Game/Overlays/Toolbar/DigitalClockDisplay.cs @@ -7,8 +7,10 @@ using System; using osu.Framework.Allocation; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osuTK; namespace osu.Game.Overlays.Toolbar { @@ -17,6 +19,8 @@ namespace osu.Game.Overlays.Toolbar private OsuSpriteText realTime; private OsuSpriteText gameTime; + private FillFlowContainer runningText; + private bool showRuntime = true; public bool ShowRuntime @@ -52,17 +56,36 @@ namespace osu.Game.Overlays.Toolbar [BackgroundDependencyLoader] private void load(OsuColour colours) { - AutoSizeAxes = Axes.Y; + AutoSizeAxes = Axes.Both; InternalChildren = new Drawable[] { - realTime = new OsuSpriteText(), - gameTime = new OsuSpriteText + realTime = new OsuSpriteText + { + Font = OsuFont.Default.With(fixedWidth: true), + Spacing = new Vector2(-1.5f, 0), + }, + runningText = new FillFlowContainer { Y = 14, Colour = colours.PinkLight, - Font = OsuFont.Default.With(size: 10, weight: FontWeight.SemiBold), - } + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(2, 0), + Children = new Drawable[] + { + new OsuSpriteText + { + Text = "running", + Font = OsuFont.Default.With(size: 10, weight: FontWeight.SemiBold), + }, + gameTime = new OsuSpriteText + { + Font = OsuFont.Default.With(size: 10, fixedWidth: true, weight: FontWeight.SemiBold), + Spacing = new Vector2(-0.5f, 0), + } + } + }, }; updateMetrics(); @@ -71,14 +94,12 @@ namespace osu.Game.Overlays.Toolbar protected override void UpdateDisplay(DateTimeOffset now) { realTime.Text = now.ToLocalisableString(use24HourDisplay ? @"HH:mm:ss" : @"h:mm:ss tt"); - gameTime.Text = $"running {new TimeSpan(TimeSpan.TicksPerSecond * (int)(Clock.CurrentTime / 1000)):c}"; + gameTime.Text = $"{new TimeSpan(TimeSpan.TicksPerSecond * (int)(Clock.CurrentTime / 1000)):c}"; } private void updateMetrics() { - Width = showRuntime || !use24HourDisplay ? 66 : 45; // Allows for space for game time up to 99 days (in the padding area since this is quite rare). - - gameTime.FadeTo(showRuntime ? 1 : 0); + runningText.FadeTo(showRuntime ? 1 : 0); } } } From 7eb32ef35139793b5513c792eb3a5608fee3c207 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 16 Feb 2025 13:43:16 -0800 Subject: [PATCH 500/801] Fix team flag layout on user profile --- .../Profile/Header/TopHeaderContainer.cs | 60 +++++++++++++------ 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index 5f404375e6..d6bc726c18 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -42,9 +42,10 @@ namespace osu.Game.Overlays.Profile.Header private ExternalLinkButton openUserExternally = null!; private OsuSpriteText titleText = null!; private UpdateableFlag userFlag = null!; - private UpdateableTeamFlag teamFlag = null!; private OsuHoverContainer userCountryContainer = null!; private OsuSpriteText userCountryText = null!; + private UpdateableTeamFlag teamFlag = null!; + private OsuSpriteText teamText = null!; private GroupBadgeFlow groupBadgeFlow = null!; private ToggleCoverButton coverToggle = null!; private PreviousUsernamesDisplay previousUsernamesDisplay = null!; @@ -161,27 +162,51 @@ namespace osu.Game.Overlays.Profile.Header { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), Children = new Drawable[] { - userFlag = new UpdateableFlag - { - Size = new Vector2(28, 20), - }, - teamFlag = new UpdateableTeamFlag - { - Size = new Vector2(40, 20), - }, - userCountryContainer = new OsuHoverContainer + new FillFlowContainer { AutoSizeAxes = Axes.Both, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Margin = new MarginPadding { Left = 5 }, - Child = userCountryText = new OsuSpriteText + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4, 0), + Children = new Drawable[] { - Font = OsuFont.GetFont(size: 14f, weight: FontWeight.Regular), - }, + userFlag = new UpdateableFlag + { + Size = new Vector2(28, 20), + }, + userCountryContainer = new OsuHoverContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Child = userCountryText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 14f, weight: FontWeight.Regular), + }, + }, + } }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4, 0), + Children = new Drawable[] + { + teamFlag = new UpdateableTeamFlag + { + Size = new Vector2(40, 20), + }, + teamText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: 14f, weight: FontWeight.Regular), + }, + } + } } }, } @@ -220,9 +245,10 @@ namespace osu.Game.Overlays.Profile.Header usernameText.Text = user?.Username ?? string.Empty; openUserExternally.Link = $@"{api.Endpoints.WebsiteUrl}/users/{user?.Id ?? 0}"; userFlag.CountryCode = user?.CountryCode ?? default; - teamFlag.Team = user?.Team; userCountryText.Text = (user?.CountryCode ?? default).GetDescription(); userCountryContainer.Action = () => rankingsOverlay?.ShowCountry(user?.CountryCode ?? default); + teamFlag.Team = user?.Team; + teamText.Text = user?.Team?.Name ?? string.Empty; supporterTag.SupportLevel = user?.SupportLevel ?? 0; titleText.Text = user?.Title ?? string.Empty; titleText.Colour = Color4Extensions.FromHex(user?.Colour ?? "fff"); From 1b333ad51c2147f5ab950a03f7de49a07721c01a Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 16 Feb 2025 17:53:34 -0500 Subject: [PATCH 501/801] Add sample team to user profile test scene --- .../Visual/Online/TestSceneUserProfileOverlay.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs index d16ed46bd2..a4a9816337 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs @@ -346,6 +346,13 @@ namespace osu.Game.Tests.Visual.Online Twitter = "test_user", Discord = "test_user", Website = "https://google.com", + Team = new APITeam + { + Id = 1, + Name = "Collective Wangs", + ShortName = "WANG", + FlagUrl = "https://assets.ppy.sh/teams/logo/1/wanglogo.jpg", + } }; } } From afc2c521955f00654c3c824bebe294afabf4d221 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 16 Feb 2025 17:55:10 -0500 Subject: [PATCH 502/801] Add proper spacing between username, title, and country/team row --- osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index d6bc726c18..3d9539ce1f 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -156,10 +156,11 @@ namespace osu.Game.Overlays.Profile.Header titleText = new OsuSpriteText { Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular), - Margin = new MarginPadding { Bottom = 5 } + Margin = new MarginPadding { Bottom = 3 }, }, new FillFlowContainer { + Margin = new MarginPadding { Top = 3 }, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(10, 0), From 65cae7c7aafda7e764a3070958778f901d9f4c74 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 17 Feb 2025 15:02:38 +0900 Subject: [PATCH 503/801] Fix inverted condition --- osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 79728fc4b2..4ab20f8bb0 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -764,7 +764,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (ExitConfirmed) return true; - if (api.State.Value == APIState.Online) + if (api.State.Value != APIState.Online) return true; bool hasUnsavedChanges = room.RoomID == null && room.Playlist.Count > 0; From d5566831d22fb170e1a21e522c84863eb788cd7e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Feb 2025 15:06:35 +0900 Subject: [PATCH 504/801] Stop beat divisor "slider" from accepting focus --- osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index 43a2abe4c4..b8f2695259 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -398,6 +398,8 @@ namespace osu.Game.Screens.Edit.Compose.Components private readonly BindableBeatDivisor beatDivisor; + public override bool AcceptsFocus => false; + public TickSliderBar(BindableBeatDivisor beatDivisor) { CurrentNumber.BindTo(this.beatDivisor = beatDivisor); From 2738221c0b9ab579623a02d9f9b6ef7d0cd45dd6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Feb 2025 15:07:21 +0900 Subject: [PATCH 505/801] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 6bbd432ee7..f4d49763ab 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index ca2604858c..0d95dfbd06 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From db4a4a1723b48f64bb88c5289c143f9b13705e0a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Feb 2025 15:09:51 +0900 Subject: [PATCH 506/801] Minor bump some packages --- .../osu.Game.Rulesets.EmptyFreeform.Tests.csproj | 2 +- .../osu.Game.Rulesets.Pippidon.Tests.csproj | 2 +- ...osu.Game.Rulesets.EmptyScrolling.Tests.csproj | 2 +- .../osu.Game.Rulesets.Pippidon.Tests.csproj | 2 +- osu.Desktop/osu.Desktop.csproj | 2 +- .../osu.Game.Rulesets.Catch.Tests.csproj | 2 +- .../osu.Game.Rulesets.Mania.Tests.csproj | 2 +- .../osu.Game.Rulesets.Osu.Tests.csproj | 2 +- .../osu.Game.Rulesets.Taiko.Tests.csproj | 2 +- .../Navigation/TestSceneScreenNavigation.cs | 2 +- .../TestSceneAddPlaylistToCollectionButton.cs | 7 +++++-- osu.Game.Tests/osu.Game.Tests.csproj | 2 +- .../osu.Game.Tournament.Tests.csproj | 2 +- .../Profile/Header/Components/FollowersButton.cs | 2 +- osu.Game/osu.Game.csproj | 16 ++++++++-------- 15 files changed, 26 insertions(+), 23 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj index 1d368e9bd1..86f73a37d4 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj @@ -9,7 +9,7 @@ false - + diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index d69bc78b8f..51c0233942 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -9,7 +9,7 @@ false - + diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj index 7ac269f65f..ed4e8631ea 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj @@ -9,7 +9,7 @@ false - + diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index d69bc78b8f..51c0233942 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -9,7 +9,7 @@ false - + diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 21c570a7b2..05d5bb19fb 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -24,7 +24,7 @@ - + diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj index 56ee208670..fc1b13f3ad 100644 --- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj +++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj @@ -1,7 +1,7 @@  - + diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj index 5e4bad279b..edb01b044e 100644 --- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj +++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj @@ -1,7 +1,7 @@  - + diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index 267dc98985..6510568555 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -1,7 +1,7 @@  - + diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index 523df4c259..e498989a79 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -1,7 +1,7 @@  - + diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 88b482ab4c..8c4fcc461c 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -11,6 +11,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Extensions; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; @@ -52,7 +53,6 @@ using osu.Game.Tests.Resources; using osu.Game.Utils; using osuTK; using osuTK.Input; -using SharpCompress; namespace osu.Game.Tests.Visual.Navigation { diff --git a/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs index 46c93d9ae2..abfc5c4d0e 100644 --- a/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs +++ b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs @@ -20,7 +20,6 @@ using osu.Game.Rulesets.Osu; using osu.Game.Screens.OnlinePlay.Playlists; using osuTK; using osuTK.Input; -using SharpCompress; namespace osu.Game.Tests.Visual.Playlists { @@ -53,7 +52,11 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("clear realm", () => Realm.Realm.Write(() => Realm.Realm.RemoveAll())); - AddStep("clear notifications", () => notificationOverlay.AllNotifications.Empty()); + AddStep("clear notifications", () => + { + foreach (var notification in notificationOverlay.AllNotifications) + notification.Close(runFlingAnimation: false); + }); importBeatmap(); diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index e78a3ea4f3..a1f43505f0 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -1,7 +1,7 @@  - + diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index 1daf5a446e..8437a1bc4e 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -4,7 +4,7 @@ osu.Game.Tournament.Tests.TournamentTestRunner - + diff --git a/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs b/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs index c4425643fd..b93f996ec2 100644 --- a/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; @@ -16,7 +17,6 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Notifications; using osu.Game.Resources.Localisation.Web; -using SharpCompress; namespace osu.Game.Overlays.Profile.Header.Components { diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index edf471ce8f..3793efd829 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -22,12 +22,12 @@ - - - - - - + + + + + + @@ -37,9 +37,9 @@ - + - + From eaf36796213de0c446e89115b5f71a757a06e959 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Feb 2025 17:17:07 +0900 Subject: [PATCH 507/801] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 3793efd829..6b5392eec6 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 8423d9de9b6447a42110ca69136c236175431439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Feb 2025 09:39:43 +0100 Subject: [PATCH 508/801] Fix distance snap grid colours being off-by-one in certain cases Closes https://github.com/ppy/osu/issues/31909. Previously: https://github.com/ppy/osu/pull/30062. Happening because of rounding errors - in this case the beat index pre-flooring was something like a 0.003 off of a full beat, which would get floored down rather than rounded up which created the discrepancy. But also we don't want to round *too* far, which is why this frankenstein solution has to exist I think. This is probably all exacerbated by stable not handling decimal control point start times. Would add tests if not for the fact that this is like extremely annoying to test. --- .../Edit/Compose/Components/DistanceSnapGrid.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs index dd1671cfdd..88e28df8e3 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs @@ -11,6 +11,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Layout; +using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Types; @@ -148,7 +149,18 @@ namespace osu.Game.Screens.Edit.Compose.Components { var timingPoint = Beatmap.ControlPointInfo.TimingPointAt(StartTime); double beatLength = timingPoint.BeatLength / beatDivisor.Value; - int beatIndex = (int)Math.Floor((StartTime - timingPoint.Time) / beatLength); + double fractionalBeatIndex = (StartTime - timingPoint.Time) / beatLength; + int beatIndex = (int)Math.Round(fractionalBeatIndex); + // `fractionalBeatIndex` could differ from `beatIndex` for two reasons: + // - rounding errors (which can be exacerbated by timing point start times being truncated by/for stable), + // - `StartTime` is not snapped to the beat. + // in case 1, we want rounding to occur to prevent an off-by-one, + // as `StartTime` *is* quantised to the beat. but it just doesn't look like it because floats do float things. + // in case 2, we want *flooring* to occur, to prevent a possible off-by-one + // because of the rounding snapping forward by a chunk of time significantly too high to be considered a rounding error. + // the tolerance margin chosen here is arbitrary and can be adjusted if more cases of this are found. + if (Precision.DefinitelyBigger(beatIndex, fractionalBeatIndex, 0.005)) + beatIndex = (int)Math.Floor(fractionalBeatIndex); var colour = BindableBeatDivisor.GetColourFor(BindableBeatDivisor.GetDivisorForBeatIndex(beatIndex + placementIndex + 1, beatDivisor.Value), Colours); From 2b4b21beb6c50b12e0daf4031b1dcb4fab75b3d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Feb 2025 09:45:09 +0100 Subject: [PATCH 509/801] Fix distance snap grid line opacity being incorrect on non-1.0x velocities Noticed in passing. --- .../Edit/Compose/Components/CircularDistanceSnapGrid.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs index 164a209958..8c7afd2aeb 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs @@ -63,7 +63,7 @@ namespace osu.Game.Screens.Edit.Compose.Components const float thickness = 4; float diameter = (offset + (i + 1) * DistanceBetweenTicks + thickness / 2) * 2; - AddInternal(new Ring(StartTime, GetColourForIndexFromPlacement(i)) + AddInternal(new Ring(StartTime, GetColourForIndexFromPlacement(i), SliderVelocitySource) { Position = StartPosition, Origin = Anchor.Centre, @@ -128,12 +128,14 @@ namespace osu.Game.Screens.Edit.Compose.Components private EditorClock? editorClock { get; set; } private readonly double startTime; + private readonly IHasSliderVelocity? sliderVelocitySource; private readonly Color4 baseColour; - public Ring(double startTime, Color4 baseColour) + public Ring(double startTime, Color4 baseColour, IHasSliderVelocity? sliderVelocitySource) { this.startTime = startTime; + this.sliderVelocitySource = sliderVelocitySource; Colour = this.baseColour = baseColour; @@ -150,7 +152,7 @@ namespace osu.Game.Screens.Edit.Compose.Components float distanceSpacingMultiplier = (float)snapProvider.DistanceSpacingMultiplier.Value; double timeFromReferencePoint = editorClock.CurrentTime - startTime; - float distanceForCurrentTime = snapProvider.DurationToDistance(timeFromReferencePoint, startTime) + float distanceForCurrentTime = snapProvider.DurationToDistance(timeFromReferencePoint, startTime, sliderVelocitySource) * distanceSpacingMultiplier; float timeBasedAlpha = 1 - Math.Clamp(Math.Abs(distanceForCurrentTime - Size.X / 2) / 30, 0, 1); From 5304ea2446b922cbfccef4bbefb058e30c224590 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Feb 2025 22:42:03 +0900 Subject: [PATCH 510/801] Fix minor typo --- osu.Game/Localisation/BeatmapSubmissionStrings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/BeatmapSubmissionStrings.cs b/osu.Game/Localisation/BeatmapSubmissionStrings.cs index 3abe8cc515..0cf0498daa 100644 --- a/osu.Game/Localisation/BeatmapSubmissionStrings.cs +++ b/osu.Game/Localisation/BeatmapSubmissionStrings.cs @@ -140,9 +140,9 @@ namespace osu.Game.Localisation public static LocalisableString LoadInBrowserAfterSubmission => new TranslatableString(getKey(@"load_in_browser_after_submission"), @"Load in browser after submission"); /// - /// "Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost." + /// "Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that this process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost." /// - public static LocalisableString LegacyExportDisclaimer => new TranslatableString(getKey(@"legacy_export_disclaimer"), @"Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost."); + public static LocalisableString LegacyExportDisclaimer => new TranslatableString(getKey(@"legacy_export_disclaimer"), @"Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that this process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost."); /// /// "Empty beatmaps cannot be submitted." From f37a56c3079ac78935069d7135c68a42d9dcc59f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Feb 2025 15:01:05 +0100 Subject: [PATCH 511/801] Fix nudge operations incurring FP error from coordinate space conversions Closes https://github.com/ppy/osu/issues/31915. Reproduction of aforementioned issue requires 1280x720 resolution, which should also be a good way to confirm that this does anything. To me this is also equal-parts-bugfix, equal-parts-code-quality PR, because tell me: what on earth was this code ever doing at `ComposeBlueprintContainer` level? Nudging by one playfield-space-unit doesn't even *make sense* in something like taiko or mania. --- .../Edit/CatchSelectionHandler.cs | 62 +++++++++++++++++ .../Edit/OsuSelectionHandler.cs | 62 +++++++++++++++++ .../Components/ComposeBlueprintContainer.cs | 66 ------------------- 3 files changed, 124 insertions(+), 66 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs index a2784126eb..a7cd84aed5 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Input.Events; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects; @@ -12,6 +13,7 @@ using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit.Compose.Components; using osuTK; +using osuTK.Input; using Direction = osu.Framework.Graphics.Direction; namespace osu.Game.Rulesets.Catch.Edit @@ -38,6 +40,13 @@ namespace osu.Game.Rulesets.Catch.Edit return true; } + moveSelection(deltaX); + + return true; + } + + private void moveSelection(float deltaX) + { EditorBeatmap.PerformOnSelection(h => { if (!(h is CatchHitObject catchObject)) return; @@ -48,7 +57,60 @@ namespace osu.Game.Rulesets.Catch.Edit foreach (var nested in catchObject.NestedHitObjects.OfType()) nested.OriginalX += deltaX; }); + } + private bool nudgeMovementActive; + + protected override bool OnKeyDown(KeyDownEvent e) + { + // Until the keys below are global actions, this will prevent conflicts with "seek between sample points" + // which has a default of ctrl+shift+arrows. + if (e.ShiftPressed) + return false; + + if (e.ControlPressed) + { + switch (e.Key) + { + case Key.Left: + return nudgeSelection(-1); + + case Key.Right: + return nudgeSelection(1); + } + } + + return false; + } + + protected override void OnKeyUp(KeyUpEvent e) + { + base.OnKeyUp(e); + + if (nudgeMovementActive && !e.ControlPressed) + { + EditorBeatmap.EndChange(); + nudgeMovementActive = false; + } + } + + /// + /// Move the current selection spatially by the specified delta, in gamefield coordinates (ie. the same coordinates as the blueprints). + /// + private bool nudgeSelection(float deltaX) + { + if (!nudgeMovementActive) + { + nudgeMovementActive = true; + EditorBeatmap.BeginChange(); + } + + var firstBlueprint = SelectedBlueprints.FirstOrDefault(); + + if (firstBlueprint == null) + return false; + + moveSelection(deltaX); return true; } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index bac0a5e273..3a1ff34fb9 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Osu.Edit SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider); } + private bool nudgeMovementActive; + protected override bool OnKeyDown(KeyDownEvent e) { if (e.Key == Key.M && e.ControlPressed && e.ShiftPressed) @@ -48,9 +50,43 @@ namespace osu.Game.Rulesets.Osu.Edit return true; } + // Until the keys below are global actions, this will prevent conflicts with "seek between sample points" + // which has a default of ctrl+shift+arrows. + if (e.ShiftPressed) + return false; + + if (e.ControlPressed) + { + switch (e.Key) + { + case Key.Left: + return nudgeSelection(new Vector2(-1, 0)); + + case Key.Right: + return nudgeSelection(new Vector2(1, 0)); + + case Key.Up: + return nudgeSelection(new Vector2(0, -1)); + + case Key.Down: + return nudgeSelection(new Vector2(0, 1)); + } + } + return false; } + protected override void OnKeyUp(KeyUpEvent e) + { + base.OnKeyUp(e); + + if (nudgeMovementActive && !e.ControlPressed) + { + EditorBeatmap.EndChange(); + nudgeMovementActive = false; + } + } + public override bool HandleMovement(MoveSelectionEvent moveEvent) { var hitObjects = selectedMovableObjects; @@ -70,6 +106,13 @@ namespace osu.Game.Rulesets.Osu.Edit if (hitObjects.Any(h => Precision.AlmostEquals(localDelta, -h.StackOffset))) return true; + moveObjects(hitObjects, localDelta); + + return true; + } + + private void moveObjects(OsuHitObject[] hitObjects, Vector2 localDelta) + { // this will potentially move the selection out of bounds... foreach (var h in hitObjects) h.Position += localDelta; @@ -81,7 +124,26 @@ namespace osu.Game.Rulesets.Osu.Edit // this intentionally bypasses the editor `UpdateState()` / beatmap processor flow for performance reasons, // as the entire flow is too expensive to run on every movement. Scheduler.AddOnce(OsuBeatmapProcessor.ApplyStacking, EditorBeatmap); + } + /// + /// Move the current selection spatially by the specified delta, in gamefield coordinates (ie. the same coordinates as the blueprints). + /// + /// + private bool nudgeSelection(Vector2 delta) + { + if (!nudgeMovementActive) + { + nudgeMovementActive = true; + EditorBeatmap.BeginChange(); + } + + var firstBlueprint = SelectedBlueprints.FirstOrDefault(); + + if (firstBlueprint == null) + return false; + + moveObjects(selectedMovableObjects, delta); return true; } diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index e82f6395d0..4c57eee971 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -27,7 +27,6 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit.Components.TernaryButtons; using osuTK; -using osuTK.Input; namespace osu.Game.Screens.Edit.Compose.Components { @@ -112,71 +111,6 @@ namespace osu.Game.Screens.Edit.Compose.Components blueprint.DrawableObject = drawableObject; } - private bool nudgeMovementActive; - - protected override bool OnKeyDown(KeyDownEvent e) - { - // Until the keys below are global actions, this will prevent conflicts with "seek between sample points" - // which has a default of ctrl+shift+arrows. - if (e.ShiftPressed) - return false; - - if (e.ControlPressed) - { - switch (e.Key) - { - case Key.Left: - return nudgeSelection(new Vector2(-1, 0)); - - case Key.Right: - return nudgeSelection(new Vector2(1, 0)); - - case Key.Up: - return nudgeSelection(new Vector2(0, -1)); - - case Key.Down: - return nudgeSelection(new Vector2(0, 1)); - } - } - - return false; - } - - protected override void OnKeyUp(KeyUpEvent e) - { - base.OnKeyUp(e); - - if (nudgeMovementActive && !e.ControlPressed) - { - Beatmap.EndChange(); - nudgeMovementActive = false; - } - } - - /// - /// Move the current selection spatially by the specified delta, in gamefield coordinates (ie. the same coordinates as the blueprints). - /// - /// - private bool nudgeSelection(Vector2 delta) - { - if (!nudgeMovementActive) - { - nudgeMovementActive = true; - Beatmap.BeginChange(); - } - - var firstBlueprint = SelectionHandler.SelectedBlueprints.FirstOrDefault(); - - if (firstBlueprint == null) - return false; - - // convert to game space coordinates - delta = firstBlueprint.ToScreenSpace(delta) - firstBlueprint.ToScreenSpace(Vector2.Zero); - - SelectionHandler.HandleMovement(new MoveSelectionEvent(firstBlueprint, delta)); - return true; - } - private void updatePlacementNewCombo() { if (CurrentHitObjectPlacement?.HitObject is IHasComboInformation c) From f5b485a44d1fb35be22c1b224837798b989248fd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Feb 2025 12:58:54 +0900 Subject: [PATCH 512/801] Stop "hold for HUD" key binding from blocking other key presses I don't think there's a good reason for this to be blocking. Closes https://github.com/ppy/osu/issues/31274. --- osu.Game/Screens/Play/HUDOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index f670e2f628..8bfa8dd6ff 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -414,7 +414,7 @@ namespace osu.Game.Screens.Play case GlobalAction.HoldForHUD: holdingForHUD.Value = true; - return true; + return false; case GlobalAction.ToggleInGameInterface: switch (configVisibilityMode.Value) From 20dbe096e03e043143388eab62e1650a3be1ea2a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Feb 2025 13:04:38 +0900 Subject: [PATCH 513/801] Refactor slightly --- osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs index 73d0403e3f..d331b691d5 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs @@ -19,15 +19,18 @@ namespace osu.Game.Rulesets.Osu.Mods typeof(OsuModTargetPractice), }).ToArray(); - [SettingSource("Fail when missing on a slider tail")] - public BindableBool SliderTailMiss { get; } = new BindableBool(); + [SettingSource("Also fail when missing a slider tail")] + public BindableBool FailOnSliderTail { get; } = new BindableBool(); protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) { - if (SliderTailMiss.Value && result.HitObject is SliderTailCircle && result.Type == HitResult.IgnoreMiss) + if (base.FailCondition(healthProcessor, result)) return true; - return base.FailCondition(healthProcessor, result); + if (FailOnSliderTail.Value && result.HitObject is SliderTailCircle && !result.IsHit) + return true; + + return false; } } } From 2d8e35be32a923049c717b3ae5906804097d67b6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Feb 2025 13:08:33 +0900 Subject: [PATCH 514/801] Add test coverage --- .../Mods/TestSceneOsuModSuddenDeath.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSuddenDeath.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSuddenDeath.cs index 688cf70f71..23dd2123c3 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSuddenDeath.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSuddenDeath.cs @@ -24,11 +24,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { } - [Test] - public void TestMissTail() => CreateModTest(new ModTestData + [TestCase(true)] + [TestCase(false)] + public void TestMissTail(bool tailMiss) => CreateModTest(new ModTestData { - Mod = new OsuModSuddenDeath(), - PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false), + Mod = new OsuModSuddenDeath + { + FailOnSliderTail = { Value = tailMiss } + }, + PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(tailMiss), Autoplay = false, CreateBeatmap = () => new Beatmap { From 77e40140e5b5fc1c83892492e9809dc4b1b708e3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Feb 2025 13:41:30 +0900 Subject: [PATCH 515/801] Fix selected sliders sometimes not being clickable in editor Closes https://github.com/ppy/osu/issues/31918. Regressed with https://github.com/ppy/osu/commit/1648f2efa306f587714178f113e69d8ad8c4ac02 for obvious reasons. --- .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index f7c25b43dd..39c0681dba 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -626,7 +626,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { - if (BodyPiece.ReceivePositionalInputAt(screenSpacePos) && DrawableObject.Body.Alpha > 0) + if (BodyPiece.ReceivePositionalInputAt(screenSpacePos) && (IsSelected || DrawableObject.Body.Alpha > 0)) return true; if (ControlPointVisualiser == null) From 8e25c9445234616f10053b5f2bba193e10444da9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Feb 2025 14:12:14 +0900 Subject: [PATCH 516/801] Fix kiai fountains sometimes not displaying when they should MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous logic was very wrong, as the check would only occur on each beat. But that's not how kiai sections work – they can be placed at any timestamp, even if that doesn't align with a beat. In addition, the rate limiting has been removed because it didn't exist on stable and causes some fountains to be missed. Overlap scenarios are already handled internally by the `StarFountain` class. Closes https://github.com/ppy/osu/issues/31855. --- .../Containers/BeatSyncedContainer.cs | 37 +++++++++++-------- osu.Game/Screens/Menu/KiaiMenuFountains.cs | 19 +++------- .../Screens/Play/KiaiGameplayFountains.cs | 21 ++++------- 3 files changed, 34 insertions(+), 43 deletions(-) diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index 7210371ebf..4331b91e61 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -73,6 +73,16 @@ namespace osu.Game.Graphics.Containers /// protected bool IsBeatSyncedWithTrack { get; private set; } + /// + /// The most valid timing point, updated every frame. + /// + protected TimingControlPoint TimingPoint { get; private set; } = TimingControlPoint.DEFAULT; + + /// + /// The most valid effect point, updated every frame. + /// + protected EffectControlPoint EffectPoint { get; private set; } = EffectControlPoint.DEFAULT; + [Resolved] protected IBeatSyncProvider BeatSyncSource { get; private set; } = null!; @@ -82,9 +92,6 @@ namespace osu.Game.Graphics.Containers protected override void Update() { - TimingControlPoint timingPoint; - EffectControlPoint effectPoint; - IsBeatSyncedWithTrack = BeatSyncSource.Clock.IsRunning; double currentTrackTime; @@ -102,8 +109,8 @@ namespace osu.Game.Graphics.Containers currentTrackTime = BeatSyncSource.Clock.CurrentTime + early; - timingPoint = BeatSyncSource.ControlPoints?.TimingPointAt(currentTrackTime) ?? TimingControlPoint.DEFAULT; - effectPoint = BeatSyncSource.ControlPoints?.EffectPointAt(currentTrackTime) ?? EffectControlPoint.DEFAULT; + TimingPoint = BeatSyncSource.ControlPoints?.TimingPointAt(currentTrackTime) ?? TimingControlPoint.DEFAULT; + EffectPoint = BeatSyncSource.ControlPoints?.EffectPointAt(currentTrackTime) ?? EffectControlPoint.DEFAULT; } else { @@ -111,28 +118,28 @@ namespace osu.Game.Graphics.Containers // we still want to show an idle animation, so use this container's time instead. currentTrackTime = Clock.CurrentTime + EarlyActivationMilliseconds; - timingPoint = TimingControlPoint.DEFAULT; - effectPoint = EffectControlPoint.DEFAULT; + TimingPoint = TimingControlPoint.DEFAULT; + EffectPoint = EffectControlPoint.DEFAULT; } - double beatLength = timingPoint.BeatLength / Divisor; + double beatLength = TimingPoint.BeatLength / Divisor; while (beatLength < MinimumBeatLength) beatLength *= 2; - int beatIndex = (int)((currentTrackTime - timingPoint.Time) / beatLength) - (timingPoint.OmitFirstBarLine ? 1 : 0); + int beatIndex = (int)((currentTrackTime - TimingPoint.Time) / beatLength) - (TimingPoint.OmitFirstBarLine ? 1 : 0); // The beats before the start of the first control point are off by 1, this should do the trick - if (currentTrackTime < timingPoint.Time) + if (currentTrackTime < TimingPoint.Time) beatIndex--; - TimeUntilNextBeat = (timingPoint.Time - currentTrackTime) % beatLength; + TimeUntilNextBeat = (TimingPoint.Time - currentTrackTime) % beatLength; if (TimeUntilNextBeat <= 0) TimeUntilNextBeat += beatLength; TimeSinceLastBeat = beatLength - TimeUntilNextBeat; - if (ReferenceEquals(timingPoint, lastTimingPoint) && beatIndex == lastBeat) + if (ReferenceEquals(TimingPoint, lastTimingPoint) && beatIndex == lastBeat) return; // as this event is sometimes used for sound triggers where `BeginDelayedSequence` has no effect, avoid firing it if too far away from the beat. @@ -140,13 +147,13 @@ namespace osu.Game.Graphics.Containers if (AllowMistimedEventFiring || Math.Abs(TimeSinceLastBeat) < MISTIMED_ALLOWANCE) { using (BeginDelayedSequence(-TimeSinceLastBeat)) - OnNewBeat(beatIndex, timingPoint, effectPoint, BeatSyncSource.CurrentAmplitudes); + OnNewBeat(beatIndex, TimingPoint, EffectPoint, BeatSyncSource.CurrentAmplitudes); } lastBeat = beatIndex; - lastTimingPoint = timingPoint; + lastTimingPoint = TimingPoint; - IsKiaiTime = effectPoint.KiaiMode; + IsKiaiTime = EffectPoint.KiaiMode; } } } diff --git a/osu.Game/Screens/Menu/KiaiMenuFountains.cs b/osu.Game/Screens/Menu/KiaiMenuFountains.cs index 07c06dcdb9..7978e9fa91 100644 --- a/osu.Game/Screens/Menu/KiaiMenuFountains.cs +++ b/osu.Game/Screens/Menu/KiaiMenuFountains.cs @@ -3,10 +3,8 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Audio.Track; using osu.Framework.Graphics; using osu.Framework.Utils; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.Containers; namespace osu.Game.Screens.Menu @@ -40,27 +38,22 @@ namespace osu.Game.Screens.Menu private bool isTriggered; - private double? lastTrigger; - - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) + protected override void Update() { - base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + base.Update(); - if (effectPoint.KiaiMode && !isTriggered) + if (EffectPoint.KiaiMode && !isTriggered) { - bool isNearEffectPoint = Math.Abs(BeatSyncSource.Clock.CurrentTime - effectPoint.Time) < 500; + bool isNearEffectPoint = Math.Abs(BeatSyncSource.Clock.CurrentTime - EffectPoint.Time) < 500; if (isNearEffectPoint) Shoot(); } - isTriggered = effectPoint.KiaiMode; + isTriggered = EffectPoint.KiaiMode; } public void Shoot() { - if (lastTrigger != null && Clock.CurrentTime - lastTrigger < 500) - return; - int direction = RNG.Next(-1, 2); switch (direction) @@ -80,8 +73,6 @@ namespace osu.Game.Screens.Menu rightFountain.Shoot(1); break; } - - lastTrigger = Clock.CurrentTime; } } } diff --git a/osu.Game/Screens/Play/KiaiGameplayFountains.cs b/osu.Game/Screens/Play/KiaiGameplayFountains.cs index fd9596c838..19a9c2b6e5 100644 --- a/osu.Game/Screens/Play/KiaiGameplayFountains.cs +++ b/osu.Game/Screens/Play/KiaiGameplayFountains.cs @@ -1,15 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable using System; using osu.Framework.Allocation; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Logging; using osu.Framework.Utils; using osu.Game.Configuration; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.Containers; using osu.Game.Screens.Menu; @@ -48,33 +46,28 @@ namespace osu.Game.Screens.Play private bool isTriggered; - private double? lastTrigger; - - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) + protected override void Update() { - base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + base.Update(); if (!kiaiStarFountains.Value) return; - if (effectPoint.KiaiMode && !isTriggered) + if (EffectPoint.KiaiMode && !isTriggered) { - bool isNearEffectPoint = Math.Abs(BeatSyncSource.Clock.CurrentTime - effectPoint.Time) < 500; + Logger.Log("shooting"); + bool isNearEffectPoint = Math.Abs(BeatSyncSource.Clock.CurrentTime - EffectPoint.Time) < 500; if (isNearEffectPoint) Shoot(); } - isTriggered = effectPoint.KiaiMode; + isTriggered = EffectPoint.KiaiMode; } public void Shoot() { - if (lastTrigger != null && Clock.CurrentTime - lastTrigger < 500) - return; - leftFountain.Shoot(1); rightFountain.Shoot(-1); - lastTrigger = Clock.CurrentTime; } public partial class GameplayStarFountain : StarFountain From 88ec204d264f17020d75e54eb2b6430361f35995 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Feb 2025 16:22:57 +0900 Subject: [PATCH 517/801] User inheritance to avoid `Piece` structural nightmare --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 2 +- .../TestSceneBeatmapCarouselV2GroupPanel.cs | 12 +- .../{CarouselPanelPiece.cs => PanelBase.cs} | 58 +++-- osu.Game/Screens/SelectV2/PanelBeatmap.cs | 158 ++++++------- osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 68 +++--- .../SelectV2/PanelBeatmapStandalone.cs | 221 ++++++++---------- osu.Game/Screens/SelectV2/PanelGroup.cs | 111 ++++----- .../SelectV2/PanelGroupStarDifficulty.cs | 149 ++++++++++++ .../SelectV2/PanelGroupStarDificulty.cs | 187 --------------- 9 files changed, 425 insertions(+), 541 deletions(-) rename osu.Game/Screens/SelectV2/{CarouselPanelPiece.cs => PanelBase.cs} (86%) create mode 100644 osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs delete mode 100644 osu.Game/Screens/SelectV2/PanelGroupStarDificulty.cs diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 2c422e0a85..2c902a466f 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -189,7 +189,7 @@ namespace osu.Game.Tests.Visual.SongSelect .Where(p => ((ICarouselPanel)p).Item?.IsVisible == true) .OrderBy(p => p.Y) .ElementAt(index) - .ChildrenOfType().Single() + .ChildrenOfType().Single() .TriggerClick(); }); } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs index 711a3b881d..9b07f01e52 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs @@ -49,29 +49,29 @@ namespace osu.Game.Tests.Visual.SongSelectV2 KeyboardSelected = { Value = true }, Expanded = { Value = true } }, - new PanelGroupStarDificulty + new PanelGroupStarDifficulty { Item = new CarouselItem(new GroupDefinition(1, "1")) }, - new PanelGroupStarDificulty + new PanelGroupStarDifficulty { Item = new CarouselItem(new GroupDefinition(3, "3")), Expanded = { Value = true } }, - new PanelGroupStarDificulty + new PanelGroupStarDifficulty { Item = new CarouselItem(new GroupDefinition(5, "5")), }, - new PanelGroupStarDificulty + new PanelGroupStarDifficulty { Item = new CarouselItem(new GroupDefinition(7, "7")), Expanded = { Value = true } }, - new PanelGroupStarDificulty + new PanelGroupStarDifficulty { Item = new CarouselItem(new GroupDefinition(8, "8")), }, - new PanelGroupStarDificulty + new PanelGroupStarDifficulty { Item = new CarouselItem(new GroupDefinition(9, "9")), Expanded = { Value = true } diff --git a/osu.Game/Screens/SelectV2/CarouselPanelPiece.cs b/osu.Game/Screens/SelectV2/PanelBase.cs similarity index 86% rename from osu.Game/Screens/SelectV2/CarouselPanelPiece.cs rename to osu.Game/Screens/SelectV2/PanelBase.cs index 5aefa57bb5..d5a087dbb2 100644 --- a/osu.Game/Screens/SelectV2/CarouselPanelPiece.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -9,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Graphics; @@ -19,7 +19,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public partial class CarouselPanelPiece : Container + public abstract partial class PanelBase : PoolableDrawable, ICarouselPanel { private const float corner_radius = 10; @@ -43,7 +43,7 @@ namespace osu.Game.Screens.SelectV2 public Container TopLevelContent { get; } - protected override Container Content { get; } + protected Container Content { get; } public Drawable Background { @@ -67,11 +67,6 @@ namespace osu.Game.Screens.SelectV2 } } - public readonly BindableBool Active = new BindableBool(); - public readonly BindableBool KeyboardActive = new BindableBool(); - - public Action? Action { get; init; } - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { var inputRectangle = TopLevelContent.DrawRectangle; @@ -82,7 +77,7 @@ namespace osu.Game.Screens.SelectV2 return inputRectangle.Contains(TopLevelContent.ToLocalSpace(screenSpacePos)); } - public CarouselPanelPiece(float panelXOffset) + protected PanelBase(float panelXOffset = 0) { this.panelXOffset = panelXOffset; @@ -183,8 +178,17 @@ namespace osu.Game.Screens.SelectV2 { base.LoadComplete(); - Active.BindValueChanged(_ => updateDisplay()); - KeyboardActive.BindValueChanged(_ => updateDisplay(), true); + Expanded.BindValueChanged(_ => updateDisplay()); + KeyboardSelected.BindValueChanged(_ => updateDisplay(), true); + } + + [Resolved] + private BeatmapCarousel? carousel { get; set; } + + protected override bool OnClick(ClickEvent e) + { + carousel?.Activate(Item!); + return true; } public void Flash() @@ -194,7 +198,7 @@ namespace osu.Game.Screens.SelectV2 private void updateDisplay() { - backgroundLayer.TransformTo(nameof(Padding), backgroundLayer.Padding with { Vertical = Active.Value ? 2f : 0f }, duration, Easing.OutQuint); + backgroundLayer.TransformTo(nameof(Padding), backgroundLayer.Padding with { Vertical = Expanded.Value ? 2f : 0f }, duration, Easing.OutQuint); var backgroundColour = accentColour ?? Color4.White; var edgeEffectColour = accentColour ?? Color4Extensions.FromHex(@"4EBFFF"); @@ -202,7 +206,7 @@ namespace osu.Game.Screens.SelectV2 backgroundAccentGradient.FadeColour(ColourInfo.GradientHorizontal(backgroundColour.Opacity(0.25f), backgroundColour.Opacity(0f)), duration, Easing.OutQuint); backgroundBorder.FadeColour(backgroundColour, duration, Easing.OutQuint); - TopLevelContent.FadeEdgeEffectTo(Active.Value ? edgeEffectColour.Opacity(0.5f) : Color4.Black.Opacity(0.4f), duration, Easing.OutQuint); + TopLevelContent.FadeEdgeEffectTo(Expanded.Value ? edgeEffectColour.Opacity(0.5f) : Color4.Black.Opacity(0.4f), duration, Easing.OutQuint); updateXOffset(); updateHover(); @@ -212,10 +216,10 @@ namespace osu.Game.Screens.SelectV2 { float x = panelXOffset + active_x_offset + keyboard_active_x_offset + left_edge_x_offset; - if (Active.Value) + if (Expanded.Value) x -= active_x_offset; - if (KeyboardActive.Value) + if (KeyboardSelected.Value) x -= keyboard_active_x_offset; this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); @@ -223,7 +227,7 @@ namespace osu.Game.Screens.SelectV2 private void updateHover() { - bool hovered = IsHovered || KeyboardActive.Value; + bool hovered = IsHovered || KeyboardSelected.Value; if (hovered) hoverLayer.FadeIn(100, Easing.OutQuint); @@ -243,17 +247,27 @@ namespace osu.Game.Screens.SelectV2 base.OnHoverLost(e); } - protected override bool OnClick(ClickEvent e) - { - Action?.Invoke(); - return true; - } - protected override void Update() { base.Update(); Content.Padding = Content.Padding with { Left = iconContainer.DrawWidth }; backgroundLayerHorizontalPadding.Padding = new MarginPadding { Left = iconContainer.DrawWidth }; } + + #region ICarouselPanel + + public CarouselItem? Item { get; set; } + public BindableBool Selected { get; } = new BindableBool(); + public BindableBool Expanded { get; } = new BindableBool(); + public BindableBool KeyboardSelected { get; } = new BindableBool(); + + public double DrawYPosition { get; set; } + + public virtual void Activated() + { + activationFlash.FadeOutFromOne(500, Easing.OutQuint); + } + + #endregion } } diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index 93ef814f2e..48d15f6857 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -8,7 +8,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Pooling; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; @@ -23,7 +22,7 @@ using osuTK; namespace osu.Game.Screens.SelectV2 { - public partial class PanelBeatmap : PoolableDrawable, ICarouselPanel + public partial class PanelBeatmap : PanelBase { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; @@ -33,7 +32,6 @@ namespace osu.Game.Screens.SelectV2 private const float duration = 500; - private CarouselPanelPiece panel = null!; private StarCounter starCounter = null!; private ConstrainedIconContainer difficultyIcon = null!; private OsuSpriteText keyCountText = null!; @@ -54,9 +52,6 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private BeatmapDifficultyCache difficultyCache { get; set; } = null!; - [Resolved] - private BeatmapCarousel? carousel { get; set; } - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { var inputRectangle = DrawRectangle; @@ -86,84 +81,81 @@ namespace osu.Game.Screens.SelectV2 Width = 1f; Height = HEIGHT; - InternalChild = panel = new CarouselPanelPiece(difficulty_x_offset) + Icon = difficultyIcon = new ConstrainedIconContainer { - Action = () => carousel?.Activate(Item!), - Icon = difficultyIcon = new ConstrainedIconContainer + Size = new Vector2(20), + Margin = new MarginPadding { Horizontal = 5f }, + Colour = colourProvider.Background5, + }; + + Content.Children = new[] + { + new FillFlowContainer { - Size = new Vector2(20), - Margin = new MarginPadding { Horizontal = 5f }, - Colour = colourProvider.Background5, - }, - Children = new[] - { - new FillFlowContainer + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Padding = new MarginPadding { Left = 10f }, + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Padding = new MarginPadding { Left = 10f }, - Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Both, - Children = new Drawable[] + new FillFlowContainer { - new FillFlowContainer + Direction = FillDirection.Horizontal, + Spacing = new Vector2(3, 0), + AutoSizeAxes = Axes.Both, + Children = new Drawable[] { - Direction = FillDirection.Horizontal, - Spacing = new Vector2(3, 0), - AutoSizeAxes = Axes.Both, - Children = new Drawable[] + starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small) { - starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - difficultyRank = new TopLocalRank - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Scale = new Vector2(0.75f) - }, - starCounter = new StarCounter - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Scale = new Vector2(0.4f) - } + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + difficultyRank = new TopLocalRank + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Scale = new Vector2(0.75f) + }, + starCounter = new StarCounter + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Scale = new Vector2(0.4f) } - }, - new FillFlowContainer + } + }, + new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Children = new[] { - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - Children = new[] + keyCountText = new OsuSpriteText { - keyCountText = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Alpha = 0, - }, - difficultyText = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Margin = new MarginPadding { Right = 8f }, - }, - authorText = new OsuSpriteText - { - Colour = colourProvider.Content2, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold), - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft - } + Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Alpha = 0, + }, + difficultyText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding { Right = 8f }, + }, + authorText = new OsuSpriteText + { + Colour = colourProvider.Content2, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft } } } - }, - } + } + }, }; } @@ -183,8 +175,8 @@ namespace osu.Game.Screens.SelectV2 updateKeyCount(); }, true); - Selected.BindValueChanged(s => panel.Active.Value = s.NewValue, true); - KeyboardSelected.BindValueChanged(k => panel.KeyboardActive.Value = k.NewValue, true); + Selected.BindValueChanged(s => Expanded.Value = s.NewValue, true); + KeyboardSelected.BindValueChanged(k => KeyboardSelected.Value = k.NewValue, true); } protected override void PrepareForUse() @@ -261,23 +253,7 @@ namespace osu.Game.Screens.SelectV2 var starRatingColour = colours.ForStarDifficulty(starDifficulty.Stars); starCounter.FadeColour(starRatingColour, duration, Easing.OutQuint); - panel.AccentColour = starRatingColour; + AccentColour = starRatingColour; } - - #region ICarouselPanel - - public CarouselItem? Item { get; set; } - public BindableBool Selected { get; } = new BindableBool(); - public BindableBool Expanded { get; } = new BindableBool(); - public BindableBool KeyboardSelected { get; } = new BindableBool(); - - public double DrawYPosition { get; set; } - - public void Activated() - { - panel.Flash(); - } - - #endregion } } diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index 2904cda9de..742fe6b6e6 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -4,10 +4,8 @@ using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Beatmaps; @@ -19,7 +17,7 @@ using osuTK; namespace osu.Game.Screens.SelectV2 { - public partial class PanelBeatmapSet : PoolableDrawable, ICarouselPanel + public partial class PanelBeatmapSet : PanelBase { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; @@ -29,7 +27,6 @@ namespace osu.Game.Screens.SelectV2 private const float duration = 500; - private CarouselPanelPiece panel = null!; private BeatmapSetPanelBackground background = null!; private OsuSpriteText titleText = null!; @@ -39,15 +36,17 @@ namespace osu.Game.Screens.SelectV2 private BeatmapSetOnlineStatusPill statusPill = null!; private DifficultySpectrumDisplay difficultiesDisplay = null!; - [Resolved] - private BeatmapCarousel? carousel { get; set; } - [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; [Resolved] private BeatmapManager beatmaps { get; set; } = null!; + public PanelBeatmapSet() + : base(set_x_offset) + { + } + [BackgroundDependencyLoader] private void load() { @@ -56,27 +55,28 @@ namespace osu.Game.Screens.SelectV2 RelativeSizeAxes = Axes.X; Height = HEIGHT; - InternalChild = panel = new CarouselPanelPiece(set_x_offset) + Icon = chevronIcon = new Container { - Action = () => carousel?.Activate(Item!), - Icon = chevronIcon = new Container + Size = new Vector2(22), + Child = new SpriteIcon { - Size = new Vector2(22), - Child = new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Icon = FontAwesome.Solid.ChevronRight, - Size = new Vector2(12), - X = 1f, - Colour = colourProvider.Background5, - }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.ChevronRight, + Size = new Vector2(12), + X = 1f, + Colour = colourProvider.Background5, }, - Background = background = new BeatmapSetPanelBackground - { - RelativeSizeAxes = Axes.Both, - }, - Child = new FillFlowContainer + }; + + Background = background = new BeatmapSetPanelBackground + { + RelativeSizeAxes = Axes.Both, + }; + + Content.Children = new[] + { + new FillFlowContainer { AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, @@ -132,12 +132,11 @@ namespace osu.Game.Screens.SelectV2 base.LoadComplete(); Expanded.BindValueChanged(_ => onExpanded(), true); - KeyboardSelected.BindValueChanged(k => panel.KeyboardActive.Value = k.NewValue, true); + KeyboardSelected.BindValueChanged(k => KeyboardSelected.Value = k.NewValue, true); } private void onExpanded() { - panel.Active.Value = Expanded.Value; chevronIcon.ResizeWidthTo(Expanded.Value ? 22 : 0f, duration, Easing.OutQuint); chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); } @@ -171,20 +170,5 @@ namespace osu.Game.Screens.SelectV2 updateButton.BeatmapSet = null; difficultiesDisplay.BeatmapSet = null; } - - #region ICarouselPanel - - public CarouselItem? Item { get; set; } - public BindableBool Selected { get; } = new BindableBool(); - public BindableBool Expanded { get; } = new BindableBool(); - public BindableBool KeyboardSelected { get; } = new BindableBool(); - - public double DrawYPosition { get; set; } - - public void Activated() - { - } - - #endregion } } diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs index c858e039ec..c94a337cd9 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -10,7 +9,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Pooling; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; @@ -25,7 +23,7 @@ using osuTK; namespace osu.Game.Screens.SelectV2 { - public partial class PanelBeatmapStandalone : PoolableDrawable, ICarouselPanel + public partial class PanelBeatmapStandalone : PanelBase { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; @@ -35,9 +33,6 @@ namespace osu.Game.Screens.SelectV2 private const float duration = 500; - [Resolved] - private BeatmapCarousel? carousel { get; set; } - [Resolved] private IBindable ruleset { get; set; } = null!; @@ -59,7 +54,6 @@ namespace osu.Game.Screens.SelectV2 private IBindable? starDifficultyBindable; private CancellationTokenSource? starDifficultyCancellationSource; - private CarouselPanelPiece panel = null!; private BeatmapSetPanelBackground background = null!; private OsuSpriteText titleText = null!; @@ -75,6 +69,11 @@ namespace osu.Game.Screens.SelectV2 private OsuSpriteText difficultyName = null!; private OsuSpriteText difficultyAuthor = null!; + public PanelBeatmapStandalone() + : base(standalone_x_offset) + { + } + [BackgroundDependencyLoader] private void load() { @@ -84,107 +83,105 @@ namespace osu.Game.Screens.SelectV2 Width = 1f; Height = HEIGHT; - InternalChild = panel = new CarouselPanelPiece(standalone_x_offset) + Icon = difficultyIcon = new ConstrainedIconContainer { - Action = () => carousel?.Activate(Item!), - Icon = difficultyIcon = new ConstrainedIconContainer + Size = new Vector2(20), + Margin = new MarginPadding { Horizontal = 5f }, + Colour = colourProvider.Background5, + }; + + Background = background = new BeatmapSetPanelBackground + { + RelativeSizeAxes = Axes.Both, + }; + + Content.Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Top = 7.5f, Left = 15, Bottom = 5 }, + Children = new Drawable[] { - Size = new Vector2(20), - Margin = new MarginPadding { Horizontal = 5f }, - Colour = colourProvider.Background5, - }, - Background = background = new BeatmapSetPanelBackground - { - RelativeSizeAxes = Axes.Both, - }, - Child = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Padding = new MarginPadding { Top = 7.5f, Left = 15, Bottom = 5 }, - Children = new Drawable[] + titleText = new OsuSpriteText { - titleText = new OsuSpriteText + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), + Shadow = true, + }, + artistText = new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), + Shadow = true, + }, + new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Top = 5f }, + Children = new Drawable[] { - Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), - Shadow = true, - }, - artistText = new OsuSpriteText - { - Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), - Shadow = true, - }, - new FillFlowContainer - { - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Top = 5f }, - Children = new Drawable[] + updateButton = new UpdateBeatmapSetButton { - updateButton = new UpdateBeatmapSetButton - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Margin = new MarginPadding { Right = 5f, Top = -2f }, - }, - statusPill = new BeatmapSetOnlineStatusPill - { - AutoSizeAxes = Axes.Both, - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - TextSize = 11, - TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, - Margin = new MarginPadding { Right = 5f }, - }, - difficultyLine = new FillFlowContainer - { - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - difficultyStarRating = new StarRatingDisplay(default, StarRatingDisplaySize.Small) - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Scale = new Vector2(8f / 9f), - Margin = new MarginPadding { Right = 5f }, - }, - difficultyRank = new TopLocalRank - { - Scale = new Vector2(8f / 11), - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Margin = new MarginPadding { Right = 5f }, - }, - difficultyKeyCountText = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Alpha = 0, - Margin = new MarginPadding { Bottom = 2f }, - }, - difficultyName = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, - Margin = new MarginPadding { Right = 5f, Bottom = 2f }, - }, - difficultyAuthor = new OsuSpriteText - { - Colour = colourProvider.Content2, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold), - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, - Margin = new MarginPadding { Right = 5f, Bottom = 2f }, - } - } - }, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Right = 5f, Top = -2f }, }, - } + statusPill = new BeatmapSetOnlineStatusPill + { + AutoSizeAxes = Axes.Both, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + TextSize = 11, + TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, + Margin = new MarginPadding { Right = 5f }, + }, + difficultyLine = new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + difficultyStarRating = new StarRatingDisplay(default, StarRatingDisplaySize.Small) + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Scale = new Vector2(8f / 9f), + Margin = new MarginPadding { Right = 5f }, + }, + difficultyRank = new TopLocalRank + { + Scale = new Vector2(8f / 11), + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Margin = new MarginPadding { Right = 5f }, + }, + difficultyKeyCountText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Alpha = 0, + Margin = new MarginPadding { Bottom = 2f }, + }, + difficultyName = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + Margin = new MarginPadding { Right = 5f, Bottom = 2f }, + }, + difficultyAuthor = new OsuSpriteText + { + Colour = colourProvider.Content2, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + Margin = new MarginPadding { Right = 5f, Bottom = 2f }, + } + } + }, + }, } - }, + } }; } @@ -203,9 +200,6 @@ namespace osu.Game.Screens.SelectV2 computeStarRating(); updateKeyCount(); }, true); - - Selected.BindValueChanged(s => panel.Active.Value = s.NewValue, true); - KeyboardSelected.BindValueChanged(k => panel.KeyboardActive.Value = k.NewValue, true); } protected override void PrepareForUse() @@ -289,26 +283,9 @@ namespace osu.Game.Screens.SelectV2 { var starDifficulty = starDifficultyBindable?.Value ?? default; - panel.AccentColour = colours.ForStarDifficulty(starDifficulty.Stars); + AccentColour = colours.ForStarDifficulty(starDifficulty.Stars); difficultyIcon.FadeColour(starDifficulty.Stars > 6.5f ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint); difficultyStarRating.Current.Value = starDifficulty; } - - #region ICarouselPanel - - public CarouselItem? Item { get; set; } - public BindableBool Selected { get; } = new BindableBool(); - public BindableBool Expanded { get; } = new BindableBool(); - public BindableBool KeyboardSelected { get; } = new BindableBool(); - - public double DrawYPosition { get; set; } - - public void Activated() - { - // sets should never be activated. - throw new InvalidOperationException(); - } - - #endregion } } diff --git a/osu.Game/Screens/SelectV2/PanelGroup.cs b/osu.Game/Screens/SelectV2/PanelGroup.cs index cdd0695147..2b4fb9e4a9 100644 --- a/osu.Game/Screens/SelectV2/PanelGroup.cs +++ b/osu.Game/Screens/SelectV2/PanelGroup.cs @@ -3,11 +3,9 @@ using System.Diagnostics; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; @@ -18,16 +16,12 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public partial class PanelGroup : PoolableDrawable, ICarouselPanel + public partial class PanelGroup : PanelBase { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; private const float duration = 500; - [Resolved] - private BeatmapCarousel? carousel { get; set; } - - private CarouselPanelPiece panel = null!; private Drawable chevronIcon = null!; private OsuSpriteText titleText = null!; @@ -39,57 +33,53 @@ namespace osu.Game.Screens.SelectV2 RelativeSizeAxes = Axes.X; Height = HEIGHT; - InternalChild = panel = new CarouselPanelPiece(0) + Icon = chevronIcon = new SpriteIcon { - Action = () => carousel?.Activate(Item!), - Icon = chevronIcon = new SpriteIcon + AlwaysPresent = true, + Icon = FontAwesome.Solid.ChevronDown, + Size = new Vector2(12), + Margin = new MarginPadding { Horizontal = 5f }, + X = 2f, + Colour = colourProvider.Background3, + }; + Background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Dark1, + }; + AccentColour = colourProvider.Highlight1; + Content.Children = new Drawable[] + { + titleText = new OsuSpriteText { - AlwaysPresent = true, - Icon = FontAwesome.Solid.ChevronDown, - Size = new Vector2(12), - Margin = new MarginPadding { Horizontal = 5f }, - X = 2f, - Colour = colourProvider.Background3, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + X = 10f, }, - Background = new Box + new CircularContainer { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Dark1, - }, - AccentColour = colourProvider.Highlight1, - Children = new Drawable[] - { - titleText = new OsuSpriteText + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(50f, 14f), + Margin = new MarginPadding { Right = 20f }, + Masking = true, + Children = new Drawable[] { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - X = 10f, - }, - new CircularContainer - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Size = new Vector2(50f, 14f), - Margin = new MarginPadding { Right = 20f }, - Masking = true, - Children = new Drawable[] + new Box { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black.Opacity(0.7f), - }, - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), - // TODO: requires Carousel/CarouselItem-side implementation - Text = "43", - UseFullGlyphHeight = false, - } + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.7f), }, - } + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), + // TODO: requires Carousel/CarouselItem-side implementation + Text = "43", + UseFullGlyphHeight = false, + } + }, } }; } @@ -99,14 +89,10 @@ namespace osu.Game.Screens.SelectV2 base.LoadComplete(); Expanded.BindValueChanged(_ => onExpanded(), true); - KeyboardSelected.BindValueChanged(k => panel.KeyboardActive.Value = k.NewValue, true); } private void onExpanded() { - panel.Active.Value = Expanded.Value; - panel.Flash(); - chevronIcon.ResizeWidthTo(Expanded.Value ? 12f : 0f, duration, Easing.OutQuint); chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); } @@ -124,20 +110,5 @@ namespace osu.Game.Screens.SelectV2 FinishTransforms(true); this.FadeInFromZero(500, Easing.OutQuint); } - - #region ICarouselPanel - - public CarouselItem? Item { get; set; } - public BindableBool Selected { get; } = new BindableBool(); - public BindableBool Expanded { get; } = new BindableBool(); - public BindableBool KeyboardSelected { get; } = new BindableBool(); - - public double DrawYPosition { get; set; } - - public void Activated() - { - } - - #endregion } } diff --git a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs new file mode 100644 index 0000000000..736a0f71dc --- /dev/null +++ b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs @@ -0,0 +1,149 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class PanelGroupStarDifficulty : PanelBase + { + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; + + private const float duration = 500; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + private Drawable chevronIcon = null!; + private Box contentBackground = null!; + private StarRatingDisplay starRatingDisplay = null!; + private StarCounter starCounter = null!; + + [BackgroundDependencyLoader] + private void load() + { + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + RelativeSizeAxes = Axes.X; + Height = HEIGHT; + + Icon = chevronIcon = new SpriteIcon + { + AlwaysPresent = true, + Icon = FontAwesome.Solid.ChevronDown, + Size = new Vector2(12), + Margin = new MarginPadding { Horizontal = 5f }, + X = 2f, + }; + Background = contentBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Dark1, + }; + AccentColour = colourProvider.Highlight1; + Content.Children = new Drawable[] + { + new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(10f, 0f), + Margin = new MarginPadding { Left = 10f }, + Children = new Drawable[] + { + starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + starCounter = new StarCounter + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Scale = new Vector2(8f / 20f), + }, + } + }, + new CircularContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(50f, 14f), + Margin = new MarginPadding { Right = 20f }, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.7f), + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), + // TODO: requires Carousel/CarouselItem-side implementation + Text = "43", + UseFullGlyphHeight = false, + } + }, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Expanded.BindValueChanged(_ => onExpanded(), true); + } + + private void onExpanded() + { + chevronIcon.ResizeWidthTo(Expanded.Value ? 12f : 0f, duration, Easing.OutQuint); + chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + Debug.Assert(Item != null); + + int starNumber = (int)((GroupDefinition)Item.Model).Data; + + Color4 colour = starNumber >= 9 ? OsuColour.Gray(0.2f) : colours.ForStarDifficulty(starNumber); + Color4 contentColour = starNumber >= 7 ? colours.Orange1 : colourProvider.Background5; + + AccentColour = colour; + contentBackground.Colour = colour.Darken(0.3f); + + starRatingDisplay.Current.Value = new StarDifficulty(starNumber, 0); + starCounter.Current = starNumber; + + chevronIcon.Colour = contentColour; + starCounter.Colour = contentColour; + + this.FadeInFromZero(500, Easing.OutQuint); + } + } +} diff --git a/osu.Game/Screens/SelectV2/PanelGroupStarDificulty.cs b/osu.Game/Screens/SelectV2/PanelGroupStarDificulty.cs deleted file mode 100644 index 2215e643bd..0000000000 --- a/osu.Game/Screens/SelectV2/PanelGroupStarDificulty.cs +++ /dev/null @@ -1,187 +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; -using System.Diagnostics; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Pooling; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Drawables; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Overlays; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Screens.SelectV2 -{ - public partial class PanelGroupStarDificulty : PoolableDrawable, ICarouselPanel - { - public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; - - private const float duration = 500; - - [Resolved] - private BeatmapCarousel? carousel { get; set; } - - [Resolved] - private OsuColour colours { get; set; } = null!; - - [Resolved] - private OverlayColourProvider colourProvider { get; set; } = null!; - - private CarouselPanelPiece panel = null!; - private Drawable chevronIcon = null!; - private Box contentBackground = null!; - private StarRatingDisplay starRatingDisplay = null!; - private StarCounter starCounter = null!; - - [BackgroundDependencyLoader] - private void load() - { - Anchor = Anchor.TopRight; - Origin = Anchor.TopRight; - RelativeSizeAxes = Axes.X; - Height = HEIGHT; - - InternalChild = panel = new CarouselPanelPiece(0) - { - Action = onAction, - Icon = chevronIcon = new SpriteIcon - { - AlwaysPresent = true, - Icon = FontAwesome.Solid.ChevronDown, - Size = new Vector2(12), - Margin = new MarginPadding { Horizontal = 5f }, - X = 2f, - }, - Background = contentBackground = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Dark1, - }, - AccentColour = colourProvider.Highlight1, - Children = new Drawable[] - { - new FillFlowContainer - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(10f, 0f), - Margin = new MarginPadding { Left = 10f }, - Children = new Drawable[] - { - starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - starCounter = new StarCounter - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Scale = new Vector2(8f / 20f), - }, - } - }, - new CircularContainer - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Size = new Vector2(50f, 14f), - Margin = new MarginPadding { Right = 20f }, - Masking = true, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black.Opacity(0.7f), - }, - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), - // TODO: requires Carousel/CarouselItem-side implementation - Text = "43", - UseFullGlyphHeight = false, - } - }, - } - } - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - Expanded.BindValueChanged(_ => onExpanded(), true); - KeyboardSelected.BindValueChanged(k => panel.KeyboardActive.Value = k.NewValue, true); - } - - private void onExpanded() - { - panel.Active.Value = Expanded.Value; - panel.Flash(); - - chevronIcon.ResizeWidthTo(Expanded.Value ? 12f : 0f, duration, Easing.OutQuint); - chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); - } - - protected override void PrepareForUse() - { - base.PrepareForUse(); - - Debug.Assert(Item != null); - - int starNumber = (int)((GroupDefinition)Item.Model).Data; - - Color4 colour = starNumber >= 9 ? OsuColour.Gray(0.2f) : colours.ForStarDifficulty(starNumber); - Color4 contentColour = starNumber >= 7 ? colours.Orange1 : colourProvider.Background5; - - panel.AccentColour = colour; - contentBackground.Colour = colour.Darken(0.3f); - - starRatingDisplay.Current.Value = new StarDifficulty(starNumber, 0); - starCounter.Current = starNumber; - - chevronIcon.Colour = contentColour; - starCounter.Colour = contentColour; - - this.FadeInFromZero(500, Easing.OutQuint); - } - - private void onAction() - { - if (carousel != null) - carousel.CurrentSelection = Item!.Model; - } - - #region ICarouselPanel - - public CarouselItem? Item { get; set; } - public BindableBool Selected { get; } = new BindableBool(); - public BindableBool Expanded { get; } = new BindableBool(); - public BindableBool KeyboardSelected { get; } = new BindableBool(); - - public double DrawYPosition { get; set; } - - public void Activated() - { - // sets should never be activated. - throw new InvalidOperationException(); - } - - #endregion - } -} From 5de9584171cfcbc6394e5bc52c547d5ebac4573e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Feb 2025 16:24:04 +0900 Subject: [PATCH 518/801] Move `PanelXOffset` to `init` property rather than ctor Feels better to me. --- osu.Game/Screens/SelectV2/PanelBase.cs | 53 +++++++------------ osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 6 +-- .../SelectV2/PanelBeatmapStandalone.cs | 6 +-- 3 files changed, 21 insertions(+), 44 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs index d5a087dbb2..9773d93f45 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -29,31 +29,25 @@ namespace osu.Game.Screens.SelectV2 private const float duration = 500; - private readonly float panelXOffset; + protected float PanelXOffset { get; init; } - private readonly Box backgroundBorder; - private readonly Box backgroundGradient; - private readonly Box backgroundAccentGradient; - private readonly Container backgroundLayer; - private readonly Container backgroundLayerHorizontalPadding; - private readonly Container backgroundContainer; - private readonly Container iconContainer; - private readonly Box activationFlash; - private readonly Box hoverLayer; + private Box backgroundBorder = null!; + private Box backgroundGradient = null!; + private Box backgroundAccentGradient = null!; + private Container backgroundLayer = null!; + private Container backgroundLayerHorizontalPadding = null!; + private Container backgroundContainer = null!; + private Container iconContainer = null!; + private Box activationFlash = null!; + private Box hoverLayer = null!; - public Container TopLevelContent { get; } + public Container TopLevelContent { get; private set; } = null!; - protected Container Content { get; } + protected Container Content { get; private set; } = null!; - public Drawable Background - { - set => backgroundContainer.Child = value; - } + public Drawable Background { set => backgroundContainer.Child = value; } - public Drawable Icon - { - set => iconContainer.Child = value; - } + public Drawable Icon { set => iconContainer.Child = value; } private Color4? accentColour; @@ -77,10 +71,9 @@ namespace osu.Game.Screens.SelectV2 return inputRectangle.Contains(TopLevelContent.ToLocalSpace(screenSpacePos)); } - protected PanelBase(float panelXOffset = 0) + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider, OsuColour colours) { - this.panelXOffset = panelXOffset; - RelativeSizeAxes = Axes.Both; InternalChild = TopLevelContent = new Container @@ -147,7 +140,7 @@ namespace osu.Game.Screens.SelectV2 Content = new Container { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Right = panelXOffset + corner_radius }, + Padding = new MarginPadding { Right = PanelXOffset + corner_radius }, }, hoverLayer = new Box { @@ -165,11 +158,7 @@ namespace osu.Game.Screens.SelectV2 new HoverSounds(), } }; - } - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider, OsuColour colours) - { hoverLayer.Colour = colours.Blue.Opacity(0.1f); backgroundGradient.Colour = ColourInfo.GradientHorizontal(colourProvider.Background3, colourProvider.Background4); } @@ -187,15 +176,11 @@ namespace osu.Game.Screens.SelectV2 protected override bool OnClick(ClickEvent e) { + activationFlash.FadeOutFromOne(500, Easing.OutQuint); carousel?.Activate(Item!); return true; } - public void Flash() - { - activationFlash.FadeOutFromOne(500, Easing.OutQuint); - } - private void updateDisplay() { backgroundLayer.TransformTo(nameof(Padding), backgroundLayer.Padding with { Vertical = Expanded.Value ? 2f : 0f }, duration, Easing.OutQuint); @@ -214,7 +199,7 @@ namespace osu.Game.Screens.SelectV2 private void updateXOffset() { - float x = panelXOffset + active_x_offset + keyboard_active_x_offset + left_edge_x_offset; + float x = PanelXOffset + active_x_offset + keyboard_active_x_offset + left_edge_x_offset; if (Expanded.Value) x -= active_x_offset; diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index 742fe6b6e6..6ac52acac0 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -21,10 +21,6 @@ namespace osu.Game.Screens.SelectV2 { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; - // todo: this should be replaced with information from CarouselItem about how deep is PanelBeatmap in the carousel - // (i.e. whether it's under a beatmap set that's under a group, or just under a top-level beatmap set). - private const float set_x_offset = 20f; // constant X offset for beatmap set/standalone panels specifically. - private const float duration = 500; private BeatmapSetPanelBackground background = null!; @@ -43,8 +39,8 @@ namespace osu.Game.Screens.SelectV2 private BeatmapManager beatmaps { get; set; } = null!; public PanelBeatmapSet() - : base(set_x_offset) { + PanelXOffset = 20f; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs index c94a337cd9..89f9df332f 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs @@ -27,10 +27,6 @@ namespace osu.Game.Screens.SelectV2 { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; - // todo: this should be replaced with information from CarouselItem about how deep is PanelBeatmap in the carousel - // (i.e. whether it's under a beatmap set that's under a group, or just under a top-level beatmap set). - private const float standalone_x_offset = 20f; // constant X offset for beatmap set/standalone panels specifically. - private const float duration = 500; [Resolved] @@ -70,8 +66,8 @@ namespace osu.Game.Screens.SelectV2 private OsuSpriteText difficultyAuthor = null!; public PanelBeatmapStandalone() - : base(standalone_x_offset) { + PanelXOffset = 20; } [BackgroundDependencyLoader] From 644fb29843a9c33b137cf1056dea659b561815b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Feb 2025 16:27:19 +0900 Subject: [PATCH 519/801] Fix input handling not matching latest `master` logic --- osu.Game/Screens/SelectV2/PanelBase.cs | 10 ---------- osu.Game/Screens/SelectV2/PanelBeatmap.cs | 14 +++++++------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs index 9773d93f45..d0499f44cb 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -61,16 +61,6 @@ namespace osu.Game.Screens.SelectV2 } } - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) - { - var inputRectangle = TopLevelContent.DrawRectangle; - - // Cover potential gaps introduced by the spacing between panels. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - - return inputRectangle.Contains(TopLevelContent.ToLocalSpace(screenSpacePos)); - } - [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, OsuColour colours) { diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index 48d15f6857..69e8e34c40 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -52,6 +52,12 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private BeatmapDifficultyCache difficultyCache { get; set; } = null!; + [Resolved] + private IBindable ruleset { get; set; } = null!; + + [Resolved] + private IBindable> mods { get; set; } = null!; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { var inputRectangle = DrawRectangle; @@ -60,17 +66,11 @@ namespace osu.Game.Screens.SelectV2 // // Caveat is that for simplicity, we are covering the full spacing, so panels with frontmost depth will have a slightly // larger hit target. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); + inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING }); return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); } - [Resolved] - private IBindable ruleset { get; set; } = null!; - - [Resolved] - private IBindable> mods { get; set; } = null!; - [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { From 7e1984452fb7601330dcf9b0b693cdb17d41ca1f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Feb 2025 16:32:12 +0900 Subject: [PATCH 520/801] Tidy up remaining common code --- osu.Game/Screens/SelectV2/PanelBase.cs | 12 +++++++++- osu.Game/Screens/SelectV2/PanelBeatmap.cs | 16 ++----------- osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 10 ++------ .../SelectV2/PanelBeatmapStandalone.cs | 12 ++-------- osu.Game/Screens/SelectV2/PanelGroup.cs | 10 ++------ .../SelectV2/PanelGroupStarDifficulty.cs | 23 +++++++------------ 6 files changed, 27 insertions(+), 56 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs index d0499f44cb..805cbac8eb 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -64,7 +64,11 @@ namespace osu.Game.Screens.SelectV2 [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, OsuColour colours) { - RelativeSizeAxes = Axes.Both; + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + + RelativeSizeAxes = Axes.X; + Height = CarouselItem.DEFAULT_HEIGHT; InternalChild = TopLevelContent = new Container { @@ -161,6 +165,12 @@ namespace osu.Game.Screens.SelectV2 KeyboardSelected.BindValueChanged(_ => updateDisplay(), true); } + protected override void PrepareForUse() + { + base.PrepareForUse(); + this.FadeInFromZero(duration, Easing.OutQuint); + } + [Resolved] private BeatmapCarousel? carousel { get; set; } diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index 69e8e34c40..dcac460905 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -26,12 +26,6 @@ namespace osu.Game.Screens.SelectV2 { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; - // todo: this should be replaced with information from CarouselItem about how deep is PanelBeatmap in the carousel - // (i.e. whether it's under a beatmap set that's under a group, or just under a top-level beatmap set). - private const float difficulty_x_offset = 100f; // constant X offset for beatmap difficulty panels specifically. - - private const float duration = 500; - private StarCounter starCounter = null!; private ConstrainedIconContainer difficultyIcon = null!; private OsuSpriteText keyCountText = null!; @@ -74,11 +68,6 @@ namespace osu.Game.Screens.SelectV2 [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { - Anchor = Anchor.TopRight; - Origin = Anchor.TopRight; - - RelativeSizeAxes = Axes.X; - Width = 1f; Height = HEIGHT; Icon = difficultyIcon = new ConstrainedIconContainer @@ -194,9 +183,6 @@ namespace osu.Game.Screens.SelectV2 computeStarRating(); updateKeyCount(); - - FinishTransforms(true); - this.FadeInFromZero(duration, Easing.OutQuint); } protected override void FreeAfterUse() @@ -244,6 +230,8 @@ namespace osu.Game.Screens.SelectV2 private void updateDisplay() { + const float duration = 500; + var starDifficulty = starDifficultyBindable?.Value ?? default; starRatingDisplay.Current.Value = starDifficulty; diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index 6ac52acac0..5c38fe8e04 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -21,8 +21,6 @@ namespace osu.Game.Screens.SelectV2 { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; - private const float duration = 500; - private BeatmapSetPanelBackground background = null!; private OsuSpriteText titleText = null!; @@ -46,9 +44,6 @@ namespace osu.Game.Screens.SelectV2 [BackgroundDependencyLoader] private void load() { - Anchor = Anchor.TopRight; - Origin = Anchor.TopRight; - RelativeSizeAxes = Axes.X; Height = HEIGHT; Icon = chevronIcon = new Container @@ -133,6 +128,8 @@ namespace osu.Game.Screens.SelectV2 private void onExpanded() { + const float duration = 500; + chevronIcon.ResizeWidthTo(Expanded.Value ? 22 : 0f, duration, Easing.OutQuint); chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); } @@ -153,9 +150,6 @@ namespace osu.Game.Screens.SelectV2 updateButton.BeatmapSet = beatmapSet; statusPill.Status = beatmapSet.Status; difficultiesDisplay.BeatmapSet = beatmapSet; - - FinishTransforms(true); - this.FadeInFromZero(duration, Easing.OutQuint); } protected override void FreeAfterUse() diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs index 89f9df332f..231c7274be 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs @@ -27,8 +27,6 @@ namespace osu.Game.Screens.SelectV2 { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; - private const float duration = 500; - [Resolved] private IBindable ruleset { get; set; } = null!; @@ -73,10 +71,6 @@ namespace osu.Game.Screens.SelectV2 [BackgroundDependencyLoader] private void load() { - Anchor = Anchor.TopRight; - Origin = Anchor.TopRight; - RelativeSizeAxes = Axes.X; - Width = 1f; Height = HEIGHT; Icon = difficultyIcon = new ConstrainedIconContainer @@ -224,10 +218,6 @@ namespace osu.Game.Screens.SelectV2 difficultyLine.Show(); computeStarRating(); - - FinishTransforms(true); - - this.FadeInFromZero(duration, Easing.OutQuint); } protected override void FreeAfterUse() @@ -277,6 +267,8 @@ namespace osu.Game.Screens.SelectV2 private void updateDisplay() { + const float duration = 500; + var starDifficulty = starDifficultyBindable?.Value ?? default; AccentColour = colours.ForStarDifficulty(starDifficulty.Stars); diff --git a/osu.Game/Screens/SelectV2/PanelGroup.cs b/osu.Game/Screens/SelectV2/PanelGroup.cs index 2b4fb9e4a9..ecb64f4797 100644 --- a/osu.Game/Screens/SelectV2/PanelGroup.cs +++ b/osu.Game/Screens/SelectV2/PanelGroup.cs @@ -20,17 +20,12 @@ namespace osu.Game.Screens.SelectV2 { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; - private const float duration = 500; - private Drawable chevronIcon = null!; private OsuSpriteText titleText = null!; [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { - Anchor = Anchor.TopRight; - Origin = Anchor.TopRight; - RelativeSizeAxes = Axes.X; Height = HEIGHT; Icon = chevronIcon = new SpriteIcon @@ -93,6 +88,8 @@ namespace osu.Game.Screens.SelectV2 private void onExpanded() { + const float duration = 500; + chevronIcon.ResizeWidthTo(Expanded.Value ? 12f : 0f, duration, Easing.OutQuint); chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); } @@ -106,9 +103,6 @@ namespace osu.Game.Screens.SelectV2 GroupDefinition group = (GroupDefinition)Item.Model; titleText.Text = group.Title; - - FinishTransforms(true); - this.FadeInFromZero(500, Easing.OutQuint); } } } diff --git a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs index 736a0f71dc..0dc5a2f365 100644 --- a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs +++ b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs @@ -21,10 +21,6 @@ namespace osu.Game.Screens.SelectV2 { public partial class PanelGroupStarDifficulty : PanelBase { - public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; - - private const float duration = 500; - [Resolved] private OsuColour colours { get; set; } = null!; @@ -39,10 +35,7 @@ namespace osu.Game.Screens.SelectV2 [BackgroundDependencyLoader] private void load() { - Anchor = Anchor.TopRight; - Origin = Anchor.TopRight; - RelativeSizeAxes = Axes.X; - Height = HEIGHT; + Height = PanelGroup.HEIGHT; Icon = chevronIcon = new SpriteIcon { @@ -117,12 +110,6 @@ namespace osu.Game.Screens.SelectV2 Expanded.BindValueChanged(_ => onExpanded(), true); } - private void onExpanded() - { - chevronIcon.ResizeWidthTo(Expanded.Value ? 12f : 0f, duration, Easing.OutQuint); - chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); - } - protected override void PrepareForUse() { base.PrepareForUse(); @@ -142,8 +129,14 @@ namespace osu.Game.Screens.SelectV2 chevronIcon.Colour = contentColour; starCounter.Colour = contentColour; + } - this.FadeInFromZero(500, Easing.OutQuint); + private void onExpanded() + { + const float duration = 500; + + chevronIcon.ResizeWidthTo(Expanded.Value ? 12f : 0f, duration, Easing.OutQuint); + chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); } } } From 8299dfc6f2341f82ac1baeeb40967a5391296de8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Feb 2025 10:10:51 +0100 Subject: [PATCH 521/801] Add local guard before scheduled placeholder user set When API is in `RequiresSecondFactorAuth` state, `attemptConnect()` is called over and over in a loop, with no sleeping, which means that the scheduler accumulates hundreds of thousands of these delegates. Sure you could add a sleep in there maybe, but it seems pretty wasteful to have the `localUser.IsDefault` guard *inside* the schedule anyway, so this is what I opted for. --- osu.Game/Online/API/APIAccess.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 88f9b3f242..711866b2aa 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -238,7 +238,8 @@ namespace osu.Game.Online.API /// Whether the connection attempt was successful. private void attemptConnect() { - Scheduler.Add(setPlaceholderLocalUser, false); + if (localUser.IsDefault) + Scheduler.Add(setPlaceholderLocalUser, false); // save the username at this point, if the user requested for it to be. config.SetValue(OsuSetting.Username, config.Get(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty); From d6552f00bed2359296df91050062a3786c59493e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Feb 2025 10:14:00 +0100 Subject: [PATCH 522/801] Do not attempt to automatically reconnect if there is no login to use Because it'll fail anyway - there is either no username or no password. The reason why this is important is that the block was also setting API state to `Connecting`. --- osu.Game/Online/API/APIAccess.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 711866b2aa..a90fccc1c0 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -244,7 +244,7 @@ namespace osu.Game.Online.API // save the username at this point, if the user requested for it to be. config.SetValue(OsuSetting.Username, config.Get(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty); - if (!authentication.HasValidAccessToken) + if (!authentication.HasValidAccessToken && HasLogin) { state.Value = APIState.Connecting; LastLoginError = null; From 930e02300f200388e5398fee1e34f14108f09d78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Feb 2025 10:20:38 +0100 Subject: [PATCH 523/801] Do not allow flushed requests to transition API into `Failing` state Flushes are assumed to have already come from a definitive state change (read: disconnection). Allowing the exceptions that come from failing the flushed requests to trigger the `Failing` code paths makes completely incorrect behaviour possible. --- osu.Game/Online/API/APIAccess.cs | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index a90fccc1c0..479fc99805 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -253,6 +253,10 @@ namespace osu.Game.Online.API { authentication.AuthenticateWithLogin(ProvidedUsername, password); } + catch (WebRequestFlushedException) + { + return; + } catch (Exception e) { //todo: this fails even on network-related issues. we should probably handle those differently. @@ -313,7 +317,7 @@ namespace osu.Game.Online.API log.Add(@"Login no longer valid"); Logout(); } - else + else if (ex is not WebRequestFlushedException) { state.Value = APIState.Failing; } @@ -494,6 +498,11 @@ namespace osu.Game.Online.API handleWebException(we); return false; } + catch (WebRequestFlushedException wrf) + { + log.Add(wrf.Message); + return false; + } catch (Exception ex) { Logger.Error(ex, "Error occurred while handling an API request."); @@ -575,7 +584,7 @@ namespace osu.Game.Online.API if (failOldRequests) { foreach (var req in oldQueueRequests) - req.Fail(new WebException($@"Request failed from flush operation (state {state.Value})")); + req.Fail(new WebRequestFlushedException(state.Value)); } } } @@ -606,7 +615,11 @@ namespace osu.Game.Online.API return; var friendsReq = new GetFriendsRequest(); - friendsReq.Failure += _ => state.Value = APIState.Failing; + friendsReq.Failure += ex => + { + if (ex is not WebRequestFlushedException) + state.Value = APIState.Failing; + }; friendsReq.Success += res => { var existingFriends = friends.Select(f => f.TargetID).ToHashSet(); @@ -631,6 +644,14 @@ namespace osu.Game.Online.API flushQueue(); cancellationToken.Cancel(); } + + private class WebRequestFlushedException : Exception + { + public WebRequestFlushedException(APIState state) + : base($@"Request failed from flush operation (state {state})") + { + } + } } internal class GuestUser : APIUser From b3aba537b5f081978ea4d349562ec8c02289f737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Feb 2025 11:33:30 +0100 Subject: [PATCH 524/801] Add missing early return As spotted in testing with production. Would cause submission to proceed even if the export did, with an empty archive. --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 201888e078..f62b793918 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -249,6 +249,7 @@ namespace osu.Game.Screens.Edit.Submission exportProgressNotification = null; Logger.Log($"Beatmap set submission failed on export: {ex}"); allowExit(); + return; } exportStep.SetCompleted(); From e6174f195cf2fdfedf9c9a054172136e3b5b2efb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Feb 2025 12:06:42 +0100 Subject: [PATCH 525/801] Ensure `EditorBeatmap.PerformOnSelection()` marks objects in selection as updated Closes https://github.com/ppy/osu/issues/28791. The reason why nudging was not changing hyperdash state in catch was that `EditorBeatmap.Update()` was not being called on the objects that were being modified, therefore postprocessing was not performed, therefore hyperdash state was not being recomputed. Looking at the usage sites of `EditorBeatmap.PerformOnSelection()`, about two-thirds of callers called `Update()` themselves on the objects they mutated, and the rest didn't. I'd say that's the failure of the abstraction and it should be `PerformOnSelection()`'s responsibility to call `Update()` there. Yes in some of the cases here this will cause extraneous calls that weren't done before, but the method is already heavily disclaimed as 'expensive', so I'd say usability should come first. --- osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs | 6 ------ .../Edit/Compose/Components/EditorBlueprintContainer.cs | 8 +------- .../Edit/Compose/Components/EditorSelectionHandler.cs | 9 --------- osu.Game/Screens/Edit/EditorBeatmap.cs | 5 +++++ 4 files changed, 6 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs index be2a5ac144..364324087b 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs @@ -62,10 +62,7 @@ namespace osu.Game.Rulesets.Taiko.Edit if (h is not TaikoStrongableHitObject strongable) return; if (strongable.IsStrong != state) - { strongable.IsStrong = state; - EditorBeatmap.Update(strongable); - } }); } @@ -77,10 +74,7 @@ namespace osu.Game.Rulesets.Taiko.Edit EditorBeatmap.PerformOnSelection(h => { if (h is Hit taikoHit) - { taikoHit.Type = state ? HitType.Rim : HitType.Centre; - EditorBeatmap.Update(h); - } }); } diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs index e67644baaa..e8de1eaad9 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs @@ -81,13 +81,7 @@ namespace osu.Game.Screens.Edit.Compose.Components double offset = result.Time.Value - referenceTime; if (offset != 0) - { - Beatmap.PerformOnSelection(obj => - { - obj.StartTime += offset; - Beatmap.Update(obj); - }); - } + Beatmap.PerformOnSelection(obj => obj.StartTime += offset); } protected override void AddBlueprintFor(HitObject item) diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index cd6e25734a..f9e7ef6df8 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -355,8 +355,6 @@ namespace osu.Game.Screens.Edit.Compose.Components for (int i = 0; i < hasRepeats.NodeSamples.Count; ++i) hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(s => s.Name == HitSampleInfo.HIT_NORMAL ? s.With(newBank: bankName) : s).ToList(); } - - EditorBeatmap.Update(h); }); } @@ -390,8 +388,6 @@ namespace osu.Game.Screens.Edit.Compose.Components hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(s => s.Name != HitSampleInfo.HIT_NORMAL ? bankName == HIT_BANK_AUTO ? s.With(newBank: normalBank, newEditorAutoBank: true) : s.With(newBank: bankName, newEditorAutoBank: false) : s).ToList(); } } - - EditorBeatmap.Update(h); }); } @@ -439,8 +435,6 @@ namespace osu.Game.Screens.Edit.Compose.Components node.Add(hitSample); } } - - EditorBeatmap.Update(h); }); } @@ -462,8 +456,6 @@ namespace osu.Game.Screens.Edit.Compose.Components for (int i = 0; i < hasRepeats.NodeSamples.Count; ++i) hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Where(s => s.Name != sampleName).ToList(); } - - EditorBeatmap.Update(h); }); } @@ -484,7 +476,6 @@ namespace osu.Game.Screens.Edit.Compose.Components if (comboInfo == null || comboInfo.NewCombo == state) return; comboInfo.NewCombo = state; - EditorBeatmap.Update(h); }); } diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 44f9646889..254336e963 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -312,8 +312,13 @@ namespace osu.Game.Screens.Edit return; BeginChange(); + foreach (var h in SelectedHitObjects) + { action(h); + Update(h); + } + EndChange(); } From 7566da8663f8f9f33a87842b7eca5339a0fe43da Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Feb 2025 23:52:08 +0900 Subject: [PATCH 526/801] Add sleep to reduce spinning when waiting on two factor auth --- osu.Game/Online/API/APIAccess.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 479fc99805..36712fbdaa 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -190,7 +190,10 @@ namespace osu.Game.Online.API attemptConnect(); if (state.Value != APIState.Online) + { + Thread.Sleep(50); continue; + } } // hard bail if we can't get a valid access token. From 687c9d6e174e6e339f9de9641322c7e67e245834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 19 Feb 2025 12:45:37 +0100 Subject: [PATCH 527/801] Send "notify on discussion replies" setting value in beatmap creation request --- .../Online/API/Requests/PutBeatmapSetRequest.cs | 14 ++++++++++---- .../Edit/Submission/BeatmapSubmissionScreen.cs | 4 ++-- .../Edit/Submission/BeatmapSubmissionSettings.cs | 2 ++ .../Edit/Submission/ScreenSubmissionSettings.cs | 4 ++-- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs b/osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs index fb25749786..ec233b5df8 100644 --- a/osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs +++ b/osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs @@ -11,6 +11,7 @@ using osu.Framework.IO.Network; using osu.Framework.Localisation; using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Screens.Edit.Submission; namespace osu.Game.Online.API.Requests { @@ -42,22 +43,27 @@ namespace osu.Game.Online.API.Requests [JsonProperty("target")] public BeatmapSubmissionTarget SubmissionTarget { get; init; } + [JsonProperty("notify_on_discussion_replies")] + public bool NotifyOnDiscussionReplies { get; init; } + private PutBeatmapSetRequest() { } - public static PutBeatmapSetRequest CreateNew(uint beatmapCount, BeatmapSubmissionTarget target) => new PutBeatmapSetRequest + public static PutBeatmapSetRequest CreateNew(uint beatmapCount, BeatmapSubmissionSettings settings) => new PutBeatmapSetRequest { BeatmapsToCreate = beatmapCount, - SubmissionTarget = target, + SubmissionTarget = settings.Target.Value, + NotifyOnDiscussionReplies = settings.NotifyOnDiscussionReplies.Value, }; - public static PutBeatmapSetRequest UpdateExisting(uint beatmapSetId, IEnumerable beatmapsToKeep, uint beatmapsToCreate, BeatmapSubmissionTarget target) => new PutBeatmapSetRequest + public static PutBeatmapSetRequest UpdateExisting(uint beatmapSetId, IEnumerable beatmapsToKeep, uint beatmapsToCreate, BeatmapSubmissionSettings settings) => new PutBeatmapSetRequest { BeatmapSetID = beatmapSetId, BeatmapsToKeep = beatmapsToKeep.ToArray(), BeatmapsToCreate = beatmapsToCreate, - SubmissionTarget = target, + SubmissionTarget = settings.Target.Value, + NotifyOnDiscussionReplies = settings.NotifyOnDiscussionReplies.Value, }; protected override WebRequest CreateWebRequest() diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index f62b793918..66139bacec 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -192,8 +192,8 @@ namespace osu.Game.Screens.Edit.Submission (uint)Beatmap.Value.BeatmapSetInfo.OnlineID, Beatmap.Value.BeatmapSetInfo.Beatmaps.Where(b => b.OnlineID > 0).Select(b => (uint)b.OnlineID).ToArray(), (uint)Beatmap.Value.BeatmapSetInfo.Beatmaps.Count(b => b.OnlineID <= 0), - settings.Target.Value) - : PutBeatmapSetRequest.CreateNew((uint)Beatmap.Value.BeatmapSetInfo.Beatmaps.Count, settings.Target.Value); + settings) + : PutBeatmapSetRequest.CreateNew((uint)Beatmap.Value.BeatmapSetInfo.Beatmaps.Count, settings); createRequest.Success += async response => { diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs index 359dc11f39..8cccc339a6 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs @@ -9,5 +9,7 @@ namespace osu.Game.Screens.Edit.Submission public class BeatmapSubmissionSettings { public Bindable Target { get; } = new Bindable(); + + public Bindable NotifyOnDiscussionReplies { get; } = new Bindable(); } } diff --git a/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs b/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs index 08b4d9f712..969105b5c6 100644 --- a/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs +++ b/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs @@ -28,7 +28,7 @@ namespace osu.Game.Screens.Edit.Submission [BackgroundDependencyLoader] private void load(OsuConfigManager configManager, OsuColour colours, BeatmapSubmissionSettings settings) { - configManager.BindWith(OsuSetting.EditorSubmissionNotifyOnDiscussionReplies, notifyOnDiscussionReplies); + configManager.BindWith(OsuSetting.EditorSubmissionNotifyOnDiscussionReplies, settings.NotifyOnDiscussionReplies); configManager.BindWith(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission, loadInBrowserAfterSubmission); Content.Add(new FillFlowContainer @@ -47,7 +47,7 @@ namespace osu.Game.Screens.Edit.Submission new FormCheckBox { Caption = BeatmapSubmissionStrings.NotifyOnDiscussionReplies, - Current = notifyOnDiscussionReplies, + Current = settings.NotifyOnDiscussionReplies, }, new FormCheckBox { From aa9e1ac8b4bf0154dff870269221d49e7e1c98d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 19 Feb 2025 12:46:04 +0100 Subject: [PATCH 528/801] Specify endpoint for production instance of beatmap submission service --- osu.Game/Online/ProductionEndpointConfiguration.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/ProductionEndpointConfiguration.cs b/osu.Game/Online/ProductionEndpointConfiguration.cs index 6e06abbeed..20583c8c7e 100644 --- a/osu.Game/Online/ProductionEndpointConfiguration.cs +++ b/osu.Game/Online/ProductionEndpointConfiguration.cs @@ -13,6 +13,7 @@ namespace osu.Game.Online SpectatorUrl = "https://spectator.ppy.sh/spectator"; MultiplayerUrl = "https://spectator.ppy.sh/multiplayer"; MetadataUrl = "https://spectator.ppy.sh/metadata"; + BeatmapSubmissionServiceUrl = "https://bss.ppy.sh"; } } } From f9d91431fd0e4eaf4c26d8125b078cdd2ec23c19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 19 Feb 2025 15:13:48 +0100 Subject: [PATCH 529/801] Fix multiplayer spectator not working with freestyle It's no longer possible to just assume that using the ambient `WorkingBeatmap` is gonna work. Bit dodgy but seems to work and also I'd hope that `WorkingBeatmapCache` makes this not overly taxing. If there are concerns this can probably be an async load or something. --- .../Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index 1b03452df7..2a40021ee0 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -53,7 +53,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public Score? Score { get; private set; } [Resolved] - private IBindable beatmap { get; set; } = null!; + private BeatmapManager beatmapManager { get; set; } = null!; private readonly AudioAdjustments clockAdjustmentsFromMods = new AudioAdjustments(); private readonly BindableDouble volumeAdjustment = new BindableDouble(); @@ -89,7 +89,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate Score = score; - gameplayContent.Child = new PlayerIsolationContainer(beatmap.Value, Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods) + var workingBeatmap = beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.BeatmapInfo); + workingBeatmap.LoadTrack(); + gameplayContent.Child = new PlayerIsolationContainer(workingBeatmap, Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods) { RelativeSizeAxes = Axes.Both, Child = stack = new OsuScreenStack From a274b9a1fd9f59200061f7fe794de3b8c112e0cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 19 Feb 2025 15:24:58 +0100 Subject: [PATCH 530/801] Fix test --- .../Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index 0a3d48828e..bd483f0fa1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -372,7 +372,8 @@ namespace osu.Game.Tests.Visual.Multiplayer sendFrames(getPlayerIds(4), 300); - AddUntilStep("wait for correct track speed", () => Beatmap.Value.Track.Rate, () => Is.EqualTo(1.5)); + AddUntilStep("wait for correct track speed", + () => this.ChildrenOfType().All(player => player.ClockAdjustmentsFromMods.AggregateTempo.Value == 1.5)); } [Test] From 092d80cf1b330b21ad7a10a09f25e9336999f523 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 19 Feb 2025 10:20:04 -0500 Subject: [PATCH 531/801] Fix `PanelBeatmapStandalone` not handling selection state --- osu.Game/Screens/SelectV2/PanelBeatmap.cs | 1 - osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index dcac460905..b27e5cae14 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -165,7 +165,6 @@ namespace osu.Game.Screens.SelectV2 }, true); Selected.BindValueChanged(s => Expanded.Value = s.NewValue, true); - KeyboardSelected.BindValueChanged(k => KeyboardSelected.Value = k.NewValue, true); } protected override void PrepareForUse() diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs index 231c7274be..948311a86e 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs @@ -190,6 +190,8 @@ namespace osu.Game.Screens.SelectV2 computeStarRating(); updateKeyCount(); }, true); + + Selected.BindValueChanged(s => Expanded.Value = s.NewValue, true); } protected override void PrepareForUse() From e91706f41843b48020f514c42c42ed446a565f83 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Feb 2025 14:26:33 +0900 Subject: [PATCH 532/801] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index f4d49763ab..7dfe2f9d1f 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 0d95dfbd06..a40bc145ff 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 4da3752f956d2d68dbc263969d81478a5577536d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Feb 2025 14:42:26 +0900 Subject: [PATCH 533/801] Update flag test resources in line with web rename --- osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs | 2 +- osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs index a4a9816337..2972f69cba 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs @@ -351,7 +351,7 @@ namespace osu.Game.Tests.Visual.Online Id = 1, Name = "Collective Wangs", ShortName = "WANG", - FlagUrl = "https://assets.ppy.sh/teams/logo/1/wanglogo.jpg", + FlagUrl = "https://assets.ppy.sh/teams/flag/1/wanglogo.jpg", } }; } diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index d73fd5ab22..3e3fe03329 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -228,7 +228,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay { Name = "Collective Wangs", ShortName = "WANG", - FlagUrl = "https://assets.ppy.sh/teams/logo/1/wanglogo.jpg", + FlagUrl = "https://assets.ppy.sh/teams/flag/1/wanglogo.jpg", } : null, }) From 1c53d93a8f3cf68888e74919ee75639fe70ffe05 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Feb 2025 15:32:47 +0900 Subject: [PATCH 534/801] Add disposal and pre-check before reloading audio track --- .../OnlinePlay/Multiplayer/Spectate/PlayerArea.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index 2a40021ee0..31bd711ade 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -60,6 +61,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private readonly Container gameplayContent; private readonly LoadingLayer loadingLayer; private OsuScreenStack? stack; + private Track? loadedTrack; public PlayerArea(int userId, SpectatorPlayerClock clock) { @@ -90,7 +92,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate Score = score; var workingBeatmap = beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.BeatmapInfo); - workingBeatmap.LoadTrack(); + if (!workingBeatmap.TrackLoaded) + loadedTrack = workingBeatmap.LoadTrack(); gameplayContent.Child = new PlayerIsolationContainer(workingBeatmap, Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods) { RelativeSizeAxes = Axes.Both, @@ -129,6 +132,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public override bool PropagatePositionalInputSubTree => false; public override bool PropagateNonPositionalInputSubTree => false; + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + loadedTrack?.Dispose(); + } + /// /// Isolates each player instance from the game-wide ruleset/beatmap/mods (to allow for different players having different settings). /// From 81b4f0d8caf176aa070846ddf79f54346803fa2f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Feb 2025 15:49:40 +0900 Subject: [PATCH 535/801] Add comments regarding jank --- .../Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index 31bd711ade..7e4aae99da 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -91,9 +91,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate Score = score; + // Required for freestyle, where each player may be playing a different beatmap. var workingBeatmap = beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.BeatmapInfo); + + // Required to avoid crashes, but we really don't want to be doing this if we can avoid it. + // If we get to fixing this, we will want to investigate every access to `Track` in gameplay. if (!workingBeatmap.TrackLoaded) loadedTrack = workingBeatmap.LoadTrack(); + gameplayContent.Child = new PlayerIsolationContainer(workingBeatmap, Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods) { RelativeSizeAxes = Axes.Both, From d8bba16809a8f55899afc843fe0010c43b0acc87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 20 Feb 2025 14:38:54 +0100 Subject: [PATCH 536/801] Update framework Pulls in fix for https://github.com/ppy/osu/issues/31956. --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 7dfe2f9d1f..d49acd7b27 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index a40bc145ff..5ca49e80f6 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 1e3d5d7d8150c916af4a059791f1d3c4b5a6f5a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Feb 2025 23:05:43 +0900 Subject: [PATCH 537/801] Remove left-over debug code --- osu.Game/Screens/Play/KiaiGameplayFountains.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Play/KiaiGameplayFountains.cs b/osu.Game/Screens/Play/KiaiGameplayFountains.cs index 19a9c2b6e5..f7d96dd10f 100644 --- a/osu.Game/Screens/Play/KiaiGameplayFountains.cs +++ b/osu.Game/Screens/Play/KiaiGameplayFountains.cs @@ -55,7 +55,6 @@ namespace osu.Game.Screens.Play if (EffectPoint.KiaiMode && !isTriggered) { - Logger.Log("shooting"); bool isNearEffectPoint = Math.Abs(BeatSyncSource.Clock.CurrentTime - EffectPoint.Time) < 500; if (isNearEffectPoint) Shoot(); From b4d270045b5fa3840c726add260933d145a78b9f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 20 Feb 2025 09:29:18 -0500 Subject: [PATCH 538/801] Publicise base draw size property --- osu.Android/OsuGameAndroid.cs | 2 +- osu.Game/OsuGame.cs | 2 +- osu.iOS/OsuGameIOS.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index e725f9245f..932fc8454e 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -21,7 +21,7 @@ namespace osu.Android [Cached] private readonly OsuGameActivity gameActivity; - protected override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth); + public override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth); public OsuGameAndroid(OsuGameActivity activity) : base(null) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index d379392a7d..d23d27c89e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -818,7 +818,7 @@ namespace osu.Game /// Adjust the globally applied in every . /// Useful for changing how the game handles different aspect ratios. /// - protected internal virtual Vector2 ScalingContainerTargetDrawSize { get; } = new Vector2(1024, 768); + public virtual Vector2 ScalingContainerTargetDrawSize { get; } = new Vector2(1024, 768); protected override Container CreateScalingContainer() => new ScalingContainer(ScalingMode.Everything); diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index 883e89e38a..96b8fb9804 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -23,7 +23,7 @@ namespace osu.iOS public override bool HideUnlicensedContent => true; - protected override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth); + public override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth); public OsuGameIOS(AppDelegate appDelegate) { From 4f4d2b3b3fdd24a6ab3d0a8388e6afd729806483 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Feb 2025 23:42:32 +0900 Subject: [PATCH 539/801] Fix results screen applause playing too loud during multiplayer spectating --- .../Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs | 5 +++++ osu.Game/Screens/Ranking/ResultsScreen.cs | 2 ++ 2 files changed, 7 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index 7e4aae99da..393d34bc1a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -9,6 +9,7 @@ using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; @@ -128,8 +129,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate get => mute; set { + if (mute == value) + return; + mute = value; volumeAdjustment.Value = value ? 0 : 1; + Logger.Log($"{(mute ? "muting" : "unmuting")} player {UserId}"); } } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index b10684b22e..fe0d805cee 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -317,6 +317,8 @@ namespace osu.Game.Screens.Ranking if (!this.IsCurrentScreen() || s != rankApplauseSound) return; + AddInternal(rankApplauseSound); + rankApplauseSound.VolumeTo(applause_volume); rankApplauseSound.Play(); }); From 7dc5ad2f0e21c85a66f925abf5133d741fcdf937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 20 Feb 2025 15:44:30 +0100 Subject: [PATCH 540/801] Adjust handling of team flags with non-matching aspect ratio to match web --- .../Components/DrawableTeamFlag.cs | 19 ++++++++++++++----- .../Users/Drawables/UpdateableTeamFlag.cs | 11 ++++++++++- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tournament/Components/DrawableTeamFlag.cs b/osu.Game.Tournament/Components/DrawableTeamFlag.cs index aef854bb8d..90638a7758 100644 --- a/osu.Game.Tournament/Components/DrawableTeamFlag.cs +++ b/osu.Game.Tournament/Components/DrawableTeamFlag.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Tournament.Models; @@ -35,12 +36,20 @@ namespace osu.Game.Tournament.Components Size = new Vector2(75, 54); Masking = true; CornerRadius = 5; - Child = flagSprite = new Sprite + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - FillMode = FillMode.Fill + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.FromHex("333"), + }, + flagSprite = new Sprite + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fit + }, }; (flag = team.FlagName.GetBoundCopy()).BindValueChanged(_ => flagSprite.Texture = textures.Get($@"Flags/{team.FlagName}"), true); diff --git a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs index 9c2bbb7e3e..2fcec66aa7 100644 --- a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs +++ b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Localisation; @@ -75,10 +76,18 @@ namespace osu.Game.Users.Drawables InternalChildren = new Drawable[] { new HoverClickSounds(), + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.FromHex("333"), + }, new Sprite { RelativeSizeAxes = Axes.Both, - Texture = textures.Get(team.FlagUrl) + Texture = textures.Get(team.FlagUrl), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fit, } }; } From a75ec75a8fa5e44fede74c4f1c4e275e6f2abee5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Feb 2025 23:48:21 +0900 Subject: [PATCH 541/801] Fix using --- osu.Game/Screens/Play/KiaiGameplayFountains.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Play/KiaiGameplayFountains.cs b/osu.Game/Screens/Play/KiaiGameplayFountains.cs index f7d96dd10f..d4e61dc5a0 100644 --- a/osu.Game/Screens/Play/KiaiGameplayFountains.cs +++ b/osu.Game/Screens/Play/KiaiGameplayFountains.cs @@ -5,7 +5,6 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Logging; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Graphics.Containers; From 440a776bd7c9edcf935b2da3d1bc07c65fc71663 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 20 Feb 2025 09:29:28 -0500 Subject: [PATCH 542/801] Scale catch down to remain playable on mobile --- .../UI/CatchPlayfieldAdjustmentContainer.cs | 54 +++++++++++++------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs index 74dfa6c1fd..3b9cca8ef0 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.UI; @@ -15,6 +16,8 @@ namespace osu.Game.Rulesets.Catch.UI protected override Container Content => content; private readonly Container content; + private readonly Container scaleContainer; + public CatchPlayfieldAdjustmentContainer() { const float base_game_width = 1024f; @@ -26,30 +29,49 @@ namespace osu.Game.Rulesets.Catch.UI Anchor = Anchor.Centre; Origin = Anchor.Centre; - InternalChild = new Container + InternalChild = scaleContainer = new Container { - // This container limits vertical visibility of the playfield to ensure fairness between wide and tall resolutions (i.e. tall resolutions should not see more fruits). - // Note that the container still extends across the screen horizontally, so that hit explosions at the sides of the playfield do not get cut off. - Name = "Visible area", Anchor = Anchor.Centre, Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - Height = base_game_height + extra_bottom_space, - Y = extra_bottom_space / 2, - Masking = true, + RelativeSizeAxes = Axes.Both, Child = new Container { - Name = "Playable area", - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - // playfields in stable are positioned vertically at three fourths the difference between the playfield height and the window height in stable. - Y = base_game_height * ((1 - playfield_size_adjust) / 4 * 3), - Size = new Vector2(base_game_width, base_game_height) * playfield_size_adjust, - Child = content = new ScalingContainer { RelativeSizeAxes = Axes.Both } - }, + // This container limits vertical visibility of the playfield to ensure fairness between wide and tall resolutions (i.e. tall resolutions should not see more fruits). + // Note that the container still extends across the screen horizontally, so that hit explosions at the sides of the playfield do not get cut off. + Name = "Visible area", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Height = base_game_height + extra_bottom_space, + Y = extra_bottom_space / 2, + Masking = true, + Child = new Container + { + Name = "Playable area", + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + // playfields in stable are positioned vertically at three fourths the difference between the playfield height and the window height in stable. + Y = base_game_height * ((1 - playfield_size_adjust) / 4 * 3), + Size = new Vector2(base_game_width, base_game_height) * playfield_size_adjust, + Child = content = new ScalingContainer { RelativeSizeAxes = Axes.Both } + }, + } }; } + [BackgroundDependencyLoader] + private void load(OsuGame? osuGame) + { + if (osuGame != null) + { + // on mobile platforms where the base aspect ratio is wider, the catch playfield + // needs to be scaled down to remain playable. + const float base_aspect_ratio = 1024f / 768f; + float aspectRatio = osuGame.ScalingContainerTargetDrawSize.X / osuGame.ScalingContainerTargetDrawSize.Y; + scaleContainer.Scale = new Vector2(base_aspect_ratio / aspectRatio); + } + } + /// /// A which scales its content relative to a target width. /// From 7bd5b745e923ae3eea737c3dd13a43f975832cbb Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 20 Feb 2025 09:30:14 -0500 Subject: [PATCH 543/801] Scale taiko down to remain playable --- .../UI/TaikoPlayfieldAdjustmentContainer.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs index c67f61052c..6a9e5789de 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Taiko.Beatmaps; @@ -19,6 +21,9 @@ namespace osu.Game.Rulesets.Taiko.UI public readonly IBindable LockPlayfieldAspectRange = new BindableBool(true); + [Resolved] + private OsuGame? osuGame { get; set; } + public TaikoPlayfieldAdjustmentContainer() { RelativeSizeAxes = Axes.X; @@ -56,6 +61,18 @@ namespace osu.Game.Rulesets.Taiko.UI relativeHeight = Math.Min(relativeHeight, 1f / 3f); Scale = new Vector2(Math.Max((Parent!.ChildSize.Y / 768f) * (relativeHeight / base_relative_height), 1f)); + + // on mobile platforms where the base aspect ratio is wider, the taiko playfield + // needs to be scaled down to remain playable. + if (RuntimeInfo.IsMobile && osuGame != null) + { + const float base_aspect_ratio = 1024f / 768f; + float gameAspectRatio = osuGame.ScalingContainerTargetDrawSize.X / osuGame.ScalingContainerTargetDrawSize.Y; + // this magic scale is unexplainable, but required so the playfield doesn't become too zoomed out as the aspect ratio increases. + const float magic_scale = 1.25f; + Scale *= magic_scale * new Vector2(base_aspect_ratio / gameAspectRatio); + } + Width = 1 / Scale.X; } From b1112623dca15dfcac3995d3ac289be0ccb96840 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 20 Feb 2025 09:29:53 -0500 Subject: [PATCH 544/801] Fix taiko touch controls sizing logic --- osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs b/osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs index 0b7f6f621a..53d129e7ca 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs @@ -59,11 +59,10 @@ namespace osu.Game.Rulesets.Taiko.UI { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, - RelativeSizeAxes = Axes.X, - Height = 350, + RelativeSizeAxes = Axes.Both, + Height = 0.45f, Y = 20, Masking = true, - FillMode = FillMode.Fit, Children = new Drawable[] { mainContent = new Container From 49c192b173640ddae1543a23eff4b6059f51f250 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 21 Feb 2025 16:19:05 +0900 Subject: [PATCH 545/801] Fix wrong beatmap attributes in multiplayer spectate --- osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index 393d34bc1a..b8f0a67a46 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -154,12 +154,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private partial class PlayerIsolationContainer : Container { [Cached] + [Cached(typeof(IBindable))] private readonly Bindable ruleset = new Bindable(); [Cached] + [Cached(typeof(IBindable))] private readonly Bindable beatmap = new Bindable(); [Cached] + [Cached(typeof(IBindable>))] private readonly Bindable> mods = new Bindable>(); public PlayerIsolationContainer(WorkingBeatmap beatmap, RulesetInfo ruleset, IReadOnlyList mods) From f868f03e1b75556418c8cfd6576781c3324d3fd7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 21 Feb 2025 16:38:55 +0900 Subject: [PATCH 546/801] Fix host change sounds playing when exiting multiplayer rooms --- .../Online/Multiplayer/MultiplayerClient.cs | 6 +++++ .../Multiplayer/MultiplayerRoomSounds.cs | 27 ++++++------------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 97161cce48..2d445ea25a 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -51,6 +51,11 @@ namespace osu.Game.Online.Multiplayer /// public event Action? UserKicked; + /// + /// Invoked when the room's host is changed. + /// + public event Action? HostChanged; + /// /// Invoked when a new item is added to the playlist. /// @@ -531,6 +536,7 @@ namespace osu.Game.Online.Multiplayer Room.Host = user; APIRoom.Host = user?.User; + HostChanged?.Invoke(user); RoomUpdated?.Invoke(); }, false); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs index d53e485c86..cdf4e96bad 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -20,7 +19,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private Sample? userJoinedSample; private Sample? userLeftSample; private Sample? userKickedSample; - private MultiplayerRoomUser? host; [BackgroundDependencyLoader] private void load(AudioManager audio) @@ -35,25 +33,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { base.LoadComplete(); - client.RoomUpdated += onRoomUpdated; client.UserJoined += onUserJoined; client.UserLeft += onUserLeft; client.UserKicked += onUserKicked; - updateState(); - } - - private void onRoomUpdated() => Scheduler.AddOnce(updateState); - - private void updateState() - { - if (EqualityComparer.Default.Equals(host, client.Room?.Host)) - return; - - // only play sound when the host changes from an already-existing host. - if (host != null) - Scheduler.AddOnce(() => hostChangedSample?.Play()); - - host = client.Room?.Host; + client.HostChanged += onHostChanged; } private void onUserJoined(MultiplayerRoomUser user) @@ -65,16 +48,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onUserKicked(MultiplayerRoomUser user) => Scheduler.AddOnce(() => userKickedSample?.Play()); + private void onHostChanged(MultiplayerRoomUser? host) + { + if (host != null) + Scheduler.AddOnce(() => hostChangedSample?.Play()); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); if (client.IsNotNull()) { - client.RoomUpdated -= onRoomUpdated; client.UserJoined -= onUserJoined; client.UserLeft -= onUserLeft; client.UserKicked -= onUserKicked; + client.HostChanged -= onHostChanged; } } } From fa49b30b5cc077f807f60fd6964bf5416f5ec845 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 21 Feb 2025 11:30:52 +0100 Subject: [PATCH 547/801] Attempt to fix spectator list showing other users in multiplayer room even if they're not spectating better Maybe closes https://github.com/ppy/osu/issues/31972. Not sure. I have no reproduction scenario to work with, no solid understanding of how the issue can happen, and if this doesn't fix it, then I'm not even entirely sure how this can ever be fixed client-side. The working theory is that not watching updates to the room provoked a situation wherein the room was temporarily not in a correct state when `WatchingUsers` changed, therefore the collection change callback failed to exclude other players in the room from display. I'm only PRing this because of the `next-release` tag on the issue. --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 80 ++++++++++++++++------ 1 file changed, 60 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 4297c62712..98b3ede874 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -2,13 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -38,8 +38,9 @@ namespace osu.Game.Screens.Play.HUD public BindableColour4 HeaderColour { get; } = new BindableColour4(Colour4.White); private BindableList watchingUsers { get; } = new BindableList(); + private BindableList actualSpectators { get; } = new BindableList(); + private Bindable userPlayingState { get; } = new Bindable(); - private int displayedSpectatorCount; private OsuSpriteText header = null!; private FillFlowContainer mainFlow = null!; @@ -94,7 +95,9 @@ namespace osu.Game.Screens.Play.HUD ((IBindableList)watchingUsers).BindTo(client.WatchingUsers); ((IBindable)userPlayingState).BindTo(gameplayState.PlayingState); - watchingUsers.BindCollectionChanged(onSpectatorsChanged, true); + watchingUsers.BindCollectionChanged(onWatchingUsersChanged, true); + multiplayerClient.RoomUpdated += removePlayersFromMultiplayerRoom; + actualSpectators.BindCollectionChanged(onSpectatorsChanged, true); userPlayingState.BindValueChanged(_ => updateVisibility()); Font.BindValueChanged(_ => updateAppearance()); @@ -104,22 +107,55 @@ namespace osu.Game.Screens.Play.HUD this.FadeInFromZero(200, Easing.OutQuint); } - private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArgs e) + private void onWatchingUsersChanged(object? sender, NotifyCollectionChangedEventArgs e) { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + { + for (int i = 0; i < e.NewItems!.Count; i++) + actualSpectators.Add((SpectatorUser)e.NewItems![i]!); + + break; + } + + case NotifyCollectionChangedAction.Remove: + { + for (int i = 0; i < e.OldItems!.Count; i++) + actualSpectators.Remove((SpectatorUser)e.OldItems![i]!); + + break; + } + + case NotifyCollectionChangedAction.Reset: + { + actualSpectators.Clear(); + break; + } + + default: + throw new NotSupportedException(); + } + + removePlayersFromMultiplayerRoom(); + } + + private void removePlayersFromMultiplayerRoom() + { + if (multiplayerClient.Room == null) + return; + // the multiplayer gameplay leaderboard relies on calling `SpectatorClient.WatchUser()` to get updates on users' total scores. // this has an unfortunate side effect of other players showing up in `SpectatorClient.WatchingUsers`. // // we do not generally wish to display other players in the room as spectators due to that implementation detail, // therefore this code is intended to filter out those players on the client side. - // - // note that the way that this is done is rather specific to the multiplayer use case and therefore carries a lot of assumptions - // (e.g. that the `MultiplayerRoomUser`s have the correct `State` at the point wherein they issue the `WatchUser()` calls). - // the more proper way to do this (which is by subscribing to `WatchingUsers` and `RoomUpdated`, and doing a proper diff to a third list on any change of either) - // is a lot more difficult to write correctly, given that we also rely on `BindableList`'s collection changed event arguments to properly animate this component. - var excludedUserIds = new HashSet(); - if (multiplayerClient.Room != null) - excludedUserIds.UnionWith(multiplayerClient.Room.Users.Where(u => u.State != MultiplayerUserState.Spectating).Select(u => u.UserID)); + var excludedUserIds = multiplayerClient.Room.Users.Where(u => u.State != MultiplayerUserState.Spectating).Select(u => u.UserID).ToHashSet(); + actualSpectators.RemoveAll(s => excludedUserIds.Contains(s.OnlineID)); + } + private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArgs e) + { switch (e.Action) { case NotifyCollectionChangedAction.Add: @@ -129,9 +165,6 @@ namespace osu.Game.Screens.Play.HUD var spectator = (SpectatorUser)e.NewItems![i]!; int index = Math.Max(e.NewStartingIndex, 0) + i; - if (excludedUserIds.Contains(spectator.OnlineID)) - continue; - if (index >= max_spectators_displayed) break; @@ -148,10 +181,10 @@ namespace osu.Game.Screens.Play.HUD for (int i = 0; i < spectatorsFlow.Count; i++) spectatorsFlow.SetLayoutPosition(spectatorsFlow[i], i); - if (watchingUsers.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed) + if (actualSpectators.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed) { for (int i = spectatorsFlow.Count; i < max_spectators_displayed; i++) - addNewSpectatorToList(i, watchingUsers[i]); + addNewSpectatorToList(i, actualSpectators[i]); } break; @@ -167,8 +200,7 @@ namespace osu.Game.Screens.Play.HUD throw new NotSupportedException(); } - displayedSpectatorCount = watchingUsers.Count(s => !excludedUserIds.Contains(s.OnlineID)); - header.Text = SpectatorListStrings.SpectatorCount(displayedSpectatorCount).ToUpper(); + header.Text = SpectatorListStrings.SpectatorCount(actualSpectators.Count).ToUpper(); updateVisibility(); for (int i = 0; i < spectatorsFlow.Count; i++) @@ -193,7 +225,7 @@ namespace osu.Game.Screens.Play.HUD private void updateVisibility() { // We don't want to show spectators when we are watching a replay. - mainFlow.FadeTo(displayedSpectatorCount > 0 && userPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); + mainFlow.FadeTo(actualSpectators.Count > 0 && userPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); } private void updateAppearance() @@ -204,6 +236,14 @@ namespace osu.Game.Screens.Play.HUD Width = header.DrawWidth; } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (multiplayerClient.IsNotNull()) + multiplayerClient.RoomUpdated -= removePlayersFromMultiplayerRoom; + } + private partial class SpectatorListEntry : PoolableDrawable { public Bindable Current { get; } = new Bindable(); From a690b0bae993f06edc45fabc6ea2b5153219cc1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 21 Feb 2025 12:05:23 +0100 Subject: [PATCH 548/801] Adjust rounding tolerance in distance snap grid ring colour logic --- osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs index 88e28df8e3..8322c67def 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs @@ -159,7 +159,7 @@ namespace osu.Game.Screens.Edit.Compose.Components // in case 2, we want *flooring* to occur, to prevent a possible off-by-one // because of the rounding snapping forward by a chunk of time significantly too high to be considered a rounding error. // the tolerance margin chosen here is arbitrary and can be adjusted if more cases of this are found. - if (Precision.DefinitelyBigger(beatIndex, fractionalBeatIndex, 0.005)) + if (Precision.DefinitelyBigger(beatIndex, fractionalBeatIndex, 0.01)) beatIndex = (int)Math.Floor(fractionalBeatIndex); var colour = BindableBeatDivisor.GetColourFor(BindableBeatDivisor.GetDivisorForBeatIndex(beatIndex + placementIndex + 1, beatDivisor.Value), Colours); From de78518fea14b4c12c5a2db4bc74d65335f05521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 21 Feb 2025 12:52:59 +0100 Subject: [PATCH 549/801] Fix "use current distance snap" button incorrectly factoring in last object with velocity Closes https://github.com/ppy/osu/issues/32003. --- osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs | 6 +++++- osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs | 8 ++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs index 3c0889d027..45ce3206d2 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs @@ -1,10 +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.Linq; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osuTK; @@ -14,7 +16,9 @@ namespace osu.Game.Rulesets.Osu.Edit { public override double ReadCurrentDistanceSnap(HitObject before, HitObject after) { - float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime); + var lastObjectWithVelocity = EditorBeatmap.HitObjects.TakeWhile(ho => ho != after).OfType().LastOrDefault(); + + float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime, lastObjectWithVelocity); float actualDistance = Vector2.Distance(((OsuHitObject)before).EndPosition, ((OsuHitObject)after).Position); return actualDistance / expectedDistance; diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs index d0b279f201..4129a6fb2c 100644 --- a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Edit private EditorClock editorClock { get; set; } = null!; [Resolved] - private EditorBeatmap editorBeatmap { get; set; } = null!; + protected EditorBeatmap EditorBeatmap { get; private set; } = null!; [Resolved] private IBeatSnapProvider beatSnapProvider { get; set; } = null!; @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Edit } }); - DistanceSpacingMultiplier.Value = editorBeatmap.DistanceSpacing; + DistanceSpacingMultiplier.Value = EditorBeatmap.DistanceSpacing; DistanceSpacingMultiplier.BindValueChanged(multiplier => { distanceSpacingSlider.ContractedLabelText = $"D. S. ({multiplier.NewValue:0.##x})"; @@ -109,7 +109,7 @@ namespace osu.Game.Rulesets.Edit if (multiplier.NewValue != multiplier.OldValue) onScreenDisplay?.Display(new DistanceSpacingToast(multiplier.NewValue.ToLocalisableString(@"0.##x"), multiplier)); - editorBeatmap.DistanceSpacing = multiplier.NewValue; + EditorBeatmap.DistanceSpacing = multiplier.NewValue; }, true); DistanceSpacingMultiplier.BindDisabledChanged(disabled => distanceSpacingSlider.Alpha = disabled ? 0 : 1, true); @@ -267,7 +267,7 @@ namespace osu.Game.Rulesets.Edit public virtual float GetBeatSnapDistance(IHasSliderVelocity? withVelocity = null) { - return (float)(100 * (withVelocity?.SliderVelocityMultiplier ?? 1) * editorBeatmap.Difficulty.SliderMultiplier * 1 + return (float)(100 * (withVelocity?.SliderVelocityMultiplier ?? 1) * EditorBeatmap.Difficulty.SliderMultiplier * 1 / beatSnapProvider.BeatDivisor); } From c77fed637c89aab0e6374c307b4935fe1d6f9097 Mon Sep 17 00:00:00 2001 From: ziv_vy <134942175+ziv-vy@users.noreply.github.com> Date: Sun, 23 Feb 2025 01:01:39 +0200 Subject: [PATCH 550/801] Update MouseSettingsStrings.cs CAPITALISED ONE GODDAMN LETTER --- osu.Game/Localisation/MouseSettingsStrings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/MouseSettingsStrings.cs b/osu.Game/Localisation/MouseSettingsStrings.cs index e61af07364..9609c2dd90 100644 --- a/osu.Game/Localisation/MouseSettingsStrings.cs +++ b/osu.Game/Localisation/MouseSettingsStrings.cs @@ -25,9 +25,9 @@ namespace osu.Game.Localisation public static LocalisableString HighPrecisionMouse => new TranslatableString(getKey(@"high_precision_mouse"), @"High precision mouse"); /// - /// "Attempts to bypass any operation system mouse acceleration. On windows, this is equivalent to what used to be known as "Raw Input"." + /// "Attempts to bypass any operation system mouse acceleration. On Windows, this is equivalent to what used to be known as "Raw Input"." /// - public static LocalisableString HighPrecisionMouseTooltip => new TranslatableString(getKey(@"high_precision_mouse_tooltip"), @"Attempts to bypass any operation system mouse acceleration. On windows, this is equivalent to what used to be known as ""Raw Input""."); + public static LocalisableString HighPrecisionMouseTooltip => new TranslatableString(getKey(@"high_precision_mouse_tooltip"), @"Attempts to bypass any operation system mouse acceleration. On Windows, this is equivalent to what used to be known as ""Raw Input""."); /// /// "Confine mouse cursor to window" From d95f31dc5af463423a72981f4328fbd0f0b6c654 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sat, 22 Feb 2025 15:21:54 -0800 Subject: [PATCH 551/801] Also fix operating system terminology --- osu.Game/Localisation/MouseSettingsStrings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/MouseSettingsStrings.cs b/osu.Game/Localisation/MouseSettingsStrings.cs index 9609c2dd90..c92c3b6ddc 100644 --- a/osu.Game/Localisation/MouseSettingsStrings.cs +++ b/osu.Game/Localisation/MouseSettingsStrings.cs @@ -25,9 +25,9 @@ namespace osu.Game.Localisation public static LocalisableString HighPrecisionMouse => new TranslatableString(getKey(@"high_precision_mouse"), @"High precision mouse"); /// - /// "Attempts to bypass any operation system mouse acceleration. On Windows, this is equivalent to what used to be known as "Raw Input"." + /// "Attempts to bypass any operating system mouse acceleration. On Windows, this is equivalent to what used to be known as "Raw Input"." /// - public static LocalisableString HighPrecisionMouseTooltip => new TranslatableString(getKey(@"high_precision_mouse_tooltip"), @"Attempts to bypass any operation system mouse acceleration. On Windows, this is equivalent to what used to be known as ""Raw Input""."); + public static LocalisableString HighPrecisionMouseTooltip => new TranslatableString(getKey(@"high_precision_mouse_tooltip"), @"Attempts to bypass any operating system mouse acceleration. On Windows, this is equivalent to what used to be known as ""Raw Input""."); /// /// "Confine mouse cursor to window" From 8b2582a69d07adf343855b729dd143777abbcbf6 Mon Sep 17 00:00:00 2001 From: finadoggie <75299710+Finadoggie@users.noreply.github.com> Date: Sat, 22 Feb 2025 16:54:27 -0800 Subject: [PATCH 552/801] Add tip pressure threshold slider ingame --- .../Settings/Sections/Input/TabletSettings.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index 00ffbc1120..2cce6f18ec 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -45,6 +45,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input private readonly BindableNumber rotation = new BindableNumber { MinValue = 0, MaxValue = 360 }; + private readonly BindableNumber pressureThreshold = new BindableNumber { MinValue = 0, MaxValue = 100 }; + [Resolved] private GameHost host { get; set; } @@ -213,6 +215,13 @@ namespace osu.Game.Overlays.Settings.Sections.Input Current = sizeY, CanBeShown = { BindTarget = enabled } }, + new SettingsSlider + { + TransferValueOnCommit = true, + LabelText = "Tip Threshold", + Current = pressureThreshold, + CanBeShown = { BindTarget = enabled } + }, } }, }; @@ -267,6 +276,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input aspectRatioApplication = Schedule(() => forceAspectRatio(aspect.NewValue)); }); + pressureThreshold.BindTo(tabletHandler.PressureThreshold); + tablet.BindTo(tabletHandler.Tablet); tablet.BindValueChanged(val => Schedule(() => { From 543ad5b2a47591652d04ac66eb8730cafd7e06b9 Mon Sep 17 00:00:00 2001 From: Kunologist <2014709936@qq.com> Date: Mon, 24 Feb 2025 14:16:33 +0800 Subject: [PATCH 553/801] Add alt+wheel volume adjustment on result screen --- osu.Game/Screens/Ranking/ResultsScreen.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index fe0d805cee..8fb3c66054 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -26,6 +26,7 @@ using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Online.Placeholders; using osu.Game.Overlays; +using osu.Game.Overlays.Volume; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking.Expanded.Accuracy; @@ -122,6 +123,7 @@ namespace osu.Game.Screens.Ranking RelativeSizeAxes = Axes.Both, Children = new Drawable[] { + new GlobalScrollAdjustsVolume(), StatisticsPanel = createStatisticsPanel().With(panel => { panel.RelativeSizeAxes = Axes.Both; @@ -503,12 +505,24 @@ namespace osu.Game.Screens.Ranking { } + protected override bool OnScroll(ScrollEvent e) + { + // Match stable behaviour of only alt-scroll adjusting volume. + // This is the same behaviour as the song selection screen. + if (!e.CurrentState.Keyboard.AltPressed) + return true; + + return base.OnScroll(e); + } + protected partial class VerticalScrollContainer : OsuScrollContainer { protected override Container Content => content; private readonly Container content; + protected override bool OnScroll(ScrollEvent e) => !e.ControlPressed && !e.AltPressed && !e.ShiftPressed && !e.SuperPressed; + public VerticalScrollContainer() { Masking = false; From f4b427ee66bd169ab7270f9a4ef8f467d4ac4572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 09:15:20 +0100 Subject: [PATCH 554/801] Add failing test case --- .../TestSceneModDifficultyAdjustSettings.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs index b40d0b10d2..30470c9c17 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs @@ -18,6 +18,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osuTK; using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { @@ -220,6 +221,29 @@ namespace osu.Game.Tests.Visual.UserInterface checkBindableAtValue("Circle Size", null); } + [Test] + public void TestResetToDefaultViaDoubleClickingNub() + { + setBeatmapWithDifficultyParameters(5); + + setSliderValue("Circle Size", 3); + setExtendedLimits(true); + + checkSliderAtValue("Circle Size", 3); + checkBindableAtValue("Circle Size", 3); + + AddStep("double click circle size nub", () => + { + var nub = this.ChildrenOfType.SliderNub>().First(); + InputManager.MoveMouseTo(nub); + InputManager.Click(MouseButton.Left); + InputManager.Click(MouseButton.Left); + }); + + checkSliderAtValue("Circle Size", 5); + checkBindableAtValue("Circle Size", null); + } + [Test] public void TestModSettingChangeTracker() { From d8cb3b68d3ea90268bb142a2ef6e1de782f2040a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 09:34:52 +0100 Subject: [PATCH 555/801] Add "Team" channel type The lack of this bricks chat completely due to newtonsoft deserialisation errors: 2025-02-24 08:32:58 [verbose]: Processing response from https://dev.ppy.sh/api/v2/chat/updates failed with Newtonsoft.Json.JsonSerializationException: Error converting value "TEAM" to type 'osu.Game.Online.Chat.ChannelType'. Path 'presence[39].type', line 1, position 13765. 2025-02-24 08:32:58 [verbose]: ---> System.ArgumentException: Requested value 'TEAM' was not found. 2025-02-24 08:32:58 [verbose]: at Newtonsoft.Json.Utilities.EnumUtils.ParseEnum(Type enumType, NamingStrategy namingStrategy, String value, Boolean disallowNumber) 2025-02-24 08:32:58 [verbose]: at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.EnsureType(JsonReader reader, Object value, CultureInfo culture, JsonContract contract, Type targetType) --- osu.Game/Online/Chat/ChannelType.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Chat/ChannelType.cs b/osu.Game/Online/Chat/ChannelType.cs index bd628e90c4..4fb890c2cc 100644 --- a/osu.Game/Online/Chat/ChannelType.cs +++ b/osu.Game/Online/Chat/ChannelType.cs @@ -14,5 +14,6 @@ namespace osu.Game.Online.Chat Group, System, Announce, + Team, } } From be8ec759488e3bb5e5479341c8de400a80f3e9ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 09:39:27 +0100 Subject: [PATCH 556/801] Display team chat channel in separate group --- osu.Game/Overlays/Chat/ChannelList/ChannelList.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs index f027888962..6e874e4ed8 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs @@ -39,6 +39,7 @@ namespace osu.Game.Overlays.Chat.ChannelList public ChannelGroup AnnounceChannelGroup { get; private set; } = null!; public ChannelGroup PublicChannelGroup { get; private set; } = null!; + public ChannelGroup TeamChannelGroup { get; private set; } = null!; public ChannelGroup PrivateChannelGroup { get; private set; } = null!; private OsuScrollContainer scroll = null!; @@ -82,6 +83,7 @@ namespace osu.Game.Overlays.Chat.ChannelList AnnounceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper(), false), PublicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper(), false), selector = new ChannelListItem(ChannelListingChannel), + TeamChannelGroup = new ChannelGroup("TEAM", false), // TODO: replace with osu-web localisable string once available PrivateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper(), true), }, }, @@ -156,6 +158,9 @@ namespace osu.Game.Overlays.Chat.ChannelList case ChannelType.Announce: return AnnounceChannelGroup; + case ChannelType.Team: + return TeamChannelGroup; + default: return PublicChannelGroup; } From 194f05d2588fa7f23287b85c3968219102e33a51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 09:43:46 +0100 Subject: [PATCH 557/801] Add icons to chat channel group headers Matches web in appearance. Cross-reference: https://github.com/ppy/osu-web/blob/3c9e99eaf4bd9e73d2712f60d67f5bc95f9dfe2b/resources/js/chat/conversation-list.tsx#L13-L19 --- .../Overlays/Chat/ChannelList/ChannelList.cs | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs index 6e874e4ed8..ae68c9c82e 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs @@ -9,6 +9,7 @@ using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; using osu.Framework.Testing; @@ -80,11 +81,12 @@ namespace osu.Game.Overlays.Chat.ChannelList RelativeSizeAxes = Axes.X, } }, - AnnounceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper(), false), - PublicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper(), false), + // cross-reference for icons: https://github.com/ppy/osu-web/blob/3c9e99eaf4bd9e73d2712f60d67f5bc95f9dfe2b/resources/js/chat/conversation-list.tsx#L13-L19 + AnnounceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper(), FontAwesome.Solid.Bullhorn, false), + PublicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper(), FontAwesome.Solid.Comments, false), selector = new ChannelListItem(ChannelListingChannel), - TeamChannelGroup = new ChannelGroup("TEAM", false), // TODO: replace with osu-web localisable string once available - PrivateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper(), true), + TeamChannelGroup = new ChannelGroup("TEAM", FontAwesome.Solid.Users, false), // TODO: replace with osu-web localisable string once available + PrivateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper(), FontAwesome.Solid.Envelope, true), }, }, }, @@ -179,7 +181,7 @@ namespace osu.Game.Overlays.Chat.ChannelList private readonly bool sortByRecent; public readonly ChannelListItemFlow ItemFlow; - public ChannelGroup(LocalisableString label, bool sortByRecent) + public ChannelGroup(LocalisableString label, IconUsage icon, bool sortByRecent) { this.sortByRecent = sortByRecent; Direction = FillDirection.Vertical; @@ -189,11 +191,26 @@ namespace osu.Game.Overlays.Chat.ChannelList Children = new Drawable[] { - new OsuSpriteText + new FillFlowContainer { - Text = label, - Margin = new MarginPadding { Left = 18, Bottom = 5 }, - Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new OsuSpriteText + { + Text = label, + Margin = new MarginPadding { Left = 18, Bottom = 5 }, + Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold), + }, + new SpriteIcon + { + Icon = icon, + Size = new Vector2(12), + }, + } }, ItemFlow = new ChannelListItemFlow(sortByRecent) { From 4ac4b308e10d041dec5960f808ce2d295171f3d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 09:48:03 +0100 Subject: [PATCH 558/801] Add visual test coverage of team channels --- .../Visual/Online/TestSceneChannelList.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs b/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs index 5f77e084da..8f8cf036f1 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs @@ -115,6 +115,12 @@ namespace osu.Game.Tests.Visual.Online channelList.AddChannel(createRandomPrivateChannel()); }); + AddStep("Add Team Channels", () => + { + for (int i = 0; i < 10; i++) + channelList.AddChannel(createRandomTeamChannel()); + }); + AddStep("Add Announce Channels", () => { for (int i = 0; i < 2; i++) @@ -189,5 +195,16 @@ namespace osu.Game.Tests.Visual.Online Id = id, }; } + + private Channel createRandomTeamChannel() + { + int id = TestResources.GetNextTestID(); + return new Channel + { + Name = $"Team {id}", + Type = ChannelType.Team, + Id = id, + }; + } } } From 0312467c8840067054c6326d9da821329b7bf01e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 12:30:37 +0100 Subject: [PATCH 559/801] Fix hash comparison being case sensitive when choosing files for partial beatmap submission Noticed when investigating https://github.com/ppy/osu/issues/32059, and also a likely cause for user reports like https://discord.com/channels/188630481301012481/1097318920991559880/1342962553101357066. Honestly I have no solid defence, Your Honour. I guess this just must not have been tested on the client side, only relied on server-side testing. --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 66139bacec..13981bcb69 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -285,7 +285,7 @@ namespace osu.Game.Screens.Edit.Submission continue; } - if (localHash != onlineHash) + if (!localHash.Equals(onlineHash, StringComparison.OrdinalIgnoreCase)) filesToUpdate.Add(filename); } From 41db3c1501bbfc40f1eb9952fa9d332319ff347b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 14:30:55 +0100 Subject: [PATCH 560/801] Fix taiko swell ending samples playing at results sometimes Closes https://github.com/ppy/osu/issues/32052. Sooooo... this is going to be a rant... To understand why this is going to require a rant, dear reader, please do the following: 1. Read the issue thread and follow the reproduction scenario (download map linked, fire up autoplay, seek near end, wait for results, hear the sample spam). 2. Now exit out to song select, *hide the toolbar*, and attempt reproducing the issue again. 3. Depending on ambient mood, laugh or cry. Now, *why on earth* would the *TOOLBAR* have any bearing on anything? Well, the chain of failure is something like this: - The toolbar hides for the duration of gameplay, naturally. - When progressing to results, the toolbar gets automatically unhidden. - This triggers invalidations on `ScrollingHitObjectContainer`. I'm not precisely sure which property it is that triggers the invalidations, but one clearly does. It may be position or size or whichever. - When the invalidation is triggered on `layoutCache`, the next `Update()` call is going to recompute lifetimes for ALL hitobject entries. - In case of swells, it happens that the calculated lifetime end of the swell is larger than what it actually ended up being determined as at the instant of judging the swell, and thus, the swell is *resurrected*, reassigned a DHO, and the DHO calls `UpdateState()` and plays the sample again despite the `samplePlayed` flag in `LegacySwell`, because all of that is ephemeral state that does not survive a hitobject getting resurrected. Now I *could* just fix this locally to the swell, maybe, by having some time lenience check, but the fact that hitobjects can be resurrected by the *toolbar* appearing, of all possible causes in the world, feels just completely wrong. So I'm adding a local check in SHOC to not overwrite lifetime ends of judged object entries. The reason why I'm making that check specific to end time is that I can see valid reasons why you would want to recompute lifetime *start* even on a judged object (such as playfield geometry changing in a significant way). I can't think of a valid reason to do that to lifetime *end*. --- .../Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 7841e65935..8b0076afa1 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -247,7 +247,12 @@ namespace osu.Game.Rulesets.UI.Scrolling // It is required that we set a lifetime end here to ensure that in scenarios like loading a Player instance to a seeked // location in a beatmap doesn't churn every hit object into a DrawableHitObject. Even in a pooled scenario, the overhead // of this can be quite crippling. - entry.LifetimeEnd = entry.HitObject.GetEndTime() + timeRange.Value; + // + // However, additionally do not attempt to alter lifetime of judged entries. + // This is to prevent freak accidents like objects suddenly becoming alive because of this estimate assigning a later lifetime + // than the object itself decided it should have when it underwent judgement. + if (!entry.Judged) + entry.LifetimeEnd = entry.HitObject.GetEndTime() + timeRange.Value; } private void updateLayoutRecursive(DrawableHitObject hitObject, double? parentHitObjectStartTime = null) From e8f7bcb6e625a6360b2bd4487186ac075e07ddc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 15:06:02 +0100 Subject: [PATCH 561/801] Only show team channel section when there is a team channel --- osu.Game.Tests/Visual/Online/TestSceneChannelList.cs | 6 +----- osu.Game/Overlays/Chat/ChannelList/ChannelList.cs | 7 +++---- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs b/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs index 8f8cf036f1..364240502a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs @@ -115,11 +115,7 @@ namespace osu.Game.Tests.Visual.Online channelList.AddChannel(createRandomPrivateChannel()); }); - AddStep("Add Team Channels", () => - { - for (int i = 0; i < 10; i++) - channelList.AddChannel(createRandomTeamChannel()); - }); + AddStep("Add Team Channel", () => channelList.AddChannel(createRandomTeamChannel())); AddStep("Add Announce Channels", () => { diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs index ae68c9c82e..c0fc349c2c 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs @@ -106,6 +106,7 @@ namespace osu.Game.Overlays.Chat.ChannelList }; selector.OnRequestSelect += chan => OnRequestSelect?.Invoke(chan); + updateVisibility(); } public void AddChannel(Channel channel) @@ -170,10 +171,8 @@ namespace osu.Game.Overlays.Chat.ChannelList private void updateVisibility() { - if (AnnounceChannelGroup.ItemFlow.Children.Count == 0) - AnnounceChannelGroup.Hide(); - else - AnnounceChannelGroup.Show(); + AnnounceChannelGroup.Alpha = AnnounceChannelGroup.ItemFlow.Any() ? 1 : 0; + TeamChannelGroup.Alpha = TeamChannelGroup.ItemFlow.Any() ? 1 : 0; } public partial class ChannelGroup : FillFlowContainer From e13aa4a99b353f994e9bcf6c6df58d18f466bf66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 15:10:20 +0100 Subject: [PATCH 562/801] Do not allow leaving team channels --- osu.Game/Overlays/Chat/ChannelList/ChannelList.cs | 8 ++++++-- osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs index c0fc349c2c..0a89775cc7 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs @@ -114,9 +114,13 @@ namespace osu.Game.Overlays.Chat.ChannelList if (channelMap.ContainsKey(channel)) return; - ChannelListItem item = new ChannelListItem(channel); + ChannelListItem item = new ChannelListItem(channel) + { + CanLeave = channel.Type != ChannelType.Team + }; item.OnRequestSelect += chan => OnRequestSelect?.Invoke(chan); - item.OnRequestLeave += chan => OnRequestLeave?.Invoke(chan); + if (item.CanLeave) + item.OnRequestLeave += chan => OnRequestLeave?.Invoke(chan); ChannelGroup group = getGroupFromChannel(channel); channelMap.Add(channel, item); diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs index b197fe199d..6107f130ec 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs @@ -24,6 +24,8 @@ namespace osu.Game.Overlays.Chat.ChannelList public partial class ChannelListItem : OsuClickableContainer, IFilterable { public event Action? OnRequestSelect; + + public bool CanLeave { get; init; } = true; public event Action? OnRequestLeave; public readonly Channel Channel; @@ -160,7 +162,7 @@ namespace osu.Game.Overlays.Chat.ChannelList private ChannelListItemCloseButton? createCloseButton() { - if (isSelector) + if (isSelector || !CanLeave) return null; return new ChannelListItemCloseButton From c82cf4092879167f60bd76729730350d882c248f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 15:24:18 +0100 Subject: [PATCH 563/801] Do not give swell ticks any visual representation Why is this a thing at all? How has it survived this long? I don't know. As far as I can tell this only manifests on selected beatmaps with "slow swells" that spend the entire beatmap moving in the background. On other beatmaps the tick is faded out, probably due to the initial transform application that normally "works" but fails hard on these slow swells. Can be seen on https://osu.ppy.sh/beatmapsets/1432454#taiko/2948222. --- .../Objects/Drawables/DrawableSwellTick.cs | 7 +------ .../Objects/Drawables/DrawableTaikoHitObject.cs | 6 +++++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs index 04dd01e066..88554ba257 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs @@ -4,9 +4,7 @@ #nullable disable using JetBrains.Annotations; -using osu.Framework.Graphics; using osu.Framework.Input.Events; -using osu.Game.Rulesets.Taiko.Skinning.Default; using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -25,8 +23,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { } - protected override void UpdateInitialTransforms() => this.FadeOut(); - public void TriggerResult(bool hit) { HitObject.StartTime = Time.Current; @@ -43,7 +39,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public override bool OnPressed(KeyBindingPressEvent e) => false; - protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumRollTick), - _ => new TickPiece()); + protected override SkinnableDrawable CreateMainPiece() => null; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index 0cf9651965..520ac2ba80 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -154,9 +154,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (MainPiece != null) Content.Remove(MainPiece, true); - Content.Add(MainPiece = CreateMainPiece()); + MainPiece = CreateMainPiece(); + + if (MainPiece != null) + Content.Add(MainPiece); } + [CanBeNull] protected abstract SkinnableDrawable CreateMainPiece(); } } From 1f562ab47d5f6959f66e0091ec82b778c3539f7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 09:18:19 +0100 Subject: [PATCH 564/801] Fix double-clicking difficulty adjust sliders not resetting the value to default correctly - Closes https://github.com/ppy/osu/issues/31888 - Supersedes / closes https://github.com/ppy/osu/pull/32060 --- .../Mods/OsuModDifficultyAdjust.cs | 9 +------- .../UserInterface/RoundedSliderBar.cs | 18 +++++++++++---- .../Mods/DifficultyAdjustSettingsControl.cs | 23 +++++++++++++------ 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index f35b1abc42..10282ff988 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -3,7 +3,6 @@ using System.Linq; using osu.Framework.Bindables; -using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -63,13 +62,7 @@ namespace osu.Game.Rulesets.Osu.Mods private partial class ApproachRateSettingsControl : DifficultyAdjustSettingsControl { - protected override RoundedSliderBar CreateSlider(BindableNumber current) => - new ApproachRateSlider - { - RelativeSizeAxes = Axes.X, - Current = current, - KeyboardStep = 0.1f, - }; + protected override RoundedSliderBar CreateSlider(BindableNumber current) => new ApproachRateSlider(); /// /// A slider bar with more detailed approach rate info for its given value diff --git a/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs b/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs index aeab7c34b2..9a0183da64 100644 --- a/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Overlays; using Vector2 = osuTK.Vector2; @@ -52,10 +53,21 @@ namespace osu.Game.Graphics.UserInterface } } + /// + /// The action to use to reset the value of to the default. + /// Triggered on double click. + /// + public Action ResetToDefault { get; internal set; } + public RoundedSliderBar() { Height = Nub.HEIGHT; RangePadding = Nub.DEFAULT_EXPANDED_SIZE / 2; + ResetToDefault = () => + { + if (!Current.Disabled) + Current.SetDefault(); + }; Children = new Drawable[] { new Container @@ -102,11 +114,7 @@ namespace osu.Game.Graphics.UserInterface Origin = Anchor.TopCentre, RelativePositionAxes = Axes.X, Current = { Value = true }, - OnDoubleClicked = () => - { - if (!Current.Disabled) - Current.SetDefault(); - }, + OnDoubleClicked = () => ResetToDefault.Invoke(), }, }, hoverClickSounds = new HoverClickSounds() diff --git a/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs b/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs index d04d7636ec..6697a8d848 100644 --- a/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs +++ b/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs @@ -31,12 +31,7 @@ namespace osu.Game.Rulesets.Mods protected sealed override Drawable CreateControl() => new SliderControl(sliderDisplayCurrent, CreateSlider); - protected virtual RoundedSliderBar CreateSlider(BindableNumber current) => new RoundedSliderBar - { - RelativeSizeAxes = Axes.X, - Current = current, - KeyboardStep = 0.1f, - }; + protected virtual RoundedSliderBar CreateSlider(BindableNumber current) => new RoundedSliderBar(); /// /// Guards against beatmap values displayed on slider bars being transferred to user override. @@ -111,7 +106,21 @@ namespace osu.Game.Rulesets.Mods { InternalChildren = new Drawable[] { - createSlider(currentNumber) + createSlider(currentNumber).With(slider => + { + slider.RelativeSizeAxes = Axes.X; + slider.Current = currentNumber; + slider.KeyboardStep = 0.1f; + // this looks redundant, but isn't because of the various games this component plays + // (`Current` is nullable and represents the underlying setting value, + // `currentNumber` is not nullable and represents what is getting displayed, + // therefore without this, double-clicking the slider would reset `currentNumber` to its bogus default of 0). + slider.ResetToDefault = () => + { + if (!Current.Disabled) + Current.SetDefault(); + }; + }) }; AutoSizeAxes = Axes.Y; From fc2d8bfe5f3b4ed3d1a0f7652dd84601e8115b75 Mon Sep 17 00:00:00 2001 From: finadoggie <75299710+Finadoggie@users.noreply.github.com> Date: Tue, 25 Feb 2025 00:25:51 -0800 Subject: [PATCH 565/801] Clamp slider from 0 to 1 --- osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index 2cce6f18ec..9d70e49659 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -45,7 +45,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input private readonly BindableNumber rotation = new BindableNumber { MinValue = 0, MaxValue = 360 }; - private readonly BindableNumber pressureThreshold = new BindableNumber { MinValue = 0, MaxValue = 100 }; + private readonly BindableNumber pressureThreshold = new BindableNumber { MinValue = 0.0f, MaxValue = 1.0f, Precision = 0.005f }; [Resolved] private GameHost host { get; set; } From e97c2fee0d2a57d2d13c2e20a76370daa325cd4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Feb 2025 12:57:38 +0100 Subject: [PATCH 566/801] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index d49acd7b27..d4b49e492a 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 5ca49e80f6..d10a3d649a 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 13ca8c20f6fa71bd196e30a5987cb112cbc7214f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 25 Feb 2025 21:54:13 +0900 Subject: [PATCH 567/801] Make results screens use tasks to fetch scores --- .../Visual/Ranking/TestSceneResultsScreen.cs | 17 +-- .../Spectate/MultiSpectatorResultsScreen.cs | 7 +- .../Playlists/PlaylistItemResultsScreen.cs | 112 ++++++++++-------- .../PlaylistItemScoreResultsScreen.cs | 5 +- .../PlaylistItemUserBestResultsScreen.cs | 5 +- osu.Game/Screens/Ranking/ResultsScreen.cs | 51 +++----- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 31 +++-- 7 files changed, 117 insertions(+), 111 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 3a08756090..4acbdb4a76 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -17,7 +17,6 @@ using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Osu; @@ -416,7 +415,7 @@ namespace osu.Game.Tests.Visual.Ranking RetryOverlay = InternalChildren.OfType().SingleOrDefault(); } - protected override APIRequest FetchScores(Action> scoresCallback) + protected override Task> FetchScores() { var scores = new List(); @@ -428,9 +427,7 @@ namespace osu.Game.Tests.Visual.Ranking scores.Add(score); } - scoresCallback.Invoke(scores); - - return null; + return Task.FromResult>(scores); } } @@ -446,9 +443,9 @@ namespace osu.Game.Tests.Visual.Ranking this.fetchWaitTask = fetchWaitTask ?? Task.CompletedTask; } - protected override APIRequest FetchScores(Action> scoresCallback) + protected override Task> FetchScores() { - Task.Run(async () => + return Task.Run>(async () => { await fetchWaitTask; @@ -461,12 +458,10 @@ namespace osu.Game.Tests.Visual.Ranking scores.Add(score); } - scoresCallback?.Invoke(scores); - Schedule(() => FetchCompleted = true); - }); - return null; + return scores; + }); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs index c240bbea0c..6e2f90e3b5 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs @@ -1,9 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; -using osu.Game.Online.API; +using System.Threading.Tasks; using osu.Game.Scoring; using osu.Game.Screens.Play; @@ -23,8 +22,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate Scheduler.AddDelayed(() => StatisticsPanel.ToggleVisibility(), 1000); } - protected override APIRequest? FetchScores(Action> scoresCallback) => null; + protected override Task> FetchScores() => Task.FromResult>([]); - protected override APIRequest? FetchNextPage(int direction, Action> scoresCallback) => null; + protected override Task> FetchNextPage(int direction) => Task.FromResult>([]); } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index 13ef5d6f64..ed90b3b1ae 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -76,16 +77,21 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected abstract APIRequest CreateScoreRequest(); - protected sealed override APIRequest FetchScores(Action> scoresCallback) + protected override async Task> FetchScores() { // This performs two requests: // 1. A request to show the relevant score (and scores around). // 2. If that fails, a request to index the room starting from the highest score. + var requestTaskSource = new TaskCompletionSource(); var userScoreReq = CreateScoreRequest(); + userScoreReq.Success += requestTaskSource.SetResult; + userScoreReq.Failure += requestTaskSource.SetException; + API.Queue(userScoreReq); - userScoreReq.Success += userScore => + try { + var userScore = await requestTaskSource.Task; var allScores = new List { userScore }; // Other scores could have arrived between score submission and entering the results screen. Ensure the local player score position is up to date. @@ -113,88 +119,96 @@ namespace osu.Game.Screens.OnlinePlay.Playlists setPositions(lowerScores, userScore.Position.Value, 1); } - Schedule(() => - { - PerformSuccessCallback(scoresCallback, allScores); - hideLoadingSpinners(); - }); - }; - - // On failure, fallback to a normal index. - userScoreReq.Failure += _ => API.Queue(createIndexRequest(scoresCallback)); - - return userScoreReq; + return TransformScores(allScores); + } + catch (OperationCanceledException) + { + return []; + } + catch + { + return await fetchScoresAround(); + } + finally + { + Schedule(() => hideLoadingSpinners()); + } } - protected override APIRequest? FetchNextPage(int direction, Action> scoresCallback) + protected override async Task> FetchNextPage(int direction) { Debug.Assert(direction == 1 || direction == -1); MultiplayerScores? pivot = direction == -1 ? higherScores : lowerScores; - if (pivot?.Cursor == null) - return null; + return []; - if (pivot == higherScores) - LeftSpinner.Show(); - else - RightSpinner.Show(); + Schedule(() => + { + if (pivot == higherScores) + LeftSpinner.Show(); + else + RightSpinner.Show(); + }); - return createIndexRequest(scoresCallback, pivot); + return await fetchScoresAround(pivot); } /// /// Creates a with an optional score pivot. /// /// Does not queue the request. - /// The callback to perform with the resulting scores. /// An optional score pivot to retrieve scores around. Can be null to retrieve scores from the highest score. - /// The indexing . - private APIRequest createIndexRequest(Action> scoresCallback, MultiplayerScores? pivot = null) + private async Task> fetchScoresAround(MultiplayerScores? pivot = null) { + var requestTaskSource = new TaskCompletionSource(); var indexReq = pivot != null ? new IndexPlaylistScoresRequest(RoomId, PlaylistItem.ID, pivot.Cursor, pivot.Params) : new IndexPlaylistScoresRequest(RoomId, PlaylistItem.ID); + indexReq.Success += requestTaskSource.SetResult; + indexReq.Failure += requestTaskSource.SetException; + API.Queue(indexReq); - indexReq.Success += r => + try { + var index = await requestTaskSource.Task; + if (pivot == lowerScores) { - lowerScores = r; - setPositions(r, pivot, 1); + lowerScores = index; + setPositions(index, pivot, 1); } else { - higherScores = r; - setPositions(r, pivot, -1); + higherScores = index; + setPositions(index, pivot, -1); } - Schedule(() => - { - PerformSuccessCallback(scoresCallback, r.Scores, r); - hideLoadingSpinners(r); - }); - }; - - indexReq.Failure += _ => hideLoadingSpinners(pivot); - - return indexReq; + return TransformScores(index.Scores, index); + } + catch (OperationCanceledException) + { + return []; + } + finally + { + Schedule(() => hideLoadingSpinners(pivot)); + } } /// /// Transforms returned into s, ensure the is put into a sane state, and invokes a given success callback. /// - /// The callback to invoke with the final s. /// The s that were retrieved from s. /// An optional pivot around which the scores were retrieved. - protected virtual ScoreInfo[] PerformSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null) + protected virtual ScoreInfo[] TransformScores(List scores, MultiplayerScores? pivot = null) { - var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray(); - - // Invoke callback to add the scores. Exclude the score provided to this screen since it's added already. - callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID)); - - return scoreInfos; + // Exclude the score provided to this screen since it's added already. + return scores + .Where(s => s.ID != Score?.OnlineID) + .Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, Beatmap.Value.BeatmapInfo)) + .OrderByTotalScore() + .ToArray(); } private void hideLoadingSpinners(MultiplayerScores? pivot = null) @@ -213,7 +227,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// The to set positions on. /// The pivot. /// The amount to increment the pivot position by for each in . - private void setPositions(MultiplayerScores scores, MultiplayerScores? pivot, int increment) + private static void setPositions(MultiplayerScores scores, MultiplayerScores? pivot, int increment) => setPositions(scores, pivot?.Scores[^1].Position ?? 0, increment); /// @@ -222,7 +236,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// The to set positions on. /// The pivot position. /// The amount to increment the pivot position by for each in . - private void setPositions(MultiplayerScores scores, int pivotPosition, int increment) + private static void setPositions(MultiplayerScores scores, int pivotPosition, int increment) { foreach (var s in scores.Scores) { diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs index 05c03a4b28..c6c10e4d91 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using osu.Game.Online.API; @@ -31,9 +30,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override APIRequest CreateScoreRequest() => new ShowPlaylistScoreRequest(RoomId, PlaylistItem.ID, scoreId); - protected override ScoreInfo[] PerformSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null) + protected override ScoreInfo[] TransformScores(List scores, MultiplayerScores? pivot = null) { - var scoreInfos = base.PerformSuccessCallback(callback, scores, pivot); + var scoreInfos = base.TransformScores(scores, pivot); Schedule(() => SelectedScore.Value ??= scoreInfos.SingleOrDefault(s => s.OnlineID == scoreId)); return scoreInfos; } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs index 5b20496dba..1a0df0291c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using osu.Game.Online.API; @@ -25,9 +24,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override APIRequest CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, userId); - protected override ScoreInfo[] PerformSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null) + protected override ScoreInfo[] TransformScores(List scores, MultiplayerScores? pivot = null) { - var scoreInfos = base.PerformSuccessCallback(callback, scores, pivot); + var scoreInfos = base.TransformScores(scores, pivot); Schedule(() => { diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index fe0d805cee..11e90a06b9 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -5,10 +5,12 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -23,7 +25,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Localisation; -using osu.Game.Online.API; using osu.Game.Online.Placeholders; using osu.Game.Overlays; using osu.Game.Scoring; @@ -60,9 +61,6 @@ namespace osu.Game.Screens.Ranking private bool skipExitTransition; - [Resolved] - private IAPIProvider api { get; set; } = null!; - protected StatisticsPanel StatisticsPanel { get; private set; } = null!; private Drawable bottomPanel = null!; @@ -237,10 +235,7 @@ namespace osu.Game.Screens.Ranking { base.LoadComplete(); - var req = FetchScores(fetchScoresCallback); - - if (req != null) - api.Queue(req); + FetchScores().ContinueWith(t => addScores(t.GetResultSafely())); StatisticsPanel.State.BindValueChanged(onStatisticsStateChanged, true); } @@ -251,18 +246,16 @@ namespace osu.Game.Screens.Ranking if (lastFetchCompleted) { - APIRequest? nextPageRequest = null; + Task> nextPageTask = Task.FromResult>([]); if (ScorePanelList.IsScrolledToStart) - nextPageRequest = FetchNextPage(-1, fetchScoresCallback); + nextPageTask = FetchNextPage(-1); else if (ScorePanelList.IsScrolledToEnd) - nextPageRequest = FetchNextPage(1, fetchScoresCallback); + nextPageTask = FetchNextPage(1); - if (nextPageRequest != null) - { - lastFetchCompleted = false; - api.Queue(nextPageRequest); - } + nextPageTask.ContinueWith(t => addScores(t.GetResultSafely()), TaskContinuationOptions.OnlyOnRanToCompletion); + + lastFetchCompleted = nextPageTask.IsCompletedSuccessfully; } } @@ -329,17 +322,13 @@ namespace osu.Game.Screens.Ranking /// /// Performs a fetch/refresh of scores to be displayed. /// - /// A callback which should be called when fetching is completed. Scheduling is not required. - /// An responsible for the fetch operation. This will be queued and performed automatically. - protected virtual APIRequest? FetchScores(Action> scoresCallback) => null; + protected virtual Task> FetchScores() => Task.FromResult>([]); /// - /// Performs a fetch of the next page of scores. This is invoked every frame until a non-null is returned. + /// Performs a fetch of the next page of scores. This is invoked every frame. /// /// The fetch direction. -1 to fetch scores greater than the current start of the list, and 1 to fetch scores lower than the current end of the list. - /// A callback which should be called when fetching is completed. Scheduling is not required. - /// An responsible for the fetch operation. This will be queued and performed automatically. - protected virtual APIRequest? FetchNextPage(int direction, Action> scoresCallback) => null; + protected virtual Task> FetchNextPage(int direction) => Task.FromResult>([]); /// /// Creates the to be used to display extended information about scores. @@ -351,10 +340,14 @@ namespace osu.Game.Screens.Ranking : new StatisticsPanel(); } - private void fetchScoresCallback(IEnumerable scores) => Schedule(() => + private void addScores(IEnumerable scores) => Schedule(() => { foreach (var s in scores) - addScore(s); + { + var panel = ScorePanelList.AddScore(s); + if (detachedPanel != null) + panel.Alpha = 0; + } // allow a frame for scroll container to adjust its dimensions with the added scores before fetching again. Schedule(() => lastFetchCompleted = true); @@ -409,14 +402,6 @@ namespace osu.Game.Screens.Ranking return false; } - private void addScore(ScoreInfo score) - { - var panel = ScorePanelList.AddScore(score); - - if (detachedPanel != null) - panel.Alpha = 0; - } - private ScorePanel? detachedPanel; private void onStatisticsStateChanged(ValueChangedEvent state) diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 9f7604aa82..0593d5f91f 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -4,11 +4,13 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Scoring; @@ -21,26 +23,36 @@ namespace osu.Game.Screens.Ranking [Resolved] private RulesetStore rulesets { get; set; } = null!; + [Resolved] + private IAPIProvider api { get; set; } = null!; + public SoloResultsScreen(ScoreInfo score) : base(score) { } - protected override APIRequest? FetchScores(Action> scoresCallback) + protected override async Task> FetchScores() { Debug.Assert(Score != null); if (Score.BeatmapInfo!.OnlineID <= 0 || Score.BeatmapInfo.Status <= BeatmapOnlineStatus.Pending) - return null; + return []; + + var requestTaskSource = new TaskCompletionSource(); getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset); - getScoreRequest.Success += r => + getScoreRequest.Success += requestTaskSource.SetResult; + getScoreRequest.Failure += requestTaskSource.SetException; + api.Queue(getScoreRequest); + + try { + var scores = await requestTaskSource.Task; var toDisplay = new List(); - for (int i = 0; i < r.Scores.Count; ++i) + for (int i = 0; i < scores.Scores.Count; ++i) { - var score = r.Scores[i]; + var score = scores.Scores[i]; int position = i + 1; if (score.MatchesOnlineID(Score)) @@ -58,9 +70,12 @@ namespace osu.Game.Screens.Ranking } } - scoresCallback.Invoke(toDisplay); - }; - return getScoreRequest; + return toDisplay; + } + catch (OperationCanceledException) + { + return []; + } } protected override void Dispose(bool isDisposing) From dfae11101f8b968611a442691b794066a52538c7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 25 Feb 2025 22:37:12 +0900 Subject: [PATCH 568/801] Populate playlists results screen with online beatmaps --- osu.Game/Online/Rooms/MultiplayerScore.cs | 3 +++ .../Playlists/PlaylistItemResultsScreen.cs | 26 ++++++++++++++++--- .../PlaylistItemScoreResultsScreen.cs | 5 ++-- .../PlaylistItemUserBestResultsScreen.cs | 5 ++-- 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/osu.Game/Online/Rooms/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs index 2adee26da3..74eaea8dbc 100644 --- a/osu.Game/Online/Rooms/MultiplayerScore.cs +++ b/osu.Game/Online/Rooms/MultiplayerScore.cs @@ -80,6 +80,9 @@ namespace osu.Game.Online.Rooms [JsonProperty("ruleset_id")] public int RulesetId { get; set; } + [JsonProperty("beatmap_id")] + public int BeatmapId { get; set; } + public ScoreInfo CreateScoreInfo(ScoreManager scoreManager, RulesetStore rulesets, [NotNull] BeatmapInfo beatmap) { var ruleset = rulesets.GetRuleset(RulesetId); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index ed90b3b1ae..bba30ec312 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -9,8 +9,11 @@ using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Scoring; @@ -39,6 +42,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists [Resolved] protected RulesetStore Rulesets { get; private set; } = null!; + [Resolved] + private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; + protected PlaylistItemResultsScreen(ScoreInfo? score, long roomId, PlaylistItem playlistItem) : base(score) { @@ -119,7 +125,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists setPositions(lowerScores, userScore.Position.Value, 1); } - return TransformScores(allScores); + return await TransformScores(allScores); } catch (OperationCanceledException) { @@ -184,7 +190,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists setPositions(index, pivot, -1); } - return TransformScores(index.Scores, index); + return await TransformScores(index.Scores, index); } catch (OperationCanceledException) { @@ -201,12 +207,24 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// /// The s that were retrieved from s. /// An optional pivot around which the scores were retrieved. - protected virtual ScoreInfo[] TransformScores(List scores, MultiplayerScores? pivot = null) + protected virtual async Task TransformScores(List scores, MultiplayerScores? pivot = null) { + APIBeatmap?[] beatmaps = await beatmapLookupCache.GetBeatmapsAsync(scores.Select(s => s.BeatmapId).Distinct().ToArray()); + + // Minimal data required to get various components in this screen to display correctly. + Dictionary beatmapsById = beatmaps.Where(b => b != null).ToDictionary(b => b!.OnlineID, b => new BeatmapInfo + { + Difficulty = new BeatmapDifficulty(b!.Difficulty), + DifficultyName = b.DifficultyName, + StarRating = b.StarRating, + Length = b.Length, + BPM = b.BPM + }); + // Exclude the score provided to this screen since it's added already. return scores .Where(s => s.ID != Score?.OnlineID) - .Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, Beatmap.Value.BeatmapInfo)) + .Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, beatmapsById.GetValueOrDefault(s.BeatmapId) ?? Beatmap.Value.BeatmapInfo)) .OrderByTotalScore() .ToArray(); } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs index c6c10e4d91..f74b30c3f7 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Scoring; @@ -30,9 +31,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override APIRequest CreateScoreRequest() => new ShowPlaylistScoreRequest(RoomId, PlaylistItem.ID, scoreId); - protected override ScoreInfo[] TransformScores(List scores, MultiplayerScores? pivot = null) + protected override async Task TransformScores(List scores, MultiplayerScores? pivot = null) { - var scoreInfos = base.TransformScores(scores, pivot); + var scoreInfos = await base.TransformScores(scores, pivot); Schedule(() => SelectedScore.Value ??= scoreInfos.SingleOrDefault(s => s.OnlineID == scoreId)); return scoreInfos; } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs index 1a0df0291c..2e763666a7 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Scoring; @@ -24,9 +25,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override APIRequest CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, userId); - protected override ScoreInfo[] TransformScores(List scores, MultiplayerScores? pivot = null) + protected override async Task TransformScores(List scores, MultiplayerScores? pivot = null) { - var scoreInfos = base.TransformScores(scores, pivot); + var scoreInfos = await base.TransformScores(scores, pivot); Schedule(() => { From 8a27b6689edf50cace897a3009640ff1ba8b2e7e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 25 Feb 2025 22:51:36 +0900 Subject: [PATCH 569/801] Replace virtual async method with better abstraction --- .../Playlists/PlaylistItemResultsScreen.cs | 9 ++++----- .../Playlists/PlaylistItemScoreResultsScreen.cs | 8 +++----- .../Playlists/PlaylistItemUserBestResultsScreen.cs | 14 ++++---------- osu.Game/Screens/Ranking/ResultsScreen.cs | 10 ++++++++++ 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index bba30ec312..e9ba3bdb70 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -125,7 +125,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists setPositions(lowerScores, userScore.Position.Value, 1); } - return await TransformScores(allScores); + return await transformScores(allScores); } catch (OperationCanceledException) { @@ -190,7 +190,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists setPositions(index, pivot, -1); } - return await TransformScores(index.Scores, index); + return await transformScores(index.Scores); } catch (OperationCanceledException) { @@ -203,11 +203,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } /// - /// Transforms returned into s, ensure the is put into a sane state, and invokes a given success callback. + /// Transforms returned into s. /// /// The s that were retrieved from s. - /// An optional pivot around which the scores were retrieved. - protected virtual async Task TransformScores(List scores, MultiplayerScores? pivot = null) + private async Task transformScores(List scores) { APIBeatmap?[] beatmaps = await beatmapLookupCache.GetBeatmapsAsync(scores.Select(s => s.BeatmapId).Distinct().ToArray()); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs index f74b30c3f7..7f386cd293 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Scoring; @@ -31,11 +30,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override APIRequest CreateScoreRequest() => new ShowPlaylistScoreRequest(RoomId, PlaylistItem.ID, scoreId); - protected override async Task TransformScores(List scores, MultiplayerScores? pivot = null) + protected override void OnScoresAdded(IEnumerable scores) { - var scoreInfos = await base.TransformScores(scores, pivot); - Schedule(() => SelectedScore.Value ??= scoreInfos.SingleOrDefault(s => s.OnlineID == scoreId)); - return scoreInfos; + base.OnScoresAdded(scores); + SelectedScore.Value ??= scores.SingleOrDefault(s => s.OnlineID == scoreId); } } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs index 2e763666a7..faeef93b71 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Scoring; @@ -25,17 +24,12 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override APIRequest CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, userId); - protected override async Task TransformScores(List scores, MultiplayerScores? pivot = null) + protected override void OnScoresAdded(IEnumerable scores) { - var scoreInfos = await base.TransformScores(scores, pivot); + base.OnScoresAdded(scores); - Schedule(() => - { - // Prefer selecting the local user's score, or otherwise default to the first visible score. - SelectedScore.Value ??= scoreInfos.FirstOrDefault(s => s.UserID == userId) ?? scoreInfos.FirstOrDefault(); - }); - - return scoreInfos; + // Prefer selecting the local user's score, or otherwise default to the first visible score. + SelectedScore.Value ??= scores.FirstOrDefault(s => s.UserID == userId) ?? scores.FirstOrDefault(); } } } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 11e90a06b9..ce86ac0815 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -357,8 +357,18 @@ namespace osu.Game.Screens.Ranking // This can happen if for example a beatmap that is part of a playlist hasn't been played yet. VerticalScrollContent.Add(new MessagePlaceholder(LeaderboardStrings.NoRecordsYet)); } + + OnScoresAdded(scores); }); + /// + /// Invoked after online scores are fetched and added to the list. + /// + /// The scores that were added. + protected virtual void OnScoresAdded(IEnumerable scores) + { + } + public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); From 3b5bf391da57e4ed3efcfd60f6e6fd3724f35b6d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 25 Feb 2025 22:55:55 +0900 Subject: [PATCH 570/801] Arrays instead of enumerables --- .../Visual/Ranking/TestSceneResultsScreen.cs | 21 +++++++++---------- .../Spectate/MultiSpectatorResultsScreen.cs | 5 ++--- .../Playlists/PlaylistItemResultsScreen.cs | 6 +++--- .../PlaylistItemScoreResultsScreen.cs | 3 +-- .../PlaylistItemUserBestResultsScreen.cs | 3 +-- osu.Game/Screens/Ranking/ResultsScreen.cs | 10 ++++----- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 4 ++-- 7 files changed, 24 insertions(+), 28 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 4acbdb4a76..b19288fd99 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -4,7 +4,6 @@ #nullable disable using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using NUnit.Framework; @@ -415,19 +414,19 @@ namespace osu.Game.Tests.Visual.Ranking RetryOverlay = InternalChildren.OfType().SingleOrDefault(); } - protected override Task> FetchScores() + protected override Task FetchScores() { - var scores = new List(); + var scores = new ScoreInfo[20]; - for (int i = 0; i < 20; i++) + for (int i = 0; i < scores.Length; i++) { var score = TestResources.CreateTestScoreInfo(); score.TotalScore += 10 - i; score.HasOnlineReplay = true; - scores.Add(score); + scores[i] = score; } - return Task.FromResult>(scores); + return Task.FromResult(scores); } } @@ -443,19 +442,19 @@ namespace osu.Game.Tests.Visual.Ranking this.fetchWaitTask = fetchWaitTask ?? Task.CompletedTask; } - protected override Task> FetchScores() + protected override Task FetchScores() { - return Task.Run>(async () => + return Task.Run(async () => { await fetchWaitTask; - var scores = new List(); + var scores = new ScoreInfo[20]; - for (int i = 0; i < 20; i++) + for (int i = 0; i < scores.Length; i++) { var score = TestResources.CreateTestScoreInfo(); score.TotalScore += 10 - i; - scores.Add(score); + scores[i] = score; } Schedule(() => FetchCompleted = true); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs index 6e2f90e3b5..3cf1661c8d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using System.Threading.Tasks; using osu.Game.Scoring; using osu.Game.Screens.Play; @@ -22,8 +21,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate Scheduler.AddDelayed(() => StatisticsPanel.ToggleVisibility(), 1000); } - protected override Task> FetchScores() => Task.FromResult>([]); + protected override Task FetchScores() => Task.FromResult([]); - protected override Task> FetchNextPage(int direction) => Task.FromResult>([]); + protected override Task FetchNextPage(int direction) => Task.FromResult([]); } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index e9ba3bdb70..0063bcd5f5 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -83,7 +83,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected abstract APIRequest CreateScoreRequest(); - protected override async Task> FetchScores() + protected override async Task FetchScores() { // This performs two requests: // 1. A request to show the relevant score (and scores around). @@ -141,7 +141,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } } - protected override async Task> FetchNextPage(int direction) + protected override async Task FetchNextPage(int direction) { Debug.Assert(direction == 1 || direction == -1); @@ -165,7 +165,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// /// Does not queue the request. /// An optional score pivot to retrieve scores around. Can be null to retrieve scores from the highest score. - private async Task> fetchScoresAround(MultiplayerScores? pivot = null) + private async Task fetchScoresAround(MultiplayerScores? pivot = null) { var requestTaskSource = new TaskCompletionSource(); var indexReq = pivot != null diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs index 7f386cd293..74b12b6d3c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using System.Linq; using osu.Game.Online.API; using osu.Game.Online.Rooms; @@ -30,7 +29,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override APIRequest CreateScoreRequest() => new ShowPlaylistScoreRequest(RoomId, PlaylistItem.ID, scoreId); - protected override void OnScoresAdded(IEnumerable scores) + protected override void OnScoresAdded(ScoreInfo[] scores) { base.OnScoresAdded(scores); SelectedScore.Value ??= scores.SingleOrDefault(s => s.OnlineID == scoreId); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs index faeef93b71..866b094178 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using System.Linq; using osu.Game.Online.API; using osu.Game.Online.Rooms; @@ -24,7 +23,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override APIRequest CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, userId); - protected override void OnScoresAdded(IEnumerable scores) + protected override void OnScoresAdded(ScoreInfo[] scores) { base.OnScoresAdded(scores); diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index ce86ac0815..cfee2aa77d 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -246,7 +246,7 @@ namespace osu.Game.Screens.Ranking if (lastFetchCompleted) { - Task> nextPageTask = Task.FromResult>([]); + Task nextPageTask = Task.FromResult([]); if (ScorePanelList.IsScrolledToStart) nextPageTask = FetchNextPage(-1); @@ -322,13 +322,13 @@ namespace osu.Game.Screens.Ranking /// /// Performs a fetch/refresh of scores to be displayed. /// - protected virtual Task> FetchScores() => Task.FromResult>([]); + protected virtual Task FetchScores() => Task.FromResult([]); /// /// Performs a fetch of the next page of scores. This is invoked every frame. /// /// The fetch direction. -1 to fetch scores greater than the current start of the list, and 1 to fetch scores lower than the current end of the list. - protected virtual Task> FetchNextPage(int direction) => Task.FromResult>([]); + protected virtual Task FetchNextPage(int direction) => Task.FromResult([]); /// /// Creates the to be used to display extended information about scores. @@ -340,7 +340,7 @@ namespace osu.Game.Screens.Ranking : new StatisticsPanel(); } - private void addScores(IEnumerable scores) => Schedule(() => + private void addScores(ScoreInfo[] scores) => Schedule(() => { foreach (var s in scores) { @@ -365,7 +365,7 @@ namespace osu.Game.Screens.Ranking /// Invoked after online scores are fetched and added to the list. /// /// The scores that were added. - protected virtual void OnScoresAdded(IEnumerable scores) + protected virtual void OnScoresAdded(ScoreInfo[] scores) { } diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 0593d5f91f..9fdffce644 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -31,7 +31,7 @@ namespace osu.Game.Screens.Ranking { } - protected override async Task> FetchScores() + protected override async Task FetchScores() { Debug.Assert(Score != null); @@ -70,7 +70,7 @@ namespace osu.Game.Screens.Ranking } } - return toDisplay; + return toDisplay.ToArray(); } catch (OperationCanceledException) { From 116b5a335a658023e3b58d3ec5caedd78230a3d4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 25 Feb 2025 22:56:38 +0900 Subject: [PATCH 571/801] `ConfigureAwait(false)` everywhere --- .../Playlists/PlaylistItemResultsScreen.cs | 14 +++++++------- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index 0063bcd5f5..975cff0b68 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -97,7 +97,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists try { - var userScore = await requestTaskSource.Task; + var userScore = await requestTaskSource.Task.ConfigureAwait(false); var allScores = new List { userScore }; // Other scores could have arrived between score submission and entering the results screen. Ensure the local player score position is up to date. @@ -125,7 +125,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists setPositions(lowerScores, userScore.Position.Value, 1); } - return await transformScores(allScores); + return await transformScores(allScores).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -133,7 +133,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } catch { - return await fetchScoresAround(); + return await fetchScoresAround().ConfigureAwait(false); } finally { @@ -157,7 +157,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists RightSpinner.Show(); }); - return await fetchScoresAround(pivot); + return await fetchScoresAround(pivot).ConfigureAwait(false); } /// @@ -177,7 +177,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists try { - var index = await requestTaskSource.Task; + var index = await requestTaskSource.Task.ConfigureAwait(false); if (pivot == lowerScores) { @@ -190,7 +190,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists setPositions(index, pivot, -1); } - return await transformScores(index.Scores); + return await transformScores(index.Scores).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -208,7 +208,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// The s that were retrieved from s. private async Task transformScores(List scores) { - APIBeatmap?[] beatmaps = await beatmapLookupCache.GetBeatmapsAsync(scores.Select(s => s.BeatmapId).Distinct().ToArray()); + APIBeatmap?[] beatmaps = await beatmapLookupCache.GetBeatmapsAsync(scores.Select(s => s.BeatmapId).Distinct().ToArray()).ConfigureAwait(false); // Minimal data required to get various components in this screen to display correctly. Dictionary beatmapsById = beatmaps.Where(b => b != null).ToDictionary(b => b!.OnlineID, b => new BeatmapInfo diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 9fdffce644..73bed3383b 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -47,7 +47,7 @@ namespace osu.Game.Screens.Ranking try { - var scores = await requestTaskSource.Task; + var scores = await requestTaskSource.Task.ConfigureAwait(false); var toDisplay = new List(); for (int i = 0; i < scores.Scores.Count; ++i) From bb457ca8e2fa2283d159fd214f6854046f38cebb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 25 Feb 2025 23:17:02 +0900 Subject: [PATCH 572/801] Clean up completion handling --- osu.Game/Screens/Ranking/ResultsScreen.cs | 51 +++++++++++++---------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index cfee2aa77d..397ad9c0b1 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -10,7 +10,6 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -66,7 +65,7 @@ namespace osu.Game.Screens.Ranking private Drawable bottomPanel = null!; private Container detachedPanelContainer = null!; - private bool lastFetchCompleted; + private Task lastFetchTask = Task.CompletedTask; /// /// Whether the user can retry the beatmap from the results screen. @@ -235,7 +234,7 @@ namespace osu.Game.Screens.Ranking { base.LoadComplete(); - FetchScores().ContinueWith(t => addScores(t.GetResultSafely())); + lastFetchTask = Task.Run(async () => await addScores(await FetchScores().ConfigureAwait(false)).ConfigureAwait(false)); StatisticsPanel.State.BindValueChanged(onStatisticsStateChanged, true); } @@ -244,18 +243,17 @@ namespace osu.Game.Screens.Ranking { base.Update(); - if (lastFetchCompleted) + if (lastFetchTask.IsCompleted) { - Task nextPageTask = Task.FromResult([]); + Task? nextPageTask = null; if (ScorePanelList.IsScrolledToStart) nextPageTask = FetchNextPage(-1); else if (ScorePanelList.IsScrolledToEnd) nextPageTask = FetchNextPage(1); - nextPageTask.ContinueWith(t => addScores(t.GetResultSafely()), TaskContinuationOptions.OnlyOnRanToCompletion); - - lastFetchCompleted = nextPageTask.IsCompletedSuccessfully; + if (nextPageTask != null) + lastFetchTask = Task.Run(async () => await addScores(await nextPageTask).ConfigureAwait(false)); } } @@ -340,26 +338,33 @@ namespace osu.Game.Screens.Ranking : new StatisticsPanel(); } - private void addScores(ScoreInfo[] scores) => Schedule(() => + private Task addScores(ScoreInfo[] scores) { - foreach (var s in scores) + var tcs = new TaskCompletionSource(); + + Schedule(() => { - var panel = ScorePanelList.AddScore(s); - if (detachedPanel != null) - panel.Alpha = 0; - } + foreach (var s in scores) + { + var panel = ScorePanelList.AddScore(s); + if (detachedPanel != null) + panel.Alpha = 0; + } - // allow a frame for scroll container to adjust its dimensions with the added scores before fetching again. - Schedule(() => lastFetchCompleted = true); + // allow a frame for scroll container to adjust its dimensions with the added scores before fetching again. + Schedule(() => tcs.SetResult()); - if (ScorePanelList.IsEmpty) - { - // This can happen if for example a beatmap that is part of a playlist hasn't been played yet. - VerticalScrollContent.Add(new MessagePlaceholder(LeaderboardStrings.NoRecordsYet)); - } + if (ScorePanelList.IsEmpty) + { + // This can happen if for example a beatmap that is part of a playlist hasn't been played yet. + VerticalScrollContent.Add(new MessagePlaceholder(LeaderboardStrings.NoRecordsYet)); + } - OnScoresAdded(scores); - }); + OnScoresAdded(scores); + }); + + return tcs.Task; + } /// /// Invoked after online scores are fetched and added to the list. From baf20d84843071e7c1c26418da36d1f4ff5c5a21 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 25 Feb 2025 23:17:23 +0900 Subject: [PATCH 573/801] Fix loading spinners not hiding correctly --- .../Playlists/PlaylistItemResultsScreen.cs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index 975cff0b68..f08b1818ab 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -135,10 +135,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { return await fetchScoresAround().ConfigureAwait(false); } - finally - { - Schedule(() => hideLoadingSpinners()); - } } protected override async Task FetchNextPage(int direction) @@ -196,10 +192,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { return []; } - finally - { - Schedule(() => hideLoadingSpinners(pivot)); - } } /// @@ -228,14 +220,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists .ToArray(); } - private void hideLoadingSpinners(MultiplayerScores? pivot = null) + protected override void OnScoresAdded(ScoreInfo[] scores) { - CentreSpinner.Hide(); + base.OnScoresAdded(scores); - if (pivot == lowerScores) - RightSpinner.Hide(); - else if (pivot == higherScores) - LeftSpinner.Hide(); + CentreSpinner.Hide(); + RightSpinner.Hide(); + LeftSpinner.Hide(); } /// From 65a62d5440b57440b61578981691fd7bb6f2fb70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Feb 2025 15:38:48 +0100 Subject: [PATCH 574/801] Attempt to preserve sample control point bank when encoding beatmap This was reported internally in https://discord.com/channels/90072389919997952/1259818301517725707/1343470899357024286. The issue described was that sample specifications on control points in stable disappeared after the beatmap was updated from lazer. The reason why the sample specifications were getting dropped is that they got lost in the logic that attempts to translate per-hitobject samples that lazer has back into stable "green line" type control points. That process only attempted to preserve volume and custom sample bank, but did not keep the standard bank - likely because it's kind of superfluous information *for correct sample playback of the objects*, as the samples get encoded again for each object individually. However dropping this information makes for a subpar editing experience. The choice of which sample to pick the bank from is sort of arbitrary and I'm not sure if there's a correct one to pick. Intuitively picking the normal sample's bank (if there is one) seems most correct. --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 07e88ab956..d80d7e6b09 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -319,11 +319,13 @@ namespace osu.Game.Beatmaps.Formats SampleControlPoint createSampleControlPointFor(double time, IList samples) { int volume = samples.Max(o => o.Volume); + string bank = samples.Where(s => s.Name == HitSampleInfo.HIT_NORMAL).Select(s => s.Bank).FirstOrDefault() + ?? samples.Select(s => s.Bank).First(); int customIndex = samples.Any(o => o is ConvertHitObjectParser.LegacyHitSampleInfo) ? samples.OfType().Max(o => o.CustomSampleBank) : -1; - return new LegacyBeatmapDecoder.LegacySampleControlPoint { Time = time, SampleVolume = volume, CustomSampleBank = customIndex }; + return new LegacyBeatmapDecoder.LegacySampleControlPoint { Time = time, SampleVolume = volume, SampleBank = bank, CustomSampleBank = customIndex }; } } From 90290997a7b754a2506a4c10a8cc28cb3a0e33bd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Feb 2025 14:46:37 +0900 Subject: [PATCH 575/801] Fix score panel difficulty depending on local beatmap This is a very special case where online beatmap/ruleset models are being ferried via `ScoreInfo` in what appear to `BeatmapDifficultyCache` as local `BeatmapInfo`/`RulesetInfo` models. Here, BDC will incorrectly attempt to proceed with calculating true difficulty where it cannot, and return 0. This is fixed locally because `ScoreInfo` is a very weird model, and I'm not sure whether BDC should contain logic to work around this. --- .../Expanded/ExpandedPanelMiddleContent.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 4bc559694a..9bef6a3f3a 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -16,6 +14,7 @@ using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Configuration; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -41,10 +40,10 @@ namespace osu.Game.Screens.Ranking.Expanded private readonly List statisticDisplays = new List(); - private RollingCounter scoreCounter; + private RollingCounter scoreCounter = null!; [Resolved] - private ScoreManager scoreManager { get; set; } + private ScoreManager scoreManager { get; set; } = null!; /// /// Creates a new . @@ -63,12 +62,19 @@ namespace osu.Game.Screens.Ranking.Expanded } [BackgroundDependencyLoader] - private void load(BeatmapDifficultyCache beatmapDifficultyCache) + private void load(RealmAccess realmAccess, BeatmapDifficultyCache beatmapDifficultyCache) { var beatmap = score.BeatmapInfo!; var metadata = beatmap.BeatmapSet?.Metadata ?? beatmap.Metadata; string creator = metadata.Author.Username; + StarDifficulty starDifficulty = new StarDifficulty(beatmap.StarRating, 0); + + // In some cases, the beatmap ferried through ScoreInfo actually represents an online beatmap. + // If it isn't, we may be able to compute a more accuracy difficulty from the ruleset and mods. + if (realmAccess.Run(r => r.Find(score.BeatmapInfo!.ID)) != null) + starDifficulty = beatmapDifficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods).GetResultSafely() ?? starDifficulty; + var topStatistics = new List { new AccuracyStatistic(score.Accuracy), @@ -146,7 +152,7 @@ namespace osu.Game.Screens.Ranking.Expanded Spacing = new Vector2(5, 0), Children = new Drawable[] { - new StarRatingDisplay(beatmapDifficultyCache.GetDifficultyAsync(beatmap, score.Ruleset, score.Mods).GetResultSafely() ?? default) + new StarRatingDisplay(starDifficulty) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft From 59cfcb3595aa79ea4384bca9af4472b48ace3917 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Feb 2025 14:49:38 +0900 Subject: [PATCH 576/801] Prefer local models where available --- .../Playlists/PlaylistItemResultsScreen.cs | 52 +++++++++++++++---- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index f08b1818ab..1be0a7cf81 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -45,6 +45,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists [Resolved] private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; + [Resolved] + private BeatmapManager beatmapManager { get; set; } = null!; + protected PlaylistItemResultsScreen(ScoreInfo? score, long roomId, PlaylistItem playlistItem) : base(score) { @@ -200,22 +203,43 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// The s that were retrieved from s. private async Task transformScores(List scores) { - APIBeatmap?[] beatmaps = await beatmapLookupCache.GetBeatmapsAsync(scores.Select(s => s.BeatmapId).Distinct().ToArray()).ConfigureAwait(false); + int[] allBeatmapIds = scores.Select(s => s.BeatmapId).Distinct().ToArray(); + BeatmapInfo[] localBeatmaps = allBeatmapIds.Select(id => beatmapManager.QueryBeatmap(b => b.OnlineID == id)) + .Where(b => b != null) + .ToArray()!; - // Minimal data required to get various components in this screen to display correctly. - Dictionary beatmapsById = beatmaps.Where(b => b != null).ToDictionary(b => b!.OnlineID, b => new BeatmapInfo + int[] missingBeatmapIds = allBeatmapIds.Except(localBeatmaps.Select(b => b.OnlineID)).ToArray(); + APIBeatmap[] onlineBeatmaps = (await beatmapLookupCache.GetBeatmapsAsync(missingBeatmapIds).ConfigureAwait(false)).Where(b => b != null).ToArray()!; + + Dictionary beatmapsById = new Dictionary(); + + foreach (var beatmap in localBeatmaps) + beatmapsById[beatmap.OnlineID] = beatmap; + + foreach (var beatmap in onlineBeatmaps) { - Difficulty = new BeatmapDifficulty(b!.Difficulty), - DifficultyName = b.DifficultyName, - StarRating = b.StarRating, - Length = b.Length, - BPM = b.BPM - }); + // Minimal data required to get various components in this screen to display correctly. + beatmapsById[beatmap.OnlineID] = new BeatmapInfo + { + Difficulty = new BeatmapDifficulty(beatmap.Difficulty), + DifficultyName = beatmap.DifficultyName, + StarRating = beatmap.StarRating, + Length = beatmap.Length, + BPM = beatmap.BPM + }; + } + + // Validate that we have all beatmaps we need. + foreach (int id in allBeatmapIds) + { + if (!beatmapsById.ContainsKey(id)) + throw new MissingBeatmapException(PlaylistItem, id); + } // Exclude the score provided to this screen since it's added already. return scores .Where(s => s.ID != Score?.OnlineID) - .Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, beatmapsById.GetValueOrDefault(s.BeatmapId) ?? Beatmap.Value.BeatmapInfo)) + .Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, beatmapsById[s.BeatmapId])) .OrderByTotalScore() .ToArray(); } @@ -280,5 +304,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists X = (float)(list.ScrollableExtent - list.Current - panelOffset); } } + + private class MissingBeatmapException : Exception + { + public MissingBeatmapException(PlaylistItem item, int beatmapId) + : base($"Missing beatmap {beatmapId} for playlist item {item.ID}") + { + } + } } } From b7d431fdde61b56f6f1831c366163da54c71d021 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Feb 2025 15:04:43 +0900 Subject: [PATCH 577/801] Include author --- .../OnlinePlay/Playlists/PlaylistItemResultsScreen.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index 1be0a7cf81..53cd81b2a1 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.UserInterface; +using osu.Game.Models; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; @@ -222,6 +223,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists beatmapsById[beatmap.OnlineID] = new BeatmapInfo { Difficulty = new BeatmapDifficulty(beatmap.Difficulty), + Metadata = + { + Author = new RealmUser + { + Username = beatmap.Metadata.Author.Username, + OnlineID = beatmap.Metadata.Author.OnlineID, + } + }, DifficultyName = beatmap.DifficultyName, StarRating = beatmap.StarRating, Length = beatmap.Length, From abc12abdedfbb315996d5c16e5556cc9837d1e17 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Feb 2025 16:48:18 +0900 Subject: [PATCH 578/801] Fix `PlayerTeamFlag` skinnable component not showing team details during replay For now, let's fetch on demand. Note that song select local leaderboard has the same issue. I feel we should be doing a lot more cached lookups (probaly with persisting across game restarts). Maybe even replacing the realm user storage. An issue for another day. --- osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs b/osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs index f8ef03c58c..3f72099a45 100644 --- a/osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs +++ b/osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs @@ -3,8 +3,10 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Skinning; @@ -40,10 +42,19 @@ namespace osu.Game.Screens.Play.HUD } [BackgroundDependencyLoader] - private void load() + private void load(UserLookupCache userLookupCache) { if (gameplayState != null) - flag.Team = gameplayState.Score.ScoreInfo.User.Team; + { + if (gameplayState.Score.ScoreInfo.User.Team != null) + flag.Team = gameplayState.Score.ScoreInfo.User.Team; + else + { + // We only store very basic information about a user to realm, so there's a high chance we don't have the team information. + userLookupCache.GetUserAsync(gameplayState.Score.ScoreInfo.User.Id) + .ContinueWith(task => Schedule(() => flag.Team = task.GetResultSafely()?.Team)); + } + } else { apiUser = api.LocalUser.GetBoundCopy(); From e8b7ec0f9537db864b712ebc28ba63afabe3eeb3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Feb 2025 17:01:48 +0900 Subject: [PATCH 579/801] Adjust leaderboard score design slightly This design is about to get replaced, so I'm just making some minor adjustments since a lot of people complained about the font size in the last update. Of note, I'm only changing the font size which is one pt size lower than we'd usually use. Also overlapping the mod icons to create a bit more space (since there's already cases where they don't fit). Closes https://github.com/ppy/osu/issues/32055 as far as I'm concerned. I can read everything fine at 0.8x UI scale. --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index fc30f158f0..7306c2d21e 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -271,6 +271,7 @@ namespace osu.Game.Online.Leaderboards Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, AutoSizeAxes = Axes.Both, + Spacing = new Vector2(-10, 0), Direction = FillDirection.Horizontal, ChildrenEnumerable = Score.Mods.AsOrdered().Select(mod => new ModIcon(mod) { Scale = new Vector2(0.34f) }) }, @@ -425,7 +426,7 @@ namespace osu.Game.Online.Leaderboards public DateLabel(DateTimeOffset date) : base(date) { - Font = OsuFont.GetFont(size: 13, weight: FontWeight.Bold, italics: true); + Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold); } protected override string Format() => Date.ToShortRelativeTime(TimeSpan.FromSeconds(30)); From c7fd7cf9cd4071123ea83fb479cb8e543cdb1a0c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Feb 2025 17:39:56 +0900 Subject: [PATCH 580/801] Add missing ConfigureAwait --- osu.Game/Screens/Ranking/ResultsScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 397ad9c0b1..26b13d026c 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -253,7 +253,7 @@ namespace osu.Game.Screens.Ranking nextPageTask = FetchNextPage(1); if (nextPageTask != null) - lastFetchTask = Task.Run(async () => await addScores(await nextPageTask).ConfigureAwait(false)); + lastFetchTask = Task.Run(async () => await addScores(await nextPageTask.ConfigureAwait(false)).ConfigureAwait(false)); } } From 2738c1a8077461b65c892ad0725ca928b1b220c6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Dec 2024 16:17:36 +0900 Subject: [PATCH 581/801] Add back right-click-for-new-combo and right-click-delete when in compose mode Requested too many times to count. I'm not sure what to do about the code quality of this. It's a bit weird that there's no way to check the current composition tool from a higher level. Also it was discussed IRL that there should be some kind of hinting that existing notes will be deleted when they are hovered, but I'm not sure how well this will work in normal mapping flows, since it will display even in cases that users aren't intending to delete an object. Still willing to explore this direction though (it's just non-trivial to implement so I haven't yet). --- .../Edit/OsuHitObjectComposer.cs | 14 +++++++++ .../Components/ComposeBlueprintContainer.cs | 2 ++ .../Components/EditorSelectionHandler.cs | 29 +++++++++++++++++-- .../Compose/Components/SelectionHandler.cs | 4 ++- 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index b3e23daa99..ee386aa366 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -30,6 +30,7 @@ using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit.Components.TernaryButtons; using osu.Game.Screens.Edit.Compose.Components; using osuTK; +using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit { @@ -351,6 +352,19 @@ namespace osu.Game.Rulesets.Osu.Edit } } + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button == MouseButton.Right) + { + var osuSelectionHandler = (OsuSelectionHandler)BlueprintContainer.SelectionHandler; + + osuSelectionHandler.SelectionNewComboState.Value = + osuSelectionHandler.SelectionNewComboState.Value == TernaryState.False ? TernaryState.True : TernaryState.False; + } + + return base.OnMouseDown(e); + } + protected override bool OnKeyDown(KeyDownEvent e) { if (e.Repeat) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 4c57eee971..4414e963bf 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -387,6 +387,8 @@ namespace osu.Game.Screens.Edit.Compose.Components currentTool = value; + SelectionHandler.RightClickAlwaysQuickDeletes = currentTool is not SelectTool; + // As per stable editor, when changing tools, we should forcefully commit any pending placement. CommitIfPlacementActive(); } diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index f9e7ef6df8..e90936e38a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -10,16 +10,23 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Game.Audio; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; +using osuTK.Input; namespace osu.Game.Screens.Edit.Compose.Components { public partial class EditorSelectionHandler : SelectionHandler { + /// + /// Whether right click should delete even when shift is not held. + /// + public bool RightClickAlwaysQuickDeletes { get; set; } + /// /// A special bank name that is only used in the editor UI. /// When selected and in placement mode, the bank of the last hit object will always be used. @@ -40,6 +47,14 @@ namespace osu.Game.Screens.Edit.Compose.Components SelectedItems.CollectionChanged += onSelectedItemsChanged; } + protected override bool ShouldQuickDelete(MouseButtonEvent e) + { + if (RightClickAlwaysQuickDeletes && e.Button == MouseButton.Right) + return true; + + return base.ShouldQuickDelete(e); + } + protected override void DeleteItems(IEnumerable items) => EditorBeatmap.RemoveRange(items); #region Selection State @@ -293,7 +308,8 @@ namespace osu.Game.Screens.Edit.Compose.Components foreach ((string bankName, var bindable) in SelectionAdditionBankStates) { - bindable.Value = GetStateFromSelection(samplesInSelection.SelectMany(s => s).Where(o => o.Name != HitSampleInfo.HIT_NORMAL), h => (bankName != HIT_BANK_AUTO && h.Bank == bankName && !h.EditorAutoBank) || (bankName == HIT_BANK_AUTO && h.EditorAutoBank)); + bindable.Value = GetStateFromSelection(samplesInSelection.SelectMany(s => s).Where(o => o.Name != HitSampleInfo.HIT_NORMAL), + h => (bankName != HIT_BANK_AUTO && h.Bank == bankName && !h.EditorAutoBank) || (bankName == HIT_BANK_AUTO && h.EditorAutoBank)); } } @@ -378,14 +394,21 @@ namespace osu.Game.Screens.Edit.Compose.Components return; string normalBank = h.Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank ?? HitSampleInfo.BANK_SOFT; - h.Samples = h.Samples.Select(s => s.Name != HitSampleInfo.HIT_NORMAL ? bankName == HIT_BANK_AUTO ? s.With(newBank: normalBank, newEditorAutoBank: true) : s.With(newBank: bankName, newEditorAutoBank: false) : s).ToList(); + h.Samples = h.Samples.Select(s => + s.Name != HitSampleInfo.HIT_NORMAL + ? bankName == HIT_BANK_AUTO ? s.With(newBank: normalBank, newEditorAutoBank: true) : s.With(newBank: bankName, newEditorAutoBank: false) + : s) + .ToList(); if (h is IHasRepeats hasRepeats) { for (int i = 0; i < hasRepeats.NodeSamples.Count; ++i) { normalBank = hasRepeats.NodeSamples[i].FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank ?? HitSampleInfo.BANK_SOFT; - hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(s => s.Name != HitSampleInfo.HIT_NORMAL ? bankName == HIT_BANK_AUTO ? s.With(newBank: normalBank, newEditorAutoBank: true) : s.With(newBank: bankName, newEditorAutoBank: false) : s).ToList(); + hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(s => + s.Name != HitSampleInfo.HIT_NORMAL + ? bankName == HIT_BANK_AUTO ? s.With(newBank: normalBank, newEditorAutoBank: true) : s.With(newBank: bankName, newEditorAutoBank: false) + : s).ToList(); } } }); diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 39fff169b7..c1cb8149e7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -263,6 +263,8 @@ namespace osu.Game.Screens.Edit.Compose.Components selectedBlueprints.Remove(blueprint); } + protected virtual bool ShouldQuickDelete(MouseButtonEvent e) => e.Button == MouseButton.Middle || (e.ShiftPressed && e.Button == MouseButton.Right); + /// /// Handle a blueprint requesting selection. /// @@ -271,7 +273,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether a selection was performed. internal virtual bool MouseDownSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e) { - if (e.Button == MouseButton.Middle || (e.ShiftPressed && e.Button == MouseButton.Right)) + if (ShouldQuickDelete(e)) { handleQuickDeletion(blueprint); return true; From 896caf4a8df9fb00cb48be52725ef6448f8dc01a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Feb 2025 17:52:43 +0900 Subject: [PATCH 582/801] Update test coverage --- .../Editor/TestSceneEditorPlacement.cs | 4 +- .../Editing/TestScenePlacementBlueprint.cs | 45 ++++++++++++++++--- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditorPlacement.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditorPlacement.cs index c523652ae1..0199e98af0 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditorPlacement.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditorPlacement.cs @@ -3,9 +3,7 @@ using System.Linq; using NUnit.Framework; -using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; -using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Tests.Visual; @@ -31,7 +29,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Editor AddStep("hover over first hit", () => InputManager.MoveMouseTo(Editor.ChildrenOfType().ElementAt(1))); AddStep("hover over second hit", () => InputManager.MoveMouseTo(Editor.ChildrenOfType().ElementAt(0))); AddStep("right click", () => InputManager.Click(MouseButton.Right)); - AddUntilStep("context menu open", () => Editor.ChildrenOfType().Any(menu => menu.State == MenuState.Open)); + AddUntilStep("second hit deleted", () => Editor.ChildrenOfType().Count(), () => Is.EqualTo(1)); } } } diff --git a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs index 4953cf83c9..37caccfa0d 100644 --- a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs @@ -7,6 +7,7 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Audio; using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Rulesets; using osu.Game.Rulesets.Edit; @@ -14,8 +15,10 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Edit.Components.TernaryButtons; using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Tests.Beatmaps; +using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.Editing @@ -58,17 +61,47 @@ namespace osu.Game.Tests.Visual.Editing } [Test] - public void TestContextMenu() + public void TestRightClickDuringEmptyPlacementTogglesNewCombo() { AddStep("select circle placement tool", () => InputManager.Key(Key.Number2)); AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); AddStep("place circle", () => InputManager.Click(MouseButton.Left)); - AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items); - AddStep("delete with right mouse", () => - { - InputManager.Click(MouseButton.Right); - }); + + AddStep("move mouse away from placed circle", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single().ScreenSpaceDrawQuad.TopLeft + Vector2.One)); + + AddAssert("new combo false", () => this.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(TernaryState.False)); + AddStep("click right mouse", () => InputManager.Click(MouseButton.Right)); + AddAssert("new combo true", () => this.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(TernaryState.True)); + AddStep("click right mouse", () => InputManager.Click(MouseButton.Right)); + AddAssert("new combo false", () => this.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(TernaryState.False)); + } + + [Test] + public void TestRightClickDuringPlacementDeletes() + { + AddStep("select circle placement tool", () => InputManager.Key(Key.Number2)); + AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + AddStep("place circle", () => InputManager.Click(MouseButton.Left)); + AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items); + + AddStep("click right mouse", () => InputManager.Click(MouseButton.Right)); + + AddAssert("circle removed", () => EditorBeatmap.HitObjects, () => Has.Exactly(0).Items); + AddAssert("circle not selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Exactly(0).Items); + } + + [Test] + public void TestRightClickDuringSelectionShowsContextMenu() + { + AddStep("select circle placement tool", () => InputManager.Key(Key.Number2)); + AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + AddStep("place circle", () => InputManager.Click(MouseButton.Left)); + AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items); + + AddStep("select selection tool", () => InputManager.Key(Key.Number1)); + AddStep("click right mouse", () => InputManager.Click(MouseButton.Right)); + AddAssert("circle not removed", () => EditorBeatmap.HitObjects, () => Has.One.Items); AddAssert("circle selected", () => EditorBeatmap.SelectedHitObjects, () => Has.One.Items); } From c45a403fe2b87db7b43d3500fe25e348b88e27ed Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Feb 2025 18:00:18 +0900 Subject: [PATCH 583/801] Mostly revert sizes --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 7306c2d21e..0db03efb68 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -395,7 +395,7 @@ namespace osu.Game.Online.Leaderboards Origin = Anchor.CentreLeft, Text = statistic.Value, Spacing = new Vector2(-1, 0), - Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, fixedWidth: true) + Font = OsuFont.GetFont(size: 16, weight: FontWeight.Bold, fixedWidth: true) }, }, }; @@ -426,7 +426,7 @@ namespace osu.Game.Online.Leaderboards public DateLabel(DateTimeOffset date) : base(date) { - Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold); + Font = OsuFont.GetFont(size: 16, weight: FontWeight.Bold); } protected override string Format() => Date.ToShortRelativeTime(TimeSpan.FromSeconds(30)); From 3dde024650cc1564369dc0f23b462f876871400a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Feb 2025 18:00:16 +0900 Subject: [PATCH 584/801] Replace error handling with logs - Handling all errors matches master a little bit better. Logging exceptions in any case. - Not throwing when beatmaps are missing simplifies tests. --- .../Playlists/PlaylistItemResultsScreen.cs | 21 +++++++------------ osu.Game/Screens/Ranking/SoloResultsScreen.cs | 4 +++- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index 53cd81b2a1..572bf535f7 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.UserInterface; @@ -131,10 +132,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return await transformScores(allScores).ConfigureAwait(false); } - catch (OperationCanceledException) - { - return []; - } catch { return await fetchScoresAround().ConfigureAwait(false); @@ -192,8 +189,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return await transformScores(index.Scores).ConfigureAwait(false); } - catch (OperationCanceledException) + catch (Exception ex) { + Logger.Log($"Failed to fetch scores (room: {RoomId}, item: {PlaylistItem.ID}): {ex}"); return []; } } @@ -242,7 +240,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists foreach (int id in allBeatmapIds) { if (!beatmapsById.ContainsKey(id)) - throw new MissingBeatmapException(PlaylistItem, id); + { + Logger.Log($"Failed to fetch beatmap {id} to display scores for playlist item {PlaylistItem.ID}"); + beatmapsById[id] = Beatmap.Value.BeatmapInfo; + } } // Exclude the score provided to this screen since it's added already. @@ -313,13 +314,5 @@ namespace osu.Game.Screens.OnlinePlay.Playlists X = (float)(list.ScrollableExtent - list.Current - panelOffset); } } - - private class MissingBeatmapException : Exception - { - public MissingBeatmapException(PlaylistItem item, int beatmapId) - : base($"Missing beatmap {beatmapId} for playlist item {item.ID}") - { - } - } } } diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 73bed3383b..3486d81e8a 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Extensions; using osu.Game.Online.API; @@ -72,8 +73,9 @@ namespace osu.Game.Screens.Ranking return toDisplay.ToArray(); } - catch (OperationCanceledException) + catch (Exception ex) { + Logger.Log($"Failed to fetch scores (beatmap: {Score.BeatmapInfo}, ruleset: {Score.Ruleset}): {ex}"); return []; } } From c280c8fa1c463c280aee473b6c987d46a271dd25 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Feb 2025 18:31:06 +0900 Subject: [PATCH 585/801] Add support to tests Somewhat informal because it isn't super easy to handle. --- .../TestScenePlaylistsResultsScreen.cs | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 33bd573617..dc5fb20e16 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -7,9 +7,12 @@ using System.Linq; using System.Net; using Newtonsoft.Json.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Database; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; @@ -32,6 +35,9 @@ namespace osu.Game.Tests.Visual.Playlists private const int scores_per_result = 10; private const int real_user_position = 200; + [Cached] + private readonly BeatmapLookupCache beatmapLookupCache = new BeatmapLookupCache(); + private ResultsScreen resultsScreen = null!; private int lowestScoreId; // Score ID of the lowest score in the list. @@ -41,6 +47,11 @@ namespace osu.Game.Tests.Visual.Playlists private int totalCount; private ScoreInfo userScore = null!; + public TestScenePlaylistsResultsScreen() + { + Add(beatmapLookupCache); + } + [SetUpSteps] public override void SetUpSteps() { @@ -279,6 +290,25 @@ namespace osu.Game.Tests.Visual.Playlists case IndexPlaylistScoresRequest: break; + case GetBeatmapsRequest getBeatmaps: + getBeatmaps.TriggerSuccess(new GetBeatmapsResponse + { + Beatmaps = getBeatmaps.BeatmapIds.Select(id => new APIBeatmap + { + OnlineID = id, + StarRating = id, + DifficultyName = $"Beatmap {id}", + BeatmapSet = new APIBeatmapSet + { + Title = $"Title {id}", + Artist = $"Artist {id}", + AuthorString = $"Author {id}" + } + }).ToList() + }); + + return true; + default: return false; } @@ -346,6 +376,7 @@ namespace osu.Game.Tests.Visual.Playlists Position = real_user_position, MaxCombo = userScore.MaxCombo, User = userScore.User, + BeatmapId = RNG.Next(0, 7), ScoresAround = new MultiplayerScoresAround { Higher = new MultiplayerScores(), @@ -364,6 +395,7 @@ namespace osu.Game.Tests.Visual.Playlists Passed = true, Rank = userScore.Rank, MaxCombo = userScore.MaxCombo, + BeatmapId = RNG.Next(0, 7), User = new APIUser { Id = 2 + i, @@ -379,6 +411,7 @@ namespace osu.Game.Tests.Visual.Playlists Passed = true, Rank = userScore.Rank, MaxCombo = userScore.MaxCombo, + BeatmapId = RNG.Next(0, 7), User = new APIUser { Id = 2 + i, @@ -396,7 +429,7 @@ namespace osu.Game.Tests.Visual.Playlists return multiplayerUserScore; } - private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req, bool noScores = false) + private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req, bool noScores) { var result = new IndexedMultiplayerScores(); @@ -413,6 +446,7 @@ namespace osu.Game.Tests.Visual.Playlists Passed = true, Rank = ScoreRank.X, MaxCombo = 1000, + BeatmapId = RNG.Next(0, 7), User = new APIUser { Id = 2 + i, From 76bf03b05dd92938290c23631e00f82fc945f631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Feb 2025 10:56:28 +0100 Subject: [PATCH 586/801] Add failing decoder test case for too many combo colours --- .../Formats/LegacyBeatmapDecoderTest.cs | 29 ++++++++ .../Resources/too-many-combo-colours.osu | 73 +++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 osu.Game.Tests/Resources/too-many-combo-colours.osu diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index adb1755c11..9747b654ae 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -404,6 +404,35 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestComboColourCountIsLimitedToEight() + { + var decoder = new LegacySkinDecoder(); + + using (var resStream = TestResources.OpenResource("too-many-combo-colours.osu")) + using (var stream = new LineBufferedReader(resStream)) + { + var comboColors = decoder.Decode(stream).ComboColours; + + Debug.Assert(comboColors != null); + + Color4[] expectedColors = + { + new Color4(142, 199, 255, 255), + new Color4(255, 128, 128, 255), + new Color4(128, 255, 255, 255), + new Color4(128, 255, 128, 255), + new Color4(255, 187, 255, 255), + new Color4(255, 177, 140, 255), + new Color4(100, 100, 100, 255), + new Color4(142, 199, 255, 255), + }; + Assert.AreEqual(expectedColors.Length, comboColors.Count); + for (int i = 0; i < expectedColors.Length; i++) + Assert.AreEqual(expectedColors[i], comboColors[i]); + } + } + [Test] public void TestGetLastObjectTime() { diff --git a/osu.Game.Tests/Resources/too-many-combo-colours.osu b/osu.Game.Tests/Resources/too-many-combo-colours.osu new file mode 100644 index 0000000000..477e362a6d --- /dev/null +++ b/osu.Game.Tests/Resources/too-many-combo-colours.osu @@ -0,0 +1,73 @@ +osu file format v14 + +[General] +AudioFilename: 03. Renatus - Soleily 192kbps.mp3 +AudioLeadIn: 0 +PreviewTime: 164471 +Countdown: 0 +SampleSet: Soft +StackLeniency: 0.7 +Mode: 0 +LetterboxInBreaks: 0 +WidescreenStoryboard: 0 + +[Editor] +Bookmarks: 11505,22054,32604,43153,53703,64252,74802,85351,95901,106450,116999,119637,130186,140735,151285,161834,164471,175020,185570,196119,206669,209306 +DistanceSpacing: 1.8 +BeatDivisor: 4 +GridSize: 4 +TimelineZoom: 2 + +[Metadata] +Title:Renatus +TitleUnicode:Renatus +Artist:Soleily +ArtistUnicode:Soleily +Creator:Gamu +Version:Insane +Source: +Tags:MBC7 Unisphere 地球ヤバイEP Chikyu Yabai +BeatmapID:557821 +BeatmapSetID:241526 + +[Difficulty] +HPDrainRate:6.5 +CircleSize:4 +OverallDifficulty:8 +ApproachRate:9 +SliderMultiplier:1.8 +SliderTickRate:2 + +[Events] +//Background and Video events +0,0,"machinetop_background.jpg",0,0 +//Break Periods +2,122474,140135 +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Sound Samples + +[TimingPoints] +956,329.67032967033,4,2,0,60,1,0 + + +[Colours] +Combo1:142,199,255 +Combo2:255,128,128 +Combo3:128,255,255 +Combo4:128,255,128 +Combo5:255,187,255 +Combo6:255,177,140 +Combo7:100,100,100 +Combo8:142,199,255 +Combo9:255,128,128 +Combo10:128,255,255 +Combo11:128,255,128 +Combo12:255,187,255 +Combo13:255,177,140 +Combo14:100,100,100 + +[HitObjects] +192,168,956,6,0,P|184:128|200:80,1,90,4|0,1:2|0:0,0:0:0:0: From c2875423eeb264752954ab56f01a8ec2f702510d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Feb 2025 18:58:29 +0900 Subject: [PATCH 587/801] Cleanup score fetching a bit --- osu.Game/Screens/Ranking/ResultsScreen.cs | 51 ++++++++++++++++------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 26b13d026c..010f7e1a93 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -234,27 +234,19 @@ namespace osu.Game.Screens.Ranking { base.LoadComplete(); - lastFetchTask = Task.Run(async () => await addScores(await FetchScores().ConfigureAwait(false)).ConfigureAwait(false)); - StatisticsPanel.State.BindValueChanged(onStatisticsStateChanged, true); + + fetchScores(null); } protected override void Update() { base.Update(); - if (lastFetchTask.IsCompleted) - { - Task? nextPageTask = null; - - if (ScorePanelList.IsScrolledToStart) - nextPageTask = FetchNextPage(-1); - else if (ScorePanelList.IsScrolledToEnd) - nextPageTask = FetchNextPage(1); - - if (nextPageTask != null) - lastFetchTask = Task.Run(async () => await addScores(await nextPageTask.ConfigureAwait(false)).ConfigureAwait(false)); - } + if (ScorePanelList.IsScrolledToStart) + fetchScores(-1); + else if (ScorePanelList.IsScrolledToEnd) + fetchScores(1); } #region Applause @@ -317,6 +309,37 @@ namespace osu.Game.Screens.Ranking #endregion + /// + /// Fetches the next page of scores in the given direction. + /// + /// The direction, or null to fetch any scores. + private void fetchScores(int? direction) + { + Debug.Assert(direction == null || direction == -1 || direction == 1); + + if (!lastFetchTask.IsCompleted) + return; + + lastFetchTask = Task.Run(async () => + { + ScoreInfo[] scores; + + switch (direction) + { + default: + scores = await FetchScores().ConfigureAwait(false); + break; + + case -1: + case 1: + scores = await FetchNextPage(direction.Value).ConfigureAwait(false); + break; + } + + await addScores(scores).ConfigureAwait(false); + }); + } + /// /// Performs a fetch/refresh of scores to be displayed. /// From e48d36ad1edd2226b5e7afd9e3bc3e397d00d7e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Feb 2025 11:10:33 +0100 Subject: [PATCH 588/801] Add failing encoder test case for too many combo colours --- .../Formats/LegacyBeatmapEncoderTest.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index c8a09786ec..caebf52026 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -28,6 +28,7 @@ using osu.Game.Skinning; using osu.Game.Storyboards; using osu.Game.Tests.Resources; using osuTK; +using osuTK.Graphics; namespace osu.Game.Tests.Beatmaps.Formats { @@ -184,6 +185,32 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(decodedSlider.Path.ControlPoints.Count, Is.EqualTo(5)); } + [Test] + public void TestOnlyEightComboColoursEncoded() + { + var beatmapSkin = new LegacyBeatmapSkin(new BeatmapInfo(), null) + { + Configuration = + { + CustomComboColours = + { + new Color4(1, 1, 1, 255), + new Color4(2, 2, 2, 255), + new Color4(3, 3, 3, 255), + new Color4(4, 4, 4, 255), + new Color4(5, 5, 5, 255), + new Color4(6, 6, 6, 255), + new Color4(7, 7, 7, 255), + new Color4(8, 8, 8, 255), + new Color4(9, 9, 9, 255), + } + } + }; + + var decodedAfterEncode = decodeFromLegacy(encodeToLegacy((new Beatmap(), beatmapSkin)), string.Empty); + Assert.That(decodedAfterEncode.skin.Configuration.CustomComboColours, Has.Count.EqualTo(8)); + } + private bool areComboColoursEqual(IHasComboColours a, IHasComboColours b) { // equal to null, no need to SequenceEqual @@ -212,6 +239,8 @@ namespace osu.Game.Tests.Beatmaps.Formats { var beatmap = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(reader); var beatmapSkin = new TestLegacySkin(beatmaps_resource_store, name); + stream.Seek(0, SeekOrigin.Begin); + beatmapSkin.Configuration = new LegacySkinDecoder().Decode(reader); return (convert(beatmap), beatmapSkin); } } From 2167c7b8d56bbba00a2167f093a1ddf77d09baf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Feb 2025 11:13:57 +0100 Subject: [PATCH 589/801] Limit beatmap encoder & decoder to at most 8 combo colours --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 07e88ab956..5529828de2 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -349,7 +349,7 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine("[Colours]"); - for (int i = 0; i < colours.Count; i++) + for (int i = 0; i < Math.Min(colours.Count, LegacyBeatmapDecoder.MAX_COMBO_COLOUR_COUNT); i++) { var comboColour = colours[i]; diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index ca4fadf458..6c290c4f1c 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -18,6 +18,8 @@ namespace osu.Game.Beatmaps.Formats { public const int LATEST_VERSION = 14; + public const int MAX_COMBO_COLOUR_COUNT = 8; + /// /// The .osu format (beatmap) version. /// @@ -126,7 +128,9 @@ namespace osu.Game.Beatmaps.Formats string[] split = pair.Value.Split(','); Color4 colour = convertSettingStringToColor4(split, allowAlpha, pair); - bool isCombo = pair.Key.StartsWith(@"Combo", StringComparison.Ordinal); + bool isCombo = pair.Key.StartsWith(@"Combo", StringComparison.Ordinal) + && int.TryParse(pair.Key[5..], out int comboIndex) + && comboIndex >= 1 && comboIndex <= MAX_COMBO_COLOUR_COUNT; if (isCombo) { From 6b76b8ccdda0ffe4a0b7d47e7fe3ddfd38e70d30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Feb 2025 11:16:37 +0100 Subject: [PATCH 590/801] Do not allow adding more than 8 combo colours in editor --- osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs | 10 ++++++---- osu.Game/Screens/Edit/Setup/ColoursSection.cs | 9 +++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs b/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs index fad58841e3..258a97d79c 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs @@ -31,9 +31,12 @@ namespace osu.Game.Graphics.UserInterfaceV2 public LocalisableString Caption { get; init; } public LocalisableString HintText { get; init; } + public BindableBool CanAdd { get; } = new BindableBool(true); + private Box background = null!; private FormFieldCaption caption = null!; private FillFlowContainer flow = null!; + private RoundedButton addButton = null!; [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; @@ -47,8 +50,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 Masking = true; CornerRadius = 5; - RoundedButton button; - InternalChildren = new Drawable[] { background = new Box @@ -76,7 +77,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 AutoSizeAxes = Axes.Y, Direction = FillDirection.Full, Spacing = new Vector2(5), - Child = button = new RoundedButton + Child = addButton = new RoundedButton { Action = addNewColour, Size = new Vector2(70), @@ -87,7 +88,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 }, }; - flow.SetLayoutPosition(button, float.MaxValue); + flow.SetLayoutPosition(addButton, float.MaxValue); } protected override void LoadComplete() @@ -99,6 +100,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 if (args.Action != NotifyCollectionChangedAction.Replace) updateColours(); }, true); + CanAdd.BindValueChanged(_ => addButton.Alpha = CanAdd.Value ? 1 : 0, true); updateState(); } diff --git a/osu.Game/Screens/Edit/Setup/ColoursSection.cs b/osu.Game/Screens/Edit/Setup/ColoursSection.cs index 8de7f86523..865fe05c54 100644 --- a/osu.Game/Screens/Edit/Setup/ColoursSection.cs +++ b/osu.Game/Screens/Edit/Setup/ColoursSection.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; +using osu.Game.Beatmaps.Formats; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; using osu.Game.Skinning; @@ -54,6 +55,8 @@ namespace osu.Game.Screens.Edit.Setup Beatmap.BeatmapSkin.ComboColours.Clear(); Beatmap.BeatmapSkin.ComboColours.AddRange(comboColours.Colours); + updateAddButtonVisibility(); + syncingColours = false; } }); @@ -68,8 +71,14 @@ namespace osu.Game.Screens.Edit.Setup comboColours.Colours.Clear(); comboColours.Colours.AddRange(Beatmap.BeatmapSkin?.ComboColours); + updateAddButtonVisibility(); + syncingColours = false; }); + + updateAddButtonVisibility(); + + void updateAddButtonVisibility() => comboColours.CanAdd.Value = comboColours.Colours.Count < LegacyBeatmapDecoder.MAX_COMBO_COLOUR_COUNT; } } } From f3632a466fbf88484d2c3be9e461a9e7610e40da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Feb 2025 12:01:30 +0100 Subject: [PATCH 591/801] Prevent closing team chat channels via Ctrl-W As pointed out in https://github.com/ppy/osu/pull/32079#issuecomment-2680297760. The comment suggested putting that logic in `ChannelManager` but honestly I kinda don't see it working out. It'd probably be multiple boolean arguments for `leaveChannel()` (because `sendLeaveRequest` or whatever already exists), and then there's this one usage in tournament client: https://github.com/ppy/osu/blob/31aded69714cf205c215893368d1f148c9a73319/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs#L57-L58 I'm not sure how that would interact with this particular change, but I think there is a nonzero possibility that it would interact badly. So in general I kinda just prefer steering clear of all that and adding a local one-liner. --- osu.Game/Overlays/ChatOverlay.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index c49afa3a66..7f4ba3e2e2 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -228,7 +228,8 @@ namespace osu.Game.Overlays return true; case PlatformAction.DocumentClose: - channelManager.LeaveChannel(currentChannel.Value); + if (currentChannel.Value?.Type != ChannelType.Team) + channelManager.LeaveChannel(currentChannel.Value); return true; case PlatformAction.TabRestore: From d3c4afe65d8d86edb8c391d6db96849ef4f48709 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 27 Feb 2025 13:16:51 +0900 Subject: [PATCH 592/801] Fix typo --- osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 9bef6a3f3a..0190a6f959 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -71,7 +71,7 @@ namespace osu.Game.Screens.Ranking.Expanded StarDifficulty starDifficulty = new StarDifficulty(beatmap.StarRating, 0); // In some cases, the beatmap ferried through ScoreInfo actually represents an online beatmap. - // If it isn't, we may be able to compute a more accuracy difficulty from the ruleset and mods. + // If it isn't, we may be able to compute a more accurate difficulty from the ruleset and mods. if (realmAccess.Run(r => r.Find(score.BeatmapInfo!.ID)) != null) starDifficulty = beatmapDifficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods).GetResultSafely() ?? starDifficulty; From d31588939c03fb365cf7acd09b6a441a49f100f7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 27 Feb 2025 13:39:16 +0900 Subject: [PATCH 593/801] Disallow attempting to close multiplayer rooms --- osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs | 10 +--------- .../Multiplayer/MultiplayerLoungeSubScreen.cs | 3 +++ .../OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs | 11 +++++++++++ 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 0e08e398a4..30e7b0d31b 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -21,7 +21,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Rulesets; @@ -361,14 +360,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge api.Queue(req); } - public void Close(Room room) - { - Debug.Assert(room.RoomID != null); - - var request = new ClosePlaylistRequest(room.RoomID.Value); - request.Success += RefreshRooms; - api.Queue(request); - } + public abstract void Close(Room room); /// /// Push a room as a new subscreen. diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index 873a9cde88..8f2490f77a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -99,6 +99,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer }); } + public override void Close(Room room) + => throw new NotSupportedException("Cannot close multiplayer rooms."); + protected override void OpenNewRoom(Room room) { if (!client.IsConnected.Value) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs index 6ed367328c..9de13eb270 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs @@ -4,12 +4,14 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; @@ -74,6 +76,15 @@ namespace osu.Game.Screens.OnlinePlay.Playlists api.Queue(joinRoomRequest); } + public override void Close(Room room) + { + Debug.Assert(room.RoomID != null); + + var request = new ClosePlaylistRequest(room.RoomID.Value); + request.Success += RefreshRooms; + api.Queue(request); + } + protected override OsuButton CreateNewRoomButton() => new CreatePlaylistsRoomButton(); protected override Room CreateNewRoom() From 47ca5c90a5bada5733c89376916236b29c69467f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 27 Feb 2025 14:50:35 +0900 Subject: [PATCH 594/801] Refactor post-join setup to not pass delegates around --- .../Online/Multiplayer/MultiplayerClient.cs | 77 +++++++++++-------- 1 file changed, 47 insertions(+), 30 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 636cba719b..1f85aa5d45 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -170,13 +170,23 @@ namespace osu.Game.Online.Multiplayer private readonly TaskChain joinOrLeaveTaskChain = new TaskChain(); private CancellationTokenSource? joinCancellationSource; + /// + /// Creates and joins a described by an API . + /// + /// The API describing the room to create. + /// If the current user is already in another room. public async Task CreateRoom(Room room) { if (Room != null) throw new InvalidOperationException("Cannot create a multiplayer room while already in one."); var cancellationSource = joinCancellationSource = new CancellationTokenSource(); - await initRoom(room, r => CreateRoomInternal(new MultiplayerRoom(room)), cancellationSource.Token).ConfigureAwait(false); + + await joinOrLeaveTaskChain.Add(async () => + { + var multiplayerRoom = await CreateRoomInternal(new MultiplayerRoom(room)).ConfigureAwait(false); + await setupJoinedRoom(room, multiplayerRoom, cancellationSource.Token).ConfigureAwait(false); + }, cancellationSource.Token).ConfigureAwait(false); } /// @@ -184,54 +194,61 @@ namespace osu.Game.Online.Multiplayer /// /// The API . /// An optional password to use for the join operation. + /// If the current user is already in another room, or does not represent an active room. public async Task JoinRoom(Room room, string? password = null) { if (Room != null) throw new InvalidOperationException("Cannot join a multiplayer room while already in one."); - Debug.Assert(room.RoomID != null); + if (room.RoomID == null) + throw new InvalidOperationException("Cannot join an inactive room."); var cancellationSource = joinCancellationSource = new CancellationTokenSource(); - await initRoom(room, r => JoinRoomInternal(room.RoomID.Value, password ?? room.Password), cancellationSource.Token).ConfigureAwait(false); - } - private async Task initRoom(Room room, Func> initFunc, CancellationToken cancellationToken) - { await joinOrLeaveTaskChain.Add(async () => { - // Initialise the server-side room. - MultiplayerRoom joinedRoom = await initFunc(room).ConfigureAwait(false); + var multiplayerRoom = await JoinRoomInternal(room.RoomID.Value, password ?? room.Password).ConfigureAwait(false); + await setupJoinedRoom(room, multiplayerRoom, cancellationSource.Token).ConfigureAwait(false); + }, cancellationSource.Token).ConfigureAwait(false); + } - // Populate users. - await PopulateUsers(joinedRoom.Users).ConfigureAwait(false); + /// + /// Performs post-join setup of a . + /// + /// The incoming API that was requested to be joined. + /// The resuling that was joined. + /// A token to cancel the process. + private async Task setupJoinedRoom(Room apiRoom, MultiplayerRoom joinedRoom, CancellationToken cancellationToken) + { + // Populate users. + await PopulateUsers(joinedRoom.Users).ConfigureAwait(false); - // Update the stored room (must be done on update thread for thread-safety). - await runOnUpdateThreadAsync(() => - { - Debug.Assert(Room == null); - Debug.Assert(APIRoom == null); + // Update the stored room (must be done on update thread for thread-safety). + await runOnUpdateThreadAsync(() => + { + Debug.Assert(Room == null); + Debug.Assert(APIRoom == null); - Room = joinedRoom; - APIRoom = room; + Room = joinedRoom; + APIRoom = apiRoom; - APIRoom.RoomID = joinedRoom.RoomID; - APIRoom.Playlist = joinedRoom.Playlist.Select(item => new PlaylistItem(item)).ToArray(); - APIRoom.CurrentPlaylistItem = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId); - // The server will null out the end date upon the host joining the room, but the null value is never communicated to the client. - APIRoom.EndDate = null; + APIRoom.RoomID = joinedRoom.RoomID; + APIRoom.Playlist = joinedRoom.Playlist.Select(item => new PlaylistItem(item)).ToArray(); + APIRoom.CurrentPlaylistItem = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId); + // The server will null out the end date upon the host joining the room, but the null value is never communicated to the client. + APIRoom.EndDate = null; - Debug.Assert(LocalUser != null); - addUserToAPIRoom(LocalUser); + Debug.Assert(LocalUser != null); + addUserToAPIRoom(LocalUser); - foreach (var user in joinedRoom.Users) - updateUserPlayingState(user.UserID, user.State); + foreach (var user in joinedRoom.Users) + updateUserPlayingState(user.UserID, user.State); - updateLocalRoomSettings(joinedRoom.Settings); + updateLocalRoomSettings(joinedRoom.Settings); - postServerShuttingDownNotification(); + postServerShuttingDownNotification(); - OnRoomJoined(); - }, cancellationToken).ConfigureAwait(false); + OnRoomJoined(); }, cancellationToken).ConfigureAwait(false); } From 0b453772da964dddd2ee73f677367293b26dbf2a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Feb 2025 15:14:53 +0900 Subject: [PATCH 595/801] Disable button instead of hiding (and add tooltip) --- .../Graphics/UserInterfaceV2/FormColourPalette.cs | 14 +++++++++++++- osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs | 5 ++++- .../Overlays/BeatmapSet/Buttons/FavouriteButton.cs | 5 ++--- osu.Game/Overlays/Settings/SettingsButton.cs | 5 +---- .../Screens/OnlinePlay/Components/ReadyButton.cs | 5 ++--- .../Playlists/AddPlaylistToCollectionButton.cs | 5 ++--- 6 files changed, 24 insertions(+), 15 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs b/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs index 258a97d79c..a0348fa27a 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs @@ -100,7 +100,19 @@ namespace osu.Game.Graphics.UserInterfaceV2 if (args.Action != NotifyCollectionChangedAction.Replace) updateColours(); }, true); - CanAdd.BindValueChanged(_ => addButton.Alpha = CanAdd.Value ? 1 : 0, true); + CanAdd.BindValueChanged(canAdd => + { + if (canAdd.NewValue) + { + addButton.Enabled.Value = true; + addButton.TooltipText = string.Empty; + } + else + { + addButton.Enabled.Value = false; + addButton.TooltipText = "Maximum combo colours reached"; + } + }, true); updateState(); } diff --git a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs index 6aded3fe32..9b57ebb200 100644 --- a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs +++ b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs @@ -8,6 +8,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics.Backgrounds; @@ -17,7 +18,7 @@ using osuTK.Graphics; namespace osu.Game.Graphics.UserInterfaceV2 { - public partial class RoundedButton : OsuButton, IFilterable + public partial class RoundedButton : OsuButton, IFilterable, IHasTooltip { protected TrianglesV2? Triangles { get; private set; } @@ -107,5 +108,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 } public bool FilteringActive { get; set; } + + public virtual LocalisableString TooltipText { get; set; } } } diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs index cbdb2ea190..eab394c8f6 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs @@ -7,7 +7,6 @@ using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; @@ -21,7 +20,7 @@ using APIUser = osu.Game.Online.API.Requests.Responses.APIUser; namespace osu.Game.Overlays.BeatmapSet.Buttons { - public partial class FavouriteButton : HeaderButton, IHasTooltip + public partial class FavouriteButton : HeaderButton { public readonly Bindable BeatmapSet = new Bindable(); @@ -32,7 +31,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons private readonly IBindable localUser = new Bindable(); - public LocalisableString TooltipText + public override LocalisableString TooltipText { get { diff --git a/osu.Game/Overlays/Settings/SettingsButton.cs b/osu.Game/Overlays/Settings/SettingsButton.cs index 3f5d612eb8..196ddca953 100644 --- a/osu.Game/Overlays/Settings/SettingsButton.cs +++ b/osu.Game/Overlays/Settings/SettingsButton.cs @@ -6,13 +6,12 @@ using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; using osu.Framework.Localisation; using osu.Game.Graphics.UserInterfaceV2; namespace osu.Game.Overlays.Settings { - public partial class SettingsButton : RoundedButton, IHasTooltip, IConditionalFilterable + public partial class SettingsButton : RoundedButton, IConditionalFilterable { public SettingsButton() { @@ -25,8 +24,6 @@ namespace osu.Game.Overlays.Settings public BindableBool CanBeShown { get; } = new BindableBool(true); IBindable IConditionalFilterable.CanBeShown => CanBeShown; - public LocalisableString TooltipText { get; set; } - public override IEnumerable FilterTerms { get diff --git a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs index 2e669fd1b2..56e2719e9c 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs @@ -3,7 +3,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics.Cursor; using osu.Framework.Localisation; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online; @@ -11,7 +10,7 @@ using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Components { - public abstract partial class ReadyButton : RoundedButton, IHasTooltip + public abstract partial class ReadyButton : RoundedButton { public new readonly BindableBool Enabled = new BindableBool(); @@ -29,7 +28,7 @@ namespace osu.Game.Screens.OnlinePlay.Components private void updateState() => base.Enabled.Value = availability.Value.State == DownloadState.LocallyAvailable && Enabled.Value; - public virtual LocalisableString TooltipText + public override LocalisableString TooltipText { get { diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index 741173f9a3..47629981f1 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Graphics.Cursor; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Collections; @@ -18,7 +17,7 @@ using Realms; namespace osu.Game.Screens.OnlinePlay.Playlists { - public partial class AddPlaylistToCollectionButton : RoundedButton, IHasTooltip + public partial class AddPlaylistToCollectionButton : RoundedButton { private readonly Room room; @@ -161,7 +160,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists collectionSubscription?.Dispose(); } - public LocalisableString TooltipText + public override LocalisableString TooltipText { get { From 5b318edbfbd9aa3ece3a491a9a641d7eee3a4c20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Feb 2025 14:57:42 +0100 Subject: [PATCH 596/801] Fix sliders not being selectable if the body is hidden but the head is still visible Closes https://github.com/ppy/osu/issues/31998. Previously: https://github.com/ppy/osu/commit/1648f2efa306f587714178f113e69d8ad8c4ac02, https://github.com/ppy/osu/pull/31923. Oh input handling, how I love ya. --- .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 39c0681dba..60f335c419 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -626,7 +626,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { - if (BodyPiece.ReceivePositionalInputAt(screenSpacePos) && (IsSelected || DrawableObject.Body.Alpha > 0)) + if (BodyPiece.ReceivePositionalInputAt(screenSpacePos) && (IsSelected || DrawableObject.Body.Alpha > 0 || DrawableObject.HeadCircle.Alpha > 0)) return true; if (ControlPointVisualiser == null) From 09131740992b15ca322054e5c8aee784c6eade79 Mon Sep 17 00:00:00 2001 From: Zihad Date: Fri, 28 Feb 2025 00:20:58 +0600 Subject: [PATCH 597/801] Fix settings control not visible because of previous search This also makes `SettingsPanel`'s `SearchTextBox` protected from private so that `SettingsOverlay` can access it. --- osu.Game/Overlays/SettingsOverlay.cs | 3 +++ osu.Game/Overlays/SettingsPanel.cs | 16 ++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index 1157860e03..8a39d75565 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -68,6 +68,9 @@ namespace osu.Game.Overlays public void ShowAtControl() where T : Drawable { + // if search isn't cleared then the target control won't be visible if it doesn't match the query + SearchTextBox.Current.Value = ""; + Show(); // wait for load of sections diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index df50e0f339..d8b054eaf8 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -54,7 +54,7 @@ namespace osu.Game.Overlays public SettingsSectionsContainer SectionsContainer { get; private set; } - private SeekLimitedSearchTextBox searchTextBox; + protected SeekLimitedSearchTextBox SearchTextBox; protected override string PopInSampleName => "UI/settings-pop-in"; protected override double PopInOutSampleBalance => -OsuGameBase.SFX_STEREO_STRENGTH; @@ -135,7 +135,7 @@ namespace osu.Game.Overlays }, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Child = searchTextBox = new SettingsSearchTextBox + Child = SearchTextBox = new SettingsSearchTextBox { RelativeSizeAxes = Axes.X, Origin = Anchor.TopCentre, @@ -183,8 +183,8 @@ namespace osu.Game.Overlays Sidebar?.MoveToX(0, TRANSITION_LENGTH, Easing.OutQuint); this.FadeTo(1, TRANSITION_LENGTH / 2, Easing.OutQuint); - searchTextBox.TakeFocus(); - searchTextBox.HoldFocus = true; + SearchTextBox.TakeFocus(); + SearchTextBox.HoldFocus = true; } protected virtual float ExpandedPosition => 0; @@ -199,8 +199,8 @@ namespace osu.Game.Overlays Sidebar?.MoveToX(-sidebar_width, TRANSITION_LENGTH, Easing.OutQuint); this.FadeTo(0, TRANSITION_LENGTH / 2, Easing.OutQuint); - searchTextBox.HoldFocus = false; - if (searchTextBox.HasFocus) + SearchTextBox.HoldFocus = false; + if (SearchTextBox.HasFocus) GetContainingFocusManager()!.ChangeFocus(null); } @@ -208,7 +208,7 @@ namespace osu.Game.Overlays protected override void OnFocus(FocusEvent e) { - searchTextBox.TakeFocus(); + SearchTextBox.TakeFocus(); base.OnFocus(e); } @@ -234,7 +234,7 @@ namespace osu.Game.Overlays loading.Hide(); - searchTextBox.Current.BindValueChanged(term => SectionsContainer.SearchTerm = term.NewValue, true); + SearchTextBox.Current.BindValueChanged(term => SectionsContainer.SearchTerm = term.NewValue, true); loadSidebarButtons(); }); From a659936c57a1f51b917102bc737bfbc22187973e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 13:19:19 +0900 Subject: [PATCH 598/801] Inline some methods --- .../Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs | 4 +--- .../OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs | 8 ++------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index eda3bace40..f74de26f1f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -444,7 +444,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match if (!ApplyButton.Enabled.Value) return; - hideError(); + ErrorText.FadeOut(50); Debug.Assert(applyingSettingsOperation == null); applyingSettingsOperation = ongoingOperationTracker.BeginOperation(); @@ -480,8 +480,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match } } - private void hideError() => ErrorText.FadeOut(50); - private void onSuccess() => Schedule(() => { Debug.Assert(applyingSettingsOperation != null); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs index b3d1d577ed..9c0363f40e 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs @@ -437,7 +437,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (!ApplyButton.Enabled.Value) return; - hideError(); + ErrorText.FadeOut(50); room.Name = NameField.Text; room.Availability = AvailabilityPicker.Current.Value; @@ -448,15 +448,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists loadingLayer.Show(); var req = new CreateRoomRequest(room); - req.Success += onSuccess; + req.Success += _ => loadingLayer.Hide(); req.Failure += e => onError(req.Response?.Error ?? e.Message); api.Queue(req); } - private void hideError() => ErrorText.FadeOut(50); - - private void onSuccess(Room room) => loadingLayer.Hide(); - private void onError(string text) { // see https://github.com/ppy/osu-web/blob/2c97aaeb64fb4ed97c747d8383a35b30f57428c7/app/Models/Multiplayer/PlaylistItem.php#L48. From e1723ec1bbfe40e70754b1971b9e1602eed4a7a5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 14:05:49 +0900 Subject: [PATCH 599/801] Adjust preview time display to not conflict with bookmarks --- .../Timelines/Summary/Parts/PreviewTimePart.cs | 5 +++++ .../Components/Timelines/Summary/SummaryTimeline.cs | 13 ++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/PreviewTimePart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/PreviewTimePart.cs index 67bb1ef500..72b58bcb5f 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/PreviewTimePart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/PreviewTimePart.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; using osu.Framework.Localisation; using osu.Game.Extensions; @@ -36,6 +37,10 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts : base(time) { Alpha = 0.8f; + + // Display as a small circle on the middle line as to not clash with other displays. + RelativeSizeAxes = Axes.None; + Height = Width = 5; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs index c01481e840..568137cce1 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs @@ -52,13 +52,6 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary }, } }, - new PreviewTimePart - { - Anchor = Anchor.Centre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.Both, - Height = 0.4f, - }, new BreakPart { Anchor = Anchor.Centre, @@ -85,6 +78,12 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary RelativeSizeAxes = Axes.Both, Height = 0.4f }, + new PreviewTimePart + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + }, new MarkerPart { RelativeSizeAxes = Axes.Both }, }; } From 3e8dafa3c51d6c6434d56ac0c51ffe4800c23fd4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 14:43:00 +0900 Subject: [PATCH 600/801] Add basic setup for mania legacy barline implementation --- .../Objects/Drawables/DrawableBarLine.cs | 3 +- .../Skinning/Default/DefaultBarLine.cs | 4 ++- .../Skinning/Legacy/LegacyBarLine.cs | 33 +++++++++++++++++++ .../Legacy/ManiaLegacySkinTransformer.cs | 2 +- 4 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs index 25fed1a84c..be0f84d7fd 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs @@ -26,6 +26,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables : base(barLine) { RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; } [BackgroundDependencyLoader] @@ -36,8 +37,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables Anchor = Anchor.Centre, Origin = Anchor.Centre, }); - - Major.BindValueChanged(major => Height = major.NewValue ? 1.7f : 1.2f, true); } protected override void OnApply() diff --git a/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs b/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs index ef75e9df11..05fba1241f 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Default [BackgroundDependencyLoader] private void load(DrawableHitObject drawableHitObject) { - RelativeSizeAxes = Axes.Both; + RelativeSizeAxes = Axes.X; // Avoid flickering due to no anti-aliasing of boxes by default. var edgeSmoothness = new Vector2(0.3f); @@ -75,6 +75,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Default private void updateMajor(ValueChangedEvent major) { + Height = major.NewValue ? 1.7f : 1.2f; + mainLine.Alpha = major.NewValue ? 0.5f : 0.2f; leftAnchor.Alpha = rightAnchor.Alpha = major.NewValue ? mainLine.Alpha * 0.3f : 0; } diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs new file mode 100644 index 0000000000..64ea1df2ae --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs @@ -0,0 +1,33 @@ +// 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 osuTK; + +namespace osu.Game.Rulesets.Mania.Skinning.Legacy +{ + public partial class LegacyBarLine : CompositeDrawable + { + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + Height = 1.2f; + + // Avoid flickering due to no anti-aliasing of boxes by default. + var edgeSmoothness = new Vector2(0.3f); + + AddInternal(new Box + { + Name = "Bar line", + EdgeSmoothness = edgeSmoothness, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.Both, + }); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index 76af569b95..c321fcda87 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -163,7 +163,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy return new LegacyStageForeground(); case ManiaSkinComponents.BarLine: - return null; // Not yet implemented. + return new LegacyBarLine(); default: throw new UnsupportedSkinComponentException(lookup); From cb29459a1e5c2d97a68a548c592ea3140513632d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 15:13:13 +0900 Subject: [PATCH 601/801] Add support for legacy osu!mania barline height and colour spec --- .../Objects/Drawables/DrawableBarLine.cs | 4 ++-- osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs | 9 +++++++-- osu.Game/Skinning/LegacyManiaSkinConfiguration.cs | 1 + osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs | 3 +++ osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 4 ++++ osu.Game/Skinning/LegacySkin.cs | 6 ++++++ 6 files changed, 23 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs index be0f84d7fd..c9fc0763a8 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs @@ -26,10 +26,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables : base(barLine) { RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; + Height = 1; } - [BackgroundDependencyLoader] + [BackgroundDependencyLoader(true)] private void load() { AddInternal(new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.BarLine), _ => new DefaultBarLine()) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs index 64ea1df2ae..ce48c49b2e 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs @@ -5,17 +5,22 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Game.Skinning; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Skinning.Legacy { public partial class LegacyBarLine : CompositeDrawable { [BackgroundDependencyLoader] - private void load() + private void load(ISkinSource skin) { + float skinHeight = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.BarLineHeight)?.Value ?? 1; + RelativeSizeAxes = Axes.X; - Height = 1.2f; + Height = 1.2f * skinHeight; + Colour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.BarLineColour)?.Value ?? Color4.White; // Avoid flickering due to no anti-aliasing of boxes by default. var edgeSmoothness = new Vector2(0.3f); diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index db1f216b6e..1e6fa44e68 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -41,6 +41,7 @@ namespace osu.Game.Skinning public float LightPosition = (480 - 413) * POSITION_SCALE_FACTOR; public float ComboPosition = 111 * POSITION_SCALE_FACTOR; public float ScorePosition = 300 * POSITION_SCALE_FACTOR; + public float BarLineHeight = 1; public bool ShowJudgementLine = true; public bool KeysUnderNotes; public int LightFramePerSecond = 60; diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index ee354de68b..e94fb23681 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -70,6 +70,9 @@ namespace osu.Game.Skinning RightStageImage, BottomStageImage, + BarLineHeight, + BarLineColour, + // ReSharper disable once InconsistentNaming Hit300g, diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index 09866ef237..2739743387 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -86,6 +86,10 @@ namespace osu.Game.Skinning parseArrayValue(pair.Value, currentConfig.ColumnWidth); break; + case "BarlineHeight": + currentConfig.BarLineHeight = float.Parse(pair.Value, CultureInfo.InvariantCulture); + break; + case "HitPosition": currentConfig.HitPosition = (480 - Math.Clamp(float.Parse(pair.Value, CultureInfo.InvariantCulture), 240, 480)) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; break; diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 08fa068830..51c1473303 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -198,9 +198,15 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.ComboBreakColour: return SkinUtils.As(getCustomColour(existing, "ColourBreak")); + case LegacyManiaSkinConfigurationLookups.BarLineColour: + return SkinUtils.As(getCustomColour(existing, "ColourBarline")); + case LegacyManiaSkinConfigurationLookups.MinimumColumnWidth: return SkinUtils.As(new Bindable(existing.MinimumColumnWidth)); + case LegacyManiaSkinConfigurationLookups.BarLineHeight: + return SkinUtils.As(new Bindable(existing.BarLineHeight)); + case LegacyManiaSkinConfigurationLookups.NoteBodyStyle: if (existing.NoteBodyStyle != null) From 306b30cb12238b48e2259d4611185821701d34a9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 28 Feb 2025 15:51:54 +0900 Subject: [PATCH 602/801] Add failing test --- .../TestSceneMultiplayerMatchSubScreen.cs | 23 +++++++++++++++++++ .../OnlinePlay/Match/DrawableMatchRoom.cs | 9 ++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index e95209f993..7058532196 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -317,6 +317,29 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("score multiplier = 1.20", () => this.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01)); } + [Test] + public void TestChangeSettingsButtonVisibleForHost() + { + AddStep("add playlist item", () => + { + SelectedRoom.Value!.Playlist = + [ + new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID + } + ]; + }); + ClickButtonWhenEnabled(); + + AddUntilStep("wait for join", () => RoomJoined); + + AddUntilStep("button visible", () => this.ChildrenOfType().Single().ChangeSettingsButton?.Alpha, () => Is.GreaterThan(0)); + AddStep("join other user", void () => MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID })); + AddStep("make other user host", () => MultiplayerClient.TransferHost(PLAYER_1_ID)); + AddAssert("button hidden", () => this.ChildrenOfType().Single().ChangeSettingsButton?.Alpha, () => Is.EqualTo(0)); + } + private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen { [Resolved(canBeNull: true)] diff --git a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs index 08bcf32edf..b10e83a05c 100644 --- a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs @@ -25,12 +25,13 @@ namespace osu.Game.Screens.OnlinePlay.Match set => selectedItem.Current = value; } + public Drawable? ChangeSettingsButton { get; private set; } + [Resolved] private IAPIProvider api { get; set; } = null!; private readonly BindableWithCurrent selectedItem = new BindableWithCurrent(); private readonly bool allowEdit; - private Drawable? editButton; public DrawableMatchRoom(Room room, bool allowEdit = true) : base(room) @@ -45,7 +46,7 @@ namespace osu.Game.Screens.OnlinePlay.Match { if (allowEdit) { - ButtonsContainer.Add(editButton = new PurpleRoundedButton + ButtonsContainer.Add(ChangeSettingsButton = new PurpleRoundedButton { RelativeSizeAxes = Axes.Y, Anchor = Anchor.Centre, @@ -73,8 +74,8 @@ namespace osu.Game.Screens.OnlinePlay.Match private void updateRoomHost() { - if (editButton != null) - editButton.Alpha = Room.Host?.Equals(api.LocalUser.Value) == true ? 1 : 0; + if (ChangeSettingsButton != null) + ChangeSettingsButton.Alpha = Room.Host?.Equals(api.LocalUser.Value) == true ? 1 : 0; } protected override UpdateableBeatmapBackgroundSprite CreateBackground() => base.CreateBackground().With(d => From a09ef5d96d0bcd9c56ccd1eb6747fa5ba6d0e449 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 28 Feb 2025 15:52:02 +0900 Subject: [PATCH 603/801] Fix API room host not being populated --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 1f85aa5d45..3c627c7a47 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -222,6 +222,8 @@ namespace osu.Game.Online.Multiplayer { // Populate users. await PopulateUsers(joinedRoom.Users).ConfigureAwait(false); + if (joinedRoom.Host != null) + await PopulateUsers([joinedRoom.Host]).ConfigureAwait(false); // Update the stored room (must be done on update thread for thread-safety). await runOnUpdateThreadAsync(() => @@ -233,6 +235,7 @@ namespace osu.Game.Online.Multiplayer APIRoom = apiRoom; APIRoom.RoomID = joinedRoom.RoomID; + APIRoom.Host = joinedRoom.Host?.User; APIRoom.Playlist = joinedRoom.Playlist.Select(item => new PlaylistItem(item)).ToArray(); APIRoom.CurrentPlaylistItem = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId); // The server will null out the end date upon the host joining the room, but the null value is never communicated to the client. From 02b950223c055aad3e192cdff99d56f2c5b2c83f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 16:06:12 +0900 Subject: [PATCH 604/801] Adjust x offsets to work again for keyboard selection --- osu.Game/Screens/SelectV2/PanelBase.cs | 13 ++++++------- osu.Game/Screens/SelectV2/PanelBeatmap.cs | 2 -- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs index 805cbac8eb..1e47401013 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -23,8 +23,6 @@ namespace osu.Game.Screens.SelectV2 { private const float corner_radius = 10; - private const float left_edge_x_offset = 20f; - private const float keyboard_active_x_offset = 25f; private const float active_x_offset = 50f; private const float duration = 500; @@ -162,6 +160,7 @@ namespace osu.Game.Screens.SelectV2 base.LoadComplete(); Expanded.BindValueChanged(_ => updateDisplay()); + Selected.BindValueChanged(_ => updateDisplay()); KeyboardSelected.BindValueChanged(_ => updateDisplay(), true); } @@ -199,13 +198,13 @@ namespace osu.Game.Screens.SelectV2 private void updateXOffset() { - float x = PanelXOffset + active_x_offset + keyboard_active_x_offset + left_edge_x_offset; + float x = PanelXOffset; - if (Expanded.Value) - x -= active_x_offset; + if (!Expanded.Value && !Selected.Value) + x += active_x_offset; - if (KeyboardSelected.Value) - x -= keyboard_active_x_offset; + if (!KeyboardSelected.Value) + x += active_x_offset * 0.5f; this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); } diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index b27e5cae14..0ce6b1a9a2 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -163,8 +163,6 @@ namespace osu.Game.Screens.SelectV2 computeStarRating(); updateKeyCount(); }, true); - - Selected.BindValueChanged(s => Expanded.Value = s.NewValue, true); } protected override void PrepareForUse() From a8fbac0f0dbf628ee284e9b3c27554d00697f1e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 16:27:18 +0900 Subject: [PATCH 605/801] Add better selection visibility via another tint layer --- osu.Game/Screens/SelectV2/PanelBase.cs | 53 +++++++++++++++++++++----- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs index 1e47401013..d3132a106e 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -38,6 +38,8 @@ namespace osu.Game.Screens.SelectV2 private Container iconContainer = null!; private Box activationFlash = null!; private Box hoverLayer = null!; + private Box keyboardSelectionLayer = null!; + private Box selectionLayer = null!; public Container TopLevelContent { get; private set; } = null!; @@ -137,6 +139,24 @@ namespace osu.Game.Screens.SelectV2 hoverLayer = new Box { Alpha = 0, + Colour = colours.Blue.Opacity(0.1f), + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + }, + selectionLayer = new Box + { + Alpha = 0, + Colour = ColourInfo.GradientHorizontal(colours.Yellow.Opacity(0), colours.Yellow.Opacity(0.5f)), + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + Width = 0.7f, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }, + keyboardSelectionLayer = new Box + { + Alpha = 0, + Colour = colours.Yellow.Opacity(0.1f), Blending = BlendingParameters.Additive, RelativeSizeAxes = Axes.Both, }, @@ -151,7 +171,6 @@ namespace osu.Game.Screens.SelectV2 } }; - hoverLayer.Colour = colours.Blue.Opacity(0.1f); backgroundGradient.Colour = ColourInfo.GradientHorizontal(colourProvider.Background3, colourProvider.Background4); } @@ -159,9 +178,27 @@ namespace osu.Game.Screens.SelectV2 { base.LoadComplete(); - Expanded.BindValueChanged(_ => updateDisplay()); - Selected.BindValueChanged(_ => updateDisplay()); - KeyboardSelected.BindValueChanged(_ => updateDisplay(), true); + Expanded.BindValueChanged(_ => updateDisplay(), true); + + Selected.BindValueChanged(selected => + { + if (selected.NewValue) + selectionLayer.FadeIn(100, Easing.OutQuint); + else + selectionLayer.FadeOut(200, Easing.OutQuint); + + updateXOffset(); + }, true); + + KeyboardSelected.BindValueChanged(selected => + { + if (selected.NewValue) + keyboardSelectionLayer.FadeIn(100, Easing.OutQuint); + else + keyboardSelectionLayer.FadeOut(1000, Easing.OutQuint); + + updateXOffset(); + }, true); } protected override void PrepareForUse() @@ -211,9 +248,7 @@ namespace osu.Game.Screens.SelectV2 private void updateHover() { - bool hovered = IsHovered || KeyboardSelected.Value; - - if (hovered) + if (IsHovered) hoverLayer.FadeIn(100, Easing.OutQuint); else hoverLayer.FadeOut(1000, Easing.OutQuint); @@ -221,13 +256,13 @@ namespace osu.Game.Screens.SelectV2 protected override bool OnHover(HoverEvent e) { - updateDisplay(); + updateHover(); return true; } protected override void OnHoverLost(HoverLostEvent e) { - updateDisplay(); + updateHover(); base.OnHoverLost(e); } From 1e46dc6b0a23cf2fa9677104b9101d8f3f94a18d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 16:27:42 +0900 Subject: [PATCH 606/801] Adjust animation duration to roughly match scroll operations Previous value felt wrong when using keyboard selection for iteration. --- osu.Game/Screens/SelectV2/PanelBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs index d3132a106e..2a32b1a95f 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -25,7 +25,7 @@ namespace osu.Game.Screens.SelectV2 private const float active_x_offset = 50f; - private const float duration = 500; + private const float duration = 400; protected float PanelXOffset { get; init; } From 51cb0bea1ce61ffd3ca8b3bdb641f8f4840601d1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 16:45:49 +0900 Subject: [PATCH 607/801] Fix carousel taking up too much space on new song select implementation --- osu.Game/Screens/SelectV2/SongSelectV2.cs | 29 +++++++++++++++++------ 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/SelectV2/SongSelectV2.cs b/osu.Game/Screens/SelectV2/SongSelectV2.cs index 3943d059f9..23139c8742 100644 --- a/osu.Game/Screens/SelectV2/SongSelectV2.cs +++ b/osu.Game/Screens/SelectV2/SongSelectV2.cs @@ -39,17 +39,32 @@ namespace osu.Game.Screens.SelectV2 { AddRangeInternal(new Drawable[] { - new Container + new GridContainer // used for max width implementation { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Bottom = ScreenFooter.HEIGHT }, - Child = new BeatmapCarousel + ColumnDimensions = new[] { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - RelativeSizeAxes = Axes.Both, - Width = 0.6f, + new Dimension(), + new Dimension(GridSizeMode.Relative, 0.5f, maxSize: 750), }, + Content = new[] + { + new[] + { + Empty(), + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Bottom = ScreenFooter.HEIGHT }, + Child = new BeatmapCarousel + { + RelativeSizeAxes = Axes.Both + }, + }, + } + } }, modSelectOverlay, }); From 0e257038e8b49400f5082570d5867c4c7ef23c3b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 16:47:57 +0900 Subject: [PATCH 608/801] Fix status pills displaying wrong --- osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs index 599d1b380a..7b99ad40de 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs @@ -101,7 +101,7 @@ namespace osu.Game.Beatmaps.Drawables { if (Status == BeatmapOnlineStatus.None) { - this.FadeOut(animation_duration, Easing.OutQuint); + Hide(); return; } From 8fc744e9dc7d0045232a6c1eda3c17160c366947 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 17:55:11 +0900 Subject: [PATCH 609/801] Make `TestSceneSongSelect` work with local database It was pointless before. --- .../SongSelectV2/TestSceneSongSelect.cs | 36 ++----------------- 1 file changed, 3 insertions(+), 33 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs index 33474d7449..6d180c76d9 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs @@ -9,16 +9,10 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; -using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; -using osu.Game.Beatmaps; -using osu.Game.Configuration; using osu.Game.Database; -using osu.Game.Online.API; -using osu.Game.Overlays; using osu.Game.Overlays.Mods; -using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mods; @@ -29,7 +23,6 @@ using osu.Game.Screens; using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; using osu.Game.Screens.SelectV2.Footer; -using osu.Game.Tests.Resources; using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelectV2 @@ -42,8 +35,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Cached] private readonly OsuLogo logo; - private BeatmapManager beatmapManager = null!; - protected override bool UseOnlineAPI => true; public TestSceneSongSelect() @@ -66,32 +57,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } [BackgroundDependencyLoader] - private void load(GameHost host, IAPIProvider onlineAPI) + private void load() { - BeatmapStore beatmapStore; - BeatmapUpdater beatmapUpdater; - BeatmapDifficultyCache difficultyCache; + RealmDetachedBeatmapStore beatmapStore; - // These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install. - // At a point we have isolated interactive test runs enough, this can likely be removed. - Dependencies.Cache(new RealmRulesetStore(Realm)); - Dependencies.Cache(Realm); - Dependencies.Cache(difficultyCache = new BeatmapDifficultyCache()); - Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, onlineAPI, Audio, Resources, host, Beatmap.Default, difficultyCache)); - Dependencies.CacheAs(beatmapUpdater = new BeatmapUpdater(beatmapManager, difficultyCache, onlineAPI, LocalStorage)); - Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore()); - - beatmapManager.ProcessBeatmap = (set, scope) => beatmapUpdater.Process(set, scope); - - MusicController music; - Dependencies.Cache(music = new MusicController()); - - // required to get bindables attached - Add(difficultyCache); - Add(music); + Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore()); Add(beatmapStore); - - Dependencies.Cache(new OsuConfigManager(LocalStorage)); } protected override void LoadComplete() @@ -109,7 +80,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("load screen", () => Stack.Push(new Screens.SelectV2.SongSelectV2())); AddUntilStep("wait for load", () => Stack.CurrentScreen is Screens.SelectV2.SongSelectV2 songSelect && songSelect.IsLoaded); - AddStep("import test beatmap", () => beatmapManager.Import(TestResources.GetTestBeatmapForImport())); } [Test] From 993473c0810e55ce0b1143f0f147e88d10c65396 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 28 Feb 2025 18:40:54 +0900 Subject: [PATCH 610/801] Pass through artist/title in beatmap transform --- .../Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index 572bf535f7..184de2f50c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -223,6 +223,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Difficulty = new BeatmapDifficulty(beatmap.Difficulty), Metadata = { + Artist = beatmap.Metadata.Artist, + Title = beatmap.Metadata.Title, Author = new RealmUser { Username = beatmap.Metadata.Author.Username, From ffef6ae1853d84120abf52f3c93382b4863bd556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 28 Feb 2025 13:34:00 +0100 Subject: [PATCH 611/801] Fix possible crash when scaling objects in editor The specific fail case here is when `s.{X,Y}` is 0, and `s{Lower,Upper}Bound` is `Infinity`. Because IEEE math is IEEE math, `0 * Infinity` is `NaN`. `MathHelper.Clamp()` is written the following way: https://github.com/ppy/osuTK/blob/af742f1afd01828efc7bc9fe77536b54aab8b419/src/osuTK/Math/MathHelper.cs#L284-L306 `Math.{Min,Max}` are both documented as reporting `NaN` when any of their operands are `NaN`: https://learn.microsoft.com/en-us/dotnet/api/system.math.min?view=net-8.0#system-math-min(system-single-system-single) https://learn.microsoft.com/en-us/dotnet/api/system.math.max?view=net-8.0#system-math-max(system-single-system-single) which means that if a `NaN` happens to sneak into the bounds, it will start spreading outwards in an uncontrolled manner, and likely crash things. In contrast, the standard library provided `Math.Clamp()` is written like so: https://github.com/dotnet/runtime/blob/577c36cee56480dec4d4610b35605b5d5836888b/src/libraries/System.Private.CoreLib/src/System/Math.cs#L711-L729 With this implementation, if either bound is `NaN`, it will essentially not be checked (because any and all comparisons involving `NaN` return false). This prevents the spread of `NaN`s, all the way to positions of hitobjects, and thus fixes the crash. --- .../Edit/OsuSelectionScaleHandler.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index e3ab95c402..4c3db207f2 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -263,12 +263,12 @@ namespace osu.Game.Rulesets.Osu.Edit { case Axes.X: (sLowerBound, sUpperBound) = computeBounds(lowerBounds - b, upperBounds - b, a); - s.X = MathHelper.Clamp(s.X, sLowerBound, sUpperBound); + s.X = Math.Clamp(s.X, sLowerBound, sUpperBound); break; case Axes.Y: (sLowerBound, sUpperBound) = computeBounds(lowerBounds - a, upperBounds - a, b); - s.Y = MathHelper.Clamp(s.Y, sLowerBound, sUpperBound); + s.Y = Math.Clamp(s.Y, sLowerBound, sUpperBound); break; case Axes.Both: @@ -276,11 +276,11 @@ namespace osu.Game.Rulesets.Osu.Edit // Therefore the ratio s.X / s.Y will be maintained (sLowerBound, sUpperBound) = computeBounds(lowerBounds, upperBounds, a * s.X + b * s.Y); s.X = s.X < 0 - ? MathHelper.Clamp(s.X, s.X * sUpperBound, s.X * sLowerBound) - : MathHelper.Clamp(s.X, s.X * sLowerBound, s.X * sUpperBound); + ? Math.Clamp(s.X, s.X * sUpperBound, s.X * sLowerBound) + : Math.Clamp(s.X, s.X * sLowerBound, s.X * sUpperBound); s.Y = s.Y < 0 - ? MathHelper.Clamp(s.Y, s.Y * sUpperBound, s.Y * sLowerBound) - : MathHelper.Clamp(s.Y, s.Y * sLowerBound, s.Y * sUpperBound); + ? Math.Clamp(s.Y, s.Y * sUpperBound, s.Y * sLowerBound) + : Math.Clamp(s.Y, s.Y * sLowerBound, s.Y * sUpperBound); break; } From 35b0ff80bb6094a32d9c5c2b93203faf491b68fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 28 Feb 2025 13:41:56 +0100 Subject: [PATCH 612/801] Mark `MathHelper.Clamp()` as banned API See previous commit for partial rationale. There's an argument to be made about the `NaN`-spreading semantics being desirable because at least something will loudly fail in that case, but I'm not so sure about that these days. It feels like either way if `NaN`s are produced, then things are outside of any control, and chances are the game can probably continue without crashing. And, this move reduces our dependence on osuTK, which has already been living on borrowed time for years now and is only awaiting someone brave to go excise it. --- CodeAnalysis/BannedSymbols.txt | 3 +++ .../Beatmaps/PippidonBeatmapConverter.cs | 4 ++-- .../Skinning/Argon/ArgonBananaPiece.cs | 3 ++- .../HitCircles/Components/HitCircleOverlapMarker.cs | 3 ++- .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 4 ++-- osu.Game/Overlays/NotificationOverlayToastTray.cs | 2 +- osu.Game/Screens/Play/HUDOverlay.cs | 6 +++--- osu.Game/Screens/Utility/CircleGameplay.cs | 4 ++-- .../Utility/SampleComponents/LatencyMovableBox.cs | 9 +++++---- osu.Game/Screens/Utility/ScrollingGameplay.cs | 2 +- 10 files changed, 23 insertions(+), 17 deletions(-) diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt index 550f7c8e11..08b79fc2c0 100644 --- a/CodeAnalysis/BannedSymbols.txt +++ b/CodeAnalysis/BannedSymbols.txt @@ -18,3 +18,6 @@ M:Humanizer.InflectorExtensions.Pascalize(System.String);Humanizer's .Pascalize( M:Humanizer.InflectorExtensions.Camelize(System.String);Humanizer's .Camelize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToCamelCase() instead. M:Humanizer.InflectorExtensions.Underscore(System.String);Humanizer's .Underscore() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToSnakeCase() instead. M:Humanizer.InflectorExtensions.Kebaberize(System.String);Humanizer's .Kebaberize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToKebabCase() instead. +M:osuTK.MathHelper.Clamp(System.Int32,System.Int32,System.Int32)~System.Int32;Use Math.Clamp() instead. +M:osuTK.MathHelper.Clamp(System.Single,System.Single,System.Single)~System.Single;This osuTK helper has unsafe semantics when one of the bounds provided is NaN. Use Math.Clamp() instead. +M:osuTK.MathHelper.Clamp(System.Double,System.Double,System.Double)~System.Double;This osuTK helper has unsafe semantics when one of the bounds provided is NaN. Use Math.Clamp() instead. diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs index 0a4fa84ce1..dd8337abee 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -9,7 +10,6 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Pippidon.Objects; using osu.Game.Rulesets.Pippidon.UI; -using osuTK; namespace osu.Game.Rulesets.Pippidon.Beatmaps { @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Pippidon.Beatmaps }; } - private int getLane(HitObject hitObject) => (int)MathHelper.Clamp( + private int getLane(HitObject hitObject) => (int)Math.Clamp( (getUsablePosition(hitObject) - minPosition) / (maxPosition - minPosition) * PippidonPlayfield.LANE_COUNT, 0, PippidonPlayfield.LANE_COUNT - 1); private float getUsablePosition(HitObject h) => (h as IHasYPosition)?.Y ?? ((IHasXPosition)h).X; diff --git a/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonBananaPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonBananaPiece.cs index 8cdb490922..810dc7eed5 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonBananaPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonBananaPiece.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -110,7 +111,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Argon double duration = ObjectState.HitObject.StartTime - ObjectState.DisplayStartTime; - fadeContent.Alpha = MathHelper.Clamp( + fadeContent.Alpha = Math.Clamp( Interpolation.ValueAt( Time.Current, 1f, 0f, ObjectState.DisplayStartTime + duration * lens_flare_start, diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs index 8ed9d0476a..7a5b01ce79 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -76,7 +77,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components if (hasReachedObject && showHitMarkers.Value) { float alpha = Interpolation.ValueAt(editorTime, 0, 1f, hitObjectTime, hitObjectTime + FADE_OUT_EXTENSION, Easing.In); - float ringScale = MathHelper.Clamp(Interpolation.ValueAt(editorTime, 0, 1f, hitObjectTime, hitObjectTime + FADE_OUT_EXTENSION / 2, Easing.OutQuint), 0, 1); + float ringScale = Math.Clamp(Interpolation.ValueAt(editorTime, 0, 1f, hitObjectTime, hitObjectTime + FADE_OUT_EXTENSION / 2, Easing.OutQuint), 0, 1); ring.Scale = new Vector2(1 + 0.1f * ringScale); content.Alpha = 0.9f * (1 - alpha); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 39c0681dba..52575bdd67 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -270,14 +270,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders if (adjustVelocity) { proposedVelocity = proposedDistance / oldDuration; - proposedDistance = MathHelper.Clamp(proposedDistance, 0.1 * oldDuration, 10 * oldDuration); + proposedDistance = Math.Clamp(proposedDistance, 0.1 * oldDuration, 10 * oldDuration); } else { double minDistance = distanceSnapProvider?.GetBeatSnapDistance() * oldVelocityMultiplier ?? 1; // Add a small amount to the proposed distance to make it easier to snap to the full length of the slider. proposedDistance = distanceSnapProvider?.FindSnappedDistance((float)proposedDistance + 1, HitObject.StartTime, HitObject) ?? proposedDistance; - proposedDistance = MathHelper.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance); + proposedDistance = Math.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance); } if (Precision.AlmostEquals(proposedDistance, HitObject.Path.Distance) && Precision.AlmostEquals(proposedVelocity, HitObject.SliderVelocityMultiplier)) diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index ddb2e02fb8..dd60e303f6 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -174,7 +174,7 @@ namespace osu.Game.Overlays } height = toastFlow.DrawHeight + 120; - alpha = MathHelper.Clamp(toastFlow.DrawHeight / 41, 0, 1) * maxNotificationAlpha; + alpha = Math.Clamp(toastFlow.DrawHeight / 41, 0, 1) * maxNotificationAlpha; } toastContentBackground.Height = (float)Interpolation.DampContinuously(toastContentBackground.Height, height, 10, Clock.ElapsedFrameTime); diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 8bfa8dd6ff..19190ac362 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -278,17 +278,17 @@ namespace osu.Game.Screens.Play processDrawables(rulesetComponents); if (lowestTopScreenSpaceRight.HasValue) - TopRightElements.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceRight.Value)).Y, 0, DrawHeight - TopRightElements.DrawHeight); + TopRightElements.Y = Math.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceRight.Value)).Y, 0, DrawHeight - TopRightElements.DrawHeight); else TopRightElements.Y = 0; if (lowestTopScreenSpaceLeft.HasValue) - LeaderboardFlow.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceLeft.Value)).Y, 0, DrawHeight - LeaderboardFlow.DrawHeight); + LeaderboardFlow.Y = Math.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceLeft.Value)).Y, 0, DrawHeight - LeaderboardFlow.DrawHeight); else LeaderboardFlow.Y = 0; if (highestBottomScreenSpace.HasValue) - bottomRightElements.Y = BottomScoringElementsHeight = -MathHelper.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - bottomRightElements.DrawHeight); + bottomRightElements.Y = BottomScoringElementsHeight = -Math.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - bottomRightElements.DrawHeight); else bottomRightElements.Y = 0; diff --git a/osu.Game/Screens/Utility/CircleGameplay.cs b/osu.Game/Screens/Utility/CircleGameplay.cs index 1f970c5121..0f328d04fb 100644 --- a/osu.Game/Screens/Utility/CircleGameplay.cs +++ b/osu.Game/Screens/Utility/CircleGameplay.cs @@ -201,8 +201,8 @@ namespace osu.Game.Screens.Utility { double preempt = (float)IBeatmapDifficultyInfo.DifficultyRange(SampleApproachRate.Value, 1800, 1200, 450); - approach.Scale = new Vector2(1 + 4 * (float)MathHelper.Clamp((HitTime - Clock.CurrentTime) / preempt, 0, 100)); - Alpha = (float)MathHelper.Clamp((Clock.CurrentTime - HitTime + 600) / 400, 0, 1); + approach.Scale = new Vector2(1 + 4 * (float)Math.Clamp((HitTime - Clock.CurrentTime) / preempt, 0, 100)); + Alpha = (float)Math.Clamp((Clock.CurrentTime - HitTime + 600) / 400, 0, 1); if (Clock.CurrentTime > HitTime + duration) Expire(); diff --git a/osu.Game/Screens/Utility/SampleComponents/LatencyMovableBox.cs b/osu.Game/Screens/Utility/SampleComponents/LatencyMovableBox.cs index dcfcf602bf..ef1b848945 100644 --- a/osu.Game/Screens/Utility/SampleComponents/LatencyMovableBox.cs +++ b/osu.Game/Screens/Utility/SampleComponents/LatencyMovableBox.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; @@ -55,22 +56,22 @@ namespace osu.Game.Screens.Utility.SampleComponents { case Key.F: case Key.Up: - box.Y = MathHelper.Clamp(box.Y - movementAmount, 0.1f, 0.9f); + box.Y = Math.Clamp(box.Y - movementAmount, 0.1f, 0.9f); break; case Key.J: case Key.Down: - box.Y = MathHelper.Clamp(box.Y + movementAmount, 0.1f, 0.9f); + box.Y = Math.Clamp(box.Y + movementAmount, 0.1f, 0.9f); break; case Key.Z: case Key.Left: - box.X = MathHelper.Clamp(box.X - movementAmount, 0.1f, 0.9f); + box.X = Math.Clamp(box.X - movementAmount, 0.1f, 0.9f); break; case Key.X: case Key.Right: - box.X = MathHelper.Clamp(box.X + movementAmount, 0.1f, 0.9f); + box.X = Math.Clamp(box.X + movementAmount, 0.1f, 0.9f); break; } } diff --git a/osu.Game/Screens/Utility/ScrollingGameplay.cs b/osu.Game/Screens/Utility/ScrollingGameplay.cs index 5038c53b4a..c0264f5734 100644 --- a/osu.Game/Screens/Utility/ScrollingGameplay.cs +++ b/osu.Game/Screens/Utility/ScrollingGameplay.cs @@ -165,7 +165,7 @@ namespace osu.Game.Screens.Utility { double preempt = (float)IBeatmapDifficultyInfo.DifficultyRange(SampleApproachRate.Value, 1800, 1200, 450); - Alpha = (float)MathHelper.Clamp((Clock.CurrentTime - HitTime + 600) / 400, 0, 1); + Alpha = (float)Math.Clamp((Clock.CurrentTime - HitTime + 600) / 400, 0, 1); Y = judgement_position - (float)((HitTime - Clock.CurrentTime) / preempt); if (Clock.CurrentTime > HitTime + duration) From 88089fb0144a54d99b2e586f2d1b8e4512494604 Mon Sep 17 00:00:00 2001 From: Zihad Date: Fri, 28 Feb 2025 19:03:39 +0600 Subject: [PATCH 613/801] make `SettingsPanel.SearchTextBox`'s setter private --- osu.Game/Overlays/SettingsPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index d8b054eaf8..9b268c573f 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -54,7 +54,7 @@ namespace osu.Game.Overlays public SettingsSectionsContainer SectionsContainer { get; private set; } - protected SeekLimitedSearchTextBox SearchTextBox; + protected SeekLimitedSearchTextBox SearchTextBox { get; private set; } protected override string PopInSampleName => "UI/settings-pop-in"; protected override double PopInOutSampleBalance => -OsuGameBase.SFX_STEREO_STRENGTH; From 0d7c00ae09d65d7c4a53abd1860d3029e1c004bd Mon Sep 17 00:00:00 2001 From: Zihad Date: Fri, 28 Feb 2025 19:04:47 +0600 Subject: [PATCH 614/801] use `Bindable.SetDefault` for clearing search text --- osu.Game/Overlays/SettingsOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index 8a39d75565..630675a717 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -69,7 +69,7 @@ namespace osu.Game.Overlays where T : Drawable { // if search isn't cleared then the target control won't be visible if it doesn't match the query - SearchTextBox.Current.Value = ""; + SearchTextBox.Current.SetDefault(); Show(); From 8032b6893274a152a12226572e89a000262c5583 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 16:59:39 +0900 Subject: [PATCH 615/801] Stop using padding for panel x offsets --- osu.Game/Screens/SelectV2/PanelBase.cs | 11 +++++++---- osu.Game/Screens/SelectV2/PanelBeatmap.cs | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs index 2a32b1a95f..1dc645ba53 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -61,6 +61,11 @@ namespace osu.Game.Screens.SelectV2 } } + // content is offset by PanelXOffset, make sure we only handle input at the actual visible + // offset region. + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => + TopLevelContent.ReceivePositionalInputAt(screenSpacePos); + [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, OsuColour colours) { @@ -219,8 +224,6 @@ namespace osu.Game.Screens.SelectV2 private void updateDisplay() { - backgroundLayer.TransformTo(nameof(Padding), backgroundLayer.Padding with { Vertical = Expanded.Value ? 2f : 0f }, duration, Easing.OutQuint); - var backgroundColour = accentColour ?? Color4.White; var edgeEffectColour = accentColour ?? Color4Extensions.FromHex(@"4EBFFF"); @@ -235,7 +238,7 @@ namespace osu.Game.Screens.SelectV2 private void updateXOffset() { - float x = PanelXOffset; + float x = PanelXOffset + corner_radius; if (!Expanded.Value && !Selected.Value) x += active_x_offset; @@ -243,7 +246,7 @@ namespace osu.Game.Screens.SelectV2 if (!KeyboardSelected.Value) x += active_x_offset * 0.5f; - this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); + TopLevelContent.MoveToX(x, duration, Easing.OutQuint); } private void updateHover() diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index 0ce6b1a9a2..d4bf3519fa 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -54,7 +54,7 @@ namespace osu.Game.Screens.SelectV2 public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { - var inputRectangle = DrawRectangle; + var inputRectangle = TopLevelContent.DrawRectangle; // Cover the gaps introduced by the spacing between BeatmapPanels so that clicks will not fall through the carousel. // @@ -62,7 +62,7 @@ namespace osu.Game.Screens.SelectV2 // larger hit target. inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING }); - return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); + return inputRectangle.Contains(TopLevelContent.ToLocalSpace(screenSpacePos)); } [BackgroundDependencyLoader] From 29c35529d27b730847d03896c04c03a9e95efd3b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 17:02:09 +0900 Subject: [PATCH 616/801] Fix activation flash being applied twice (and adjust duration) --- osu.Game/Screens/SelectV2/PanelBase.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs index 1dc645ba53..b9d9bbd20a 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -217,7 +217,6 @@ namespace osu.Game.Screens.SelectV2 protected override bool OnClick(ClickEvent e) { - activationFlash.FadeOutFromOne(500, Easing.OutQuint); carousel?.Activate(Item!); return true; } @@ -287,7 +286,7 @@ namespace osu.Game.Screens.SelectV2 public virtual void Activated() { - activationFlash.FadeOutFromOne(500, Easing.OutQuint); + activationFlash.FadeOutFromOne(1000, Easing.OutQuint); } #endregion From 4beac64bdb6c2dee8492ea8b113498b78ef5f36a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 17:19:30 +0900 Subject: [PATCH 617/801] Remove unused container level --- osu.Game/Screens/SelectV2/PanelBase.cs | 43 ++++++++++++-------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs index b9d9bbd20a..36f4f13a3b 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -32,7 +32,6 @@ namespace osu.Game.Screens.SelectV2 private Box backgroundBorder = null!; private Box backgroundGradient = null!; private Box backgroundAccentGradient = null!; - private Container backgroundLayer = null!; private Container backgroundLayerHorizontalPadding = null!; private Container backgroundContainer = null!; private Container iconContainer = null!; @@ -66,6 +65,9 @@ namespace osu.Game.Screens.SelectV2 public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => TopLevelContent.ReceivePositionalInputAt(screenSpacePos); + [Resolved] + private BeatmapCarousel? carousel { get; set; } + [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, OsuColour colours) { @@ -102,30 +104,26 @@ namespace osu.Game.Screens.SelectV2 backgroundLayerHorizontalPadding = new Container { RelativeSizeAxes = Axes.Both, - Child = backgroundLayer = new Container + Child = new Container { RelativeSizeAxes = Axes.Both, - Child = new Container + Masking = true, + CornerRadius = corner_radius, + Children = new Drawable[] { - Masking = true, - CornerRadius = corner_radius, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + backgroundGradient = new Box { - backgroundGradient = new Box - { - RelativeSizeAxes = Axes.Both, - }, - backgroundAccentGradient = new Box - { - RelativeSizeAxes = Axes.Both, - }, - backgroundContainer = new Container - { - RelativeSizeAxes = Axes.Both, - }, - } - }, + RelativeSizeAxes = Axes.Both, + }, + backgroundAccentGradient = new Box + { + RelativeSizeAxes = Axes.Both, + }, + backgroundContainer = new Container + { + RelativeSizeAxes = Axes.Both, + }, + } }, } }, @@ -212,9 +210,6 @@ namespace osu.Game.Screens.SelectV2 this.FadeInFromZero(duration, Easing.OutQuint); } - [Resolved] - private BeatmapCarousel? carousel { get; set; } - protected override bool OnClick(ClickEvent e) { carousel?.Activate(Item!); From 38de3566b14b4d08a17c806f2891fa85c82dfafd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 17:37:18 +0900 Subject: [PATCH 618/801] Adjust set panel display and animations slightly --- .../SelectV2/BeatmapSetPanelBackground.cs | 2 +- osu.Game/Screens/SelectV2/PanelBase.cs | 12 ++++++------ osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 16 +++++++++++----- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs index 435a0ad262..798acf62ee 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.SelectV2 { public partial class BeatmapSetPanelBackground : ModelBackedDrawable { - protected override bool TransformImmediately => true; + protected override double TransformDuration => 400; public WorkingBeatmap? Beatmap { diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs index 36f4f13a3b..05a1a55c03 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -25,7 +25,7 @@ namespace osu.Game.Screens.SelectV2 private const float active_x_offset = 50f; - private const float duration = 400; + protected const float DURATION = 400; protected float PanelXOffset { get; init; } @@ -207,7 +207,7 @@ namespace osu.Game.Screens.SelectV2 protected override void PrepareForUse() { base.PrepareForUse(); - this.FadeInFromZero(duration, Easing.OutQuint); + this.FadeInFromZero(DURATION, Easing.OutQuint); } protected override bool OnClick(ClickEvent e) @@ -221,10 +221,10 @@ namespace osu.Game.Screens.SelectV2 var backgroundColour = accentColour ?? Color4.White; var edgeEffectColour = accentColour ?? Color4Extensions.FromHex(@"4EBFFF"); - backgroundAccentGradient.FadeColour(ColourInfo.GradientHorizontal(backgroundColour.Opacity(0.25f), backgroundColour.Opacity(0f)), duration, Easing.OutQuint); - backgroundBorder.FadeColour(backgroundColour, duration, Easing.OutQuint); + backgroundAccentGradient.FadeColour(ColourInfo.GradientHorizontal(backgroundColour.Opacity(0.25f), backgroundColour.Opacity(0f)), DURATION, Easing.OutQuint); + backgroundBorder.FadeColour(backgroundColour, DURATION, Easing.OutQuint); - TopLevelContent.FadeEdgeEffectTo(Expanded.Value ? edgeEffectColour.Opacity(0.5f) : Color4.Black.Opacity(0.4f), duration, Easing.OutQuint); + TopLevelContent.FadeEdgeEffectTo(Expanded.Value ? edgeEffectColour.Opacity(0.5f) : Color4.Black.Opacity(0.4f), DURATION, Easing.OutQuint); updateXOffset(); updateHover(); @@ -240,7 +240,7 @@ namespace osu.Game.Screens.SelectV2 if (!KeyboardSelected.Value) x += active_x_offset * 0.5f; - TopLevelContent.MoveToX(x, duration, Easing.OutQuint); + TopLevelContent.MoveToX(x, DURATION, Easing.OutQuint); } private void updateHover() diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index 5c38fe8e04..512fbacec1 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -48,7 +48,7 @@ namespace osu.Game.Screens.SelectV2 Icon = chevronIcon = new Container { - Size = new Vector2(22), + Size = new Vector2(0, 22), Child = new SpriteIcon { Anchor = Anchor.Centre, @@ -128,10 +128,16 @@ namespace osu.Game.Screens.SelectV2 private void onExpanded() { - const float duration = 500; - - chevronIcon.ResizeWidthTo(Expanded.Value ? 22 : 0f, duration, Easing.OutQuint); - chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); + if (Expanded.Value) + { + chevronIcon.ResizeWidthTo(18, 600, Easing.OutElasticQuarter); + chevronIcon.FadeTo(1f, DURATION, Easing.OutQuint); + } + else + { + chevronIcon.ResizeWidthTo(0f, DURATION, Easing.OutQuint); + chevronIcon.FadeTo(0f, DURATION, Easing.OutQuint); + } } protected override void PrepareForUse() From 881534eb7f3d71e817d511c64ca368e0e6eca069 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Sat, 1 Mar 2025 01:51:37 +0900 Subject: [PATCH 619/801] Add SFX for kiai/star fountain activation --- osu.Game/Screens/Menu/KiaiMenuFountains.cs | 14 +++++++++++++- osu.Game/Screens/Play/KiaiGameplayFountains.cs | 14 +++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/KiaiMenuFountains.cs b/osu.Game/Screens/Menu/KiaiMenuFountains.cs index 7978e9fa91..dbbff4a9f5 100644 --- a/osu.Game/Screens/Menu/KiaiMenuFountains.cs +++ b/osu.Game/Screens/Menu/KiaiMenuFountains.cs @@ -3,6 +3,8 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Game.Graphics.Containers; @@ -14,8 +16,11 @@ namespace osu.Game.Screens.Menu private StarFountain leftFountain = null!; private StarFountain rightFountain = null!; + private Sample? sample; + private SampleChannel? sampleChannel; + [BackgroundDependencyLoader] - private void load() + private void load(AudioManager audio) { RelativeSizeAxes = Axes.Both; @@ -34,6 +39,8 @@ namespace osu.Game.Screens.Menu X = -250, }, }; + + sample = audio.Samples.Get(@"Gameplay/fountain-shoot"); } private bool isTriggered; @@ -73,6 +80,11 @@ namespace osu.Game.Screens.Menu rightFountain.Shoot(1); break; } + + // Track sample channel to avoid overlapping playback + sampleChannel?.Stop(); + sampleChannel = sample?.GetChannel(); + sampleChannel?.Play(); } } } diff --git a/osu.Game/Screens/Play/KiaiGameplayFountains.cs b/osu.Game/Screens/Play/KiaiGameplayFountains.cs index d4e61dc5a0..7e09f50133 100644 --- a/osu.Game/Screens/Play/KiaiGameplayFountains.cs +++ b/osu.Game/Screens/Play/KiaiGameplayFountains.cs @@ -3,6 +3,8 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Utils; @@ -19,8 +21,11 @@ namespace osu.Game.Screens.Play private Bindable kiaiStarFountains = null!; + private Sample? sample; + private SampleChannel? sampleChannel; + [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load(OsuConfigManager config, AudioManager audio) { kiaiStarFountains = config.GetBindable(OsuSetting.StarFountains); @@ -41,6 +46,8 @@ namespace osu.Game.Screens.Play X = -75, }, }; + + sample = audio.Samples.Get(@"Gameplay/fountain-shoot"); } private bool isTriggered; @@ -66,6 +73,11 @@ namespace osu.Game.Screens.Play { leftFountain.Shoot(1); rightFountain.Shoot(-1); + + // Track sample channel to avoid overlapping playback + sampleChannel?.Stop(); + sampleChannel = sample?.GetChannel(); + sampleChannel?.Play(); } public partial class GameplayStarFountain : StarFountain From ec6ff240f38ef69d37c50437c8f97b5fa3804c90 Mon Sep 17 00:00:00 2001 From: "Giovanni D." Date: Sun, 2 Mar 2025 00:49:04 -0800 Subject: [PATCH 620/801] Add taskbar flashing when a multiplayer game is starting --- osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 111b453adb..e5bc683d19 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -30,6 +30,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private MultiplayerClient client { get; set; } = null!; + [Resolved] + private OsuGame? game { get; set; } + private IBindable isConnected = null!; private readonly TaskCompletionSource resultsReady = new TaskCompletionSource(); @@ -142,6 +145,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (client.LocalUser?.State == MultiplayerUserState.Loaded) { + game?.Window?.Flash(); loadingDisplay.Show(); client.ChangeState(MultiplayerUserState.ReadyForGameplay); } From 35a21b44a698f0cbe84db036f03c1f26202a8d75 Mon Sep 17 00:00:00 2001 From: "Giovanni D." Date: Sun, 2 Mar 2025 20:43:32 -0800 Subject: [PATCH 621/801] Change timing of the flash --- osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs | 4 ---- .../OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs | 5 +++++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index e5bc683d19..111b453adb 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -30,9 +30,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private MultiplayerClient client { get; set; } = null!; - [Resolved] - private OsuGame? game { get; set; } - private IBindable isConnected = null!; private readonly TaskCompletionSource resultsReady = new TaskCompletionSource(); @@ -145,7 +142,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (client.LocalUser?.State == MultiplayerUserState.Loaded) { - game?.Window?.Flash(); loadingDisplay.Show(); client.ChangeState(MultiplayerUserState.ReadyForGameplay); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs index 7eb7f6610e..dd9cb56862 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs @@ -18,6 +18,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private MultiplayerClient multiplayerClient { get; set; } = null!; + [Resolved] + private OsuGame? game { get; set; } + private Player? player; public MultiplayerPlayerLoader(Func createPlayer) @@ -39,6 +42,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { base.OnPlayerLoaded(); + game?.Window?.Flash(); + multiplayerClient.ChangeState(MultiplayerUserState.Loaded) .ContinueWith(task => failAndBail(task.Exception?.Message ?? "Server error"), TaskContinuationOptions.NotOnRanToCompletion); } From ad9a963bd0fa831c30f7a79abf62a797aa087c3f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 3 Mar 2025 14:19:19 +0900 Subject: [PATCH 622/801] Exit loop when cancellation requested The following manages to create all hitobjects but proceeds to get stuck in this method: `dotnet run -- difficulty 1607040 -r:2` --- osu.Game/Rulesets/Objects/HitObject.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index 9f980769e2..d9e62ccecb 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -114,6 +114,8 @@ namespace osu.Game.Rulesets.Objects { foreach (HitObject hitObject in nestedHitObjects) { + cancellationToken.ThrowIfCancellationRequested(); + if (hitObject is IHasComboInformation n) { n.ComboIndexBindable.BindTo(hasCombo.ComboIndexBindable); From 52dad09b2011c014b2ec5acb4947aacbc3ba4d90 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 3 Mar 2025 14:19:38 +0900 Subject: [PATCH 623/801] Cancel slider generation when requested Didn't notice a particular case with this one, just came up as I was looking through code. --- osu.Game/Rulesets/Objects/SliderEventGenerator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs index f5146d1675..e5e15042ff 100644 --- a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs +++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs @@ -46,6 +46,8 @@ namespace osu.Game.Rulesets.Objects for (int span = 0; span < spanCount; span++) { + cancellationToken.ThrowIfCancellationRequested(); + double spanStartTime = startTime + span * spanDuration; bool reversed = span % 2 == 1; From 033952029eecd814a62567c58eeafb5fe3fe5c99 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 3 Mar 2025 14:46:13 +0900 Subject: [PATCH 624/801] Cancel `ApplyDefaults()` when requested Also didn't notice a particular case here, but if all code passes up until we get to the `foreach (var h in nestedHitObjects)` below, then we could end up stuck here for quite a while. --- osu.Game/Rulesets/Objects/HitObject.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index d9e62ccecb..07e07b25d3 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -104,6 +104,8 @@ namespace osu.Game.Rulesets.Objects /// The cancellation token. public void ApplyDefaults(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty, CancellationToken cancellationToken = default) { + cancellationToken.ThrowIfCancellationRequested(); + ApplyDefaultsToSelf(controlPointInfo, difficulty); nestedHitObjects.Clear(); From 47747aed3e9feb09c3b6d9f82703cedda8db3035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Mar 2025 08:40:51 +0100 Subject: [PATCH 625/801] Add guards to prevent clamp calls with invalid bounds --- osu.Game/Screens/Play/HUDOverlay.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 19190ac362..78c602d8f1 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -277,17 +277,17 @@ namespace osu.Game.Screens.Play if (rulesetComponents != null) processDrawables(rulesetComponents); - if (lowestTopScreenSpaceRight.HasValue) + if (lowestTopScreenSpaceRight.HasValue && DrawHeight - TopRightElements.DrawHeight > 0) TopRightElements.Y = Math.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceRight.Value)).Y, 0, DrawHeight - TopRightElements.DrawHeight); else TopRightElements.Y = 0; - if (lowestTopScreenSpaceLeft.HasValue) + if (lowestTopScreenSpaceLeft.HasValue && DrawHeight - LeaderboardFlow.DrawHeight > 0) LeaderboardFlow.Y = Math.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceLeft.Value)).Y, 0, DrawHeight - LeaderboardFlow.DrawHeight); else LeaderboardFlow.Y = 0; - if (highestBottomScreenSpace.HasValue) + if (highestBottomScreenSpace.HasValue && DrawHeight - bottomRightElements.DrawHeight > 0) bottomRightElements.Y = BottomScoringElementsHeight = -Math.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - bottomRightElements.DrawHeight); else bottomRightElements.Y = 0; From 0a50fb1dfac7b0898c134f98c47a459fbbeb769c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Mar 2025 09:32:27 +0100 Subject: [PATCH 626/801] Add failing test case --- .../Beatmaps/BeatmapExtensionsTest.cs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 osu.Game.Tests/Beatmaps/BeatmapExtensionsTest.cs diff --git a/osu.Game.Tests/Beatmaps/BeatmapExtensionsTest.cs b/osu.Game.Tests/Beatmaps/BeatmapExtensionsTest.cs new file mode 100644 index 0000000000..1dda2e314d --- /dev/null +++ b/osu.Game.Tests/Beatmaps/BeatmapExtensionsTest.cs @@ -0,0 +1,58 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Osu.Objects; + +namespace osu.Game.Tests.Beatmaps +{ + public class BeatmapExtensionsTest + { + [Test] + public void TestLengthCalculations() + { + var beatmap = new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 5_000 }, + new HitCircle { StartTime = 300_000 }, + new Spinner { StartTime = 280_000, Duration = 40_000 } + }, + Breaks = + { + new BreakPeriod(50_000, 75_000), + new BreakPeriod(100_000, 150_000), + } + }; + + Assert.That(beatmap.CalculatePlayableBounds(), Is.EqualTo((5_000, 320_000))); + Assert.That(beatmap.CalculatePlayableLength(), Is.EqualTo(315_000)); // 320_000 - 5_000 + Assert.That(beatmap.CalculateDrainLength(), Is.EqualTo(240_000)); // 315_000 - (25_000 + 50_000) = 315_000 - 75_000 + } + + [Test] + public void TestDrainLengthCannotGoNegative() + { + var beatmap = new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 5_000 }, + new HitCircle { StartTime = 300_000 }, + new Spinner { StartTime = 280_000, Duration = 40_000 } + }, + Breaks = + { + new BreakPeriod(0, 350_000), + } + }; + + Assert.That(beatmap.CalculatePlayableBounds(), Is.EqualTo((5_000, 320_000))); + Assert.That(beatmap.CalculatePlayableLength(), Is.EqualTo(315_000)); // 320_000 - 5_000 + Assert.That(beatmap.CalculateDrainLength(), Is.EqualTo(0)); // break period encompasses entire beatmap + } + } +} From 87fb8da3517ae0f2d0669dc3afa9b233454c49bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Mar 2025 09:35:46 +0100 Subject: [PATCH 627/801] Fix drain length calculation helper method being able to return negative durations This is the principal failure behind https://github.com/ppy/osu-server-beatmap-submission/issues/40. --- osu.Game/Beatmaps/IBeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 826d4e19a7..f95fcefd7e 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -161,7 +161,7 @@ namespace osu.Game.Beatmaps /// /// Find the total milliseconds between the first and last hittable objects, excluding any break time. /// - public static double CalculateDrainLength(this IBeatmap beatmap) => CalculatePlayableLength(beatmap.HitObjects) - beatmap.TotalBreakTime; + public static double CalculateDrainLength(this IBeatmap beatmap) => Math.Max(CalculatePlayableLength(beatmap.HitObjects) - beatmap.TotalBreakTime, 0); /// /// Find the timestamps in milliseconds of the start and end of the playable region. From 52860def6c7fb40dcd1d6291f867751c7d08aecb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Mar 2025 18:53:41 +0900 Subject: [PATCH 628/801] Always zoom timeline to centre rather than focus point Closes https://github.com/ppy/osu/issues/32183. --- .../Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs index 9db14ce4c4..b483f23d1d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs @@ -141,7 +141,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (e.AltPressed) { // zoom when holding alt. - AdjustZoomRelatively(e.ScrollDelta.Y, zoomedContent.ToLocalSpace(e.ScreenSpaceMousePosition).X); + AdjustZoomRelatively(e.ScrollDelta.Y); return true; } From f32a8e8741f4dcd8d915be78a93686ab101d1d74 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Mar 2025 18:54:46 +0900 Subject: [PATCH 629/801] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 614f1409bf..e35eaf5645 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 28f9e734f0d3dbf374d90b72e8380e1021aab98e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Mar 2025 12:23:52 +0100 Subject: [PATCH 630/801] Add failing test case --- .../TestScenePlaylistsResultsScreen.cs | 103 +++++++++++++----- 1 file changed, 76 insertions(+), 27 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index dc5fb20e16..469f7c8b74 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -69,9 +69,11 @@ namespace osu.Game.Tests.Visual.Playlists totalCount = 0; userScore = TestResources.CreateTestScoreInfo(); + userScore.OnlineID = 1; userScore.TotalScore = 0; userScore.Statistics = new Dictionary(); userScore.MaximumStatistics = new Dictionary(); + userScore.Position = real_user_position; // Beatmap is required to be an actual beatmap so the scores can get their scores correctly // calculated for standardised scoring, else the tests that rely on ordering will fall over. @@ -243,6 +245,35 @@ namespace osu.Game.Tests.Visual.Playlists AddAssert("placeholder shown", () => this.ChildrenOfType().Count(), () => Is.EqualTo(1)); } + [Test] + public void TestFetchingAllTheWayToFirstNeverDisplaysNegativePosition() + { + AddStep("set user position", () => userScore.Position = 20); + AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); + + createResultsWithScore(() => userScore); + waitForDisplay(); + + AddStep("bind delayed handler", () => bindHandler(true)); + + for (int i = 0; i < 2; i++) + { + AddStep("simulate user falling down ranking", () => userScore.Position += 2); + AddStep("scroll to left", () => resultsScreen.ChildrenOfType().Single().ChildrenOfType().Single().ScrollToStart(false)); + + AddAssert("left loading spinner shown", () => + resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Visible); + + waitForDisplay(); + + AddAssert("left loading spinner hidden", () => + resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Hidden); + } + + AddAssert("total count is 34", () => this.ChildrenOfType().Count(), () => Is.EqualTo(34)); + AddUntilStep("all panels have non-negative position", () => this.ChildrenOfType().All(p => p.ScorePosition.Value > 0)); + } + private void createResultsWithScore(Func getScore) { AddStep("load results", () => @@ -331,7 +362,7 @@ namespace osu.Game.Tests.Visual.Playlists if (userScore == null) triggerFail(s); else - triggerSuccess(s, createUserResponse(userScore)); + triggerSuccess(s, () => createUserResponse(userScore)); break; @@ -339,12 +370,12 @@ namespace osu.Game.Tests.Visual.Playlists if (userScore == null) triggerFail(u); else - triggerSuccess(u, createUserResponse(userScore)); + triggerSuccess(u, () => createUserResponse(userScore)); break; case IndexPlaylistScoresRequest i: - triggerSuccess(i, createIndexResponse(i, noScores)); + triggerSuccess(i, () => createIndexResponse(i, noScores)); break; } }, delay); @@ -352,11 +383,11 @@ namespace osu.Game.Tests.Visual.Playlists return true; }; - private void triggerSuccess(APIRequest req, T result) + private void triggerSuccess(APIRequest req, Func result) where T : class { requestComplete = true; - req.TriggerSuccess(result); + req.TriggerSuccess(result.Invoke()); } private void triggerFail(APIRequest req) @@ -367,28 +398,13 @@ namespace osu.Game.Tests.Visual.Playlists private MultiplayerScore createUserResponse(ScoreInfo userScore) { - var multiplayerUserScore = new MultiplayerScore - { - ID = highestScoreId, - Accuracy = userScore.Accuracy, - Passed = userScore.Passed, - Rank = userScore.Rank, - Position = real_user_position, - MaxCombo = userScore.MaxCombo, - User = userScore.User, - BeatmapId = RNG.Next(0, 7), - ScoresAround = new MultiplayerScoresAround - { - Higher = new MultiplayerScores(), - Lower = new MultiplayerScores() - } - }; + var multiplayerUserScore = createMultiplayerUserScore(userScore); totalCount++; for (int i = 1; i <= scores_per_result; i++) { - multiplayerUserScore.ScoresAround.Lower.Scores.Add(new MultiplayerScore + multiplayerUserScore.ScoresAround!.Lower!.Scores.Add(new MultiplayerScore { ID = getNextLowestScoreId(), Accuracy = userScore.Accuracy, @@ -404,7 +420,7 @@ namespace osu.Game.Tests.Visual.Playlists }, }); - multiplayerUserScore.ScoresAround.Higher.Scores.Add(new MultiplayerScore + multiplayerUserScore.ScoresAround!.Higher!.Scores.Add(new MultiplayerScore { ID = getNextHighestScoreId(), Accuracy = userScore.Accuracy, @@ -423,12 +439,32 @@ namespace osu.Game.Tests.Visual.Playlists totalCount += 2; } - addCursor(multiplayerUserScore.ScoresAround.Lower); - addCursor(multiplayerUserScore.ScoresAround.Higher); + addCursor(multiplayerUserScore.ScoresAround!.Lower!); + addCursor(multiplayerUserScore.ScoresAround!.Higher!); return multiplayerUserScore; } + private MultiplayerScore createMultiplayerUserScore(ScoreInfo userScore) + { + return new MultiplayerScore + { + ID = highestScoreId, + Accuracy = userScore.Accuracy, + Passed = userScore.Passed, + Rank = userScore.Rank, + Position = userScore.Position, + MaxCombo = userScore.MaxCombo, + User = userScore.User, + BeatmapId = RNG.Next(0, 7), + ScoresAround = new MultiplayerScoresAround + { + Higher = new MultiplayerScores(), + Lower = new MultiplayerScores() + } + }; + } + private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req, bool noScores) { var result = new IndexedMultiplayerScores(); @@ -437,11 +473,21 @@ namespace osu.Game.Tests.Visual.Playlists string sort = req.IndexParams?.Properties["sort"].ToObject() ?? "score_desc"; + bool reachedEnd = false; + for (int i = 1; i <= scores_per_result; i++) { + int nextId = sort == "score_asc" ? getNextHighestScoreId() : getNextLowestScoreId(); + + if (userScore.OnlineID - nextId >= userScore.Position) + { + reachedEnd = true; + break; + } + result.Scores.Add(new MultiplayerScore { - ID = sort == "score_asc" ? getNextHighestScoreId() : getNextLowestScoreId(), + ID = nextId, Accuracy = 1, Passed = true, Rank = ScoreRank.X, @@ -458,7 +504,10 @@ namespace osu.Game.Tests.Visual.Playlists totalCount++; } - addCursor(result); + if (!reachedEnd) + addCursor(result); + + result.UserScore = createMultiplayerUserScore(userScore); return result; } From bf4fa58f72c61ff217c2d20a48f86d9aa65a4862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Mar 2025 12:56:28 +0100 Subject: [PATCH 631/801] Fix playlists results screens potentially displaying negative score positions Closes https://github.com/ppy/osu/issues/31434. --- .../Playlists/PlaylistItemResultsScreen.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index 184de2f50c..0e539936d8 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -185,6 +185,24 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { higherScores = index; setPositions(index, pivot, -1); + + // when paginating the results, it's possible for the user's score to naturally fall down the rankings. + // unmitigated, this can cause scores at the very top of the rankings to have zero or negative positions + // because the positions are counted backwards from the user's score, which has increased in this case during pagination. + // if this happens, just give the top score the first position. + // note that this isn't 100% correct, but it *is* however the most reliable way to mask the problem. + int smallestPosition = index.Scores.Min(s => s.Position ?? 1); + + if (smallestPosition < 1) + { + int offset = 1 - smallestPosition; + + foreach (var scorePanel in ScorePanelList.GetScorePanels()) + scorePanel.ScorePosition.Value += offset; + + foreach (var score in index.Scores) + score.Position += offset; + } } return await transformScores(index.Scores).ConfigureAwait(false); From d7d5eec58ca4e5438ba22686c7f5f1c1f0a70ca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Mar 2025 13:52:10 +0100 Subject: [PATCH 632/801] Update failing assertions Change in behaviour is expected in this case. --- .../Visual/Editing/TestSceneZoomableScrollContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs index 1c8a18e131..2c84e76b2e 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs @@ -128,12 +128,12 @@ namespace osu.Game.Tests.Visual.Editing AddStep("Press alt down", () => InputManager.PressKey(Key.AltLeft)); AddStep("Scroll by 3", () => InputManager.ScrollBy(new Vector2(0, 3))); AddAssert("Box not at 0", () => !Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft)); - AddAssert("Box 1/4 at 1/4", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.25f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X)); + AddAssert("Box 1/2 at 1/2", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.5f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.5f * scrollQuad.Size.X)); // Scroll out at 0.25 AddStep("Scroll by -3", () => InputManager.ScrollBy(new Vector2(0, -3))); AddAssert("Box at 0", () => Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft)); - AddAssert("Box 1/4 at 1/4", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.25f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X)); + AddAssert("Box 1/2 at 1/2", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.5f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.5f * scrollQuad.Size.X)); AddStep("Release alt", () => InputManager.ReleaseKey(Key.AltLeft)); } From 23a5d6dc401a9944a544eae923da134fa75a090f Mon Sep 17 00:00:00 2001 From: andy840119 Date: Mon, 3 Mar 2025 22:09:48 +0800 Subject: [PATCH 633/801] This method is not being used anymore. see: https://github.com/ppy/osu/pull/26643 --- .../Screens/Edit/Compose/Components/SelectionHandler.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 39fff169b7..bfe7fe523f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -151,14 +151,6 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public virtual SelectionRotationHandler CreateRotationHandler() => new SelectionRotationHandler(); - /// - /// Handles the selected items being scaled. - /// - /// The delta scale to apply, in local coordinates. - /// The point of reference where the scale is originating from. - /// Whether any items could be scaled. - public virtual bool HandleScale(Vector2 scale, Anchor anchor) => false; - /// /// Creates the handler to use for scale operations. /// From cab849b5d91cb1aab055798d1b1e353feba0c598 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 3 Mar 2025 14:23:39 -0800 Subject: [PATCH 634/801] Use web localisable string for team channel label --- osu.Game/Overlays/Chat/ChannelList/ChannelList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs index 0a89775cc7..03f6923455 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs @@ -85,7 +85,7 @@ namespace osu.Game.Overlays.Chat.ChannelList AnnounceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper(), FontAwesome.Solid.Bullhorn, false), PublicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper(), FontAwesome.Solid.Comments, false), selector = new ChannelListItem(ChannelListingChannel), - TeamChannelGroup = new ChannelGroup("TEAM", FontAwesome.Solid.Users, false), // TODO: replace with osu-web localisable string once available + TeamChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleTEAM.ToUpper(), FontAwesome.Solid.Users, false), PrivateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper(), FontAwesome.Solid.Envelope, true), }, }, From 550ff85550056bb947e67ead816c87004885da91 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Mar 2025 11:22:47 +0900 Subject: [PATCH 635/801] Cancel difficulty calculation after 10 seconds by default --- .../Difficulty/DifficultyCalculator.cs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 14acc9b908..add24f7866 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -62,6 +62,11 @@ namespace osu.Game.Rulesets.Difficulty /// A structure describing the difficulty of the beatmap. public DifficultyAttributes Calculate([NotNull] IEnumerable mods, CancellationToken cancellationToken = default) { + using var timedCancellationSource = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + if (!cancellationToken.CanBeCanceled) + cancellationToken = timedCancellationSource.Token; + cancellationToken.ThrowIfCancellationRequested(); preProcess(mods, cancellationToken); @@ -98,6 +103,11 @@ namespace osu.Game.Rulesets.Difficulty /// The set of . public List CalculateTimed([NotNull] IEnumerable mods, CancellationToken cancellationToken = default) { + using var timedCancellationSource = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + if (!cancellationToken.CanBeCanceled) + cancellationToken = timedCancellationSource.Token; + cancellationToken.ThrowIfCancellationRequested(); preProcess(mods, cancellationToken); @@ -166,15 +176,10 @@ namespace osu.Game.Rulesets.Difficulty /// /// The original list of s. /// The cancellation token. - private void preProcess([NotNull] IEnumerable mods, CancellationToken cancellationToken = default) + private void preProcess([NotNull] IEnumerable mods, CancellationToken cancellationToken) { playableMods = mods.Select(m => m.DeepClone()).ToArray(); - - // Only pass through the cancellation token if it's non-default. - // This allows for the default timeout to be applied for playable beatmap construction. - Beatmap = cancellationToken == default - ? beatmap.GetPlayableBeatmap(ruleset, playableMods) - : beatmap.GetPlayableBeatmap(ruleset, playableMods, cancellationToken); + Beatmap = beatmap.GetPlayableBeatmap(ruleset, playableMods, cancellationToken); var track = new TrackVirtual(10000); playableMods.OfType().ForEach(m => m.ApplyToTrack(track)); From df25734834b005d4b072e0338aaa892e4f776d1c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Mar 2025 11:36:36 +0900 Subject: [PATCH 636/801] Fix intermittent score panel test --- .../Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index 02a321d22f..eade5aaf5d 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Ranking showPanel(TestResources.CreateTestScoreInfo(beatmap)); }); - AddAssert("pp display faded out", () => + AddUntilStep("pp display faded out", () => { var ppDisplay = this.ChildrenOfType().Single(); return ppDisplay.Alpha == 0.5 && ppDisplay.TooltipText == ResultsScreenStrings.NoPPForUnrankedBeatmaps; @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.Ranking showPanel(score); }); - AddAssert("pp display faded out", () => + AddUntilStep("pp display faded out", () => { var ppDisplay = this.ChildrenOfType().Single(); return ppDisplay.Alpha == 0.5 && ppDisplay.TooltipText == ResultsScreenStrings.NoPPForUnrankedMods; @@ -116,7 +116,7 @@ namespace osu.Game.Tests.Visual.Ranking showPanel(score); }); - AddAssert("pp display faded out", () => this.ChildrenOfType().Single().Alpha == 1); + AddUntilStep("pp display faded out", () => this.ChildrenOfType().Single().Alpha == 1); } [Test] From 963df165df34db1c0020c44bb5c0c343fb24cab1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Mar 2025 12:33:33 +0900 Subject: [PATCH 637/801] Add failing test --- .../StatefulMultiplayerClientTest.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs index 559db16751..a6d715df62 100644 --- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs +++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs @@ -6,6 +6,7 @@ using Humanizer; using NUnit.Framework; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Testing; +using osu.Game.Extensions; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; @@ -93,6 +94,29 @@ namespace osu.Game.Tests.NonVisual.Multiplayer checkPlayingUserCount(1); } + [Test] + public void TestJoinRoomWithManyUsers() + { + AddStep("leave room", () => MultiplayerClient.LeaveRoom()); + AddUntilStep("wait for room part", () => !RoomJoined); + + AddStep("create room with many users", () => + { + var newRoom = new Room(); + newRoom.CopyFrom(SelectedRoom.Value!); + + newRoom.RoomID = null; + MultiplayerClient.RoomSetupAction = room => + { + room.Users.AddRange(Enumerable.Range(PLAYER_1_ID, 100).Select(id => new MultiplayerRoomUser(id))); + }; + + RoomManager.CreateRoom(newRoom); + }); + + AddUntilStep("wait for room join", () => RoomJoined); + } + private void checkPlayingUserCount(int expectedCount) => AddAssert($"{"user".ToQuantity(expectedCount)} playing", () => MultiplayerClient.CurrentMatchPlayingUserIds.Count == expectedCount); From 3024a98658a62a4042d9946a8a72c85c98b8be97 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Mar 2025 12:34:02 +0900 Subject: [PATCH 638/801] Fix unable to join multiplayer rooms with many users --- .../Online/API/Requests/GetUsersRequest.cs | 8 +++--- .../Online/Multiplayer/MultiplayerClient.cs | 27 ++++++++++--------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/osu.Game/Online/API/Requests/GetUsersRequest.cs b/osu.Game/Online/API/Requests/GetUsersRequest.cs index cd75ff4e31..fe7ba8c33d 100644 --- a/osu.Game/Online/API/Requests/GetUsersRequest.cs +++ b/osu.Game/Online/API/Requests/GetUsersRequest.cs @@ -13,14 +13,14 @@ namespace osu.Game.Online.API.Requests /// public class GetUsersRequest : APIRequest { - public readonly int[] UserIds; + public const int MAX_IDS_PER_REQUEST = 50; - private const int max_ids_per_request = 50; + public readonly int[] UserIds; public GetUsersRequest(int[] userIds) { - if (userIds.Length > max_ids_per_request) - throw new ArgumentException($"{nameof(GetUsersRequest)} calls only support up to {max_ids_per_request} IDs at once"); + if (userIds.Length > MAX_IDS_PER_REQUEST) + throw new ArgumentException($"{nameof(GetUsersRequest)} calls only support up to {MAX_IDS_PER_REQUEST} IDs at once"); UserIds = userIds; } diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 2d445ea25a..9abc013b66 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -815,19 +815,22 @@ namespace osu.Game.Online.Multiplayer /// The s to populate. protected async Task PopulateUsers(IEnumerable multiplayerUsers) { - var request = new GetUsersRequest(multiplayerUsers.Select(u => u.UserID).Distinct().ToArray()); - - await API.PerformAsync(request).ConfigureAwait(false); - - if (request.Response == null) - return; - - Dictionary users = request.Response.Users.ToDictionary(user => user.Id); - - foreach (var multiplayerUser in multiplayerUsers) + foreach (int[] userChunk in multiplayerUsers.Select(u => u.UserID).Distinct().Chunk(GetUsersRequest.MAX_IDS_PER_REQUEST)) { - if (users.TryGetValue(multiplayerUser.UserID, out var user)) - multiplayerUser.User = user; + var request = new GetUsersRequest(userChunk); + + await API.PerformAsync(request).ConfigureAwait(false); + + if (request.Response == null) + return; + + Dictionary users = request.Response.Users.ToDictionary(user => user.Id); + + foreach (var multiplayerUser in multiplayerUsers) + { + if (users.TryGetValue(multiplayerUser.UserID, out var user)) + multiplayerUser.User = user; + } } } From 4a00662092a13cd1e6352400ec76403dff80f657 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Mar 2025 14:02:45 +0900 Subject: [PATCH 639/801] Fix thread safety when kicking multiplayer users --- .../Online/Multiplayer/MultiplayerClient.cs | 61 ++++++++++--------- .../OnlinePlay/Multiplayer/Multiplayer.cs | 8 +-- 2 files changed, 35 insertions(+), 34 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 59a4547e9e..91b4ed448c 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -486,18 +486,44 @@ namespace osu.Game.Online.Multiplayer }, false); } - Task IMultiplayerClient.UserLeft(MultiplayerRoomUser user) => - handleUserLeft(user, UserLeft); + Task IMultiplayerClient.UserLeft(MultiplayerRoomUser user) + { + Scheduler.Add(() => handleUserLeft(user, UserLeft), false); + return Task.CompletedTask; + } Task IMultiplayerClient.UserKicked(MultiplayerRoomUser user) { - if (LocalUser == null) - return Task.CompletedTask; + Scheduler.Add(() => + { + if (LocalUser == null) + return; - if (user.Equals(LocalUser)) - LeaveRoom(); + if (user.Equals(LocalUser)) + LeaveRoom(); - return handleUserLeft(user, UserKicked); + handleUserLeft(user, UserKicked); + }, false); + + return Task.CompletedTask; + } + + private void handleUserLeft(MultiplayerRoomUser user, Action? callback) + { + Debug.Assert(ThreadSafety.IsUpdateThread); + + if (Room == null) + return; + + Room.Users.Remove(user); + PlayingUserIds.Remove(user.UserID); + + Debug.Assert(APIRoom != null); + APIRoom.RecentParticipants = APIRoom.RecentParticipants.Where(u => u.Id != user.UserID).ToArray(); + APIRoom.ParticipantCount--; + + callback?.Invoke(user); + RoomUpdated?.Invoke(); } async Task IMultiplayerClient.Invited(int invitedBy, long roomID, string password) @@ -544,27 +570,6 @@ namespace osu.Game.Online.Multiplayer APIRoom.ParticipantCount++; } - private Task handleUserLeft(MultiplayerRoomUser user, Action? callback) - { - Scheduler.Add(() => - { - if (Room == null) - return; - - Room.Users.Remove(user); - PlayingUserIds.Remove(user.UserID); - - Debug.Assert(APIRoom != null); - APIRoom.RecentParticipants = APIRoom.RecentParticipants.Where(u => u.Id != user.UserID).ToArray(); - APIRoom.ParticipantCount--; - - callback?.Invoke(user); - RoomUpdated?.Invoke(); - }, false); - - return Task.CompletedTask; - } - Task IMultiplayerClient.HostChanged(int userId) { Scheduler.Add(() => diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index dfed32aebc..0b06a16d98 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -28,11 +28,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onRoomUpdated() { - if (client.Room == null) + if (client.Room == null || client.LocalUser == null) return; - Debug.Assert(client.LocalUser != null); - // If the user exits gameplay before score submission completes, we'll transition to idle when results has been prepared. if (client.LocalUser.State == MultiplayerUserState.Results && this.IsCurrentScreen()) transitionFromResults(); @@ -62,11 +60,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { base.OnResuming(e); - if (client.Room == null) + if (client.Room == null || client.LocalUser == null) return; - Debug.Assert(client.LocalUser != null); - if (!(e.Last is MultiplayerPlayerLoader playerLoader)) return; From b73a872b94f1053f57612ad32c1d07c88b8d908c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Mar 2025 14:11:32 +0900 Subject: [PATCH 640/801] Fix broken test --- .../NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs index 959f09361f..4019ff6730 100644 --- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs +++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs @@ -97,16 +97,12 @@ namespace osu.Game.Tests.NonVisual.Multiplayer AddStep("create room with many users", () => { - var newRoom = new Room(); - newRoom.CopyFrom(SelectedRoom.Value!); - - newRoom.RoomID = null; MultiplayerClient.RoomSetupAction = room => { room.Users.AddRange(Enumerable.Range(PLAYER_1_ID, 100).Select(id => new MultiplayerRoomUser(id))); }; - RoomManager.CreateRoom(newRoom); + MultiplayerClient.JoinRoom(MultiplayerClient.ServerSideRooms.Single()).ConfigureAwait(false); }); AddUntilStep("wait for room join", () => RoomJoined); From 0696cfa4f241516b83face7a526b8626de01930c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 14:40:33 +0900 Subject: [PATCH 641/801] `LoungePollingComponent` -> `LoungeListingPoller` --- .../Visual/Multiplayer/TestSceneMultiplayer.cs | 2 +- ...ollingComponent.cs => LoungeListingPoller.cs} | 4 ++-- .../Screens/OnlinePlay/Lounge/LoungeSubScreen.cs | 16 ++++++++-------- 3 files changed, 11 insertions(+), 11 deletions(-) rename osu.Game/Screens/OnlinePlay/Lounge/{LoungePollingComponent.cs => LoungeListingPoller.cs} (91%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index a87216287d..ec0117a990 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -805,7 +805,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for room", () => this.ChildrenOfType().Any()); AddStep("select room", () => InputManager.Key(Key.Down)); - AddStep("disable polling", () => this.ChildrenOfType().Single().TimeBetweenPolls.Value = 0); + AddStep("disable polling", () => this.ChildrenOfType().Single().TimeBetweenPolls.Value = 0); AddStep("change server-side settings", () => { multiplayerClient.ServerSideRooms[0].Name = "New name"; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungePollingComponent.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeListingPoller.cs similarity index 91% rename from osu.Game/Screens/OnlinePlay/Lounge/LoungePollingComponent.cs rename to osu.Game/Screens/OnlinePlay/Lounge/LoungeListingPoller.cs index 420a96cf8a..d92ae7eb6e 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungePollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeListingPoller.cs @@ -14,9 +14,9 @@ using osu.Game.Screens.OnlinePlay.Lounge.Components; namespace osu.Game.Screens.OnlinePlay.Lounge { /// - /// A that polls for the lounge listing. + /// Polls for rooms for the main lounge listing. /// - public partial class LoungePollingComponent : PollingComponent + public partial class LoungeListingPoller : PollingComponent { [Resolved] private IAPIProvider api { get; set; } = null!; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index e83334eb69..12c0bb12e2 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -75,7 +75,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private readonly IBindable operationInProgress = new Bindable(); private readonly IBindable isIdle = new BindableBool(); private RoomsContainer roomsContainer = null!; - private LoungePollingComponent pollingComponent = null!; + private LoungeListingPoller listingPoller = null!; private PopoverContainer popoverContainer = null!; private LoadingLayer loadingLayer = null!; private SearchTextBox searchTextBox = null!; @@ -92,7 +92,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge InternalChildren = new Drawable[] { - pollingComponent = new LoungePollingComponent + listingPoller = new LoungeListingPoller { RoomsReceived = onListingReceived, Filter = { BindTarget = filter } @@ -187,7 +187,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { roomsContainer.Rooms.Clear(); hasListingResults.Value = false; - pollingComponent.PollImmediately(); + listingPoller.PollImmediately(); }); updateFilter(); @@ -268,7 +268,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge onReturning(); // Poll for any newly-created rooms (including potentially the user's own). - pollingComponent.PollImmediately(); + listingPoller.PollImmediately(); } public override bool OnExiting(ScreenExitEvent e) @@ -379,7 +379,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected virtual void OpenNewRoom(Room room) => this.Push(CreateRoomSubScreen(room)); - public void RefreshRooms() => pollingComponent.PollImmediately(); + public void RefreshRooms() => listingPoller.PollImmediately(); private void updateLoadingLayer() { @@ -392,11 +392,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private void updatePollingRate(bool isCurrentScreen) { if (!isCurrentScreen) - pollingComponent.TimeBetweenPolls.Value = 0; + listingPoller.TimeBetweenPolls.Value = 0; else - pollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 120000 : 15000; + listingPoller.TimeBetweenPolls.Value = isIdle.Value ? 120000 : 15000; - Logger.Log($"Polling adjusted (listing: {pollingComponent.TimeBetweenPolls.Value})"); + Logger.Log($"Polling adjusted (listing: {listingPoller.TimeBetweenPolls.Value})"); } protected abstract OsuButton CreateNewRoomButton(); From 77d5b1d5dd605f94b81205d00fc055697e77a7ef Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Mar 2025 14:36:54 +0900 Subject: [PATCH 642/801] Fix multiplayer not joining correct chat channel --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 1 + osu.Game/Online/Multiplayer/MultiplayerRoom.cs | 7 +++++++ osu.Game/Online/Rooms/Room.cs | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 59a4547e9e..82836a00f0 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -235,6 +235,7 @@ namespace osu.Game.Online.Multiplayer APIRoom = apiRoom; APIRoom.RoomID = joinedRoom.RoomID; + APIRoom.ChannelId = joinedRoom.ChannelID; APIRoom.Host = joinedRoom.Host?.User; APIRoom.Playlist = joinedRoom.Playlist.Select(item => new PlaylistItem(item)).ToArray(); APIRoom.CurrentPlaylistItem = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId); diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs index f7bd4490ff..b8b90d907f 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs @@ -59,6 +59,12 @@ namespace osu.Game.Online.Multiplayer [Key(7)] public IList ActiveCountdowns { get; set; } = new List(); + /// + /// The ID of the chat channel for the room. + /// + [Key(8)] + public int ChannelID { get; set; } + [JsonConstructor] [SerializationConstructor] public MultiplayerRoom(long roomId) @@ -69,6 +75,7 @@ namespace osu.Game.Online.Multiplayer public MultiplayerRoom(Room room) { RoomID = room.RoomID ?? 0; + ChannelID = room.ChannelId; Settings = new MultiplayerRoomSettings(room); Host = room.Host != null ? new MultiplayerRoomUser(room.Host.OnlineID) : null; Playlist = room.Playlist.Select(p => new MultiplayerPlaylistItem(p)).ToArray(); diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index c5e292a19d..e965f9c187 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -242,7 +242,7 @@ namespace osu.Game.Online.Rooms public int ChannelId { get => channelId; - private set => SetField(ref channelId, value); + set => SetField(ref channelId, value); } /// From 9e8a6117280fa4ccf1dbe7fb545ca072f397d085 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 15:05:12 +0900 Subject: [PATCH 643/801] Rename `RoomsContainer` and scope down bindables --- .../TestSceneLoungeRoomsContainer.cs | 4 +-- .../TestScenePlaylistsLoungeSubScreen.cs | 16 +++++----- .../OnlinePlay/DrawableRoomPlaylist.cs | 2 +- .../{RoomsContainer.cs => RoomListing.cs} | 29 ++++++++++++++----- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 14 ++++----- 5 files changed, 39 insertions(+), 26 deletions(-) rename osu.Game/Screens/OnlinePlay/Lounge/Components/{RoomsContainer.cs => RoomListing.cs} (91%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index 772eb91174..b43433fe8d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public partial class TestSceneLoungeRoomsContainer : OnlinePlayTestScene { private BindableList rooms = null!; - private RoomsContainer container = null!; + private RoomListing container = null!; public override void SetUpSteps() { @@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Anchor = Anchor.Centre, Origin = Anchor.Centre, Width = 0.5f, - Child = container = new RoomsContainer + Child = container = new RoomListing { RelativeSizeAxes = Axes.Both, Rooms = { BindTarget = rooms }, diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs index 35bf6dc28a..ceb3a32402 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Playlists AddUntilStep("wait for present", () => loungeScreen.IsCurrentScreen()); } - private RoomsContainer roomsContainer => loungeScreen.ChildrenOfType().First(); + private RoomListing roomListing => loungeScreen.ChildrenOfType().First(); [Test] public void TestManyRooms() @@ -41,13 +41,13 @@ namespace osu.Game.Tests.Visual.Playlists createRooms(GenerateRooms(30)); - AddStep("move mouse to third room", () => InputManager.MoveMouseTo(roomsContainer.DrawableRooms[2])); + AddStep("move mouse to third room", () => InputManager.MoveMouseTo(roomListing.DrawableRooms[2])); AddStep("hold down", () => InputManager.PressButton(MouseButton.Left)); - AddStep("drag to top", () => InputManager.MoveMouseTo(roomsContainer.DrawableRooms[0])); + AddStep("drag to top", () => InputManager.MoveMouseTo(roomListing.DrawableRooms[0])); AddAssert("first and second room masked", () - => !checkRoomVisible(roomsContainer.DrawableRooms[0]) && - !checkRoomVisible(roomsContainer.DrawableRooms[1])); + => !checkRoomVisible(roomListing.DrawableRooms[0]) && + !checkRoomVisible(roomListing.DrawableRooms[1])); } [Test] @@ -55,10 +55,10 @@ namespace osu.Game.Tests.Visual.Playlists { createRooms(GenerateRooms(30)); - AddStep("select last room", () => roomsContainer.DrawableRooms[^1].TriggerClick()); + AddStep("select last room", () => roomListing.DrawableRooms[^1].TriggerClick()); - AddUntilStep("first room is masked", () => !checkRoomVisible(roomsContainer.DrawableRooms[0])); - AddUntilStep("last room is not masked", () => checkRoomVisible(roomsContainer.DrawableRooms[^1])); + AddUntilStep("first room is masked", () => !checkRoomVisible(roomListing.DrawableRooms[0])); + AddUntilStep("last room is not masked", () => checkRoomVisible(roomListing.DrawableRooms[^1])); } private bool checkRoomVisible(DrawableRoom room) => diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index 207e0bdf55..c9d8365852 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -204,7 +204,7 @@ namespace osu.Game.Screens.OnlinePlay ScrollContainer.ScrollIntoView(drawableItem); } - #region Key selection logic (shared with BeatmapCarousel and RoomsContainer) + #region Key selection logic (shared with BeatmapCarousel and RoomListing) public bool OnPressed(KeyBindingPressEvent e) { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs similarity index 91% rename from osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs rename to osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs index 65f969bc7b..1c3db87aaf 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs @@ -21,12 +21,25 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Lounge.Components { - public partial class RoomsContainer : CompositeDrawable, IKeyBindingHandler + public partial class RoomListing : CompositeDrawable, IKeyBindingHandler { + /// + /// Rooms which should be displayed. Should be managed externally. + /// public readonly BindableList Rooms = new BindableList(); - public readonly Bindable SelectedRoom = new Bindable(); + + /// + /// The current filter criteria. Should be managed externally. + /// public readonly Bindable Filter = new Bindable(); + /// + /// The currently user-selected room. + /// + public IBindable SelectedRoom => selectedRoom; + + private readonly Bindable selectedRoom = new Bindable(); + public IReadOnlyList DrawableRooms => roomFlow.FlowingChildren.Cast().ToArray(); private readonly ScrollContainer scroll; @@ -35,7 +48,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components // handle deselection public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; - public RoomsContainer() + public RoomListing() { InternalChild = scroll = new OsuScrollContainer { @@ -158,7 +171,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { foreach (var room in rooms) { - var drawableRoom = new DrawableLoungeRoom(room) { SelectedRoom = SelectedRoom }; + var drawableRoom = new DrawableLoungeRoom(room) { SelectedRoom = selectedRoom }; roomFlow.Add(drawableRoom); @@ -177,7 +190,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components // selection may have a lease due to being in a sub screen. if (SelectedRoom.Value == r && !SelectedRoom.Disabled) - SelectedRoom.Value = null; + selectedRoom.Value = null; } } @@ -187,13 +200,13 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components // selection may have a lease due to being in a sub screen. if (!SelectedRoom.Disabled) - SelectedRoom.Value = null; + selectedRoom.Value = null; } protected override bool OnClick(ClickEvent e) { if (!SelectedRoom.Disabled) - SelectedRoom.Value = null; + selectedRoom.Value = null; return base.OnClick(e); } @@ -240,7 +253,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components // we already have a valid selection only change selection if we still have a room to switch to. if (room != null) - SelectedRoom.Value = room; + selectedRoom.Value = room; } #endregion diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 12c0bb12e2..c1c65a744a 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected override BackgroundScreen CreateBackground() => new LoungeBackgroundScreen { - SelectedRoom = { BindTarget = roomsContainer.SelectedRoom } + SelectedRoom = { BindTarget = roomListing.SelectedRoom } }; protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby(); @@ -74,7 +74,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private readonly Bindable hasListingResults = new Bindable(); private readonly IBindable operationInProgress = new Bindable(); private readonly IBindable isIdle = new BindableBool(); - private RoomsContainer roomsContainer = null!; + private RoomListing roomListing = null!; private LoungeListingPoller listingPoller = null!; private PopoverContainer popoverContainer = null!; private LoadingLayer loadingLayer = null!; @@ -106,7 +106,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge Horizontal = WaveOverlayContainer.WIDTH_PADDING, Top = Header.HEIGHT + controls_area_height + 20, }, - Child = roomsContainer = new RoomsContainer + Child = roomListing = new RoomListing { RelativeSizeAxes = Axes.Both, Filter = { BindTarget = filter }, @@ -185,7 +185,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge filter.BindValueChanged(_ => { - roomsContainer.Rooms.Clear(); + roomListing.Rooms.Clear(); hasListingResults.Value = false; listingPoller.PollImmediately(); }); @@ -195,11 +195,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private void onListingReceived(Room[] result) { - Dictionary localRoomsById = roomsContainer.Rooms.ToDictionary(r => r.RoomID!.Value); + Dictionary localRoomsById = roomListing.Rooms.ToDictionary(r => r.RoomID!.Value); Dictionary resultRoomsById = result.ToDictionary(r => r.RoomID!.Value); // Remove all local rooms no longer in the result set. - roomsContainer.Rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value)); + roomListing.Rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value)); // Add or update local rooms with the result set. foreach (var r in result) @@ -207,7 +207,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge if (localRoomsById.TryGetValue(r.RoomID!.Value, out Room? existingRoom)) existingRoom.CopyFrom(r); else - roomsContainer.Rooms.Add(r); + roomListing.Rooms.Add(r); } hasListingResults.Value = true; From a0888a7f2c5f7839243c7502a755643dddb664d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 15:51:08 +0900 Subject: [PATCH 644/801] Attempt to fix common editor test failures See https://github.com/ppy/osu/actions/runs/13623586844/job/38143232417?pr=32180 for one example. Arguably the bindable usage in [`ControlPointPart`](https://github.com/ppy/osu/blob/2365b065a4994f38fe67bab7d193e5a09bee538c/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs#L24-L26) is dangerous, but it's only dangerous in tests (because control points aren't mutated outside the editor) so I'm willing to turn a blind eye for now to favour async loading support. --- .../Editing/TestSceneEditorBeatmapCreation.cs | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index b7990b64c1..1413c4f436 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -171,6 +171,8 @@ namespace osu.Game.Tests.Visual.Editing return difficultyName != null && difficultyName != firstDifficultyName; }); + ensureEditorLoaded(); + AddAssert("created difficulty has timing point", () => { var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single(); @@ -215,6 +217,8 @@ namespace osu.Game.Tests.Visual.Editing return difficultyName != null && difficultyName != previousDifficultyName; }); + ensureEditorLoaded(); + AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = previousDifficultyName = Guid.NewGuid().ToString()); AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 })); AddStep("add effect points", () => @@ -239,6 +243,8 @@ namespace osu.Game.Tests.Visual.Editing return difficultyName != null && difficultyName != previousDifficultyName; }); + ensureEditorLoaded(); + AddAssert("created difficulty has timing point", () => { var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single(); @@ -287,6 +293,8 @@ namespace osu.Game.Tests.Visual.Editing return difficultyName != null && difficultyName != firstDifficultyName; }); + ensureEditorLoaded(); + AddAssert("created difficulty has timing point", () => { var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single(); @@ -367,6 +375,8 @@ namespace osu.Game.Tests.Visual.Editing return difficultyName != null && difficultyName != originalDifficultyName; }); + ensureEditorLoaded(); + AddAssert("created difficulty has copy suffix in name", () => EditorBeatmap.BeatmapInfo.DifficultyName == copyDifficultyName); AddAssert("created difficulty has timing point", () => { @@ -377,7 +387,9 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("approach rate correctly copied", () => EditorBeatmap.Difficulty.ApproachRate == 4); AddAssert("combo colours correctly copied", () => EditorBeatmap.BeatmapSkin.AsNonNull().ComboColours.Count == 2); + ensureEditorLoaded(); AddAssert("status is modified", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.LocallyModified); + AddAssert("online ID not copied", () => EditorBeatmap.BeatmapInfo.OnlineID == -1); AddStep("save beatmap", () => Editor.Save()); @@ -440,6 +452,8 @@ namespace osu.Game.Tests.Visual.Editing return difficultyName != null && difficultyName != originalDifficultyName; }); + ensureEditorLoaded(); + AddStep("save without changes", () => Editor.Save()); AddAssert("collection still points to old beatmap", () => !collection.BeatmapMD5Hashes.Contains(EditorBeatmap.BeatmapInfo.MD5Hash) @@ -477,6 +491,9 @@ namespace osu.Game.Tests.Visual.Editing string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; return difficultyName != null && difficultyName != "New Difficulty"; }); + + ensureEditorLoaded(); + AddAssert("new difficulty has correct name", () => EditorBeatmap.BeatmapInfo.DifficultyName == "New Difficulty (1)"); AddAssert("new difficulty persisted", () => { @@ -514,6 +531,10 @@ namespace osu.Game.Tests.Visual.Editing return difficultyName != null && difficultyName != duplicate_difficulty_name; }); + ensureEditorLoaded(); + + ensureEditorLoaded(); + AddStep("set difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = duplicate_difficulty_name); AddStep("try to save beatmap", () => Editor.Save()); AddAssert("beatmap set not corrupted", () => @@ -540,6 +561,8 @@ namespace osu.Game.Tests.Visual.Editing return set != null && set.PerformRead(s => s.Beatmaps.Count == 1 && s.Files.Count == 1); }); + ensureEditorLoaded(); + AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new CatchRuleset().RulesetInfo)); AddUntilStep("wait for created", () => @@ -547,7 +570,8 @@ namespace osu.Game.Tests.Visual.Editing string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; return difficultyName != null && difficultyName != duplicate_difficulty_name; }); - AddUntilStep("wait for editor load", () => Editor.IsLoaded && DialogOverlay.IsLoaded); + + ensureEditorLoaded(); AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { @@ -584,6 +608,9 @@ namespace osu.Game.Tests.Visual.Editing string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; return difficultyName != null && difficultyName == "New Difficulty"; }); + + ensureEditorLoaded(); + AddAssert("new difficulty persisted", () => { var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId); @@ -610,6 +637,9 @@ namespace osu.Game.Tests.Visual.Editing string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; return difficultyName != null && difficultyName == "New Difficulty (1)"; }); + + ensureEditorLoaded(); + AddAssert("new difficulty persisted", () => { var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId); @@ -735,6 +765,8 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("other audio not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio (1).mp3")); } + private void ensureEditorLoaded() => AddUntilStep("wait for editor load", () => Editor.IsLoaded && DialogOverlay.IsLoaded); + private void createNewDifficulty() { string? currentDifficulty = null; @@ -748,13 +780,14 @@ namespace osu.Game.Tests.Visual.Editing AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction()); + AddUntilStep("wait for created", () => { string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; return difficultyName != null && difficultyName != currentDifficulty; }); + ensureEditorLoaded(); - AddUntilStep("wait for editor load", () => Editor.IsLoaded); AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup); AddUntilStep("wait for load", () => Editor.ChildrenOfType().Any()); } @@ -765,7 +798,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep($"switch to difficulty #{index + 1}", () => Editor.SwitchToDifficulty(Beatmap.Value.BeatmapSetInfo.Beatmaps.ElementAt(index))); - AddUntilStep("wait for editor load", () => Editor.IsLoaded); + ensureEditorLoaded(); AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup); AddUntilStep("wait for load", () => Editor.ChildrenOfType().Any()); } From 4085ee805a717e2f0869a445b294b08d9730e2e6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 15:29:13 +0900 Subject: [PATCH 645/801] Adjust scale and display of rooms in multiplayer lounge Just a quick pass because the rooms were definitely larger than they should be. --- .../Lounge/Components/RoomListing.cs | 25 ++++- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 104 ++++++++++++------ 2 files changed, 90 insertions(+), 39 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs index 1c3db87aaf..0276601656 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs @@ -45,14 +45,20 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private readonly ScrollContainer scroll; private readonly FillFlowContainer roomFlow; + private const float display_scale = 0.8f; + // handle deselection public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; public RoomListing() { - InternalChild = scroll = new OsuScrollContainer + InternalChild = scroll = new Scroll { + Masking = false, RelativeSizeAxes = Axes.Both, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Width = display_scale, ScrollbarOverlapsContent = false, Padding = new MarginPadding { Right = 5 }, Child = new OsuContextMenuContainer @@ -64,12 +70,18 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, - Spacing = new Vector2(10), + Spacing = new Vector2(5), + Margin = new MarginPadding { Vertical = 10 }, } } }; } + private partial class Scroll : OsuScrollContainer + { + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + } + protected override void LoadComplete() { SelectedRoom.BindValueChanged(onSelectedRoomChanged, true); @@ -171,7 +183,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { foreach (var room in rooms) { - var drawableRoom = new DrawableLoungeRoom(room) { SelectedRoom = selectedRoom }; + var drawableRoom = new DrawableLoungeRoom(room) + { + SelectedRoom = selectedRoom, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(display_scale), + Width = 1 / display_scale, + }; roomFlow.Add(drawableRoom); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index c1c65a744a..c84f49fef6 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -8,9 +8,12 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; 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.Logging; @@ -27,6 +30,7 @@ using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Users; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Lounge { @@ -85,11 +89,15 @@ namespace osu.Game.Screens.OnlinePlay.Lounge [BackgroundDependencyLoader(true)] private void load() { + Masking = true; + const float controls_area_height = 25f; if (idleTracker != null) isIdle.BindTo(idleTracker.IsIdle); + Color4 bg = Color4Extensions.FromHex("#070405"); + InternalChildren = new Drawable[] { listingPoller = new LoungeListingPoller @@ -113,56 +121,80 @@ namespace osu.Game.Screens.OnlinePlay.Lounge } }, loadingLayer = new LoadingLayer(true), - new FillFlowContainer + new Container { - Name = @"Header area flow", + Name = "Header area", RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = WaveOverlayContainer.WIDTH_PADDING }, - Direction = FillDirection.Vertical, Children = new Drawable[] { - new Container + new Box { - RelativeSizeAxes = Axes.X, - Height = Header.HEIGHT, - Child = searchTextBox = new BasicSearchTextBox - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - RelativeSizeAxes = Axes.X, - Width = 0.6f, - }, + Colour = ColourInfo.GradientVertical(bg, bg.Opacity(0.75f)), + RelativeSizeAxes = Axes.Both, + Height = 0.8f, }, - new Container + new Box { + Colour = ColourInfo.GradientVertical(bg.Opacity(0.75f), bg.Opacity(0)), + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.Both, + Y = 0.8f, + // Intentionally taller than the header for a more gradual fade + Height = 0.5f, + }, + new FillFlowContainer + { + Name = @"Header area flow", RelativeSizeAxes = Axes.X, - Height = controls_area_height, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.WIDTH_PADDING }, + Direction = FillDirection.Vertical, Children = new Drawable[] { - Buttons.WithChild(CreateNewRoomButton().With(d => + new Container { - d.Anchor = Anchor.BottomLeft; - d.Origin = Anchor.BottomLeft; - d.Size = new Vector2(150, 37.5f); - d.Action = () => Open(); - })), - new FillFlowContainer - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10), - ChildrenEnumerable = CreateFilterControls().Select(f => f.With(d => + RelativeSizeAxes = Axes.X, + Height = Header.HEIGHT, + Child = searchTextBox = new BasicSearchTextBox { - d.Anchor = Anchor.TopRight; - d.Origin = Anchor.TopRight; - })) + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.X, + Width = 0.6f, + }, + }, + new Container + { + RelativeSizeAxes = Axes.X, + Height = controls_area_height, + Children = new Drawable[] + { + Buttons.WithChild(CreateNewRoomButton().With(d => + { + d.Anchor = Anchor.BottomLeft; + d.Origin = Anchor.BottomLeft; + d.Size = new Vector2(150, 37.5f); + d.Action = () => Open(); + })), + new FillFlowContainer + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10), + ChildrenEnumerable = CreateFilterControls().Select(f => f.With(d => + { + d.Anchor = Anchor.TopRight; + d.Origin = Anchor.TopRight; + })) + } + } } - } - } - }, + }, + }, + } }, }; } From 4a16b4bd984f8564eec8c940be245ffb3f5014ee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 16:15:40 +0900 Subject: [PATCH 646/801] Fix typo in xmldoc --- osu.Game/Online/Chat/ChannelManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 74e85c595c..e9ca0a8ed2 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -411,7 +411,7 @@ namespace osu.Game.Online.Chat } /// - /// Find an existing channel instance for the provided channel. Lookup is performed basd on ID. + /// Find an existing channel instance for the provided channel. Lookup is performed based on ID. /// The provided channel may be used if an existing instance is not found. /// /// A candidate channel to be used for lookup or permanently on lookup failure. From b19c2c7f9faae5025ece2352e5617b29d6f744f9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Mar 2025 17:01:41 +0900 Subject: [PATCH 647/801] Update recently-added test --- .../Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index c01cb70955..e5e4921a17 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -318,7 +318,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add playlist item", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { From 5b0e54a77d712e4b7b924eeb6d2092dc5aa8848a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Mar 2025 17:22:19 +0900 Subject: [PATCH 648/801] Remove duplicated assert --- osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 1413c4f436..996e87ff8a 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -533,8 +533,6 @@ namespace osu.Game.Tests.Visual.Editing ensureEditorLoaded(); - ensureEditorLoaded(); - AddStep("set difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = duplicate_difficulty_name); AddStep("try to save beatmap", () => Editor.Save()); AddAssert("beatmap set not corrupted", () => From f0d6641adf8a4076c886c9dfa321d2281e925361 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 17:44:48 +0900 Subject: [PATCH 649/801] Add basic subclassing and implement beatmap-start flow --- .../SongSelectV2/TestSceneSongSelect.cs | 4 +- .../TestSceneSongSelectNavigation.cs | 2 +- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 8 ++++ osu.Game/Screens/SelectV2/SoloSongSelect.cs | 28 +++++++++++++ .../{SongSelectV2.cs => SongSelect.cs} | 41 ++++++++++++------- 5 files changed, 65 insertions(+), 18 deletions(-) create mode 100644 osu.Game/Screens/SelectV2/SoloSongSelect.cs rename osu.Game/Screens/SelectV2/{SongSelectV2.cs => SongSelect.cs} (84%) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs index 6d180c76d9..630f3c95ee 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs @@ -78,8 +78,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { base.SetUpSteps(); - AddStep("load screen", () => Stack.Push(new Screens.SelectV2.SongSelectV2())); - AddUntilStep("wait for load", () => Stack.CurrentScreen is Screens.SelectV2.SongSelectV2 songSelect && songSelect.IsLoaded); + AddStep("load screen", () => Stack.Push(new Screens.SelectV2.SoloSongSelect())); + AddUntilStep("wait for load", () => Stack.CurrentScreen is Screens.SelectV2.SongSelect songSelect && songSelect.IsLoaded); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectNavigation.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectNavigation.cs index 5173cb5673..a7ca3cd18c 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectNavigation.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectNavigation.cs @@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 base.SetUpSteps(); AddStep("press enter", () => InputManager.Key(Key.Enter)); AddWaitStep("wait", 5); - PushAndConfirm(() => new Screens.SelectV2.SongSelectV2()); + PushAndConfirm(() => new Screens.SelectV2.SoloSongSelect()); } [Test] diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index c6bce228dc..7372847402 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -20,6 +20,8 @@ namespace osu.Game.Screens.SelectV2 [Cached] public partial class BeatmapCarousel : Carousel { + public Action? RequestPresentBeatmap { private get; init; } + public const float SPACING = 5f; private IBindableList detachedBeatmaps = null!; @@ -128,6 +130,12 @@ namespace osu.Game.Screens.SelectV2 return; case BeatmapInfo beatmapInfo: + if (ReferenceEquals(CurrentSelection, beatmapInfo)) + { + RequestPresentBeatmap?.Invoke(beatmapInfo); + return; + } + CurrentSelection = beatmapInfo; return; } diff --git a/osu.Game/Screens/SelectV2/SoloSongSelect.cs b/osu.Game/Screens/SelectV2/SoloSongSelect.cs new file mode 100644 index 0000000000..e6ecdc6705 --- /dev/null +++ b/osu.Game/Screens/SelectV2/SoloSongSelect.cs @@ -0,0 +1,28 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Screens; +using osu.Game.Screens.Play; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class SoloSongSelect : SongSelect + { + protected override bool OnStart() + { + this.Push(new PlayerLoaderV2(() => new SoloPlayer())); + return false; + } + + private partial class PlayerLoaderV2 : PlayerLoader + { + public override bool ShowFooter => true; + + public PlayerLoaderV2(Func createPlayer) + : base(createPlayer) + { + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/SongSelectV2.cs b/osu.Game/Screens/SelectV2/SongSelect.cs similarity index 84% rename from osu.Game/Screens/SelectV2/SongSelectV2.cs rename to osu.Game/Screens/SelectV2/SongSelect.cs index 23139c8742..5458a02583 100644 --- a/osu.Game/Screens/SelectV2/SongSelectV2.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -11,7 +10,7 @@ using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; -using osu.Game.Screens.Play; +using osu.Game.Screens.Select; using osu.Game.Screens.SelectV2.Footer; namespace osu.Game.Screens.SelectV2 @@ -20,7 +19,7 @@ namespace osu.Game.Screens.SelectV2 /// This screen is intended to house all components introduced in the new song select design to add transitions and examine the overall look. /// This will be gradually built upon and ultimately replace once everything is in place. /// - public partial class SongSelectV2 : OsuScreen + public abstract partial class SongSelect : OsuScreen { private const float logo_scale = 0.4f; @@ -29,6 +28,8 @@ namespace osu.Game.Screens.SelectV2 [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + private BeatmapCarousel carousel = null!; + public override bool ShowFooter => true; [Resolved] @@ -58,8 +59,9 @@ namespace osu.Game.Screens.SelectV2 { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Bottom = ScreenFooter.HEIGHT }, - Child = new BeatmapCarousel + Child = carousel = new BeatmapCarousel { + RequestPresentBeatmap = _ => OnStart(), RelativeSizeAxes = Axes.Both }, }, @@ -141,11 +143,17 @@ namespace osu.Game.Screens.SelectV2 logo.Action = () => { - this.Push(new PlayerLoaderV2(() => new SoloPlayer())); + OnStart(); return false; }; } + /// + /// Called when a selection is made. + /// + /// If a resultant action occurred that takes the user away from SongSelect. + protected abstract bool OnStart(); + protected override void LogoSuspending(OsuLogo logo) { base.LogoSuspending(logo); @@ -160,19 +168,22 @@ namespace osu.Game.Screens.SelectV2 logo.FadeOut(120, Easing.Out); } + /// + /// Set the query to the search text box. + /// + /// The string to search. + public void Search(string query) + { + carousel.Filter(new FilterCriteria + { + // TODO: this should only set the text of the current criteria, not use a completely new criteria. + SearchText = query, + }); + } + private partial class SoloModSelectOverlay : UserModSelectOverlay { protected override bool ShowPresets => true; } - - private partial class PlayerLoaderV2 : PlayerLoader - { - public override bool ShowFooter => true; - - public PlayerLoaderV2(Func createPlayer) - : base(createPlayer) - { - } - } } } From 1be3b990e7589b2c1f1ae8e9fec64989f79902c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 18:09:58 +0900 Subject: [PATCH 650/801] Add transition for selecting a beatmap --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 27 +++++++++++++++++ osu.Game/Screens/SelectV2/Carousel.cs | 32 ++++++++++---------- osu.Game/Screens/SelectV2/SongSelect.cs | 12 ++++++-- 3 files changed, 52 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 7372847402..1c730169eb 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -10,6 +10,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Pooling; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.UserInterface; @@ -260,6 +261,32 @@ namespace osu.Game.Screens.SelectV2 #endregion + #region Animation + + /// + /// Moves non-selected beatmaps to the right, hiding off-screen. + /// + public bool VisuallyFocusSelected { get; set; } + + private float selectionFocusOffset; + + protected override void Update() + { + base.Update(); + + selectionFocusOffset = (float)Interpolation.DampContinuously(selectionFocusOffset, VisuallyFocusSelected ? 300 : 0, 100, Time.Elapsed); + + foreach (var panel in Scroll.Panels) + { + var c = (ICarouselPanel)panel; + + if (!c.Selected.Value) + panel.X += selectionFocusOffset; + } + } + + #endregion + #region Filtering public FilterCriteria Criteria { get; private set; } = new FilterCriteria(); diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index e50281e713..1a120e69e7 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -75,7 +75,7 @@ namespace osu.Game.Screens.SelectV2 /// /// The number of items currently actualised into drawables. /// - public int VisibleItems => scroll.Panels.Count; + public int VisibleItems => Scroll.Panels.Count; /// /// The currently selected model. Generally of type T. @@ -185,7 +185,7 @@ namespace osu.Game.Screens.SelectV2 /// The item to find a related drawable representation. /// The drawable representation if it exists. protected Drawable? GetMaterialisedDrawableForItem(CarouselItem item) => - scroll.Panels.SingleOrDefault(p => ((ICarouselPanel)p).Item == item); + Scroll.Panels.SingleOrDefault(p => ((ICarouselPanel)p).Item == item); /// /// When a user is traversing the carousel via group selection keys, assert whether the item provided is a valid target. @@ -222,11 +222,11 @@ namespace osu.Game.Screens.SelectV2 #region Initialisation - private readonly CarouselScrollContainer scroll; + protected readonly CarouselScrollContainer Scroll; protected Carousel() { - InternalChild = scroll = new CarouselScrollContainer + InternalChild = Scroll = new CarouselScrollContainer { RelativeSizeAxes = Axes.Both, }; @@ -499,13 +499,13 @@ namespace osu.Game.Screens.SelectV2 // If a keyboard selection is currently made, we want to keep the view stable around the selection. // That means that we should offset the immediate scroll position by any change in Y position for the selection. if (prevKeyboard.YPosition != null && currentKeyboardSelection.YPosition != prevKeyboard.YPosition) - scroll.OffsetScrollPosition((float)(currentKeyboardSelection.YPosition!.Value - prevKeyboard.YPosition.Value)); + Scroll.OffsetScrollPosition((float)(currentKeyboardSelection.YPosition!.Value - prevKeyboard.YPosition.Value)); } private void scrollToSelection() { if (currentKeyboardSelection.CarouselItem != null) - scroll.ScrollTo(currentKeyboardSelection.CarouselItem.CarouselYPosition - visibleHalfHeight); + Scroll.ScrollTo(currentKeyboardSelection.CarouselItem.CarouselYPosition - visibleHalfHeight); } #endregion @@ -519,12 +519,12 @@ namespace osu.Game.Screens.SelectV2 /// /// The position of the lower visible bound with respect to the current scroll position. /// - private float visibleBottomBound => (float)(scroll.Current + DrawHeight + BleedBottom); + private float visibleBottomBound => (float)(Scroll.Current + DrawHeight + BleedBottom); /// /// The position of the upper visible bound with respect to the current scroll position. /// - private float visibleUpperBound => (float)(scroll.Current - BleedTop); + private float visibleUpperBound => (float)(Scroll.Current - BleedTop); /// /// Half the height of the visible content. @@ -557,7 +557,7 @@ namespace osu.Game.Screens.SelectV2 double selectedYPos = currentSelection.CarouselItem?.CarouselYPosition ?? 0; - foreach (var panel in scroll.Panels) + foreach (var panel in Scroll.Panels) { var c = (ICarouselPanel)panel; @@ -566,12 +566,12 @@ namespace osu.Game.Screens.SelectV2 continue; float normalisedDepth = (float)(Math.Abs(selectedYPos - c.DrawYPosition) / DrawHeight); - scroll.Panels.ChangeChildDepth(panel, c.Item.DepthLayer + normalisedDepth); + Scroll.Panels.ChangeChildDepth(panel, c.Item.DepthLayer + normalisedDepth); if (c.DrawYPosition != c.Item.CarouselYPosition) c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed); - Vector2 posInScroll = scroll.ToLocalSpace(panel.ScreenSpaceDrawQuad.Centre); + Vector2 posInScroll = Scroll.ToLocalSpace(panel.ScreenSpaceDrawQuad.Centre); float dist = Math.Abs(1f - posInScroll.Y / visibleHalfHeight); panel.X = offsetX(dist, visibleHalfHeight); @@ -628,7 +628,7 @@ namespace osu.Game.Screens.SelectV2 toDisplay.RemoveAll(i => !i.IsVisible); // Iterate over all panels which are already displayed and figure which need to be displayed / removed. - foreach (var panel in scroll.Panels) + foreach (var panel in Scroll.Panels) { var carouselPanel = (ICarouselPanel)panel; @@ -658,7 +658,7 @@ namespace osu.Game.Screens.SelectV2 carouselPanel.DrawYPosition = item.CarouselYPosition; carouselPanel.Item = item; - scroll.Add(drawable); + Scroll.Add(drawable); } // Update the total height of all items (to make the scroll container scrollable through the full height even though @@ -666,10 +666,10 @@ namespace osu.Game.Screens.SelectV2 if (carouselItems.Count > 0) { var lastItem = carouselItems[^1]; - scroll.SetLayoutHeight((float)(lastItem.CarouselYPosition + lastItem.DrawHeight + visibleHalfHeight)); + Scroll.SetLayoutHeight((float)(lastItem.CarouselYPosition + lastItem.DrawHeight + visibleHalfHeight)); } else - scroll.SetLayoutHeight(0); + Scroll.SetLayoutHeight(0); } private static void expirePanelImmediately(Drawable panel) @@ -713,7 +713,7 @@ namespace osu.Game.Screens.SelectV2 /// Implementation of scroll container which handles very large vertical lists by internally using double precision /// for pre-display Y values. /// - private partial class CarouselScrollContainer : UserTrackingScrollContainer, IKeyBindingHandler + protected partial class CarouselScrollContainer : UserTrackingScrollContainer, IKeyBindingHandler { public readonly Container Panels; diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 5458a02583..70452de99a 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -99,9 +99,13 @@ namespace osu.Game.Screens.SelectV2 base.OnEntering(e); } + private const double fade_duration = 300; + public override void OnResuming(ScreenTransitionEvent e) { - this.FadeIn(); + this.FadeIn(fade_duration, Easing.OutQuint); + + carousel.VisuallyFocusSelected = false; // required due to https://github.com/ppy/osu-framework/issues/3218 modSelectOverlay.SelectedMods.Disabled = false; @@ -112,16 +116,18 @@ namespace osu.Game.Screens.SelectV2 public override void OnSuspending(ScreenTransitionEvent e) { - this.Delay(400).FadeOut(); + this.Delay(100).FadeOut(fade_duration, Easing.OutQuint); modSelectOverlay.SelectedMods.UnbindFrom(Mods); + carousel.VisuallyFocusSelected = true; + base.OnSuspending(e); } public override bool OnExiting(ScreenExitEvent e) { - this.Delay(400).FadeOut(); + this.FadeOut(fade_duration, Easing.OutQuint); return base.OnExiting(e); } From 918315aa65a5d5447d8e7b98c16a81611aa5f7e9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 19:19:53 +0900 Subject: [PATCH 651/801] Split out methods so retrieving the room is not a callback function --- .../StatefulMultiplayerClientTest.cs | 3 +- .../TestSceneMultiSpectatorLeaderboard.cs | 3 +- .../TestSceneMultiSpectatorScreen.cs | 4 +- .../TestSceneMultiplayerMatchSongSelect.cs | 5 ++- .../TestSceneMultiplayerParticipantsList.cs | 3 +- .../Multiplayer/TestSceneMultiplayerPlayer.cs | 3 +- .../TestSceneMultiplayerPlaylist.cs | 4 +- .../TestSceneMultiplayerQueueList.cs | 4 +- .../TestSceneMultiplayerSpectateButton.cs | 4 +- .../Multiplayer/MultiplayerTestScene.cs | 44 ++++++++----------- 10 files changed, 43 insertions(+), 34 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs index 230a996942..8364e58bdc 100644 --- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs +++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs @@ -19,7 +19,8 @@ namespace osu.Game.Tests.NonVisual.Multiplayer public override void SetUpSteps() { base.SetUpSteps(); - JoinDefaultRoom(); + AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + WaitForJoined(); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs index 1821c2f3bc..60358dfbc4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs @@ -24,7 +24,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); - JoinDefaultRoom(); + AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + WaitForJoined(); AddStep("reset", () => { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index 3fdbe02906..aa98dc59db 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -66,7 +66,9 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("clear playing users", () => playingUsers.Clear()); - JoinDefaultRoom(r => room = r); + AddStep("create room", () => room = CreateDefaultRoom()); + AddStep("join room", () => JoinRoom(room)); + WaitForJoined(); } [TestCase(1)] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 287d7f5816..9c85bdd57a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -62,7 +62,10 @@ namespace osu.Game.Tests.Visual.Multiplayer public override void SetUpSteps() { base.SetUpSteps(); - JoinDefaultRoom(r => room = r); + + AddStep("create room", () => room = CreateDefaultRoom()); + AddStep("join room", () => JoinRoom(room)); + WaitForJoined(); } private void setUp() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index b5655afb8c..ed3fd4a6f8 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -32,7 +32,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); - JoinDefaultRoom(); + AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + WaitForJoined(); createNewParticipantsList(); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs index 1a5be48cad..99bec1e714 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs @@ -25,7 +25,8 @@ namespace osu.Game.Tests.Visual.Multiplayer public override void SetUpSteps() { base.SetUpSteps(); - JoinDefaultRoom(); + AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + WaitForJoined(); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 54932db7c6..7c8691d5d1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -47,7 +47,9 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); - JoinDefaultRoom(r => room = r); + AddStep("create room", () => room = CreateDefaultRoom()); + AddStep("join room", () => JoinRoom(room)); + WaitForJoined(); AddStep("create list", () => { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index 5eba67bab5..1a7b677798 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -43,7 +43,9 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); - JoinDefaultRoom(r => room = r); + AddStep("create room", () => room = CreateDefaultRoom()); + AddStep("join room", () => JoinRoom(room)); + WaitForJoined(); AddStep("create playlist", () => { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index f92721b04b..9e6734ce99 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -47,7 +47,9 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); - JoinDefaultRoom(r => room = r); + AddStep("create room", () => room = CreateDefaultRoom()); + AddStep("join room", () => JoinRoom(room)); + WaitForJoined(); AddStep("create button", () => { diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index 8150807f4f..ac587d3bb2 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -1,7 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Visual.OnlinePlay; @@ -24,34 +24,28 @@ namespace osu.Game.Tests.Visual.Multiplayer public bool RoomJoined => MultiplayerClient.RoomJoined; + protected Room CreateDefaultRoom() + { + return new Room + { + Name = "test name", + Type = MatchType.HeadToHead, + Playlist = + [ + new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo) + { + RulesetID = Ruleset.Value.OnlineID + } + ] + }; + } + /// /// Creates and joins a basic multiplayer room. /// - /// A callback that may be used to further set up the room. - protected void JoinDefaultRoom(Action? setupFunc = null) - { - AddStep("join room", () => - { - Room room = new Room - { - Name = "test name", - Type = MatchType.HeadToHead, - Playlist = - [ - new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo) - { - RulesetID = Ruleset.Value.OnlineID - } - ] - }; + protected void JoinRoom(Room room) => MultiplayerClient.CreateRoom(room).FireAndForget(); - setupFunc?.Invoke(room); - - MultiplayerClient.CreateRoom(room).ConfigureAwait(false); - }); - - AddUntilStep("wait for room join", () => RoomJoined); - } + protected void WaitForJoined() => AddUntilStep("wait for room join", () => RoomJoined); protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies(); } From 21d35f9dae085c6c9bee4369af6e261e98dfc21e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 19:40:31 +0900 Subject: [PATCH 652/801] Use alternative method of offsetting X that conveys flow better --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 11 ++++------- osu.Game/Screens/SelectV2/Carousel.cs | 13 +++++++++---- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 1c730169eb..1c1f6fa7fb 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -275,14 +275,11 @@ namespace osu.Game.Screens.SelectV2 base.Update(); selectionFocusOffset = (float)Interpolation.DampContinuously(selectionFocusOffset, VisuallyFocusSelected ? 300 : 0, 100, Time.Elapsed); + } - foreach (var panel in Scroll.Panels) - { - var c = (ICarouselPanel)panel; - - if (!c.Selected.Value) - panel.X += selectionFocusOffset; - } + protected override float GetPanelXOffset(Drawable panel) + { + return base.GetPanelXOffset(panel) + (((ICarouselPanel)panel).Selected.Value ? 0 : selectionFocusOffset); } #endregion diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 1a120e69e7..5339b5358b 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -571,10 +571,7 @@ namespace osu.Game.Screens.SelectV2 if (c.DrawYPosition != c.Item.CarouselYPosition) c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed); - Vector2 posInScroll = Scroll.ToLocalSpace(panel.ScreenSpaceDrawQuad.Centre); - float dist = Math.Abs(1f - posInScroll.Y / visibleHalfHeight); - - panel.X = offsetX(dist, visibleHalfHeight); + panel.X = GetPanelXOffset(panel); c.Selected.Value = c.Item == currentSelection?.CarouselItem; c.KeyboardSelected.Value = c.Item == currentKeyboardSelection?.CarouselItem; @@ -582,6 +579,14 @@ namespace osu.Game.Screens.SelectV2 } } + protected virtual float GetPanelXOffset(Drawable panel) + { + Vector2 posInScroll = Scroll.ToLocalSpace(panel.ScreenSpaceDrawQuad.Centre); + float dist = Math.Abs(1f - posInScroll.Y / visibleHalfHeight); + + return offsetX(dist, visibleHalfHeight); + } + /// /// Computes the x-offset of currently visible items. Makes the carousel appear round. /// From b5696f97a072439946e0af495e9f4191d864fad7 Mon Sep 17 00:00:00 2001 From: Zihad Date: Tue, 4 Mar 2025 03:05:03 +0600 Subject: [PATCH 653/801] Show current beatmap info in window title --- osu.Game/OsuGame.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index d23d27c89e..3b55c320b3 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -828,6 +828,8 @@ namespace osu.Game { beatmap.OldValue?.CancelAsyncLoad(); beatmap.NewValue?.BeginAsyncLoad(); + + Host.Window.Title = $"{Name} - {beatmap.NewValue.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; } private void modsChanged(ValueChangedEvent> mods) From c051ff84d293e5c2408e7ff59f55e176f0d1f1a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Mar 2025 13:04:23 +0100 Subject: [PATCH 654/801] Add UI for assigning custom tags to beatmaps Visual part for https://github.com/ppy/osu/issues/31913. Opening separately for appropriate visual UI adjustments. Also mostly ready to be hooked up to the results screen, pending merge of https://github.com/ppy/osu-web/pull/11951. --- .../Visual/Ranking/TestSceneUserTagControl.cs | 85 +++ osu.Game/Beatmaps/APIBeatmapTag.cs | 16 + osu.Game/Configuration/SessionStatics.cs | 13 +- .../API/Requests/AddBeatmapTagRequest.cs | 31 + .../Online/API/Requests/ListTagsRequest.cs | 12 + .../API/Requests/RemoveBeatmapTagRequest.cs | 29 + .../API/Requests/Responses/APIBeatmap.cs | 6 + .../Online/API/Requests/Responses/APITag.cs | 19 + .../Requests/Responses/APITagCollection.cs | 14 + osu.Game/Screens/Ranking/UserTagControl.cs | 537 ++++++++++++++++++ 10 files changed, 756 insertions(+), 6 deletions(-) create mode 100644 osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs create mode 100644 osu.Game/Beatmaps/APIBeatmapTag.cs create mode 100644 osu.Game/Online/API/Requests/AddBeatmapTagRequest.cs create mode 100644 osu.Game/Online/API/Requests/ListTagsRequest.cs create mode 100644 osu.Game/Online/API/Requests/RemoveBeatmapTagRequest.cs create mode 100644 osu.Game/Online/API/Requests/Responses/APITag.cs create mode 100644 osu.Game/Online/API/Requests/Responses/APITagCollection.cs create mode 100644 osu.Game/Screens/Ranking/UserTagControl.cs diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs b/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs new file mode 100644 index 0000000000..ebfd553815 --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs @@ -0,0 +1,85 @@ +// 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.Graphics; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Screens.Ranking; + +namespace osu.Game.Tests.Visual.Ranking +{ + public partial class TestSceneUserTagControl : OsuTestScene + { + private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("set up working beatmap", () => + { + Beatmap.Value.BeatmapInfo.OnlineID = 42; + }); + AddStep("set up network requests", () => + { + dummyAPI.HandleRequest = request => + { + switch (request) + { + case ListTagsRequest listTagsRequest: + { + Scheduler.AddDelayed(() => listTagsRequest.TriggerSuccess(new APITagCollection + { + Tags = + [ + new APITag { Id = 1, Name = "tech", Description = "Tests uncommon skills.", }, + new APITag { Id = 2, Name = "alt", Description = "Colloquial term for maps which use rhythms that encourage the player to alternate notes. Typically distinct from burst or stream maps.", }, + new APITag { Id = 3, Name = "aim", Description = "Category for difficulty relating to cursor movement.", }, + new APITag { Id = 4, Name = "tap", Description = "Category for difficulty relating to tapping input.", }, + ] + }), 500); + return true; + } + + case GetBeatmapSetRequest getBeatmapSetRequest: + { + var beatmapSet = CreateAPIBeatmapSet(Beatmap.Value.BeatmapInfo); + beatmapSet.Beatmaps.Single().TopTags = + [ + new APIBeatmapTag { TagId = 3, VoteCount = 9 }, + ]; + Scheduler.AddDelayed(() => getBeatmapSetRequest.TriggerSuccess(beatmapSet), 500); + return true; + } + + case AddBeatmapTagRequest: + case RemoveBeatmapTagRequest: + { + Scheduler.AddDelayed(request.TriggerSuccess, 500); + return true; + } + } + + return false; + }; + }); + AddStep("create control", () => + { + Child = new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Child = new UserTagControl + { + Width = 500, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }; + }); + } + } +} diff --git a/osu.Game/Beatmaps/APIBeatmapTag.cs b/osu.Game/Beatmaps/APIBeatmapTag.cs new file mode 100644 index 0000000000..5f4f9b851d --- /dev/null +++ b/osu.Game/Beatmaps/APIBeatmapTag.cs @@ -0,0 +1,16 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; + +namespace osu.Game.Beatmaps +{ + public class APIBeatmapTag + { + [JsonProperty("tag_id")] + public long TagId { get; set; } + + [JsonProperty("count")] + public int VoteCount { get; set; } + } +} diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index d2069e4027..b816d1a88b 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework; using osu.Game.Graphics.UserInterface; using osu.Game.Input; @@ -27,11 +25,12 @@ namespace osu.Game.Configuration SetDefault(Static.FeaturedArtistDisclaimerShownOnce, false); SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null); SetDefault(Static.LastModSelectPanelSamplePlaybackTime, (double?)null); - SetDefault(Static.SeasonalBackgrounds, null); + SetDefault(Static.SeasonalBackgrounds, null); SetDefault(Static.TouchInputActive, RuntimeInfo.IsMobile); - SetDefault(Static.LastLocalUserScore, null); - SetDefault(Static.LastAppliedOffsetScore, null); - SetDefault(Static.UserOnlineActivity, null); + SetDefault(Static.LastLocalUserScore, null); + SetDefault(Static.LastAppliedOffsetScore, null); + SetDefault(Static.UserOnlineActivity, null); + SetDefault(Static.AllBeatmapTags, null); } /// @@ -99,5 +98,7 @@ namespace osu.Game.Configuration /// The activity for the current user to broadcast to other players. /// UserOnlineActivity, + + AllBeatmapTags, } } diff --git a/osu.Game/Online/API/Requests/AddBeatmapTagRequest.cs b/osu.Game/Online/API/Requests/AddBeatmapTagRequest.cs new file mode 100644 index 0000000000..4fa02dc569 --- /dev/null +++ b/osu.Game/Online/API/Requests/AddBeatmapTagRequest.cs @@ -0,0 +1,31 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Globalization; +using System.Net.Http; +using osu.Framework.IO.Network; + +namespace osu.Game.Online.API.Requests +{ + public class AddBeatmapTagRequest : APIRequest + { + public int BeatmapID { get; } + public long TagID { get; } + + public AddBeatmapTagRequest(int beatmapID, long tagID) + { + BeatmapID = beatmapID; + TagID = tagID; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + req.Method = HttpMethod.Post; + req.AddParameter(@"tag_id", TagID.ToString(CultureInfo.InvariantCulture), RequestParameterType.Query); + return req; + } + + protected override string Target => $@"beatmaps/{BeatmapID}/tags"; + } +} diff --git a/osu.Game/Online/API/Requests/ListTagsRequest.cs b/osu.Game/Online/API/Requests/ListTagsRequest.cs new file mode 100644 index 0000000000..ac4b1a3e2a --- /dev/null +++ b/osu.Game/Online/API/Requests/ListTagsRequest.cs @@ -0,0 +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 osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Online.API.Requests +{ + public class ListTagsRequest : APIRequest + { + protected override string Target => "tags"; + } +} diff --git a/osu.Game/Online/API/Requests/RemoveBeatmapTagRequest.cs b/osu.Game/Online/API/Requests/RemoveBeatmapTagRequest.cs new file mode 100644 index 0000000000..8090dd2cb0 --- /dev/null +++ b/osu.Game/Online/API/Requests/RemoveBeatmapTagRequest.cs @@ -0,0 +1,29 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Net.Http; +using osu.Framework.IO.Network; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class RemoveBeatmapTagRequest : APIRequest + { + public int BeatmapID { get; } + public long TagID { get; } + + public RemoveBeatmapTagRequest(int beatmapID, long tagID) + { + BeatmapID = beatmapID; + TagID = tagID; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + req.Method = HttpMethod.Delete; + return req; + } + + protected override string Target => $@"beatmaps/{BeatmapID}/tags/{TagID}"; + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index e5ecfe2c99..f06d0ef274 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -95,6 +95,12 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"failtimes")] public APIFailTimes? FailTimes { get; set; } + [JsonProperty(@"top_tag_ids")] + public APIBeatmapTag[]? TopTags { get; set; } + + [JsonProperty(@"own_tag_ids")] + public long[]? OwnTagIds { get; set; } + [JsonProperty(@"max_combo")] public int? MaxCombo { get; set; } diff --git a/osu.Game/Online/API/Requests/Responses/APITag.cs b/osu.Game/Online/API/Requests/Responses/APITag.cs new file mode 100644 index 0000000000..4dd18663af --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APITag.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APITag + { + [JsonProperty("id")] + public long Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } = string.Empty; + + [JsonProperty("description")] + public string Description { get; set; } = string.Empty; + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APITagCollection.cs b/osu.Game/Online/API/Requests/Responses/APITagCollection.cs new file mode 100644 index 0000000000..a177699348 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APITagCollection.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 System; +using Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APITagCollection + { + [JsonProperty("tags")] + public APITag[] Tags { get; set; } = Array.Empty(); + } +} diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs new file mode 100644 index 0000000000..6b7d22a7c2 --- /dev/null +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -0,0 +1,537 @@ +// 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.Collections.Specialized; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Caching; +using osu.Framework.Extensions; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Extensions; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osuTK; + +namespace osu.Game.Screens.Ranking +{ + public partial class UserTagControl : CompositeDrawable + { + public override bool HandlePositionalInput => true; + + private readonly Cached layout = new Cached(); + + private FillFlowContainer tagFlow = null!; + private LoadingLayer loadingLayer = null!; + + private BindableList displayedTags { get; } = new BindableList(); + private BindableList extraTags { get; } = new BindableList(); + + private Bindable allTags = null!; + private readonly Bindable apiBeatmap = new Bindable(); + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private Bindable beatmap { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load(SessionStatics sessionStatics) + { + AutoSizeAxes = Axes.Y; + InternalChildren = new Drawable[] + { + new FillFlowContainer + { + Direction = FillDirection.Vertical, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(8), + Children = new Drawable[] + { + tagFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Full, + LayoutDuration = 300, + LayoutEasing = Easing.OutQuint, + Spacing = new Vector2(4), + }, + new ExtraTagsButton + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + OnTagSelected = onExtraTagSelected, + ExtraTags = { BindTarget = extraTags }, + }, + }, + }, + loadingLayer = new LoadingLayer + { + RelativeSizeAxes = Axes.Both, + State = { Value = Visibility.Visible } + }, + }; + + allTags = sessionStatics.GetBindable(Static.AllBeatmapTags); + + if (allTags.Value == null) + { + var listTagsRequest = new ListTagsRequest(); + listTagsRequest.Success += tags => allTags.Value = tags.Tags.ToArray(); + api.Queue(listTagsRequest); + } + + var getBeatmapSetRequest = new GetBeatmapSetRequest(beatmap.Value.BeatmapInfo.BeatmapSet!.OnlineID); + getBeatmapSetRequest.Success += set => apiBeatmap.Value = set.Beatmaps.SingleOrDefault(b => b.MatchesOnlineID(beatmap.Value.BeatmapInfo)); + api.Queue(getBeatmapSetRequest); + } + + private void onExtraTagSelected(UserTag tag) + { + loadingLayer.Show(); + extraTags.Remove(tag); + + var req = new AddBeatmapTagRequest(beatmap.Value.BeatmapInfo.OnlineID, tag.Id); + req.Success += () => + { + tag.Voted.Value = true; + tag.VoteCount.Value += 1; + displayedTags.Add(tag); + loadingLayer.Hide(); + }; + req.Failure += _ => extraTags.Add(tag); + api.Queue(req); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + allTags.BindValueChanged(_ => updateTags()); + apiBeatmap.BindValueChanged(_ => updateTags()); + updateTags(); + + displayedTags.BindCollectionChanged(displayTags, true); + } + + private void updateTags() + { + if (allTags.Value == null || apiBeatmap.Value?.TopTags == null) + return; + + var allTagsById = allTags.Value.ToDictionary(t => t.Id); + var ownTagIds = apiBeatmap.Value.OwnTagIds?.ToHashSet() ?? new HashSet(); + + foreach (var topTag in apiBeatmap.Value.TopTags) + { + if (allTagsById.Remove(topTag.TagId, out var tag)) + { + displayedTags.Add(new UserTag(tag) + { + VoteCount = { Value = topTag.VoteCount }, + Voted = { Value = ownTagIds.Contains(tag.Id) } + }); + } + } + + extraTags.AddRange(allTagsById.Select(t => new UserTag(t.Value))); + + loadingLayer.Hide(); + } + + private void displayTags(object? sender, NotifyCollectionChangedEventArgs e) + { + var oldItems = tagFlow.ToArray(); + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + { + for (int i = 0; i < e.NewItems!.Count; i++) + { + var tag = (UserTag)e.NewItems[i]!; + var drawableTag = new DrawableUserTag(tag); + tagFlow.Insert(tagFlow.Count, drawableTag); + tag.VoteCount.BindValueChanged(sortTags, true); + layout.Invalidate(); + } + + break; + } + + case NotifyCollectionChangedAction.Remove: + { + for (int i = 0; i < e.OldItems!.Count; i++) + { + var tag = (UserTag)e.OldItems[i]!; + tag.VoteCount.ValueChanged -= sortTags; + tagFlow.Remove(oldItems[e.OldStartingIndex + i], true); + } + + break; + } + + case NotifyCollectionChangedAction.Reset: + { + tagFlow.Clear(); + break; + } + } + } + + private void sortTags(ValueChangedEvent _) => layout.Invalidate(); + + protected override void Update() + { + base.Update(); + + if (!layout.IsValid && !IsHovered) + { + var sortedTags = new Dictionary( + displayedTags.OrderByDescending(t => t.VoteCount.Value) + .ThenByDescending(t => t.Voted.Value) + .Select((tag, index) => new KeyValuePair(tag, index))); + + foreach (var drawableTag in tagFlow) + tagFlow.SetLayoutPosition(drawableTag, sortedTags[drawableTag.UserTag]); + + layout.Validate(); + } + } + + private partial class DrawableUserTag : OsuAnimatedButton + { + public readonly UserTag UserTag; + + private readonly Bindable voteCount = new Bindable(); + private readonly BindableBool voted = new BindableBool(); + private readonly Bindable confirmed = new BindableBool(); + + private Box mainBackground = null!; + private Box voteBackground = null!; + private OsuSpriteText tagNameText = null!; + private OsuSpriteText voteCountText = null!; + private LoadingSpinner spinner = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private Bindable beatmap { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private APIRequest? requestInFlight; + + public DrawableUserTag(UserTag userTag) + { + UserTag = userTag; + voteCount.BindTo(userTag.VoteCount); + voted.BindTo(userTag.Voted); + + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + CornerRadius = 8; + Masking = true; + EdgeEffect = new EdgeEffectParameters + { + Colour = colours.Lime1, + Radius = 5, + Type = EdgeEffectType.Glow, + }; + Content.AddRange(new Drawable[] + { + mainBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Padding = new MarginPadding { Left = 6, Right = 3, Vertical = 3, }, + Spacing = new Vector2(5), + Children = new Drawable[] + { + tagNameText = new OsuSpriteText + { + Text = UserTag.Name, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + new Container + { + AutoSizeAxes = Axes.Both, + CornerRadius = 5, + Masking = true, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Children = new Drawable[] + { + voteBackground = new Box + { + RelativeSizeAxes = Axes.Both, + }, + voteCountText = new OsuSpriteText + { + Margin = new MarginPadding { Horizontal = 6, Vertical = 3, }, + }, + spinner = new LoadingSpinner(withBox: true) + { + Alpha = 0, + Size = new Vector2(18), + } + } + } + } + } + }); + + TooltipText = UserTag.Description; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + const double transition_duration = 300; + + voteCount.BindValueChanged(_ => + { + voteCountText.Text = voteCount.Value.ToLocalisableString(); + confirmed.Value = voteCount.Value >= 10; + }, true); + voted.BindValueChanged(v => + { + if (v.NewValue) + { + voteBackground.FadeColour(colours.Lime3, transition_duration, Easing.OutQuint); + voteCountText.FadeColour(Colour4.Black, transition_duration, Easing.OutQuint); + } + else + { + voteBackground.FadeColour(colours.Gray2, transition_duration, Easing.OutQuint); + voteCountText.FadeColour(Colour4.White, transition_duration, Easing.OutQuint); + } + }, true); + confirmed.BindValueChanged(c => + { + if (c.NewValue) + { + mainBackground.FadeColour(colours.Lime1, transition_duration, Easing.OutQuint); + tagNameText.FadeColour(Colour4.Black, transition_duration, Easing.OutQuint); + FadeEdgeEffectTo(0.5f, transition_duration, Easing.OutQuint); + } + else + { + mainBackground.FadeColour(colours.Gray4, transition_duration, Easing.OutQuint); + tagNameText.FadeColour(Colour4.White, transition_duration, Easing.OutQuint); + FadeEdgeEffectTo(0f, transition_duration, Easing.OutQuint); + } + }, true); + FinishTransforms(true); + + Action = () => + { + if (requestInFlight != null) + return; + + spinner.Show(); + + APIRequest request; + + switch (voted.Value) + { + case true: + var removeReq = new RemoveBeatmapTagRequest(beatmap.Value.BeatmapInfo.OnlineID, UserTag.Id); + removeReq.Success += () => + { + voteCount.Value -= 1; + voted.Value = false; + }; + request = removeReq; + break; + + case false: + var addReq = new AddBeatmapTagRequest(beatmap.Value.BeatmapInfo.OnlineID, UserTag.Id); + addReq.Success += () => + { + voteCount.Value += 1; + voted.Value = true; + }; + request = addReq; + break; + } + + request.Success += () => + { + spinner.Hide(); + requestInFlight = null; + }; + request.Failure += _ => + { + spinner.Hide(); + requestInFlight = null; + }; + api.Queue(requestInFlight = request); + }; + } + } + + private partial class ExtraTagsButton : GrayButton, IHasPopover + { + public BindableList ExtraTags { get; } = new BindableList(); + + public Action? OnTagSelected { get; set; } + + public ExtraTagsButton() + : base(FontAwesome.Solid.Plus) + { + Size = new Vector2(30); + + Action = this.ShowPopover; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ExtraTags.BindCollectionChanged((_, _) => Enabled.Value = ExtraTags.Count > 0, true); + } + + public Popover GetPopover() => new ExtraTagsPopover + { + ExtraTags = { BindTarget = ExtraTags }, + OnSelected = OnTagSelected, + }; + } + + private partial class ExtraTagsPopover : OsuPopover + { + public BindableList ExtraTags { get; } = new BindableList(); + + public Action? OnSelected { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + Child = new OsuScrollContainer + { + Width = 250, + Height = 200, + ScrollbarOverlapsContent = false, + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Right = 5 }, + Spacing = new Vector2(10), + ChildrenEnumerable = ExtraTags.Select(tag => new DrawableExtraTag(tag) + { + Action = () => + { + OnSelected?.Invoke(tag); + this.HidePopover(); + } + }) + } + }; + } + } + + private partial class DrawableExtraTag : OsuAnimatedButton + { + private readonly UserTag tag; + + public DrawableExtraTag(UserTag tag) + { + this.tag = tag; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Anchor = Origin = Anchor.Centre; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Content.AddRange(new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.GreySeaFoamDark, + Depth = float.MaxValue, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(2), + Padding = new MarginPadding(5), + Children = new Drawable[] + { + new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(weight: FontWeight.Bold)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = tag.Name, + }, + new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(size: 14)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = tag.Description, + } + } + } + }); + } + } + } + + public record UserTag + { + public long Id { get; } + public string Name { get; } + public string Description { get; set; } + public BindableInt VoteCount { get; } = new BindableInt(); + public BindableBool Voted { get; } = new BindableBool(); + + public UserTag(APITag tag) + { + Id = tag.Id; + Name = tag.Name; + Description = tag.Description; + } + } +} From 2abe75629eefb21f53ab4144210c7e3cf30ce8fc Mon Sep 17 00:00:00 2001 From: Zihad Date: Tue, 4 Mar 2025 18:28:03 +0600 Subject: [PATCH 655/801] Skip window title update for dummy beatmap --- osu.Game/OsuGame.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 3b55c320b3..fb9be8860c 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -829,7 +829,11 @@ namespace osu.Game beatmap.OldValue?.CancelAsyncLoad(); beatmap.NewValue?.BeginAsyncLoad(); - Host.Window.Title = $"{Name} - {beatmap.NewValue.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; + // prevent weird window title saying please load a beatmap + if (beatmap.NewValue is null or DummyWorkingBeatmap) + Host.Window.Title = Name; + else + Host.Window.Title = $"{Name} - {beatmap.NewValue.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; } private void modsChanged(ValueChangedEvent> mods) From dff354247eeb9490105f82991a2f98ad8b6efc02 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 22:21:36 +0900 Subject: [PATCH 656/801] Change `ModSelectOverlay.ShowPresets` to `init` --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 5 ++++- .../Visual/UserInterface/TestSceneScreenFooter.cs | 9 ++------- .../UserInterface/TestSceneScreenFooterButtonMods.cs | 3 +-- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 6 +++--- osu.Game/Screens/Select/SongSelect.cs | 10 ++++------ osu.Game/Screens/SelectV2/SongSelect.cs | 10 ++++------ osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs | 3 +-- 7 files changed, 19 insertions(+), 27 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 280497e861..6eb9263c7e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -1030,7 +1030,10 @@ namespace osu.Game.Tests.Visual.UserInterface private partial class TestModSelectOverlay : UserModSelectOverlay { - protected override bool ShowPresets => true; + public TestModSelectOverlay() + { + ShowPresets = true; + } } private class TestUnimplementedMod : Mod diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs index a4cf8a276f..fc8777068d 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.UserInterface { private DependencyProvidingContainer contentContainer = null!; private ScreenFooter screenFooter = null!; - private TestModSelectOverlay modOverlay = null!; + private UserModSelectOverlay modOverlay = null!; [SetUp] public void SetUp() => Schedule(() => @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.UserInterface }, Children = new Drawable[] { - modOverlay = new TestModSelectOverlay(), + modOverlay = new UserModSelectOverlay { ShowPresets = true }, new PopoverContainer { RelativeSizeAxes = Axes.Both, @@ -196,11 +196,6 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("external overlay content still not shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent, () => Is.Not.True); } - private partial class TestModSelectOverlay : UserModSelectOverlay - { - protected override bool ShowPresets => true; - } - private partial class TestShearedOverlayContainer : ShearedOverlayContainer { public TestShearedOverlayContainer() diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs index ba53eb83c4..e86f83ee15 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs @@ -115,11 +115,10 @@ namespace osu.Game.Tests.Visual.UserInterface private partial class TestModSelectOverlay : UserModSelectOverlay { - protected override bool ShowPresets => true; - public TestModSelectOverlay() : base(OverlayColourScheme.Aquamarine) { + ShowPresets = true; } } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index daac925dfb..ac589fbebf 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -35,7 +35,7 @@ using osuTK.Input; namespace osu.Game.Overlays.Mods { - public abstract partial class ModSelectOverlay : ShearedOverlayContainer, ISamplePlaybackDisabler, IKeyBindingHandler + public partial class ModSelectOverlay : ShearedOverlayContainer, ISamplePlaybackDisabler, IKeyBindingHandler { public const int BUTTON_WIDTH = 200; @@ -96,7 +96,7 @@ namespace osu.Game.Overlays.Mods /// /// Whether the column with available mod presets should be shown. /// - protected virtual bool ShowPresets => false; + public bool ShowPresets { get; init; } protected virtual ModColumn CreateModColumn(ModType modType) => new ModColumn(modType, false); @@ -125,7 +125,7 @@ namespace osu.Game.Overlays.Mods [Resolved] private ScreenFooter? footer { get; set; } - protected ModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Green) + public ModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Green) : base(colourScheme) { } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index c20dcb8593..1496eb96f9 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -426,7 +426,10 @@ namespace osu.Game.Screens.Select (beatmapOptionsButton = new FooterButtonOptions(), BeatmapOptions) }; - protected virtual ModSelectOverlay CreateModSelectOverlay() => new SoloModSelectOverlay(); + protected virtual ModSelectOverlay CreateModSelectOverlay() => new UserModSelectOverlay + { + ShowPresets = true, + }; private DependencyContainer dependencies = null!; @@ -1152,10 +1155,5 @@ namespace osu.Game.Screens.Select return base.OnHover(e); } } - - internal partial class SoloModSelectOverlay : UserModSelectOverlay - { - protected override bool ShowPresets => true; - } } } diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 70452de99a..ad29f846c4 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -23,7 +23,10 @@ namespace osu.Game.Screens.SelectV2 { private const float logo_scale = 0.4f; - private readonly ModSelectOverlay modSelectOverlay = new SoloModSelectOverlay(); + private readonly ModSelectOverlay modSelectOverlay = new ModSelectOverlay + { + ShowPresets = true, + }; [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); @@ -186,10 +189,5 @@ namespace osu.Game.Screens.SelectV2 SearchText = query, }); } - - private partial class SoloModSelectOverlay : UserModSelectOverlay - { - protected override bool ShowPresets => true; - } } } diff --git a/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs index 6908f7f1b4..21d0b8e7a8 100644 --- a/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs +++ b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs @@ -658,11 +658,10 @@ namespace osu.Game.Tests.Visual.Gameplay private partial class TestModSelectOverlay : UserModSelectOverlay { - protected override bool ShowPresets => false; - public TestModSelectOverlay() : base(OverlayColourScheme.Aquamarine) { + ShowPresets = false; } } } From 14b5c0bf10389b924fa8ca515c2e27457fdcc119 Mon Sep 17 00:00:00 2001 From: Zihad Date: Tue, 4 Mar 2025 19:56:48 +0600 Subject: [PATCH 657/801] Update window title in input thread --- osu.Game/OsuGame.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index fb9be8860c..a80d646e15 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -830,10 +830,11 @@ namespace osu.Game beatmap.NewValue?.BeginAsyncLoad(); // prevent weird window title saying please load a beatmap - if (beatmap.NewValue is null or DummyWorkingBeatmap) - Host.Window.Title = Name; - else - Host.Window.Title = $"{Name} - {beatmap.NewValue.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; + string newTitle = Name; + if (beatmap.NewValue != null && beatmap.NewValue is not DummyWorkingBeatmap) + newTitle = $"{Name} - {beatmap.NewValue.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; + + Host.InputThread.Scheduler.AddOnce(s => Host.Window.Title = s, newTitle); } private void modsChanged(ValueChangedEvent> mods) From 8ce6003a3e156cac95f448f40f473d9023c278df Mon Sep 17 00:00:00 2001 From: Zihad Date: Tue, 4 Mar 2025 20:36:53 +0600 Subject: [PATCH 658/801] Skip updating window title in headless mode --- osu.Game/OsuGame.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index a80d646e15..2b9e2cb9cd 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -829,6 +829,9 @@ namespace osu.Game beatmap.OldValue?.CancelAsyncLoad(); beatmap.NewValue?.BeginAsyncLoad(); + if (Host.Window == null) + return; + // prevent weird window title saying please load a beatmap string newTitle = Name; if (beatmap.NewValue != null && beatmap.NewValue is not DummyWorkingBeatmap) From 9ca12744957f9d660d13a50e13060b2aa772b0e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Mar 2025 13:51:56 +0900 Subject: [PATCH 659/801] Rename test scene to match new `RoomListing` class name --- ...TestSceneLoungeRoomsContainer.cs => TestSceneRoomListing.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename osu.Game.Tests/Visual/Multiplayer/{TestSceneLoungeRoomsContainer.cs => TestSceneRoomListing.cs} (99%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs similarity index 99% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs rename to osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs index 23e15b0501..27c5758afa 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs @@ -18,7 +18,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { - public partial class TestSceneLoungeRoomsContainer : OnlinePlayTestScene + public partial class TestSceneRoomListing : OnlinePlayTestScene { private BindableList rooms = null!; private IBindable selectedRoom = null!; From 3661107e4ffc4478ac7fe50e5189877f71b44cc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Mar 2025 08:05:15 +0100 Subject: [PATCH 660/801] Update property name in line with web changes --- osu.Game/Online/API/Requests/Responses/APIBeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index f06d0ef274..66e17739a8 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -98,7 +98,7 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"top_tag_ids")] public APIBeatmapTag[]? TopTags { get; set; } - [JsonProperty(@"own_tag_ids")] + [JsonProperty(@"current_user_tag_ids")] public long[]? OwnTagIds { get; set; } [JsonProperty(@"max_combo")] From abc4955e8131de912aaec22941d352cb558a7297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Mar 2025 09:21:47 +0100 Subject: [PATCH 661/801] Add failing test coverage --- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 71 ++++++++++++++++--- .../SongSelectComponentsTestScene.cs | 5 +- .../SongSelectV2/TestSceneLeaderboardScore.cs | 66 +++++++++++++++++ 3 files changed, 129 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index c234cc8a9c..23d6725491 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -12,6 +12,8 @@ using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Graphics.Cursor; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; using osu.Game.Overlays; @@ -20,14 +22,16 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Scoring; +using osu.Game.Screens.Select; using osu.Game.Screens.Select.Leaderboards; using osu.Game.Tests.Resources; using osu.Game.Users; using osuTK; +using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelect { - public partial class TestSceneBeatmapLeaderboard : OsuTestScene + public partial class TestSceneBeatmapLeaderboard : OsuManualInputManagerTestScene { private readonly FailableLeaderboard leaderboard; @@ -37,6 +41,7 @@ namespace osu.Game.Tests.Visual.SongSelect private ScoreManager scoreManager = null!; private RulesetStore rulesetStore = null!; private BeatmapManager beatmapManager = null!; + private PlaySongSelect songSelect = null!; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { @@ -45,25 +50,36 @@ namespace osu.Game.Tests.Visual.SongSelect dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm)); dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, API)); + dependencies.CacheAs(songSelect = new PlaySongSelect()); Dependencies.Cache(Realm); return dependencies; } + [BackgroundDependencyLoader] + private void load() + { + LoadComponent(songSelect); + } + public TestSceneBeatmapLeaderboard() { - AddRange(new Drawable[] + Add(new OsuContextMenuContainer { - dialogOverlay = new DialogOverlay + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - Depth = -1 - }, - leaderboard = new FailableLeaderboard - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Size = new Vector2(550f, 450f), - Scope = BeatmapLeaderboardScope.Global, + dialogOverlay = new DialogOverlay + { + Depth = -1 + }, + leaderboard = new FailableLeaderboard + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Size = new Vector2(550f, 450f), + Scope = BeatmapLeaderboardScope.Global, + } } }); } @@ -187,6 +203,39 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep(@"None selected", () => leaderboard.SetErrorState(LeaderboardState.NoneSelected)); } + [Test] + public void TestUseTheseModsDoesNotCopySystemMods() + { + AddStep(@"set scores", () => leaderboard.SetScores(leaderboard.Scores, new ScoreInfo + { + Position = 999, + Rank = ScoreRank.XH, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + Ruleset = new OsuRuleset().RulesetInfo, + Mods = new Mod[] { new OsuModHidden(), new ModScoreV2(), }, + User = new APIUser + { + Id = 6602580, + Username = @"waaiiru", + CountryCode = CountryCode.ES, + } + })); + AddStep("right click panel", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Right); + }); + AddStep("click use these mods", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + AddAssert("song select received HD", () => songSelect.Mods.Value.Any(m => m is OsuModHidden)); + AddAssert("song select did not receive SV2", () => !songSelect.Mods.Value.Any(m => m is ModScoreV2)); + } + private void showPersonalBestWithNullPosition() { leaderboard.SetScores(leaderboard.Scores, new ScoreInfo diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs index b7b0101a7c..8694722acc 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs @@ -6,16 +6,17 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; +using osu.Game.Graphics.Cursor; using osu.Game.Overlays; namespace osu.Game.Tests.Visual.SongSelectV2 { - public abstract partial class SongSelectComponentsTestScene : OsuTestScene + public abstract partial class SongSelectComponentsTestScene : OsuManualInputManagerTestScene { [Cached] protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); - protected override Container Content { get; } = new Container + protected override Container Content { get; } = new OsuContextMenuContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs index a7d0d70c03..26d39c9203 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs @@ -7,9 +7,11 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Rulesets.Mania; @@ -22,6 +24,7 @@ using osu.Game.Screens.SelectV2.Leaderboards; using osu.Game.Tests.Resources; using osu.Game.Users; using osuTK; +using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelectV2 { @@ -102,6 +105,69 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }); } + [Test] + public void TestUseTheseModsDoesNotCopySystemMods() + { + LeaderboardScoreV2 score = null!; + + AddStep("create content", () => + { + Children = new Drawable[] + { + fillFlow = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 2f), + Shear = new Vector2(OsuGame.SHEAR, 0) + }, + drawWidthText = new OsuSpriteText(), + }; + + var scoreInfo = new ScoreInfo + { + Position = 999, + Rank = ScoreRank.X, + Accuracy = 1, + MaxCombo = 244, + TotalScore = RNG.Next(1_800_000, 2_000_000), + MaximumStatistics = { { HitResult.Great, 3000 } }, + Mods = new Mod[] { new OsuModHidden(), new ModScoreV2(), }, + Ruleset = new OsuRuleset().RulesetInfo, + User = new APIUser + { + Id = 6602580, + Username = @"waaiiru", + CountryCode = CountryCode.ES, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg", + }, + Date = DateTimeOffset.Now.AddYears(-2), + }; + + fillFlow.Add(score = new LeaderboardScoreV2(scoreInfo) + { + Rank = scoreInfo.Position, + Shear = Vector2.Zero, + }); + + score.Show(); + }); + AddStep("right click panel", () => + { + InputManager.MoveMouseTo(score); + InputManager.Click(MouseButton.Right); + }); + AddStep("click use these mods", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + AddAssert("mods received HD", () => score.SelectedMods.Value.Any(m => m is OsuModHidden)); + AddAssert("mods did not receive SV2", () => !score.SelectedMods.Value.Any(m => m is ModScoreV2)); + } + public override void SetUpSteps() { AddToggleStep("toggle scoring mode", v => config.SetValue(OsuSetting.ScoreDisplayMode, v ? ScoringMode.Classic : ScoringMode.Standardised)); From d9a1dcf9b972af6864b3522e3048a433dbd4ef77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Mar 2025 09:25:56 +0100 Subject: [PATCH 662/801] Fix "use these mods" option applying to system mods Closes https://github.com/ppy/osu/issues/32229. --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 5 ++++- .../Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 0db03efb68..ea42c515a6 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -453,7 +453,10 @@ namespace osu.Game.Online.Leaderboards List items = new List(); if (Score.Mods.Length > 0 && songSelect != null) - items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods)); + { + // system mods should never be copied across regardless of anything. + items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods.Where(m => m.Type != ModType.System).ToArray())); + } if (Score.OnlineID > 0) items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{Score.OnlineID}"))); diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs index 978d6eca32..71cc80af49 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs @@ -781,7 +781,11 @@ namespace osu.Game.Screens.SelectV2.Leaderboards List items = new List(); if (score.Mods.Length > 0) - items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => SelectedMods.Value = score.Mods.Where(m => IsValidMod.Invoke(m)).ToArray())); + { + // system mods should never be copied across regardless of anything. + items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, + () => SelectedMods.Value = score.Mods.Where(m => IsValidMod.Invoke(m) && m.Type != ModType.System).ToArray())); + } if (score.OnlineID > 0) items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{score.OnlineID}"))); From 097dd701396a476ca5a7a5c03dbbee6b2f623ed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Mar 2025 09:33:33 +0100 Subject: [PATCH 663/801] Add another failing test --- .../DailyChallenge/TestSceneDailyChallenge.cs | 37 +++++++++++++++++++ .../OnlinePlay/TestRoomRequestsHandler.cs | 1 + 2 files changed, 38 insertions(+) diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs index 0742ed5eb9..c974a852f3 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs @@ -6,6 +6,8 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Online.Metadata; @@ -13,9 +15,11 @@ using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens.SelectV2.Leaderboards; using osu.Game.Tests.Resources; using osu.Game.Tests.Visual.Metadata; using osu.Game.Tests.Visual.OnlinePlay; +using osuTK.Input; namespace osu.Game.Tests.Visual.DailyChallenge { @@ -57,6 +61,39 @@ namespace osu.Game.Tests.Visual.DailyChallenge AddStep("push screen", () => LoadScreen(new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room))); } + [Test] + public void TestUseTheseModsUnavailableIfNoFreeMods() + { + var room = new Room + { + RoomID = 1234, + Name = "Daily Challenge: June 4, 2024", + Playlist = + [ + new PlaylistItem(TestResources.CreateTestBeatmapSetInfo().Beatmaps.First()) + { + RequiredMods = [new APIMod(new OsuModTraceable())], + AllowedMods = [] + } + ], + EndDate = DateTimeOffset.Now.AddHours(12), + Category = RoomCategory.DailyChallenge + }; + + AddStep("add room", () => API.Perform(new CreateRoomRequest(room))); + Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!; + AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room))); + AddUntilStep("wait for pushed", () => screen.IsCurrentScreen()); + AddStep("force transforms to finish", () => FinishTransforms(true)); + AddStep("right click second score", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().ElementAt(1)); + InputManager.Click(MouseButton.Right); + }); + AddAssert("use these mods not present", + () => this.ChildrenOfType().All(m => m.Items.All(item => item.Text.Value != "Use these mods"))); + } + [Test] public void TestNotifications() { diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index 0ae3a73e5d..46c1251d42 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -126,6 +126,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay MaxCombo = 100, TotalScore = 200000, User = new APIUser { Username = "worst user" }, + Mods = [new APIMod { Acronym = @"TD" }], Statistics = new Dictionary() }, }, From 0ac3a80406fa295e648e5e83e09d1e8a6c2a7773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Mar 2025 09:40:11 +0100 Subject: [PATCH 664/801] Fix "use these mods" option showing if it can't do anything Closes https://github.com/ppy/osu/issues/32230. --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 10 +++++----- .../SelectV2/Leaderboards/LeaderboardScoreV2.cs | 11 +++++------ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index ea42c515a6..28b20c0c05 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -452,11 +452,11 @@ namespace osu.Game.Online.Leaderboards { List items = new List(); - if (Score.Mods.Length > 0 && songSelect != null) - { - // system mods should never be copied across regardless of anything. - items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods.Where(m => m.Type != ModType.System).ToArray())); - } + // system mods should never be copied across regardless of anything. + var copyableMods = Score.Mods.Where(m => m.Type != ModType.System).ToArray(); + + if (copyableMods.Length > 0 && songSelect != null) + items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = copyableMods)); if (Score.OnlineID > 0) items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{Score.OnlineID}"))); diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs index 71cc80af49..b54f007f38 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs @@ -780,12 +780,11 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { List items = new List(); - if (score.Mods.Length > 0) - { - // system mods should never be copied across regardless of anything. - items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, - () => SelectedMods.Value = score.Mods.Where(m => IsValidMod.Invoke(m) && m.Type != ModType.System).ToArray())); - } + // system mods should never be copied across regardless of anything. + var copyableMods = score.Mods.Where(m => IsValidMod.Invoke(m) && m.Type != ModType.System).ToArray(); + + if (copyableMods.Length > 0) + items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => SelectedMods.Value = copyableMods)); if (score.OnlineID > 0) items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{score.OnlineID}"))); From 7975c301a846e9c28b02e3ab388912344ff95cce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Mar 2025 12:32:58 +0100 Subject: [PATCH 665/801] Try to fix test --- osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 23d6725491..bfb835cad1 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -222,6 +222,7 @@ namespace osu.Game.Tests.Visual.SongSelect CountryCode = CountryCode.ES, } })); + AddUntilStep("wait for scores", () => this.ChildrenOfType().Count(), () => Is.GreaterThan(0)); AddStep("right click panel", () => { InputManager.MoveMouseTo(this.ChildrenOfType().Single()); From f8878463119a4f341346ea856fbee2b29acc1160 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 5 Mar 2025 21:38:04 +0900 Subject: [PATCH 666/801] Reset mods when exiting to lounge To be hoenst, this is all quite abusive but I'm not sure how to do it in any better way. I especially dislike the fact that the screen's bindables are disabled yet the screen itself is allowed to set their value because they're `LeasedBindable`s. But I feel better doing this in a central location like this `ScreenStack` than in the rooms... Otherwise this specific behaviour would have to be replicated in the multiplayer screen when it goes through the same refactoring. --- .../TestSceneOnlinePlaySubScreenStack.cs | 85 +++++++++++++++++++ .../OnlinePlay/OnlinePlaySubScreenStack.cs | 32 +++++-- 2 files changed, 109 insertions(+), 8 deletions(-) create mode 100644 osu.Game.Tests/OnlinePlay/TestSceneOnlinePlaySubScreenStack.cs diff --git a/osu.Game.Tests/OnlinePlay/TestSceneOnlinePlaySubScreenStack.cs b/osu.Game.Tests/OnlinePlay/TestSceneOnlinePlaySubScreenStack.cs new file mode 100644 index 0000000000..5eb11b6370 --- /dev/null +++ b/osu.Game.Tests/OnlinePlay/TestSceneOnlinePlaySubScreenStack.cs @@ -0,0 +1,85 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Screens.OnlinePlay.Playlists; +using osu.Game.Tests.Visual.OnlinePlay; + +namespace osu.Game.Tests.OnlinePlay +{ + [HeadlessTest] + public class TestSceneOnlinePlaySubScreenStack : OnlinePlayTestScene + { + private ScreenStack stack = null!; + + [SetUp] + public void Setup() => Schedule(() => + { + Child = stack = new OnlinePlaySubScreenStack + { + RelativeSizeAxes = Axes.Both + }; + }); + + [Test] + public void TestBindablesDisabledWhenRequested() + { + AddAssert("bindables not disabled", () => Beatmap.Disabled || Ruleset.Disabled || SelectedMods.Disabled, () => Is.False); + + AddStep("push screen that disables bindables", () => stack.Push(new ScreenWithExternalBindableDisablement(true))); + AddAssert("bindables disabled", () => Beatmap.Disabled && Ruleset.Disabled && SelectedMods.Disabled, () => Is.True); + + AddStep("push screen that does not disables bindables", () => stack.Push(new ScreenWithExternalBindableDisablement(false))); + AddAssert("bindables not disabled", () => Beatmap.Disabled || Ruleset.Disabled || SelectedMods.Disabled, () => Is.False); + + AddStep("exit one screen", () => stack.Exit()); + AddAssert("bindables disabled", () => Beatmap.Disabled && Ruleset.Disabled && SelectedMods.Disabled, () => Is.True); + } + + [Test] + public void TestModsResetWhenExitToLounge() + { + AddStep("push lounge", () => stack.Push(new PlaylistsLoungeSubScreen())); + + AddStep("push screen with mod", () => stack.Push(new ScreenWithMod(new OsuModDoubleTime()))); + AddUntilStep("wait for screen to load", () => ((OsuScreen)stack.CurrentScreen).IsLoaded); + AddAssert("mod set", () => SelectedMods.Value.Count, () => Is.GreaterThan(0)); + + AddStep("exit to lounge", () => stack.Exit()); + AddAssert("mods reset", () => SelectedMods.Value.Count, () => Is.Zero); + } + + private class ScreenWithExternalBindableDisablement : OsuScreen + { + public override bool DisallowExternalBeatmapRulesetChanges { get; } + + public ScreenWithExternalBindableDisablement(bool disableBindables) + { + DisallowExternalBeatmapRulesetChanges = disableBindables; + } + } + + private class ScreenWithMod : OsuScreen + { + private readonly Mod mod; + + public ScreenWithMod(Mod mod) + { + this.mod = mod; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Mods.Value = [mod]; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs index 6695c97508..c10017f1fe 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs @@ -1,8 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Diagnostics; +using System; using osu.Framework.Screens; +using osu.Game.Screens.OnlinePlay.Lounge; namespace osu.Game.Screens.OnlinePlay { @@ -12,16 +13,31 @@ namespace osu.Game.Screens.OnlinePlay { base.ScreenChanged(prev, next); - // because this is a screen stack within a screen stack, let's manually handle disabled changes to simplify things. - var osuScreen = next as OsuScreen; + if (next is not OsuScreen osuNext) + throw new InvalidOperationException("There must always be an online play subscreen."); - Debug.Assert(osuScreen != null); + // See: OnlinePlayScreen.DisallowExternalBeatmapRulesetChanges. + // + // Bindable leases are held by the OnlinePlayScreen and NOT by the subscreens, + // because PlayerLoader needs to resolve LeasedBindables to function correctly. + // + // An unfortunate consequence of this is we need to manually control bindable + // enablement depending on what effect the subscreens want. + // + // This is a two-part process... - bool disallowChanges = osuScreen.DisallowExternalBeatmapRulesetChanges; + // First, emulate the behaviour of DisallowExternalBeatmapRulesetChanges to disable toolbar buttons. + osuNext.Beatmap.Disabled = osuNext.DisallowExternalBeatmapRulesetChanges; + osuNext.Ruleset.Disabled = osuNext.DisallowExternalBeatmapRulesetChanges; + osuNext.Mods.Disabled = osuNext.DisallowExternalBeatmapRulesetChanges; - osuScreen.Beatmap.Disabled = disallowChanges; - osuScreen.Ruleset.Disabled = disallowChanges; - osuScreen.Mods.Disabled = disallowChanges; + // Second, when an OsuScreen is exited with DisallowExternalBeatmapRulesetChanges=true, leased bindables + // are normally returned which reverts the mod and ruleset bindables to their original states. + // + // The exact behaiour of the revert is awkward to emulate, but we particularly care about resetting mods + // when returning to the lounge so that they don't stick around if the user then goes to create a new room. + if (next is LoungeSubScreen) + osuNext.Mods.Value = []; } } } From 51f794a56675ee119b57d3b2f43cb012d7f0cf2a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 5 Mar 2025 21:53:49 +0900 Subject: [PATCH 667/801] Block input to screen during gameplay --- .../OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index f762810e9d..df7f86704f 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -790,6 +790,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return false; } + // Block all input to this screen during gameplay/etc when the parent screen is no longer current. + // Normally this would be handled by ScreenStack, but we are in a child ScreenStack. + public override bool PropagatePositionalInputSubTree => base.PropagatePositionalInputSubTree && (parentScreen?.IsCurrentScreen() ?? this.IsCurrentScreen()); + + // Block all input to this screen during gameplay/etc when the parent screen is no longer current. + // Normally this would be handled by ScreenStack, but we are in a child ScreenStack. + public override bool PropagateNonPositionalInputSubTree => base.PropagateNonPositionalInputSubTree && (parentScreen?.IsCurrentScreen() ?? this.IsCurrentScreen()); + protected override BackgroundScreen CreateBackground() => new RoomBackgroundScreen(room.Playlist.FirstOrDefault()) { SelectedItem = { BindTarget = SelectedItem } From 3f461c07348581a313a727e92411769ea8c30c6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Mar 2025 14:11:44 +0100 Subject: [PATCH 668/801] Add "discard unsaved changes" operation to beatmap editor Apparently useful in modding workflows when you want to test out a few different variants of a thing. Re-uses `Ctrl-L` binding from stable. Some folks may argue that the dialog makes the hotkey pointless, but I really do want to protect users from accidental data loss, and also if you want to power through it quickly, you can hit the 1 key when the dialog shows, which will bypass the hold-to-activate period (which wasn't intentional, but so many people want a bypass at this point that we're probably keeping that behaviour for power users). --- .../Editor/TestSceneManiaEditorSaving.cs | 4 +-- .../Edit/Setup/ManiaDifficultySection.cs | 2 +- .../Input/Bindings/GlobalActionContainer.cs | 4 +++ osu.Game/Localisation/EditorDialogsStrings.cs | 5 +++ .../GlobalActionKeyBindingStrings.cs | 5 +++ .../Edit/DiscardUnsavedChangesDialog.cs | 33 +++++++++++++++++++ osu.Game/Screens/Edit/Editor.cs | 29 ++++++++++++++-- ...Dialog.cs => SaveAndReloadEditorDialog.cs} | 4 +-- 8 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 osu.Game/Screens/Edit/DiscardUnsavedChangesDialog.cs rename osu.Game/Screens/Edit/{ReloadEditorDialog.cs => SaveAndReloadEditorDialog.cs} (86%) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaEditorSaving.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaEditorSaving.cs index d9ba721646..ebaa8bcea2 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaEditorSaving.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaEditorSaving.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { keyCount.Current.Value = 8; }); - AddUntilStep("dialog visible", () => Game.ChildrenOfType().SingleOrDefault()?.CurrentDialog, Is.InstanceOf); + AddUntilStep("dialog visible", () => Game.ChildrenOfType().SingleOrDefault()?.CurrentDialog, Is.InstanceOf); AddStep("refuse", () => InputManager.Key(Key.Number2)); AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5)); @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { keyCount.Current.Value = 8; }); - AddUntilStep("dialog visible", () => Game.ChildrenOfType().Single().CurrentDialog, Is.InstanceOf); + AddUntilStep("dialog visible", () => Game.ChildrenOfType().Single().CurrentDialog, Is.InstanceOf); AddStep("acquiesce", () => InputManager.Key(Key.Number1)); AddUntilStep("beatmap became 8K", () => Game.Beatmap.Value.BeatmapInfo.Difficulty.CircleSize, () => Is.EqualTo(8)); } diff --git a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs index 48e59877df..a5c3c2264c 100644 --- a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs +++ b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs @@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup updatingKeyCount = true; - editor.Reload().ContinueWith(t => + editor.SaveAndReload().ContinueWith(t => { if (!t.GetResultSafely()) { diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index e4dc2d503b..6de2dabe2b 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -155,6 +155,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.B }, GlobalAction.EditorRemoveClosestBookmark), new KeyBinding(new[] { InputKey.Alt, InputKey.Left }, GlobalAction.EditorSeekToPreviousBookmark), new KeyBinding(new[] { InputKey.Alt, InputKey.Right }, GlobalAction.EditorSeekToNextBookmark), + new KeyBinding(new[] { InputKey.Control, InputKey.L }, GlobalAction.EditorDiscardUnsavedChanges), }; private static IEnumerable editorTestPlayKeyBindings => new[] @@ -502,6 +503,9 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleMoveControl))] EditorToggleMoveControl, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorDiscardUnsavedChanges))] + EditorDiscardUnsavedChanges, } public enum GlobalActionCategory diff --git a/osu.Game/Localisation/EditorDialogsStrings.cs b/osu.Game/Localisation/EditorDialogsStrings.cs index 94f28c617c..3617dca81f 100644 --- a/osu.Game/Localisation/EditorDialogsStrings.cs +++ b/osu.Game/Localisation/EditorDialogsStrings.cs @@ -54,6 +54,11 @@ namespace osu.Game.Localisation /// public static LocalisableString EditorReloadDialogHeader => new TranslatableString(getKey(@"editor_reload_dialog_header"), @"The editor must be reloaded to apply this change. The beatmap will be saved."); + /// + /// "Discard all unsaved changes? This cannot be undone." + /// + public static LocalisableString DiscardUnsavedChangesDialogHeader => new TranslatableString(getKey(@"discard_unsaved_changes_dialog_header"), @"Discard all unsaved changes? This cannot be undone."); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 5713df57c9..34b9e1fecc 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -459,6 +459,11 @@ namespace osu.Game.Localisation /// public static LocalisableString EditorToggleMoveControl => new TranslatableString(getKey(@"editor_toggle_move_control"), @"Toggle movement control"); + /// + /// "Discard unsaved changes" + /// + public static LocalisableString EditorDiscardUnsavedChanges => new TranslatableString(getKey(@"editor_discard_unsaved_changes"), @"Discard unsaved changes"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/Edit/DiscardUnsavedChangesDialog.cs b/osu.Game/Screens/Edit/DiscardUnsavedChangesDialog.cs new file mode 100644 index 0000000000..1867b48830 --- /dev/null +++ b/osu.Game/Screens/Edit/DiscardUnsavedChangesDialog.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics.Sprites; +using osu.Game.Localisation; +using osu.Game.Overlays.Dialog; + +namespace osu.Game.Screens.Edit +{ + public partial class DiscardUnsavedChangesDialog : PopupDialog + { + public DiscardUnsavedChangesDialog(Action exit) + { + HeaderText = EditorDialogsStrings.DiscardUnsavedChangesDialogHeader; + + Icon = FontAwesome.Solid.Trash; + + Buttons = new PopupDialogButton[] + { + new PopupDialogDangerousButton + { + Text = EditorDialogsStrings.ForgetAllChanges, + Action = exit + }, + new PopupDialogCancelButton + { + Text = EditorDialogsStrings.ContinueEditing, + }, + }; + } + } +} diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 219e14861f..bf254093b3 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -164,6 +164,7 @@ namespace osu.Game.Screens.Edit private bool switchingDifficulty; private string lastSavedHash; + private EditorMenuItem discardChangesMenuItem; private ScreenContainer screenContainer; @@ -391,6 +392,10 @@ namespace osu.Game.Screens.Edit { undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo) { Hotkey = new Hotkey(PlatformAction.Undo) }, redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo) { Hotkey = new Hotkey(PlatformAction.Redo) }, + discardChangesMenuItem = new EditorMenuItem("Discard unsaved changes", MenuItemType.Destructive, DiscardUnsavedChanges) + { + Hotkey = new Hotkey(GlobalAction.EditorDiscardUnsavedChanges) + }, new OsuMenuItemSpacer(), cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut) { Hotkey = new Hotkey(PlatformAction.Cut) }, copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy) { Hotkey = new Hotkey(PlatformAction.Copy) }, @@ -607,6 +612,8 @@ namespace osu.Game.Screens.Edit { base.Update(); clock.ProcessFrame(); + + discardChangesMenuItem.Action.Disabled = !HasUnsavedChanges; } public bool OnPressed(KeyBindingPressEvent e) @@ -821,6 +828,10 @@ namespace osu.Game.Screens.Edit case GlobalAction.EditorTestGameplay: bottomBar.TestGameplayButton.TriggerClick(); return true; + + case GlobalAction.EditorDiscardUnsavedChanges: + DiscardUnsavedChanges(); + return true; } return false; @@ -1008,6 +1019,20 @@ namespace osu.Game.Screens.Edit protected void Redo() => changeHandler?.RestoreState(1); + protected void DiscardUnsavedChanges() + { + if (!HasUnsavedChanges) + return; + + // we're not doing this via `changeHandler` because `changeHandler` has limited number of undo actions + // and therefore there's no guarantee that it even *has* the beatmap's last saved state in its history still. + dialogOverlay.Push(new DiscardUnsavedChangesDialog(() => + { + updateLastSavedHash(); // without this a second dialog will show (the standard "save unsaved changes" one that shows on exit). + SwitchToDifficulty(editorBeatmap.BeatmapInfo); + })); + } + protected void SetPreviewPointToCurrentTime() { editorBeatmap.PreviewTime.Value = (int)clock.CurrentTime; @@ -1510,11 +1535,11 @@ namespace osu.Game.Screens.Edit loader?.CancelPendingDifficultySwitch(); } - public Task Reload() + public Task SaveAndReload() { var tcs = new TaskCompletionSource(); - dialogOverlay.Push(new ReloadEditorDialog( + dialogOverlay.Push(new SaveAndReloadEditorDialog( reload: () => { bool reloadedSuccessfully = attemptMutationOperation(() => diff --git a/osu.Game/Screens/Edit/ReloadEditorDialog.cs b/osu.Game/Screens/Edit/SaveAndReloadEditorDialog.cs similarity index 86% rename from osu.Game/Screens/Edit/ReloadEditorDialog.cs rename to osu.Game/Screens/Edit/SaveAndReloadEditorDialog.cs index 72a9f81347..b73c7cfff8 100644 --- a/osu.Game/Screens/Edit/ReloadEditorDialog.cs +++ b/osu.Game/Screens/Edit/SaveAndReloadEditorDialog.cs @@ -8,9 +8,9 @@ using osu.Game.Localisation; namespace osu.Game.Screens.Edit { - public partial class ReloadEditorDialog : PopupDialog + public partial class SaveAndReloadEditorDialog : PopupDialog { - public ReloadEditorDialog(Action reload, Action cancel) + public SaveAndReloadEditorDialog(Action reload, Action cancel) { HeaderText = EditorDialogsStrings.EditorReloadDialogHeader; From 26daf085d74f5e5be049c51451df72fff6920925 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 5 Mar 2025 23:25:22 +0900 Subject: [PATCH 669/801] Add forgotten partials --- .../OnlinePlay/TestSceneOnlinePlaySubScreenStack.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/OnlinePlay/TestSceneOnlinePlaySubScreenStack.cs b/osu.Game.Tests/OnlinePlay/TestSceneOnlinePlaySubScreenStack.cs index 5eb11b6370..67b0d236ed 100644 --- a/osu.Game.Tests/OnlinePlay/TestSceneOnlinePlaySubScreenStack.cs +++ b/osu.Game.Tests/OnlinePlay/TestSceneOnlinePlaySubScreenStack.cs @@ -15,7 +15,7 @@ using osu.Game.Tests.Visual.OnlinePlay; namespace osu.Game.Tests.OnlinePlay { [HeadlessTest] - public class TestSceneOnlinePlaySubScreenStack : OnlinePlayTestScene + public partial class TestSceneOnlinePlaySubScreenStack : OnlinePlayTestScene { private ScreenStack stack = null!; @@ -56,7 +56,7 @@ namespace osu.Game.Tests.OnlinePlay AddAssert("mods reset", () => SelectedMods.Value.Count, () => Is.Zero); } - private class ScreenWithExternalBindableDisablement : OsuScreen + private partial class ScreenWithExternalBindableDisablement : OsuScreen { public override bool DisallowExternalBeatmapRulesetChanges { get; } @@ -66,7 +66,7 @@ namespace osu.Game.Tests.OnlinePlay } } - private class ScreenWithMod : OsuScreen + private partial class ScreenWithMod : OsuScreen { private readonly Mod mod; From 5feddae6c75f8f8020196362ea40646c7a08460e Mon Sep 17 00:00:00 2001 From: Zihad Date: Wed, 5 Mar 2025 21:35:24 +0600 Subject: [PATCH 670/801] Revert "Update window title in input thread" This reverts commit 14b5c0bf10389b924fa8ca515c2e27457fdcc119. This is not necessary as the title update is already scheduled on the correct thread by the framework. --- osu.Game/OsuGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 2b9e2cb9cd..e070e89c19 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -837,7 +837,7 @@ namespace osu.Game if (beatmap.NewValue != null && beatmap.NewValue is not DummyWorkingBeatmap) newTitle = $"{Name} - {beatmap.NewValue.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; - Host.InputThread.Scheduler.AddOnce(s => Host.Window.Title = s, newTitle); + Host.Window.Title = newTitle; } private void modsChanged(ValueChangedEvent> mods) From 4ae5f239cb3c342812bef639f43971ccca7d3a71 Mon Sep 17 00:00:00 2001 From: Zihad Date: Wed, 5 Mar 2025 21:41:11 +0600 Subject: [PATCH 671/801] Remove unnecessary comment --- osu.Game/OsuGame.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index e070e89c19..abe5ce21c6 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -832,7 +832,6 @@ namespace osu.Game if (Host.Window == null) return; - // prevent weird window title saying please load a beatmap string newTitle = Name; if (beatmap.NewValue != null && beatmap.NewValue is not DummyWorkingBeatmap) newTitle = $"{Name} - {beatmap.NewValue.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; From d33a8dfc3b57d3888c09a232e6d8fa3fb70c6dca Mon Sep 17 00:00:00 2001 From: Zihad Date: Wed, 5 Mar 2025 22:47:39 +0600 Subject: [PATCH 672/801] Skip updating window title for protected mapsets --- osu.Game/OsuGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index abe5ce21c6..37ff70ccb7 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -833,7 +833,7 @@ namespace osu.Game return; string newTitle = Name; - if (beatmap.NewValue != null && beatmap.NewValue is not DummyWorkingBeatmap) + if (beatmap.NewValue?.BeatmapSetInfo?.Protected == false && beatmap.NewValue is not DummyWorkingBeatmap) newTitle = $"{Name} - {beatmap.NewValue.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; Host.Window.Title = newTitle; From 02d19eaa55c05fe9149cf7771ca40342bc689bbd Mon Sep 17 00:00:00 2001 From: Zihad Date: Thu, 6 Mar 2025 01:36:59 +0600 Subject: [PATCH 673/801] Update window title changes to match osu! stable It shows beatmap metadata during gameplay, spectating, and watching replays but shows beatmap filename during editng --- osu.Game/OsuGame.cs | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 37ff70ccb7..ed71f357a5 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -421,6 +421,7 @@ namespace osu.Game SelectedMods.BindValueChanged(modsChanged); Beatmap.BindValueChanged(beatmapChanged, true); + configUserActivity.BindValueChanged(userActivityChanged); applySafeAreaConsiderations = LocalConfig.GetBindable(OsuSetting.SafeAreaConsiderations); applySafeAreaConsiderations.BindValueChanged(apply => SafeAreaContainer.SafeAreaOverrideEdges = apply.NewValue ? SafeAreaOverrideEdges : Edges.All, true); @@ -828,13 +829,41 @@ namespace osu.Game { beatmap.OldValue?.CancelAsyncLoad(); beatmap.NewValue?.BeginAsyncLoad(); + updateWindowTitle(); + } + private void userActivityChanged(ValueChangedEvent userActivity) + { + updateWindowTitle(); + } + + private void updateWindowTitle() + { if (Host.Window == null) return; + if (Beatmap.Value?.BeatmapSetInfo?.Protected != false || Beatmap.Value is DummyWorkingBeatmap) + { + Host.Window.Title = Name; + return; + } + string newTitle = Name; - if (beatmap.NewValue?.BeatmapSetInfo?.Protected == false && beatmap.NewValue is not DummyWorkingBeatmap) - newTitle = $"{Name} - {beatmap.NewValue.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; + + switch (configUserActivity.Value) + { + case UserActivity.InGame: + case UserActivity.TestingBeatmap: + case UserActivity.WatchingReplay: + newTitle = $"{Name} - {Beatmap.Value.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; + break; + + case UserActivity.EditingBeatmap: + if (Beatmap.Value.BeatmapInfo.Path != null) + newTitle = $"{Name} - {Beatmap.Value.BeatmapInfo.Path}"; + + break; + } Host.Window.Title = newTitle; } From 574f2363fff982d21d7ab42eaf130cc89000f5cb Mon Sep 17 00:00:00 2001 From: schiavoanto Date: Wed, 5 Mar 2025 23:31:35 +0100 Subject: [PATCH 674/801] Add localisation for skin management buttons in settings --- osu.Game/Localisation/CommonStrings.cs | 10 ++++++++++ osu.Game/Overlays/Settings/Sections/SkinSection.cs | 6 +++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs index 243a100029..26e344ec71 100644 --- a/osu.Game/Localisation/CommonStrings.cs +++ b/osu.Game/Localisation/CommonStrings.cs @@ -39,11 +39,21 @@ namespace osu.Game.Localisation /// public static LocalisableString Default => new TranslatableString(getKey(@"default"), @"Default"); + /// + /// "Rename" + /// + public static LocalisableString Rename => new TranslatableString(getKey(@"rename"), @"Rename"); + /// /// "Export" /// public static LocalisableString Export => new TranslatableString(getKey(@"export"), @"Export"); + /// + /// "Delete" + /// + public static LocalisableString Delete => new TranslatableString(getKey(@"delete"), @"Delete"); + /// /// "Width" /// diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index a89d5e2f4a..1f220138de 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -165,7 +165,7 @@ namespace osu.Game.Overlays.Settings.Sections [BackgroundDependencyLoader] private void load() { - Text = "Rename"; + Text = CommonStrings.Rename; Action = this.ShowPopover; } @@ -193,7 +193,7 @@ namespace osu.Game.Overlays.Settings.Sections [BackgroundDependencyLoader] private void load() { - Text = "Export"; + Text = CommonStrings.Export; Action = export; } @@ -231,7 +231,7 @@ namespace osu.Game.Overlays.Settings.Sections [BackgroundDependencyLoader] private void load() { - Text = "Delete"; + Text = CommonStrings.Delete; Action = delete; } From ee2615da53da7f537e5c920869464ce2bd13ffab Mon Sep 17 00:00:00 2001 From: schiavoanto Date: Wed, 5 Mar 2025 23:51:29 +0100 Subject: [PATCH 675/801] Use osu-web delete localisation --- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 1f220138de..84767c8619 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -27,6 +27,7 @@ using osu.Game.Screens.Select; using osu.Game.Skinning; using osuTK; using Realms; +using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; namespace osu.Game.Overlays.Settings.Sections { @@ -231,7 +232,7 @@ namespace osu.Game.Overlays.Settings.Sections [BackgroundDependencyLoader] private void load() { - Text = CommonStrings.Delete; + Text = WebCommonStrings.ButtonsDelete; Action = delete; } From 5c3695673b49fee5570f1fd0bcc0588cc2654d37 Mon Sep 17 00:00:00 2001 From: schiavoanto Date: Thu, 6 Mar 2025 00:22:47 +0100 Subject: [PATCH 676/801] Remove delete string from CommonStrings --- osu.Game/Localisation/CommonStrings.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs index 26e344ec71..f9d0feb5e2 100644 --- a/osu.Game/Localisation/CommonStrings.cs +++ b/osu.Game/Localisation/CommonStrings.cs @@ -49,11 +49,6 @@ namespace osu.Game.Localisation /// public static LocalisableString Export => new TranslatableString(getKey(@"export"), @"Export"); - /// - /// "Delete" - /// - public static LocalisableString Delete => new TranslatableString(getKey(@"delete"), @"Delete"); - /// /// "Width" /// From 50c4f9098320a6e7b46e5bcea425c96b03f1f07d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Mar 2025 12:55:59 +0900 Subject: [PATCH 677/801] Fix intermittent playlists results screen tests --- .../TestScenePlaylistsResultsScreen.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 469f7c8b74..6b73f1a5f4 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -156,13 +156,13 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); AddStep("scroll to right", () => resultsScreen.ChildrenOfType().Single().ChildrenOfType().Single().ScrollToEnd(false)); - AddAssert("right loading spinner shown", () => + AddUntilStep("right loading spinner shown", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible); waitForDisplay(); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() == beforePanelCount + scores_per_result); - AddAssert("right loading spinner hidden", () => + AddUntilStep("right loading spinner hidden", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden); } } @@ -180,26 +180,26 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); AddStep("scroll to right", () => resultsScreen.ChildrenOfType().Single().ChildrenOfType().Single().ScrollToEnd(false)); - AddAssert("right loading spinner shown", () => + AddUntilStep("right loading spinner shown", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible); waitForDisplay(); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() == beforePanelCount + scores_per_result); - AddAssert("right loading spinner hidden", () => + AddUntilStep("right loading spinner hidden", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden); AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); AddStep("bind delayed handler with no scores", () => bindHandler(delayed: true, noScores: true)); AddStep("scroll to right", () => resultsScreen.ChildrenOfType().Single().ChildrenOfType().Single().ScrollToEnd(false)); - AddAssert("right loading spinner shown", () => + AddUntilStep("right loading spinner shown", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible); waitForDisplay(); AddAssert("count not increased", () => this.ChildrenOfType().Count() == beforePanelCount); - AddAssert("right loading spinner hidden", () => + AddUntilStep("right loading spinner hidden", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden); AddAssert("no placeholders shown", () => this.ChildrenOfType().Count(), () => Is.Zero); @@ -222,13 +222,13 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); AddStep("scroll to left", () => resultsScreen.ChildrenOfType().Single().ChildrenOfType().Single().ScrollToStart(false)); - AddAssert("left loading spinner shown", () => + AddUntilStep("left loading spinner shown", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Visible); waitForDisplay(); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() == beforePanelCount + scores_per_result); - AddAssert("left loading spinner hidden", () => + AddUntilStep("left loading spinner hidden", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Hidden); } } @@ -242,7 +242,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("bind user score info handler", () => bindHandler(noScores: true)); createUserBestResults(); AddAssert("no scores visible", () => !resultsScreen.ChildrenOfType().Single().GetScorePanels().Any()); - AddAssert("placeholder shown", () => this.ChildrenOfType().Count(), () => Is.EqualTo(1)); + AddUntilStep("placeholder shown", () => this.ChildrenOfType().Count(), () => Is.EqualTo(1)); } [Test] @@ -261,12 +261,12 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("simulate user falling down ranking", () => userScore.Position += 2); AddStep("scroll to left", () => resultsScreen.ChildrenOfType().Single().ChildrenOfType().Single().ScrollToStart(false)); - AddAssert("left loading spinner shown", () => + AddUntilStep("left loading spinner shown", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Visible); waitForDisplay(); - AddAssert("left loading spinner hidden", () => + AddUntilStep("left loading spinner hidden", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Hidden); } From 0bdbaa872ceb6c8d18c68aaf175dcc95548e3a87 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Mar 2025 14:11:41 +0900 Subject: [PATCH 678/801] Simplify user style validation This commit takes half a step back. Rather than trying to preserve the beatmap style across selections, it probably makes more sense to always reset it. It is more likely for playlists to be dominated by unique beatmap sets anyway. Doing this already heavily simplifies the process. Given the above, we can rely on the fact that the user beatmap and ruleset are selected together, and are thus implicitly validated as of exiting the style selection screen. It follows that the only extra validation we need to do is to make sure that the user's ruleset is valid for the playlist item. This even makes the mod structure, which is a bit of an unfortunate scenario, somewhat tenable to look at. The user mods only need to be validated when either the playlist item or the user ruleset changes. If we ever move the ruleset selection into the style selection screen, then we can also remove the latter of the validations with similar reason as the ruleset. --- .../Playlists/PlaylistsRoomSubScreen.cs | 98 ++++++++++++------- 1 file changed, 63 insertions(+), 35 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index df7f86704f..3b180efbce 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -452,16 +452,21 @@ namespace osu.Game.Screens.OnlinePlay.Playlists isIdle.BindValueChanged(_ => updatePollingRate(), true); - beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); - beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateSelectionState()); + SelectedItem.BindValueChanged(onSelectedItemChanged); - SelectedItem.BindValueChanged(_ => updateSelectionState()); - userBeatmap.BindValueChanged(_ => updateSelectionState()); - userRuleset.BindValueChanged(_ => updateSelectionState()); - userMods.BindValueChanged(_ => updateSelectionState()); + beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); + beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateGameplayState()); + + userBeatmap.BindValueChanged(_ => updateGameplayState()); + userRuleset.BindValueChanged(_ => + { + validateUserMods(); + updateGameplayState(); + }); + userMods.BindValueChanged(_ => updateGameplayState()); updateSetupState(); - updateSelectionState(); + updateGameplayState(); } /// @@ -519,44 +524,67 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } /// - /// Responds to changes in the selected playlist item or user style (beatmap/ruleset/mods) to validate and update global states in preparation for a gameplay session. + /// Responds to changes in to validate the user style and update the global gameplay state. /// - private void updateSelectionState() + private void onSelectedItemChanged(ValueChangedEvent e) { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) + if (e.NewValue is not PlaylistItem item) return; - // Reset entire user style when disabled. - if (!item.Freestyle) + // Always resetting the user beatmap style when a new item is selected is most intuitive. + userBeatmap.Value = null; + + if (item.Freestyle) { - userBeatmap.Value = null; - userRuleset.Value = null; - } - - // Reset beatmap style when no longer from the same beatmap set. - if (userBeatmap.Value != null && userBeatmap.Value.BeatmapSet!.OnlineID != item.Beatmap.BeatmapSet!.OnlineID) - userBeatmap.Value = null; - - IBeatmapInfo gameplayBeatmap = userBeatmap.Value ?? item.Beatmap; - - // Reset ruleset style when no longer valid for the beatmap. - if (userRuleset.Value != null) - { - int beatmapRuleset = gameplayBeatmap.Ruleset.OnlineID; - if (beatmapRuleset > 0 && userRuleset.Value.OnlineID != beatmapRuleset) + // If freestyle is active, attempt to preserve the user ruleset style but only if the online item is from the osu! ruleset + // (i.e. the beatmap is generally always convertible to the current ruleset, excluding custom rulesets). + if (item.RulesetID > 0) userRuleset.Value = null; } + validateUserMods(); + + updateGameplayState(); + } + + /// + /// Lists the s that are valid to be selected for the user mod style. + /// + private Mod[] listAllowedMods() + { + if (SelectedItem.Value is not PlaylistItem item) + return []; + RulesetInfo gameplayRuleset = userRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; Ruleset rulesetInstance = gameplayRuleset.CreateInstance(); - // Remove any user mods that are no longer allowed. - Mod[] allowedMods = item.Freestyle - ? rulesetInstance.AllMods.OfType().Where(m => ModUtils.IsValidFreeModForMatchType(m, room.Type)).ToArray() - : item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); - Mod[] newUserMods = userMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); - if (!newUserMods.SequenceEqual(userMods.Value)) - userMods.Value = newUserMods; + if (item.Freestyle) + return rulesetInstance.AllMods.OfType().Where(m => ModUtils.IsValidFreeModForMatchType(m, room.Type)).ToArray(); + + return item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); + } + + /// + /// Validates the user mod style against the selected item and ruleset style. + /// + private void validateUserMods() + { + Mod[] allowedMods = listAllowedMods(); + userMods.Value = userMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); + } + + /// + /// Updates the global states in preparation for a new gameplay session. + /// + private void updateGameplayState() + { + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) + return; + + IBeatmapInfo gameplayBeatmap = userBeatmap.Value ?? item.Beatmap; + RulesetInfo gameplayRuleset = userRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; + Ruleset rulesetInstance = gameplayRuleset.CreateInstance(); + Mod[] allowedMods = listAllowedMods(); // Update global gameplay state to correspond to the new selection. // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info @@ -683,7 +711,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists beginHandlingTrack(); // Required to update beatmap/ruleset when resuming from style selection. - updateSelectionState(); + updateGameplayState(); } public override bool OnExiting(ScreenExitEvent e) From 975f4e4c7df982ff2762b0223b2ef51c19d2070e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Mar 2025 15:45:11 +0900 Subject: [PATCH 679/801] Simplify code and don't set title if already correct --- osu.Game/OsuGame.cs | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index ed71f357a5..4a9154f14b 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -421,7 +421,7 @@ namespace osu.Game SelectedMods.BindValueChanged(modsChanged); Beatmap.BindValueChanged(beatmapChanged, true); - configUserActivity.BindValueChanged(userActivityChanged); + configUserActivity.BindValueChanged(_ => updateWindowTitle()); applySafeAreaConsiderations = LocalConfig.GetBindable(OsuSetting.SafeAreaConsiderations); applySafeAreaConsiderations.BindValueChanged(apply => SafeAreaContainer.SafeAreaOverrideEdges = apply.NewValue ? SafeAreaOverrideEdges : Edges.All, true); @@ -832,26 +832,19 @@ namespace osu.Game updateWindowTitle(); } - private void userActivityChanged(ValueChangedEvent userActivity) - { - updateWindowTitle(); - } - private void updateWindowTitle() { if (Host.Window == null) return; - if (Beatmap.Value?.BeatmapSetInfo?.Protected != false || Beatmap.Value is DummyWorkingBeatmap) - { - Host.Window.Title = Name; - return; - } - - string newTitle = Name; + string newTitle; switch (configUserActivity.Value) { + default: + newTitle = Name; + break; + case UserActivity.InGame: case UserActivity.TestingBeatmap: case UserActivity.WatchingReplay: @@ -859,13 +852,12 @@ namespace osu.Game break; case UserActivity.EditingBeatmap: - if (Beatmap.Value.BeatmapInfo.Path != null) - newTitle = $"{Name} - {Beatmap.Value.BeatmapInfo.Path}"; - + newTitle = $"{Name} - {Beatmap.Value.BeatmapInfo.Path ?? "new beatmap"}"; break; } - Host.Window.Title = newTitle; + if (newTitle != Host.Window.Title) + Host.Window.Title = newTitle; } private void modsChanged(ValueChangedEvent> mods) From bdd2808fb598360fd710520f92eb5e107ff97cea Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Mar 2025 16:05:51 +0900 Subject: [PATCH 680/801] Bump difficulty calculator versions in preparation for release --- osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs | 2 +- osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs | 2 +- osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 6434adb63c..14a8ff31c5 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty private float halfCatcherWidth; - public override int Version => 20220701; + public override int Version => 20250306; public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 30339fbaa7..eb2cb95972 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty { private const double difficulty_multiplier = 0.0675; - public override int Version => 20241007; + public override int Version => 20250306; public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 7bc050d2df..e0bc0e177c 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private bool isConvert; - public override int Version => 20241007; + public override int Version => 20250306; public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) From ba3d6ddc43e0a0dead6b79b83f19585e08865759 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Mar 2025 16:09:05 +0900 Subject: [PATCH 681/801] Add a slew of tests --- .../TestScenePlaylistsRoomSubScreen.cs | 558 ++++++++++++++++++ .../Playlists/PlaylistsRoomSubScreen.cs | 38 +- 2 files changed, 577 insertions(+), 19 deletions(-) create mode 100644 osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs new file mode 100644 index 0000000000..f797f6f2ac --- /dev/null +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs @@ -0,0 +1,558 @@ +// 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.IO; +using System.Linq; +using System.Text; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Platform; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Catch.Mods; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Taiko; +using osu.Game.Rulesets.Taiko.Mods; +using osu.Game.Screens; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Screens.OnlinePlay.Playlists; +using osu.Game.Tests.Resources; +using osu.Game.Tests.Visual.OnlinePlay; + +namespace osu.Game.Tests.Visual.Playlists +{ + public partial class TestScenePlaylistsRoomSubScreen : OnlinePlayTestScene + { + private BeatmapManager beatmaps = null!; + private BeatmapSetInfo importedSet = null!; + + [BackgroundDependencyLoader] + private void load(GameHost host, AudioManager audio) + { + BeatmapStore beatmapStore; + + Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default)); + Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore()); + Dependencies.Cache(Realm); + + Add(beatmapStore); + + importedSet = beatmaps.Import(new BeatmapSetInfo + { + OnlineID = TestResources.GetNextTestID(), + Hash = new MemoryStream(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())).ComputeMD5Hash(), + DateAdded = DateTimeOffset.UtcNow, + Beatmaps = + { + new BeatmapInfo + { + OnlineID = 1, + DifficultyName = "Osu 1", + Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), + MD5Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), + Ruleset = new OsuRuleset().RulesetInfo, + Metadata = + { + Artist = "Some Artist", + Title = "Some Song", + Author = { Username = "Some Guy" }, + }, + }, + new BeatmapInfo + { + OnlineID = 2, + DifficultyName = "Osu 2", + Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), + MD5Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), + Ruleset = new OsuRuleset().RulesetInfo, + Metadata = + { + Artist = "Some Artist", + Title = "Some Song", + Author = { Username = "Some Guy" }, + }, + }, + new BeatmapInfo + { + OnlineID = 3, + DifficultyName = "Taiko 1", + Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), + MD5Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), + Ruleset = new TaikoRuleset().RulesetInfo, + Metadata = + { + Artist = "Some Artist", + Title = "Some Song", + Author = { Username = "Some Guy" }, + }, + }, + new BeatmapInfo + { + OnlineID = 4, + DifficultyName = "Taiko 2", + Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), + MD5Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), + Ruleset = new TaikoRuleset().RulesetInfo, + Metadata = + { + Artist = "Some Artist", + Title = "Some Song", + Author = { Username = "Some Guy" }, + }, + } + } + })!.PerformRead(s => s.Detach()); + } + + /// + /// Tests that the beatmap and ruleset are adjusted to follow the selected item. + /// + [Test] + public void TestBeatmapAndRuleset_FollowSelection() + { + Room room = null!; + + AddStep("add room", () => + { + room = new Room + { + RoomID = 1, + Playlist = + [ + // osu! beatmap + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + // osu! beatmap converted played in taiko + new PlaylistItem(importedSet.Beatmaps[1]) + { + RulesetID = new TaikoRuleset().RulesetInfo.OnlineID, + Freestyle = true + } + ] + }; + + API.Perform(new CreateRoomRequest(room)); + }); + + TestPlaylistsRoomSubScreen screen = null!; + AddStep("load screen", () => LoadScreen(new TestPlaylistsScreen(screen = new TestPlaylistsRoomSubScreen(room)))); + AddUntilStep("wait for load", () => screen.IsLoaded); + + AddStep("select first item", () => screen.SelectedItem.Value = room.Playlist[0]); + AddUntilStep("first beatmap selected", () => Beatmap.Value.BeatmapInfo.Equals(importedSet.Beatmaps[0])); + AddUntilStep("osu ruleset selected", () => Ruleset.Value.Equals(new OsuRuleset().RulesetInfo)); + + AddStep("select second item", () => screen.SelectedItem.Value = room.Playlist[1]); + AddUntilStep("second beatmap selected", () => Beatmap.Value.BeatmapInfo.Equals(importedSet.Beatmaps[1])); + AddUntilStep("taiko ruleset selected", () => Ruleset.Value.Equals(new TaikoRuleset().RulesetInfo)); + } + + /// + /// Tests that the beatmap style is reset when the selected item is changed. + /// + [Test] + public void TestBeatmapStyle_Reset_OnSelection() + { + Room room = null!; + + AddStep("add room", () => + { + room = new Room + { + RoomID = 1, + Playlist = + [ + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + ] + }; + + API.Perform(new CreateRoomRequest(room)); + }); + + TestPlaylistsRoomSubScreen screen = null!; + AddStep("load screen", () => LoadScreen(new TestPlaylistsScreen(screen = new TestPlaylistsRoomSubScreen(room)))); + AddUntilStep("wait for load", () => screen.IsLoaded); + + AddStep("set user beatmap style", () => screen.UserBeatmap.Value = importedSet.Beatmaps[1]); + AddUntilStep("user beatmap selected", () => Beatmap.Value.BeatmapInfo.Equals(importedSet.Beatmaps[1])); + + AddStep("select second item", () => screen.SelectedItem.Value = room.Playlist[1]); + AddUntilStep("user beatmap style reset", () => screen.UserBeatmap.Value == null); + AddUntilStep("second beatmap selected", () => Beatmap.Value.BeatmapInfo.Equals(importedSet.Beatmaps[0])); + } + + /// + /// Tests that the ruleset style is reset when the selected item is changed and it's no longer valid. + /// + [Test] + public void TestRulesetStyle_Reset_OnSelection_IfNotValid() + { + Room room = null!; + + AddStep("add room", () => + { + room = new Room + { + RoomID = 1, + Playlist = + [ + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new TaikoRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + ] + }; + + API.Perform(new CreateRoomRequest(room)); + }); + + TestPlaylistsRoomSubScreen screen = null!; + AddStep("load screen", () => LoadScreen(new TestPlaylistsScreen(screen = new TestPlaylistsRoomSubScreen(room)))); + AddUntilStep("wait for load", () => screen.IsLoaded); + + AddStep("set user ruleset style", () => screen.UserRuleset.Value = new ManiaRuleset().RulesetInfo); + AddUntilStep("user ruleset selected", () => Ruleset.Value.Equals(new ManiaRuleset().RulesetInfo)); + + AddStep("select second item", () => screen.SelectedItem.Value = room.Playlist[1]); + AddUntilStep("user ruleset style reset", () => screen.UserRuleset.Value == null); + AddUntilStep("second ruleset selected", () => Ruleset.Value.Equals(new TaikoRuleset().RulesetInfo)); + } + + /// + /// Tests that the ruleset style is preserved when the selected item is changed and the ruleset is still valid. + /// + [Test] + public void TestRulesetStyle_Preserved_OnSelection_IfStillValid() + { + Room room = null!; + + AddStep("add room", () => + { + room = new Room + { + RoomID = 1, + Playlist = + [ + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + ] + }; + + API.Perform(new CreateRoomRequest(room)); + }); + + TestPlaylistsRoomSubScreen screen = null!; + AddStep("load screen", () => LoadScreen(new TestPlaylistsScreen(screen = new TestPlaylistsRoomSubScreen(room)))); + AddUntilStep("wait for load", () => screen.IsLoaded); + + AddStep("set user ruleset style", () => screen.UserRuleset.Value = new ManiaRuleset().RulesetInfo); + AddUntilStep("user ruleset selected", () => Ruleset.Value.Equals(new ManiaRuleset().RulesetInfo)); + + AddStep("select second item", () => screen.SelectedItem.Value = room.Playlist[1]); + AddUntilStep("user ruleset style preserved", () => screen.UserRuleset.Value!.Equals(new ManiaRuleset().RulesetInfo)); + AddUntilStep("user ruleset selected", () => Ruleset.Value.Equals(new ManiaRuleset().RulesetInfo)); + } + + /// + /// Tests that mod style is reset when the selected item is changed to another with an inconvertible ruleset. + /// No user style is assumed. + /// + [Test] + public void TestModsReset_OnSelection_DifferentRuleset_NoUserStyle() + { + Room room = null!; + + AddStep("add room", () => + { + room = new Room + { + RoomID = 1, + Playlist = + [ + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new TaikoRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + ] + }; + + API.Perform(new CreateRoomRequest(room)); + }); + + TestPlaylistsRoomSubScreen screen = null!; + AddStep("load screen", () => LoadScreen(new TestPlaylistsScreen(screen = new TestPlaylistsRoomSubScreen(room)))); + AddUntilStep("wait for load", () => screen.IsLoaded); + + AddStep("set user mods", () => screen.UserMods.Value = [new OsuModDoubleTime()]); + AddUntilStep("user mods selected", () => SelectedMods.Value.OfType().Any()); + + AddStep("select second item", () => screen.SelectedItem.Value = room.Playlist[1]); + AddUntilStep("user mod style reset", () => !screen.UserMods.Value.Any()); + AddUntilStep("mods reset", () => !SelectedMods.Value.Any()); + } + + /// + /// Tests that mod style is preserved when the selected item is changed to another with the same ruleset. + /// No user style is assumed. + /// + [Test] + public void TestModsPreserved_OnSelection_SameRuleset_NoUserStyle() + { + Room room = null!; + + AddStep("add room", () => + { + room = new Room + { + RoomID = 1, + Playlist = + [ + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + ] + }; + + API.Perform(new CreateRoomRequest(room)); + }); + + TestPlaylistsRoomSubScreen screen = null!; + AddStep("load screen", () => LoadScreen(new TestPlaylistsScreen(screen = new TestPlaylistsRoomSubScreen(room)))); + AddUntilStep("wait for load", () => screen.IsLoaded); + + AddStep("set user mods", () => screen.UserMods.Value = [new OsuModDoubleTime()]); + AddUntilStep("user mods selected", () => SelectedMods.Value.OfType().Any()); + + AddStep("select second item", () => screen.SelectedItem.Value = room.Playlist[1]); + AddUntilStep("user mod style preserved", () => screen.UserMods.Value.OfType().Any()); + AddUntilStep("mods preserved", () => SelectedMods.Value.OfType().Any()); + } + + /// + /// Tests that mod style is reset when the selected item is changed to another with an inconvertible ruleset. + /// A user beatmap/ruleset style is assumed. + /// + [Test] + public void TestModsReset_OnSelection_DifferentRuleset_WithUserStyle() + { + Room room = null!; + + AddStep("add room", () => + { + room = new Room + { + RoomID = 1, + Playlist = + [ + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new TaikoRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + ] + }; + + API.Perform(new CreateRoomRequest(room)); + }); + + TestPlaylistsRoomSubScreen screen = null!; + AddStep("load screen", () => LoadScreen(new TestPlaylistsScreen(screen = new TestPlaylistsRoomSubScreen(room)))); + AddUntilStep("wait for load", () => screen.IsLoaded); + + AddStep("set user ruleset", () => screen.UserRuleset.Value = new CatchRuleset().RulesetInfo); + AddUntilStep("user ruleset selected", () => Ruleset.Value.Equals(new CatchRuleset().RulesetInfo)); + AddStep("set user mods", () => screen.UserMods.Value = [new CatchModDoubleTime()]); + AddUntilStep("user mods selected", () => SelectedMods.Value.OfType().Any()); + + AddStep("select second item", () => screen.SelectedItem.Value = room.Playlist[1]); + AddUntilStep("user mod style reset", () => !screen.UserMods.Value.Any()); + AddUntilStep("mods reset", () => !SelectedMods.Value.Any()); + } + + /// + /// Tests that mod style is preserved when the selected item is changed to another with the same ruleset. + /// A user beatmap/ruleset style is assumed. + /// + [Test] + public void TestModsPreserved_OnSelection_SameRuleset_WithStyle() + { + Room room = null!; + + AddStep("add room", () => + { + room = new Room + { + RoomID = 1, + Playlist = + [ + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new TaikoRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + ] + }; + + API.Perform(new CreateRoomRequest(room)); + }); + + TestPlaylistsRoomSubScreen screen = null!; + AddStep("load screen", () => LoadScreen(new TestPlaylistsScreen(screen = new TestPlaylistsRoomSubScreen(room)))); + AddUntilStep("wait for load", () => screen.IsLoaded); + + AddStep("set user ruleset", () => screen.UserRuleset.Value = new TaikoRuleset().RulesetInfo); + AddUntilStep("user ruleset selected", () => Ruleset.Value.Equals(new TaikoRuleset().RulesetInfo)); + AddStep("set user mods", () => screen.UserMods.Value = [new TaikoModDoubleTime()]); + AddUntilStep("user mods selected", () => SelectedMods.Value.OfType().Any()); + + AddStep("select second item", () => screen.SelectedItem.Value = room.Playlist[1]); + AddUntilStep("user mod style preserved", () => screen.UserMods.Value.OfType().Any()); + AddUntilStep("mods preserved", () => SelectedMods.Value.OfType().Any()); + } + + /// + /// Tests that the mod style is revalidated when the ruleset style is changed. + /// + [Test] + public void TestModsValidated_OnRulesetStyleChanged() + { + Room room = null!; + + AddStep("add room", () => + { + room = new Room + { + RoomID = 1, + Playlist = + [ + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + ] + }; + + API.Perform(new CreateRoomRequest(room)); + }); + + TestPlaylistsRoomSubScreen screen = null!; + AddStep("load screen", () => LoadScreen(new TestPlaylistsScreen(screen = new TestPlaylistsRoomSubScreen(room)))); + AddUntilStep("wait for load", () => screen.IsLoaded); + + AddStep("set user mods", () => screen.UserMods.Value = [new OsuModDoubleTime()]); + AddUntilStep("user mods selected", () => SelectedMods.Value.OfType().Any()); + + AddStep("set user ruleset", () => screen.UserRuleset.Value = new TaikoRuleset().RulesetInfo); + AddUntilStep("user ruleset selected", () => Ruleset.Value.Equals(new TaikoRuleset().RulesetInfo)); + AddUntilStep("user mods reset", () => !screen.UserMods.Value.Any()); + AddUntilStep("mods reset", () => !SelectedMods.Value.Any()); + } + + private partial class TestPlaylistsScreen : OsuScreen + { + public TestPlaylistsScreen(PlaylistsRoomSubScreen screen) + { + OnlinePlaySubScreenStack stack; + + InternalChildren = new Drawable[] + { + stack = new OnlinePlaySubScreenStack + { + RelativeSizeAxes = Axes.Both + }, + new BackButton + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + State = { Value = Visibility.Visible }, + Action = () => + { + if (stack.CurrentScreen is not PlaylistsRoomSubScreen) + stack.Exit(); + } + } + }; + + stack.Push(screen); + } + } + + private partial class TestPlaylistsRoomSubScreen : PlaylistsRoomSubScreen + { + public new Bindable SelectedItem => base.SelectedItem; + public new Bindable UserBeatmap => base.UserBeatmap; + public new Bindable UserRuleset => base.UserRuleset; + public new Bindable> UserMods => base.UserMods; + + public TestPlaylistsRoomSubScreen(Room room) + : base(room) + { + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 3b180efbce..6cb7962c82 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -114,9 +114,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); protected readonly Bindable SelectedItem = new Bindable(); - private readonly Bindable userBeatmap = new Bindable(); - private readonly Bindable userRuleset = new Bindable(); - private readonly Bindable> userMods = new Bindable>(Array.Empty()); + protected readonly Bindable UserBeatmap = new Bindable(); + protected readonly Bindable UserRuleset = new Bindable(); + protected readonly Bindable> UserMods = new Bindable>(Array.Empty()); private readonly IBindable isIdle = new BindableBool(); private readonly Room room; @@ -309,7 +309,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Current = userMods, + Current = UserMods, Scale = new Vector2(0.8f), } } @@ -437,7 +437,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists LoadComponent(userModsSelectOverlay = new RoomModSelectOverlay { SelectedItem = { BindTarget = SelectedItem }, - SelectedMods = { BindTarget = userMods }, + SelectedMods = { BindTarget = UserMods }, IsValidMod = _ => false }); } @@ -457,13 +457,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateGameplayState()); - userBeatmap.BindValueChanged(_ => updateGameplayState()); - userRuleset.BindValueChanged(_ => + UserBeatmap.BindValueChanged(_ => updateGameplayState()); + UserRuleset.BindValueChanged(_ => { validateUserMods(); updateGameplayState(); }); - userMods.BindValueChanged(_ => updateGameplayState()); + UserMods.BindValueChanged(_ => updateGameplayState()); updateSetupState(); updateGameplayState(); @@ -532,14 +532,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return; // Always resetting the user beatmap style when a new item is selected is most intuitive. - userBeatmap.Value = null; + UserBeatmap.Value = null; if (item.Freestyle) { // If freestyle is active, attempt to preserve the user ruleset style but only if the online item is from the osu! ruleset // (i.e. the beatmap is generally always convertible to the current ruleset, excluding custom rulesets). if (item.RulesetID > 0) - userRuleset.Value = null; + UserRuleset.Value = null; } validateUserMods(); @@ -555,7 +555,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (SelectedItem.Value is not PlaylistItem item) return []; - RulesetInfo gameplayRuleset = userRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; + RulesetInfo gameplayRuleset = UserRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; Ruleset rulesetInstance = gameplayRuleset.CreateInstance(); if (item.Freestyle) @@ -570,7 +570,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private void validateUserMods() { Mod[] allowedMods = listAllowedMods(); - userMods.Value = userMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); + UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); } /// @@ -581,8 +581,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) return; - IBeatmapInfo gameplayBeatmap = userBeatmap.Value ?? item.Beatmap; - RulesetInfo gameplayRuleset = userRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; + IBeatmapInfo gameplayBeatmap = UserBeatmap.Value ?? item.Beatmap; + RulesetInfo gameplayRuleset = UserRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; Ruleset rulesetInstance = gameplayRuleset.CreateInstance(); Mod[] allowedMods = listAllowedMods(); @@ -592,7 +592,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == beatmapId); Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); Ruleset.Value = gameplayRuleset; - Mods.Value = userMods.Value.Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToArray(); + Mods.Value = UserMods.Value.Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToArray(); // Update UI elements to reflect the new selection. bool freemods = allowedMods.Length > 0; @@ -640,8 +640,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return; // Required for validation inside the player. - RulesetInfo gameplayRuleset = userRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; - IBeatmapInfo gameplayBeatmap = userBeatmap.Value ?? item.Beatmap; + RulesetInfo gameplayRuleset = UserRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; + IBeatmapInfo gameplayBeatmap = UserBeatmap.Value ?? item.Beatmap; PlaylistItem gameplayItem = item.With(ruleset: gameplayRuleset.OnlineID, beatmap: new Optional(gameplayBeatmap)); sampleStart?.Play(); @@ -675,8 +675,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists this.Push(new PlaylistsRoomFreestyleSelect(room, item) { - Beatmap = { BindTarget = userBeatmap }, - Ruleset = { BindTarget = userRuleset } + Beatmap = { BindTarget = UserBeatmap }, + Ruleset = { BindTarget = UserRuleset } }); } From 0f0dd58b698df3f30baca8988fac285c5c87401a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Mar 2025 09:45:44 +0100 Subject: [PATCH 682/801] Fix differential submission process crashing when no files have changed Closes https://github.com/ppy/osu/issues/32247. --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 13981bcb69..2ea710d3ab 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -304,7 +304,7 @@ namespace osu.Game.Screens.Edit.Submission Logger.Log($"Beatmap submission failed on upload: {ex}"); allowExit(); }; - patchRequest.Progressed += (current, total) => uploadStep.SetInProgress((float)current / total); + patchRequest.Progressed += (current, total) => uploadStep.SetInProgress(total > 0 ? (float)current / total : null); api.Queue(patchRequest); uploadStep.SetInProgress(); From 05c7b903c360cefda1923bebae729039ad55d03c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Mar 2025 18:35:38 +0900 Subject: [PATCH 683/801] Hide mod select overlay on exit Only discovered this when running multiplayer tests on the upcoming rewrite for that screen. --- .../Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 6cb7962c82..a4e0370ed2 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -719,6 +719,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (!ensureExitConfirmed()) return true; + // Must hide this overlay because it is added to a global container. + userModsSelectOverlay.Hide(); + if (room.RoomID != null) api.Queue(new PartRoomRequest(room)); From b0fad7e83db35e9f016c11e59cb0e8588e52d440 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Mar 2025 18:37:56 +0900 Subject: [PATCH 684/801] Also hide when suspending --- .../Playlists/PlaylistsRoomSubScreen.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index a4e0370ed2..fd1f35a3fc 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -701,7 +701,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public override void OnSuspending(ScreenTransitionEvent e) { - endHandlingTrack(); + onLeaving(); base.OnSuspending(e); } @@ -719,13 +719,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (!ensureExitConfirmed()) return true; - // Must hide this overlay because it is added to a global container. - userModsSelectOverlay.Hide(); - if (room.RoomID != null) api.Queue(new PartRoomRequest(room)); - endHandlingTrack(); + onLeaving(); return base.OnExiting(e); } @@ -755,6 +752,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return base.OnBackButton(); } + private void onLeaving() + { + // Must hide this overlay because it is added to a global container. + userModsSelectOverlay.Hide(); + + endHandlingTrack(); + } + /// /// Handles changes in the track to keep it looping while active. /// From 64830e2c31dba046b8753b13aed37fa9596ef413 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Mar 2025 18:51:32 +0900 Subject: [PATCH 685/801] Use localisation --- osu.Game/Screens/Edit/Editor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index bf254093b3..80e1a656de 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -392,7 +392,7 @@ namespace osu.Game.Screens.Edit { undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo) { Hotkey = new Hotkey(PlatformAction.Undo) }, redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo) { Hotkey = new Hotkey(PlatformAction.Redo) }, - discardChangesMenuItem = new EditorMenuItem("Discard unsaved changes", MenuItemType.Destructive, DiscardUnsavedChanges) + discardChangesMenuItem = new EditorMenuItem(GlobalActionKeyBindingStrings.EditorDiscardUnsavedChanges, MenuItemType.Destructive, DiscardUnsavedChanges) { Hotkey = new Hotkey(GlobalAction.EditorDiscardUnsavedChanges) }, From 6e387761307fe5c03b83c5551f8286a974b4fae4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Mar 2025 18:55:40 +0900 Subject: [PATCH 686/801] Fix initial multiplayer room items not having freestyle --- osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index 4c4a3d97f2..3234e28166 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -80,6 +80,7 @@ namespace osu.Game.Online.Rooms PlaylistOrder = item.PlaylistOrder ?? 0; PlayedAt = item.PlayedAt; StarRating = item.Beatmap.StarRating; + Freestyle = item.Freestyle; } } } From d9b7d034ba34e587189adfb5a9c8930b5e1ef8ce Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Mar 2025 19:34:20 +0900 Subject: [PATCH 687/801] Move to file menu --- osu.Game/Screens/Edit/Editor.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 80e1a656de..f56380a34d 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -392,10 +392,6 @@ namespace osu.Game.Screens.Edit { undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo) { Hotkey = new Hotkey(PlatformAction.Undo) }, redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo) { Hotkey = new Hotkey(PlatformAction.Redo) }, - discardChangesMenuItem = new EditorMenuItem(GlobalActionKeyBindingStrings.EditorDiscardUnsavedChanges, MenuItemType.Destructive, DiscardUnsavedChanges) - { - Hotkey = new Hotkey(GlobalAction.EditorDiscardUnsavedChanges) - }, new OsuMenuItemSpacer(), cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut) { Hotkey = new Hotkey(PlatformAction.Cut) }, copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy) { Hotkey = new Hotkey(PlatformAction.Copy) }, @@ -1273,6 +1269,11 @@ namespace osu.Game.Screens.Edit saveRelatedMenuItems.Add(save); yield return save; + yield return discardChangesMenuItem = new EditorMenuItem(GlobalActionKeyBindingStrings.EditorDiscardUnsavedChanges, MenuItemType.Destructive, DiscardUnsavedChanges) + { + Hotkey = new Hotkey(GlobalAction.EditorDiscardUnsavedChanges) + }; + if (RuntimeInfo.OS != RuntimeInfo.Platform.Android) { var export = createExportMenu(); From e39b551b484ea6689ebf581e58a9324361bb894f Mon Sep 17 00:00:00 2001 From: GAMIS65 Date: Thu, 6 Mar 2025 22:48:08 +0100 Subject: [PATCH 688/801] Use localisation from osu web for the report button --- osu.Game/Overlays/Chat/DrawableChatUsername.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Chat/DrawableChatUsername.cs b/osu.Game/Overlays/Chat/DrawableChatUsername.cs index 67191f6836..9cdad507a6 100644 --- a/osu.Game/Overlays/Chat/DrawableChatUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableChatUsername.cs @@ -26,6 +26,7 @@ using osu.Game.Resources.Localisation.Web; using osuTK; using osuTK.Graphics; using ChatStrings = osu.Game.Localisation.ChatStrings; +using WebUsersStrings = osu.Game.Resources.Localisation.Web.UsersStrings; namespace osu.Game.Overlays.Chat { @@ -178,7 +179,7 @@ namespace osu.Game.Overlays.Chat } if (!user.Equals(api.LocalUser.Value)) - items.Add(new OsuMenuItem("Report", MenuItemType.Destructive, ReportRequested)); + items.Add(new OsuMenuItem(WebUsersStrings.ReportButtonText, MenuItemType.Destructive, ReportRequested)); return items.ToArray(); } From 81d35a7ebfc7a7b144112e62cf4d899522c6a9e5 Mon Sep 17 00:00:00 2001 From: GAMIS65 Date: Thu, 6 Mar 2025 23:02:22 +0100 Subject: [PATCH 689/801] Use UsersStrings instead --- osu.Game/Overlays/Chat/DrawableChatUsername.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Chat/DrawableChatUsername.cs b/osu.Game/Overlays/Chat/DrawableChatUsername.cs index 9cdad507a6..83f67d1a8a 100644 --- a/osu.Game/Overlays/Chat/DrawableChatUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableChatUsername.cs @@ -26,7 +26,6 @@ using osu.Game.Resources.Localisation.Web; using osuTK; using osuTK.Graphics; using ChatStrings = osu.Game.Localisation.ChatStrings; -using WebUsersStrings = osu.Game.Resources.Localisation.Web.UsersStrings; namespace osu.Game.Overlays.Chat { @@ -179,7 +178,7 @@ namespace osu.Game.Overlays.Chat } if (!user.Equals(api.LocalUser.Value)) - items.Add(new OsuMenuItem(WebUsersStrings.ReportButtonText, MenuItemType.Destructive, ReportRequested)); + items.Add(new OsuMenuItem(UsersStrings.ReportButtonText, MenuItemType.Destructive, ReportRequested)); return items.ToArray(); } From 18aa168a00f1bdfb019844e5e024a3b0d606dac3 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 7 Mar 2025 15:45:27 +0900 Subject: [PATCH 690/801] Allow kiai/star-fountain SFX to be skinnable --- osu.Game/Screens/Menu/KiaiMenuFountains.cs | 12 ++++++++---- osu.Game/Screens/Play/KiaiGameplayFountains.cs | 12 ++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Menu/KiaiMenuFountains.cs b/osu.Game/Screens/Menu/KiaiMenuFountains.cs index dbbff4a9f5..b103d9e573 100644 --- a/osu.Game/Screens/Menu/KiaiMenuFountains.cs +++ b/osu.Game/Screens/Menu/KiaiMenuFountains.cs @@ -3,11 +3,12 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Utils; +using osu.Game.Audio; using osu.Game.Graphics.Containers; +using osu.Game.Skinning; namespace osu.Game.Screens.Menu { @@ -16,11 +17,14 @@ namespace osu.Game.Screens.Menu private StarFountain leftFountain = null!; private StarFountain rightFountain = null!; - private Sample? sample; + [Resolved] + private ISkinSource skin { get; set; } = null!; + + private ISample? sample; private SampleChannel? sampleChannel; [BackgroundDependencyLoader] - private void load(AudioManager audio) + private void load() { RelativeSizeAxes = Axes.Both; @@ -40,7 +44,7 @@ namespace osu.Game.Screens.Menu }, }; - sample = audio.Samples.Get(@"Gameplay/fountain-shoot"); + sample = skin.GetSample(new SampleInfo(@"Gameplay/fountain-shoot")); } private bool isTriggered; diff --git a/osu.Game/Screens/Play/KiaiGameplayFountains.cs b/osu.Game/Screens/Play/KiaiGameplayFountains.cs index 7e09f50133..c8dcee2580 100644 --- a/osu.Game/Screens/Play/KiaiGameplayFountains.cs +++ b/osu.Game/Screens/Play/KiaiGameplayFountains.cs @@ -3,14 +3,15 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Utils; +using osu.Game.Audio; using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Screens.Menu; +using osu.Game.Skinning; namespace osu.Game.Screens.Play { @@ -21,11 +22,14 @@ namespace osu.Game.Screens.Play private Bindable kiaiStarFountains = null!; - private Sample? sample; + [Resolved] + private ISkinSource skin { get; set; } = null!; + + private ISample? sample; private SampleChannel? sampleChannel; [BackgroundDependencyLoader] - private void load(OsuConfigManager config, AudioManager audio) + private void load(OsuConfigManager config) { kiaiStarFountains = config.GetBindable(OsuSetting.StarFountains); @@ -47,7 +51,7 @@ namespace osu.Game.Screens.Play }, }; - sample = audio.Samples.Get(@"Gameplay/fountain-shoot"); + sample = skin.GetSample(new SampleInfo(@"Gameplay/fountain-shoot")); } private bool isTriggered; From efe1089003c8f1c35c33b9364403b90c13413b76 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 7 Mar 2025 15:46:10 +0900 Subject: [PATCH 691/801] Don't play kiai sfx when game is in background --- osu.Game/Screens/Menu/KiaiMenuFountains.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/Menu/KiaiMenuFountains.cs b/osu.Game/Screens/Menu/KiaiMenuFountains.cs index b103d9e573..e62ef31278 100644 --- a/osu.Game/Screens/Menu/KiaiMenuFountains.cs +++ b/osu.Game/Screens/Menu/KiaiMenuFountains.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; using osu.Framework.Graphics; +using osu.Framework.Platform; using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Graphics.Containers; @@ -17,6 +18,9 @@ namespace osu.Game.Screens.Menu private StarFountain leftFountain = null!; private StarFountain rightFountain = null!; + [Resolved] + private GameHost host { get; set; } = null!; + [Resolved] private ISkinSource skin { get; set; } = null!; @@ -85,6 +89,9 @@ namespace osu.Game.Screens.Menu break; } + // Don't play SFX when game is in background + if (!host.IsActive.Value) return; + // Track sample channel to avoid overlapping playback sampleChannel?.Stop(); sampleChannel = sample?.GetChannel(); From 33dccfcec8f9db04b6d098a64ca28048bce2cf0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Mar 2025 08:51:55 +0100 Subject: [PATCH 692/801] Add visual test coverage --- .../Visual/SongSelect/TestSceneBeatmapLeaderboard.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index bfb835cad1..62ca8bf831 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -181,6 +181,11 @@ namespace osu.Game.Tests.Visual.SongSelect { AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Global); AddStep(@"New Scores", () => leaderboard.SetScores(generateSampleScores(new BeatmapInfo()))); + AddStep(@"New Scores with teams", () => leaderboard.SetScores(generateSampleScores(new BeatmapInfo()).Select(s => + { + s.User.Team = new APITeam(); + return s; + }))); } [Test] @@ -473,7 +478,7 @@ namespace osu.Game.Tests.Visual.SongSelect Accuracy = 0.5140, MaxCombo = 244, TotalScore = 1707827, - Date = DateTime.Now.AddMonths(-3), + Date = DateTime.Now.AddMonths(-10), Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, BeatmapHash = beatmapInfo.Hash, From 4acdd3365aeac6570b1a420aed34877a028a6ca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Mar 2025 08:55:40 +0100 Subject: [PATCH 693/801] Fix leaderboard date text being cut off sometimes Closes https://github.com/ppy/osu/issues/32256. --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 28b20c0c05..fb5bb225c0 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -190,7 +190,7 @@ namespace osu.Game.Online.Leaderboards RelativeSizeAxes = Axes.Y, Direction = FillDirection.Horizontal, Spacing = new Vector2(5f, 0f), - Width = 114f, + Width = 130f, Masking = true, Children = new Drawable[] { From 6d22502739bf5b69a26692da0a996eac90032a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Mar 2025 09:20:50 +0100 Subject: [PATCH 694/801] Fix precise movement popover crashing if selection bounding box exceeds playfield size Closes https://github.com/ppy/osu/issues/32252. --- .../Edit/PreciseMovementPopover.cs | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs index f2cb8794b5..04d6afc925 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -127,8 +128,11 @@ namespace osu.Game.Rulesets.Osu.Edit if (relativeCheckbox.Current.Value) { - (xBindable.MinValue, xBindable.MaxValue) = (0 - initialSurroundingQuad.TopLeft.X, OsuPlayfield.BASE_SIZE.X - initialSurroundingQuad.BottomRight.X); - (yBindable.MinValue, yBindable.MaxValue) = (0 - initialSurroundingQuad.TopLeft.Y, OsuPlayfield.BASE_SIZE.Y - initialSurroundingQuad.BottomRight.Y); + xBindable.MinValue = 0 - Math.Max(initialSurroundingQuad.TopLeft.X, 0); + xBindable.MaxValue = OsuPlayfield.BASE_SIZE.X - Math.Min(initialSurroundingQuad.BottomRight.X, OsuPlayfield.BASE_SIZE.X); + + yBindable.MinValue = 0 - Math.Max(initialSurroundingQuad.TopLeft.Y, 0); + yBindable.MaxValue = OsuPlayfield.BASE_SIZE.Y - Math.Min(initialSurroundingQuad.BottomRight.Y, OsuPlayfield.BASE_SIZE.Y); xBindable.Default = yBindable.Default = 0; @@ -146,8 +150,21 @@ namespace osu.Game.Rulesets.Osu.Edit var quadRelativeToPosition = new RectangleF(initialSurroundingQuad.Location - initialPosition, initialSurroundingQuad.Size); - (xBindable.MinValue, xBindable.MaxValue) = (0 - quadRelativeToPosition.TopLeft.X, OsuPlayfield.BASE_SIZE.X - quadRelativeToPosition.BottomRight.X); - (yBindable.MinValue, yBindable.MaxValue) = (0 - quadRelativeToPosition.TopLeft.Y, OsuPlayfield.BASE_SIZE.Y - quadRelativeToPosition.BottomRight.Y); + if (initialSurroundingQuad.Width < OsuPlayfield.BASE_SIZE.X) + { + xBindable.MinValue = 0 - quadRelativeToPosition.TopLeft.X; + xBindable.MaxValue = OsuPlayfield.BASE_SIZE.X - quadRelativeToPosition.BottomRight.X; + } + else + xBindable.MinValue = xBindable.MaxValue = initialPosition.X; + + if (initialSurroundingQuad.Height < OsuPlayfield.BASE_SIZE.Y) + { + yBindable.MinValue = 0 - quadRelativeToPosition.TopLeft.Y; + yBindable.MaxValue = OsuPlayfield.BASE_SIZE.Y - quadRelativeToPosition.BottomRight.Y; + } + else + yBindable.MinValue = yBindable.MaxValue = initialPosition.Y; xBindable.Default = initialPosition.X; yBindable.Default = initialPosition.Y; From 12fa96de252f5d1b186fd7666570dd6c5f21b901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Mar 2025 10:43:22 +0100 Subject: [PATCH 695/801] Ensure that star rating reprocessing does not incur online lookup requests Yesterday after the lazer release there was a bit of a spike in the number of osu-web requests pointed at `/api/v2/beatmaps/lookup` specifically. The most likely reason for this is that prior to this commit, the star rating recalculation was fully performed by the `BeatmapUpdater.Process()` flow. This process does full metadata lookups, and while it *will* attempt to use the local `online.db` metadata cache, it *will* also fall back to API requests if the local metadata fetch fails. While that means that the local cache likely saved us from a doomsday scenario here, it *also* is the case that all of that metadata lookup stuff is *entirely unnecessary* when wanting to just update star ratings. Therefore, this splits out only the part relevant to star ratings as a separate background process, so that it can run completely locally. --- .../Database/BackgroundDataStoreProcessor.cs | 88 ++++++++++++++++--- 1 file changed, 75 insertions(+), 13 deletions(-) diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs index 1512b6be93..5053ab9a4c 100644 --- a/osu.Game/Database/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -76,8 +76,9 @@ namespace osu.Game.Database { Logger.Log("Beginning background data store processing.."); - checkForOutdatedStarRatings(); - processBeatmapSetsWithMissingMetrics(); + clearOutdatedStarRatings(); + populateMissingStarRatings(); + processOnlineBeatmapSetsWithNoUpdate(); // Note that the previous method will also update these on a fresh run. processBeatmapsWithMissingObjectCounts(); processScoresWithMissingStatistics(); @@ -100,7 +101,7 @@ namespace osu.Game.Database /// Check whether the databased difficulty calculation version matches the latest ruleset provided version. /// If it doesn't, clear out any existing difficulties so they can be incrementally recalculated. /// - private void checkForOutdatedStarRatings() + private void clearOutdatedStarRatings() { foreach (var ruleset in rulesetStore.AvailableRulesets) { @@ -132,7 +133,74 @@ namespace osu.Game.Database } } - private void processBeatmapSetsWithMissingMetrics() + /// + /// This is split out from as a separate process to prevent high server-side load + /// from the firing online requests as part of the update. + /// Star rating recalculations can be ran strictly locally. + /// + private void populateMissingStarRatings() + { + HashSet beatmapIds = new HashSet(); + + Logger.Log("Querying for beatmaps with missing star ratings..."); + + realmAccess.Run(r => + { + foreach (var b in r.All().Where(b => b.StarRating < 0 && b.BeatmapSet != null)) + beatmapIds.Add(b.ID); + }); + + if (beatmapIds.Count == 0) + return; + + Logger.Log($"Found {beatmapIds.Count} beatmaps which require star rating reprocessing."); + + var notification = showProgressNotification(beatmapIds.Count, "Reprocessing star rating for beatmaps", "beatmaps' star ratings have been updated"); + + int processedCount = 0; + int failedCount = 0; + + foreach (var id in beatmapIds) + { + if (notification?.State == ProgressNotificationState.Cancelled) + break; + + updateNotificationProgress(notification, processedCount, beatmapIds.Count); + + sleepIfRequired(); + + realmAccess.Write(r => + { + var beatmap = r.Find(id); + + if (beatmap == null) + return; + + try + { + var working = beatmapManager.GetWorkingBeatmap(beatmap); + var ruleset = working.BeatmapInfo.Ruleset.CreateInstance(); + + Debug.Assert(ruleset != null); + + var calculator = ruleset.CreateDifficultyCalculator(working); + + beatmap.StarRating = calculator.Calculate().StarRating; + ((IWorkingBeatmapCache)beatmapManager).Invalidate(beatmap); + ++processedCount; + } + catch (Exception e) + { + Logger.Log($"Background processing failed on {beatmap}: {e}"); + ++failedCount; + } + }); + } + + completeNotification(notification, processedCount, beatmapIds.Count, failedCount); + } + + private void processOnlineBeatmapSetsWithNoUpdate() { HashSet beatmapSetIds = new HashSet(); @@ -148,12 +216,7 @@ namespace osu.Game.Database // of other possible ways), but for now avoid queueing if the user isn't logged in at startup. if (api.IsLoggedIn) { - foreach (var b in r.All().Where(b => (b.StarRating < 0 || (b.OnlineID > 0 && b.LastOnlineUpdate == null)) && b.BeatmapSet != null)) - beatmapSetIds.Add(b.BeatmapSet!.ID); - } - else - { - foreach (var b in r.All().Where(b => b.StarRating < 0 && b.BeatmapSet != null)) + foreach (var b in r.All().Where(b => b.OnlineID > 0 && b.LastOnlineUpdate == null && b.BeatmapSet != null)) beatmapSetIds.Add(b.BeatmapSet!.ID); } }); @@ -161,10 +224,9 @@ namespace osu.Game.Database if (beatmapSetIds.Count == 0) return; - Logger.Log($"Found {beatmapSetIds.Count} beatmap sets which require reprocessing."); + Logger.Log($"Found {beatmapSetIds.Count} beatmap sets which require online updates."); - // Technically this is doing more than just star ratings, but easier for the end user to understand. - var notification = showProgressNotification(beatmapSetIds.Count, "Reprocessing star rating for beatmaps", "beatmaps' star ratings have been updated"); + var notification = showProgressNotification(beatmapSetIds.Count, "Updating online data for beatmaps", "beatmaps' online data have been updated"); int processedCount = 0; int failedCount = 0; From 1a7774cd196a725cc98e154fe73fbb80e29605a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Mar 2025 21:33:27 +0900 Subject: [PATCH 696/801] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index e35eaf5645..1fe29f2a21 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From d220c198858540554724f4fa4408966a3aaea6ce Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 7 Mar 2025 23:52:51 +0900 Subject: [PATCH 697/801] Add failing test --- .../TestScenePlaylistsRoomSubScreen.cs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs index f797f6f2ac..e9c4b56e04 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs @@ -17,6 +17,7 @@ using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; @@ -513,6 +514,63 @@ namespace osu.Game.Tests.Visual.Playlists AddUntilStep("mods reset", () => !SelectedMods.Value.Any()); } + /// + /// Tests that the beatmap and ruleset style are reset when the selected item is changed to one without freestyle, + /// and that the mod selection is re-validated against the item's allowed mods. + /// + [Test] + public void TestUserStyle_Reset_OnFreestyleDisabled() + { + Room room = null!; + + AddStep("add room", () => + { + room = new Room + { + RoomID = 1, + Playlist = + [ + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + AllowedMods = [new APIMod(new OsuModDoubleTime())] + }, + ] + }; + + API.Perform(new CreateRoomRequest(room)); + }); + + TestPlaylistsRoomSubScreen screen = null!; + AddStep("load screen", () => LoadScreen(new TestPlaylistsScreen(screen = new TestPlaylistsRoomSubScreen(room)))); + AddUntilStep("wait for load", () => screen.IsLoaded); + + // Set beatmap + ruleset, reset by selecting second playlist item + AddStep("set user beatmap/ruleset style", () => + { + screen.UserBeatmap.Value = importedSet.Beatmaps[1]; + screen.UserRuleset.Value = new TaikoRuleset().RulesetInfo; + }); + AddUntilStep("beatmap/ruleset set", () => Beatmap.Value.BeatmapInfo.Equals(importedSet.Beatmaps[1]) && Ruleset.Value.Equals(new TaikoRuleset().RulesetInfo)); + AddStep("select second playlist item", () => screen.SelectedItem.Value = room.Playlist[1]); + AddUntilStep("user style reset", () => screen.UserBeatmap.Value == null && screen.UserRuleset.Value == null); + AddUntilStep("beatmap/ruleset set", () => Beatmap.Value.BeatmapInfo.Equals(importedSet.Beatmaps[0]) && Ruleset.Value.Equals(new OsuRuleset().RulesetInfo)); + + AddStep("select first playlist item", () => screen.SelectedItem.Value = room.Playlist[0]); + + // Set mods (DT+HR), validate by selecting second playlist item where only DT is allowed. + AddStep("set user mods style", () => screen.UserMods.Value = [new OsuModDoubleTime(), new OsuModHardRock()]); + AddUntilStep("mods set", () => SelectedMods.Value.OfType().Any() && SelectedMods.Value.OfType().Any()); + AddStep("select second playlist item", () => screen.SelectedItem.Value = room.Playlist[1]); + AddUntilStep("user mods validated", () => screen.UserMods.Value.Count == 1 && screen.UserMods.Value.OfType().Any()); + AddUntilStep("mods set", () => SelectedMods.Value.Count == 1 && SelectedMods.Value.OfType().Any()); + } + private partial class TestPlaylistsScreen : OsuScreen { public TestPlaylistsScreen(PlaylistsRoomSubScreen screen) From eb15217a3661ec09bd0aec837450d6fe45467548 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 8 Mar 2025 00:09:32 +0900 Subject: [PATCH 698/801] Fix ruleset style not reset when freestyle disabled --- osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index fd1f35a3fc..f8dd9cd3d9 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -541,6 +541,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (item.RulesetID > 0) UserRuleset.Value = null; } + else + UserRuleset.Value = null; validateUserMods(); From e69ad867990ce7d02396313ab9f4951779e1e513 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 8 Mar 2025 00:32:56 +0900 Subject: [PATCH 699/801] Fix typo --- osu.Game.Tests/OnlinePlay/TestSceneOnlinePlaySubScreenStack.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/OnlinePlay/TestSceneOnlinePlaySubScreenStack.cs b/osu.Game.Tests/OnlinePlay/TestSceneOnlinePlaySubScreenStack.cs index 67b0d236ed..d463610034 100644 --- a/osu.Game.Tests/OnlinePlay/TestSceneOnlinePlaySubScreenStack.cs +++ b/osu.Game.Tests/OnlinePlay/TestSceneOnlinePlaySubScreenStack.cs @@ -36,7 +36,7 @@ namespace osu.Game.Tests.OnlinePlay AddStep("push screen that disables bindables", () => stack.Push(new ScreenWithExternalBindableDisablement(true))); AddAssert("bindables disabled", () => Beatmap.Disabled && Ruleset.Disabled && SelectedMods.Disabled, () => Is.True); - AddStep("push screen that does not disables bindables", () => stack.Push(new ScreenWithExternalBindableDisablement(false))); + AddStep("push screen that does not disable bindables", () => stack.Push(new ScreenWithExternalBindableDisablement(false))); AddAssert("bindables not disabled", () => Beatmap.Disabled || Ruleset.Disabled || SelectedMods.Disabled, () => Is.False); AddStep("exit one screen", () => stack.Exit()); From dbf571950c3f8b105c17fdcad5495a5f4a4738a1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 8 Mar 2025 00:34:29 +0900 Subject: [PATCH 700/801] Safguard setting states to not depend on screen load timing --- .../OnlinePlay/OnlinePlaySubScreenStack.cs | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs index c10017f1fe..65c478308f 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs @@ -2,13 +2,44 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.OnlinePlay.Lounge; namespace osu.Game.Screens.OnlinePlay { public partial class OnlinePlaySubScreenStack : OsuScreenStack { + private OsuScreenDependencies dependencies = null!; + + // Note - these are required to be unbound on disposal. + private Bindable beatmap = null!; + private Bindable ruleset = null!; + private Bindable> mods = null!; + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + // Bindables are leased by the OnlinePlayScreen, but pulled locally in order ot not rely on screen load timings. + // They will all be initially enabled while there is no screen in this stack. + dependencies = new OsuScreenDependencies(true, parent) + { + Beatmap = { Disabled = false }, + Ruleset = { Disabled = false }, + Mods = { Disabled = false } + }; + + beatmap = dependencies.Beatmap; + ruleset = dependencies.Ruleset; + mods = dependencies.Mods; + + return dependencies; + } + protected override void ScreenChanged(IScreen prev, IScreen? next) { base.ScreenChanged(prev, next); @@ -27,9 +58,9 @@ namespace osu.Game.Screens.OnlinePlay // This is a two-part process... // First, emulate the behaviour of DisallowExternalBeatmapRulesetChanges to disable toolbar buttons. - osuNext.Beatmap.Disabled = osuNext.DisallowExternalBeatmapRulesetChanges; - osuNext.Ruleset.Disabled = osuNext.DisallowExternalBeatmapRulesetChanges; - osuNext.Mods.Disabled = osuNext.DisallowExternalBeatmapRulesetChanges; + beatmap.Disabled = osuNext.DisallowExternalBeatmapRulesetChanges; + ruleset.Disabled = osuNext.DisallowExternalBeatmapRulesetChanges; + mods.Disabled = osuNext.DisallowExternalBeatmapRulesetChanges; // Second, when an OsuScreen is exited with DisallowExternalBeatmapRulesetChanges=true, leased bindables // are normally returned which reverts the mod and ruleset bindables to their original states. @@ -37,7 +68,7 @@ namespace osu.Game.Screens.OnlinePlay // The exact behaiour of the revert is awkward to emulate, but we particularly care about resetting mods // when returning to the lounge so that they don't stick around if the user then goes to create a new room. if (next is LoungeSubScreen) - osuNext.Mods.Value = []; + mods.Value = []; } } } From 2272ca1ae5420f020cf158d5de9418b9cea249fe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 8 Mar 2025 22:18:08 +0900 Subject: [PATCH 701/801] Fix namespace --- osu.Game/Online/API/Requests/RemoveBeatmapTagRequest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/RemoveBeatmapTagRequest.cs b/osu.Game/Online/API/Requests/RemoveBeatmapTagRequest.cs index 8090dd2cb0..4ac00e28f4 100644 --- a/osu.Game/Online/API/Requests/RemoveBeatmapTagRequest.cs +++ b/osu.Game/Online/API/Requests/RemoveBeatmapTagRequest.cs @@ -4,7 +4,7 @@ using System.Net.Http; using osu.Framework.IO.Network; -namespace osu.Game.Online.API.Requests.Responses +namespace osu.Game.Online.API.Requests { public class RemoveBeatmapTagRequest : APIRequest { From f845ea19b5dd16369fc1f14bb7f23b759112fc1f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 9 Mar 2025 09:57:04 +0900 Subject: [PATCH 702/801] Fix initial multiplayer room settings not applied --- .../Match/MultiplayerMatchSettingsOverlay.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index f74de26f1f..42d240c60e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -365,8 +365,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match updateRoomMaxParticipants(); updateRoomAutoStartDuration(); updateRoomPlaylist(); - - drawablePlaylist.Items.BindCollectionChanged((_, __) => room.Playlist = drawablePlaylist.Items.ToArray()); } private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e) @@ -470,6 +468,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match } else { + room.Name = NameField.Text; + room.Password = PasswordTextBox.Text; + room.Type = TypePicker.Current.Value; + room.QueueMode = QueueModeDropdown.Current.Value; + room.AutoStartDuration = TimeSpan.FromSeconds((int)startModeDropdown.Current.Value); + room.AutoSkip = AutoSkipCheckbox.Current.Value; + room.Playlist = drawablePlaylist.Items.ToArray(); + client.CreateRoom(room).ContinueWith(t => Schedule(() => { if (t.IsCompletedSuccessfully) @@ -505,10 +511,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match const string not_found_prefix = "beatmaps not found:"; if (message.StartsWith(not_found_prefix, StringComparison.Ordinal)) - { ErrorText.Text = "The selected beatmap is not available online."; - room.Playlist.SingleOrDefault()?.MarkInvalid(); - } else ErrorText.Text = message; From 7fdadbd852ef2dbb3e7a0b09315d2d485414a5e9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 9 Mar 2025 10:16:28 +0900 Subject: [PATCH 703/801] Fix error message on invalid room password --- .../Multiplayer/InvalidPasswordException.cs | 4 ++++ .../Multiplayer/MultiplayerLoungeSubScreen.cs | 15 ++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/Multiplayer/InvalidPasswordException.cs b/osu.Game/Online/Multiplayer/InvalidPasswordException.cs index b76a1cc05d..860fb90258 100644 --- a/osu.Game/Online/Multiplayer/InvalidPasswordException.cs +++ b/osu.Game/Online/Multiplayer/InvalidPasswordException.cs @@ -9,5 +9,9 @@ namespace osu.Game.Online.Multiplayer [Serializable] public class InvalidPasswordException : HubException { + public InvalidPasswordException() + : base("Invalid password") + { + } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index 93552670e9..54aa2003fa 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -84,12 +84,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer onSuccess(room); else { - const string message = "Failed to join multiplayer room."; + Exception? exception = result.Exception?.AsSingular(); - if (result.Exception != null) - Logger.Error(result.Exception, message); - - onFailure.Invoke(result.Exception?.AsSingular().Message ?? message); + if (exception?.GetHubExceptionMessage() is string message) + onFailure(message); + else + { + const string generic_failure_message = "Failed to join multiplayer room."; + if (result.Exception != null) + Logger.Error(result.Exception, generic_failure_message); + onFailure(generic_failure_message); + } } }); } From 0a6c2121536f0dddcfe840a18c3f1126d8f83aca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 9 Mar 2025 23:47:29 +0900 Subject: [PATCH 704/801] Use `SkinnableSound` to ensure samples track active skin --- osu.Game/Screens/Menu/KiaiMenuFountains.cs | 22 +++++-------------- .../Screens/Play/KiaiGameplayFountains.cs | 17 ++++---------- 2 files changed, 10 insertions(+), 29 deletions(-) diff --git a/osu.Game/Screens/Menu/KiaiMenuFountains.cs b/osu.Game/Screens/Menu/KiaiMenuFountains.cs index e62ef31278..b57012eaf7 100644 --- a/osu.Game/Screens/Menu/KiaiMenuFountains.cs +++ b/osu.Game/Screens/Menu/KiaiMenuFountains.cs @@ -3,7 +3,6 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Framework.Utils; @@ -21,18 +20,14 @@ namespace osu.Game.Screens.Menu [Resolved] private GameHost host { get; set; } = null!; - [Resolved] - private ISkinSource skin { get; set; } = null!; - - private ISample? sample; - private SampleChannel? sampleChannel; + private SkinnableSound? sample; [BackgroundDependencyLoader] private void load() { RelativeSizeAxes = Axes.Both; - Children = new[] + Children = new Drawable[] { leftFountain = new StarFountain { @@ -46,9 +41,8 @@ namespace osu.Game.Screens.Menu Origin = Anchor.BottomRight, X = -250, }, + sample = new SkinnableSound(new SampleInfo("Gameplay/fountain-shoot")) }; - - sample = skin.GetSample(new SampleInfo(@"Gameplay/fountain-shoot")); } private bool isTriggered; @@ -89,13 +83,9 @@ namespace osu.Game.Screens.Menu break; } - // Don't play SFX when game is in background - if (!host.IsActive.Value) return; - - // Track sample channel to avoid overlapping playback - sampleChannel?.Stop(); - sampleChannel = sample?.GetChannel(); - sampleChannel?.Play(); + // Don't play SFX when game is in background as it can be a bit noisy. + if (host.IsActive.Value) + sample?.Play(); } } } diff --git a/osu.Game/Screens/Play/KiaiGameplayFountains.cs b/osu.Game/Screens/Play/KiaiGameplayFountains.cs index c8dcee2580..017e66253f 100644 --- a/osu.Game/Screens/Play/KiaiGameplayFountains.cs +++ b/osu.Game/Screens/Play/KiaiGameplayFountains.cs @@ -3,7 +3,6 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Utils; @@ -22,11 +21,7 @@ namespace osu.Game.Screens.Play private Bindable kiaiStarFountains = null!; - [Resolved] - private ISkinSource skin { get; set; } = null!; - - private ISample? sample; - private SampleChannel? sampleChannel; + private SkinnableSound? sample; [BackgroundDependencyLoader] private void load(OsuConfigManager config) @@ -35,7 +30,7 @@ namespace osu.Game.Screens.Play RelativeSizeAxes = Axes.Both; - Children = new[] + Children = new Drawable[] { leftFountain = new GameplayStarFountain { @@ -49,9 +44,8 @@ namespace osu.Game.Screens.Play Origin = Anchor.BottomRight, X = -75, }, + sample = new SkinnableSound(new SampleInfo("Gameplay/fountain-shoot")) }; - - sample = skin.GetSample(new SampleInfo(@"Gameplay/fountain-shoot")); } private bool isTriggered; @@ -78,10 +72,7 @@ namespace osu.Game.Screens.Play leftFountain.Shoot(1); rightFountain.Shoot(-1); - // Track sample channel to avoid overlapping playback - sampleChannel?.Stop(); - sampleChannel = sample?.GetChannel(); - sampleChannel?.Play(); + sample?.Play(); } public partial class GameplayStarFountain : StarFountain From bbd2c33934520e34fd5601b14d1499c3b37daa3d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Mar 2025 14:45:36 +0900 Subject: [PATCH 705/801] Allow grid spacing setting up to 256 pixels Addresses https://github.com/ppy/osu/discussions/29713. I think there's valid uses of this apart from just hiding (ie values between 128 and 256) so let's just get this in. --- osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index 6220fa66b1..991d42c7b4 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Edit public BindableFloat Spacing { get; } = new BindableFloat(4f) { MinValue = 4f, - MaxValue = 128f, + MaxValue = 256f, Precision = 0.01f, }; From 3cb32c38adaaf66e401a5265eb234b9e22470c22 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Mar 2025 15:19:34 +0900 Subject: [PATCH 706/801] Disable user customisation of spectator list font / colour It's all a bit weird so let's just disable it for now. For instance, this is exposed as "text" font / colour but only affects the header. Also, no other headers are cusotmisable in similar components. --- .../Visual/Gameplay/TestSceneSpectatorList.cs | 4 ++-- osu.Game/Screens/Play/HUD/SpectatorList.cs | 13 ++++--------- osu.Game/Skinning/TrianglesSkin.cs | 2 +- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs index bd1e15d06d..1445e872b5 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs @@ -75,8 +75,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddRepeatStep("remove random user", () => ((ISpectatorClient)spectatorClient).UserEndedWatching( spectatorClient.WatchingUsers[RNG.Next(spectatorClient.WatchingUsers.Count)].OnlineID), 5); - AddStep("change font to venera", () => list.Font.Value = Typeface.Venera); - AddStep("change font to torus", () => list.Font.Value = Typeface.Torus); + AddStep("change font to venera", () => list.HeaderFont.Value = Typeface.Venera); + AddStep("change font to torus", () => list.HeaderFont.Value = Typeface.Torus); AddStep("change header colour", () => list.HeaderColour.Value = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1)); AddStep("enter break", () => playingState.Value = LocalUserPlayingState.Break); diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 4297c62712..0cc4076313 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -13,12 +13,10 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; -using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Online.Chat; using osu.Game.Localisation.HUD; -using osu.Game.Localisation.SkinComponents; +using osu.Game.Online.Chat; using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Skinning; @@ -31,10 +29,7 @@ namespace osu.Game.Screens.Play.HUD { private const int max_spectators_displayed = 10; - [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))] - public Bindable Font { get; } = new Bindable(Typeface.Torus); - - [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour), nameof(SkinnableComponentStrings.TextColourDescription))] + public Bindable HeaderFont { get; } = new Bindable(Typeface.Torus); public BindableColour4 HeaderColour { get; } = new BindableColour4(Colour4.White); private BindableList watchingUsers { get; } = new BindableList(); @@ -97,7 +92,7 @@ namespace osu.Game.Screens.Play.HUD watchingUsers.BindCollectionChanged(onSpectatorsChanged, true); userPlayingState.BindValueChanged(_ => updateVisibility()); - Font.BindValueChanged(_ => updateAppearance()); + HeaderFont.BindValueChanged(_ => updateAppearance()); HeaderColour.BindValueChanged(_ => updateAppearance(), true); FinishTransforms(true); @@ -198,7 +193,7 @@ namespace osu.Game.Screens.Play.HUD private void updateAppearance() { - header.Font = OsuFont.GetFont(Font.Value, 12, FontWeight.Bold); + header.Font = OsuFont.GetFont(HeaderFont.Value, 12, FontWeight.Bold); header.Colour = HeaderColour.Value; Width = header.DrawWidth; diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs index 06fe1c80ee..a4a967bed9 100644 --- a/osu.Game/Skinning/TrianglesSkin.cs +++ b/osu.Game/Skinning/TrianglesSkin.cs @@ -158,7 +158,7 @@ namespace osu.Game.Skinning if (spectatorList != null) { - spectatorList.Font.Value = Typeface.Venera; + spectatorList.HeaderFont.Value = Typeface.Venera; spectatorList.HeaderColour.Value = new OsuColour().BlueLighter; spectatorList.Anchor = Anchor.BottomLeft; spectatorList.Origin = Anchor.BottomLeft; From 3cf0031e16695743a6614ac08938a82e776d3528 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 10 Mar 2025 03:47:05 -0400 Subject: [PATCH 707/801] Add config settings for mania mobile modes --- .../ManiaRulesetConfigManager.cs | 8 +++++- .../ManiaSettingsSubsection.cs | 25 ++++++++++++++++++- .../Localisation/RulesetSettingsStrings.cs | 15 +++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs index d9cc224ad1..90bb2e8bd2 100644 --- a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs @@ -24,6 +24,9 @@ namespace osu.Game.Rulesets.Mania.Configuration SetDefault(ManiaRulesetSetting.ScrollSpeed, 8.0, 1.0, 40.0, 0.1); SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down); SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false); + SetDefault(ManiaRulesetSetting.PreferPortraitOnPhone, true); + SetDefault(ManiaRulesetSetting.MobileExtendedColumns, true); + SetDefault(ManiaRulesetSetting.TouchControls, false); #pragma warning disable CS0618 // Although obsolete, this is still required to populate the bindable from the database in case migration is required. @@ -55,6 +58,9 @@ namespace osu.Game.Rulesets.Mania.Configuration ScrollTime, ScrollSpeed, ScrollDirection, - TimingBasedNoteColouring + TimingBasedNoteColouring, + PreferPortraitOnPhone, + MobileExtendedColumns, + TouchControls, } } diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs index 17add32513..f87e035ecc 100644 --- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs +++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; @@ -44,8 +45,30 @@ namespace osu.Game.Rulesets.Mania Keywords = new[] { "color" }, LabelText = RulesetSettingsStrings.TimingBasedColouring, Current = config.GetBindable(ManiaRulesetSetting.TimingBasedNoteColouring), - } + }, + new SettingsCheckbox + { + LabelText = RulesetSettingsStrings.ManiaTouchControls, + Current = config.GetBindable(ManiaRulesetSetting.TouchControls), + }, }; + + if (RuntimeInfo.IsMobile) + { + AddRange(new[] + { + new SettingsCheckbox + { + LabelText = RulesetSettingsStrings.ExtendColumnsOnLandscape, + Current = config.GetBindable(ManiaRulesetSetting.MobileExtendedColumns), + }, + new SettingsCheckbox + { + LabelText = RulesetSettingsStrings.PreferPortraitOnPhone, + Current = config.GetBindable(ManiaRulesetSetting.PreferPortraitOnPhone), + } + }); + } } private partial class ManiaScrollSlider : RoundedSliderBar diff --git a/osu.Game/Localisation/RulesetSettingsStrings.cs b/osu.Game/Localisation/RulesetSettingsStrings.cs index 9434cd53de..e40d771606 100644 --- a/osu.Game/Localisation/RulesetSettingsStrings.cs +++ b/osu.Game/Localisation/RulesetSettingsStrings.cs @@ -89,6 +89,21 @@ namespace osu.Game.Localisation /// public static LocalisableString TouchControlScheme => new TranslatableString(getKey(@"touch_control_scheme"), @"Touch control scheme"); + /// + /// "Prefer portrait mode on mobile phones" + /// + public static LocalisableString PreferPortraitOnPhone => new TranslatableString(getKey(@"prefer_portrait_on_phone"), @"Prefer portrait mode on mobile phones"); + + /// + /// "Extend columns on mobile in landscape mode" + /// + public static LocalisableString ExtendColumnsOnLandscape => new TranslatableString(getKey(@"extend_columns_on_landscape"), @"Extend columns on mobile in landscape mode"); + + /// + /// "Enable touch controls instead of touchable columns" + /// + public static LocalisableString ManiaTouchControls => new TranslatableString(getKey(@"mania_touch_controls"), @"Enable touch controls instead of touchable columns"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } From 918e248cd2feaef3287da4d22848345c6695605a Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 10 Mar 2025 03:47:51 -0400 Subject: [PATCH 708/801] Hook up portarit setting with behaviour --- osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index e33cf092c3..42e481a8dd 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -50,7 +50,8 @@ namespace osu.Game.Rulesets.Mania.UI public IEnumerable BarLines; - public override bool RequiresPortraitOrientation => Beatmap.Stages.Count == 1; + public override bool RequiresPortraitOrientation + => Beatmap.Stages.Count == 1 && Config.Get(ManiaRulesetSetting.PreferPortraitOnPhone); protected override bool RelativeScaleBeatLengths => true; From edcc607f4bb5d12cf2eb4c1ccd08f75537b711a6 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 10 Mar 2025 03:48:12 -0400 Subject: [PATCH 709/801] Bring back touch control under a setting --- .../TestSceneManiaTouchInput.cs | 81 +++++++ osu.Game.Rulesets.Mania/UI/Column.cs | 12 +- .../UI/DrawableManiaRuleset.cs | 2 + .../UI/ManiaTouchInputArea.cs | 223 ++++++++++++++++++ 4 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs index dc95cd9ca0..364f7240e1 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs @@ -3,10 +3,13 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Graphics.Containers; using osu.Framework.Input; using osu.Framework.Testing; +using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.UI; using osu.Game.Tests.Visual; +using osuTK; namespace osu.Game.Rulesets.Mania.Tests { @@ -14,6 +17,11 @@ namespace osu.Game.Rulesets.Mania.Tests { protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); + [SetUp] + public void SetUp() => Schedule(() => toggleTouchControls(false)); + + #region Without touch controls + [Test] public void TestTouchInput() { @@ -63,6 +71,79 @@ namespace osu.Game.Rulesets.Mania.Tests () => Does.Not.Contain(getColumn(0).Action.Value)); } + #endregion + + #region With touch controls + + [Test] + public void TestTouchAreaNotInitiallyVisible() + { + AddStep("enable touch controls", () => toggleTouchControls(true)); + AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden); + } + + [Test] + public void TestPressReceptors() + { + AddStep("enable touch controls", () => toggleTouchControls(true)); + AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden); + + for (int i = 0; i < 4; i++) + { + int index = i; + + AddStep($"touch receptor {index}", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre))); + + AddAssert("action sent", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Contain(getReceptor(index).Action.Value)); + + AddStep($"release receptor {index}", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre))); + + AddAssert("touch area visible", () => getTouchOverlay()?.State.Value == Visibility.Visible); + } + } + + [Test] + public void TestColumnsNotTouchableWithTouchControls() + { + AddStep("enable touch controls", () => toggleTouchControls(true)); + + AddStep("touch receptor 0", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getReceptor(0).ScreenSpaceDrawQuad.Centre))); + + AddAssert("action sent", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Contain(getReceptor(0).Action.Value)); + + AddStep("release receptor 0", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(0).ScreenSpaceDrawQuad.Centre))); + + AddAssert("touch area visible", () => getTouchOverlay()?.State.Value == Visibility.Visible); + + AddStep("touch column 0", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre + new Vector2(0f, -50f)))); + + AddAssert("action not sent", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Not.Contain(getColumn(0).Action.Value)); + + AddStep("release column 0", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre + new Vector2(0f, -50f)))); + + AddAssert("action not sent", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Not.Contain(getColumn(0).Action.Value)); + } + + #endregion + + private void toggleTouchControls(bool enabled) + { + var maniaConfig = (ManiaRulesetConfigManager)RulesetConfigs.GetConfigFor(CreatePlayerRuleset())!; + maniaConfig.SetValue(ManiaRulesetSetting.TouchControls, enabled); + } + + private ManiaTouchInputArea? getTouchOverlay() => this.ChildrenOfType().SingleOrDefault(); + + private ManiaTouchInputArea.ColumnInputReceptor getReceptor(int index) => this.ChildrenOfType().ElementAt(index); + private Column getColumn(int index) => this.ChildrenOfType().ElementAt(index); } } diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 5425965897..f9f0c21f0d 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -12,6 +12,7 @@ using osu.Framework.Input.Events; using osu.Framework.Platform; using osu.Game.Extensions; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Skinning; @@ -57,6 +58,8 @@ namespace osu.Game.Rulesets.Mania.UI public readonly Bindable AccentColour = new Bindable(Color4.Black); + private IBindable touchControls = null!; + public Column(int index, bool isSpecial) { Index = index; @@ -77,7 +80,7 @@ namespace osu.Game.Rulesets.Mania.UI private ISkinSource skin { get; set; } = null!; [BackgroundDependencyLoader] - private void load(GameHost host) + private void load(GameHost host, ManiaRulesetConfigManager? rulesetConfig) { SkinnableDrawable keyArea; @@ -115,6 +118,9 @@ namespace osu.Game.Rulesets.Mania.UI RegisterPool(10, 50); RegisterPool(10, 50); RegisterPool(10, 50); + + if (rulesetConfig != null) + touchControls = rulesetConfig.GetBindable(ManiaRulesetSetting.TouchControls); } private void onSourceChanged() @@ -193,6 +199,10 @@ namespace osu.Game.Rulesets.Mania.UI protected override bool OnTouchDown(TouchDownEvent e) { + // if touch controls are enabled, disallow columns from handling touch directly. + if (touchControls.Value) + return false; + maniaInputManager?.KeyBindingContainer.TriggerPressed(Action.Value); touchActivationCount++; return true; diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index 42e481a8dd..c53329599d 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -112,6 +112,8 @@ namespace osu.Game.Rulesets.Mania.UI configScrollSpeed.BindValueChanged(speed => TargetTimeRange = ComputeScrollTime(speed.NewValue)); TimeRange.Value = TargetTimeRange = currentTimeRange = ComputeScrollTime(configScrollSpeed.Value); + + KeyBindingInputManager.Add(new ManiaTouchInputArea(this)); } protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount; diff --git a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs new file mode 100644 index 0000000000..bfa8dcab34 --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs @@ -0,0 +1,223 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Configuration; +using osu.Game.Rulesets.Mania.Configuration; +using osuTK; + +namespace osu.Game.Rulesets.Mania.UI +{ + /// + /// An overlay that captures and displays osu!mania mouse and touch input. + /// + public partial class ManiaTouchInputArea : VisibilityContainer + { + private readonly DrawableManiaRuleset drawableRuleset; + + // visibility state affects our child. we always want to handle input. + public override bool PropagatePositionalInputSubTree => true; + public override bool PropagateNonPositionalInputSubTree => true; + + [SettingSource("Spacing", "The spacing between receptors.")] + public BindableFloat Spacing { get; } = new BindableFloat(10) + { + Precision = 1, + MinValue = 0, + MaxValue = 100, + }; + + [SettingSource("Opacity", "The receptor opacity.")] + public BindableFloat Opacity { get; } = new BindableFloat(1) + { + Precision = 0.1f, + MinValue = 0, + MaxValue = 1 + }; + + [Resolved] + private ManiaRulesetConfigManager rulesetConfig { get; set; } = null!; + + private GridContainer gridContainer = null!; + + private readonly BindableBool touchControls = new BindableBool(); + + public ManiaTouchInputArea(DrawableManiaRuleset drawableRuleset) + { + this.drawableRuleset = drawableRuleset; + + Anchor = Anchor.BottomCentre; + Origin = Anchor.BottomCentre; + + RelativeSizeAxes = Axes.Both; + Height = 0.5f; + } + + [BackgroundDependencyLoader] + private void load() + { + List receptorGridContent = new List(); + List receptorGridDimensions = new List(); + + bool first = true; + + foreach (var stage in drawableRuleset.Playfield.Stages) + { + foreach (var column in stage.Columns) + { + if (!first) + { + receptorGridContent.Add(new Gutter { Spacing = { BindTarget = Spacing } }); + receptorGridDimensions.Add(new Dimension(GridSizeMode.AutoSize)); + } + + receptorGridContent.Add(new ColumnInputReceptor + { + Action = { BindTarget = column.Action }, + Enabled = { BindTarget = touchControls }, + }); + receptorGridDimensions.Add(new Dimension()); + + first = false; + } + } + + InternalChild = gridContainer = new GridContainer + { + RelativeSizeAxes = Axes.Both, + AlwaysPresent = true, + Content = new[] { receptorGridContent.ToArray() }, + ColumnDimensions = receptorGridDimensions.ToArray() + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + rulesetConfig.BindWith(ManiaRulesetSetting.TouchControls, touchControls); + Opacity.BindValueChanged(o => Alpha = o.NewValue, true); + } + + protected override bool OnKeyDown(KeyDownEvent e) + { + // Hide whenever the keyboard is used. + Hide(); + return false; + } + + protected override bool OnTouchDown(TouchDownEvent e) + { + if (touchControls.Value) + { + Show(); + return true; + } + + return false; + } + + protected override void PopIn() + { + gridContainer.FadeIn(500, Easing.OutQuint); + } + + protected override void PopOut() + { + gridContainer.FadeOut(300); + } + + public partial class ColumnInputReceptor : CompositeDrawable + { + public readonly IBindable Action = new Bindable(); + public readonly IBindable Enabled = new BindableBool(); + + private readonly Box highlightOverlay; + + [Resolved] + private ManiaInputManager? inputManager { get; set; } + + private bool isPressed; + + public ColumnInputReceptor() + { + RelativeSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 10, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.15f, + }, + highlightOverlay = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + Blending = BlendingParameters.Additive, + } + } + } + }; + } + + protected override bool OnTouchDown(TouchDownEvent e) + { + if (Enabled.Value) + { + updateButton(true); + return false; // handled by parent container to show overlay. + } + + return false; + } + + protected override void OnTouchUp(TouchUpEvent e) + { + updateButton(false); + } + + private void updateButton(bool press) + { + if (press == isPressed) + return; + + isPressed = press; + + if (press) + { + inputManager?.KeyBindingContainer.TriggerPressed(Action.Value); + highlightOverlay.FadeTo(0.1f, 80, Easing.OutQuint); + } + else + { + inputManager?.KeyBindingContainer.TriggerReleased(Action.Value); + highlightOverlay.FadeTo(0, 400, Easing.OutQuint); + } + } + } + + private partial class Gutter : Drawable + { + public readonly IBindable Spacing = new Bindable(); + + public Gutter() + { + Spacing.BindValueChanged(s => Size = new Vector2(s.NewValue)); + } + } + } +} From 7ca9d8392d4854ae9ee21a537eda6692ee35a147 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Mar 2025 17:17:20 +0900 Subject: [PATCH 710/801] Cache ruleset instance to avoid instantiation per beatmap processed --- osu.Game/Database/BackgroundDataStoreProcessor.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs index 5053ab9a4c..5a1c4a4721 100644 --- a/osu.Game/Database/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -160,7 +160,17 @@ namespace osu.Game.Database int processedCount = 0; int failedCount = 0; - foreach (var id in beatmapIds) + Dictionary rulesetCache = new Dictionary(); + + Ruleset getRuleset(RulesetInfo rulesetInfo) + { + if (!rulesetCache.TryGetValue(rulesetInfo.ShortName, out var ruleset)) + ruleset = rulesetCache[rulesetInfo.ShortName] = rulesetInfo.CreateInstance(); + + return ruleset; + } + + foreach (Guid id in beatmapIds) { if (notification?.State == ProgressNotificationState.Cancelled) break; @@ -179,7 +189,7 @@ namespace osu.Game.Database try { var working = beatmapManager.GetWorkingBeatmap(beatmap); - var ruleset = working.BeatmapInfo.Ruleset.CreateInstance(); + var ruleset = getRuleset(working.BeatmapInfo.Ruleset); Debug.Assert(ruleset != null); From 27ead5a383dd2bf9884d6a33ac9697909a693592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Mar 2025 09:18:49 +0100 Subject: [PATCH 711/801] Use `CurrentMatchPlayingUserIds` instead of `RoomUpdated` --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 98b3ede874..17e77f5238 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -8,7 +8,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -37,7 +36,8 @@ namespace osu.Game.Screens.Play.HUD [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour), nameof(SkinnableComponentStrings.TextColourDescription))] public BindableColour4 HeaderColour { get; } = new BindableColour4(Colour4.White); - private BindableList watchingUsers { get; } = new BindableList(); + private IBindableList watchingUsers { get; } = new BindableList(); + private IBindableList multiplayerPlayers { get; } = new BindableList(); private BindableList actualSpectators { get; } = new BindableList(); private Bindable userPlayingState { get; } = new Bindable(); @@ -92,11 +92,14 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); - ((IBindableList)watchingUsers).BindTo(client.WatchingUsers); ((IBindable)userPlayingState).BindTo(gameplayState.PlayingState); + multiplayerPlayers.BindTo(multiplayerClient.CurrentMatchPlayingUserIds); + multiplayerPlayers.BindCollectionChanged((_, _) => removePlayersFromMultiplayerRoom()); + + watchingUsers.BindTo(client.WatchingUsers); watchingUsers.BindCollectionChanged(onWatchingUsersChanged, true); - multiplayerClient.RoomUpdated += removePlayersFromMultiplayerRoom; + actualSpectators.BindCollectionChanged(onSpectatorsChanged, true); userPlayingState.BindValueChanged(_ => updateVisibility()); @@ -236,14 +239,6 @@ namespace osu.Game.Screens.Play.HUD Width = header.DrawWidth; } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (multiplayerClient.IsNotNull()) - multiplayerClient.RoomUpdated -= removePlayersFromMultiplayerRoom; - } - private partial class SpectatorListEntry : PoolableDrawable { public Bindable Current { get; } = new Bindable(); From 1f749250ac189bc64f90497cc6a3ed927c27612b Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 10 Mar 2025 03:49:00 -0400 Subject: [PATCH 712/801] Bring back mobile landscape column extension logic under a setting --- osu.Game.Rulesets.Mania/UI/ColumnFlow.cs | 44 +++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs index 5614a13a48..fcbe9138eb 100644 --- a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs +++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs @@ -3,12 +3,17 @@ #nullable disable +using System; +using osu.Framework; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Mania.UI { @@ -56,15 +61,26 @@ namespace osu.Game.Rulesets.Mania.UI private ISkinSource currentSkin; + private IBindable mobileExtendedColumns = null!; + [BackgroundDependencyLoader] - private void load(ISkinSource skin) + private void load(ISkinSource skin, ManiaRulesetConfigManager rulesetConfig) { currentSkin = skin; + mobileExtendedColumns = rulesetConfig.GetBindable(ManiaRulesetSetting.MobileExtendedColumns); + mobileExtendedColumns.BindValueChanged(_ => updateMobileSizing()); + skin.SourceChanged += onSkinChanged; onSkinChanged(); } + protected override void LoadComplete() + { + base.LoadComplete(); + updateMobileSizing(); + } + private void onSkinChanged() { for (int i = 0; i < stageDefinition.Columns; i++) @@ -89,6 +105,8 @@ namespace osu.Game.Rulesets.Mania.UI columns[i].Width = width.Value; } + + updateMobileSizing(); } /// @@ -101,6 +119,30 @@ namespace osu.Game.Rulesets.Mania.UI Content[column] = columns[column].Child = content; } + private void updateMobileSizing() + { + if (!IsLoaded || !RuntimeInfo.IsMobile || !mobileExtendedColumns.Value) + return; + + // GridContainer+CellContainer containing this stage (gets split up for dual stages). + Vector2? containingCell = this.FindClosestParent()?.Parent?.DrawSize; + + // Will be null in tests. + if (containingCell == null || containingCell.Value.X < containingCell.Value.Y) + return; + + float aspectRatio = containingCell.Value.X / containingCell.Value.Y; + + // 2.83 is a mostly arbitrary scale-up (170 / 60, based on original implementation for argon) + float mobileAdjust = 2.83f * Math.Min(1, 7f / stageDefinition.Columns); + // 1.92 is a "reference" mobile screen aspect ratio for phones. + // We should scale it back for cases like tablets which aren't so extreme. + mobileAdjust *= aspectRatio / 1.92f; + + for (int i = 0; i < stageDefinition.Columns; i++) + columns[i].Width *= mobileAdjust; + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From 857b8637ae26c117d54549f6c069a03b80e1449e Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 10 Mar 2025 04:36:51 -0400 Subject: [PATCH 713/801] Replace toggles with a single dropdown --- .../TestSceneManiaTouchInput.cs | 2 +- .../ManiaRulesetConfigManager.cs | 8 ++----- .../ManiaMobilePlayStyle.cs | 20 ++++++++++++++++ .../ManiaSettingsSubsection.cs | 24 +++---------------- osu.Game.Rulesets.Mania/UI/Column.cs | 8 +++---- osu.Game.Rulesets.Mania/UI/ColumnFlow.cs | 8 +++---- .../UI/DrawableManiaRuleset.cs | 5 ++-- .../UI/ManiaTouchInputArea.cs | 6 ++++- .../Localisation/RulesetSettingsStrings.cs | 17 ++++++++----- 9 files changed, 53 insertions(+), 45 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/ManiaMobilePlayStyle.cs diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs index 364f7240e1..b33c74d4e9 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs @@ -137,7 +137,7 @@ namespace osu.Game.Rulesets.Mania.Tests private void toggleTouchControls(bool enabled) { var maniaConfig = (ManiaRulesetConfigManager)RulesetConfigs.GetConfigFor(CreatePlayerRuleset())!; - maniaConfig.SetValue(ManiaRulesetSetting.TouchControls, enabled); + maniaConfig.SetValue(ManiaRulesetSetting.MobilePlayStyle, enabled ? ManiaMobilePlayStyle.TouchControls : ManiaMobilePlayStyle.TouchableColumns); } private ManiaTouchInputArea? getTouchOverlay() => this.ChildrenOfType().SingleOrDefault(); diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs index 90bb2e8bd2..10a3236178 100644 --- a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs @@ -24,9 +24,7 @@ namespace osu.Game.Rulesets.Mania.Configuration SetDefault(ManiaRulesetSetting.ScrollSpeed, 8.0, 1.0, 40.0, 0.1); SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down); SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false); - SetDefault(ManiaRulesetSetting.PreferPortraitOnPhone, true); - SetDefault(ManiaRulesetSetting.MobileExtendedColumns, true); - SetDefault(ManiaRulesetSetting.TouchControls, false); + SetDefault(ManiaRulesetSetting.MobilePlayStyle, ManiaMobilePlayStyle.TouchableColumns); #pragma warning disable CS0618 // Although obsolete, this is still required to populate the bindable from the database in case migration is required. @@ -59,8 +57,6 @@ namespace osu.Game.Rulesets.Mania.Configuration ScrollSpeed, ScrollDirection, TimingBasedNoteColouring, - PreferPortraitOnPhone, - MobileExtendedColumns, - TouchControls, + MobilePlayStyle, } } diff --git a/osu.Game.Rulesets.Mania/ManiaMobilePlayStyle.cs b/osu.Game.Rulesets.Mania/ManiaMobilePlayStyle.cs new file mode 100644 index 0000000000..e6b1224fd3 --- /dev/null +++ b/osu.Game.Rulesets.Mania/ManiaMobilePlayStyle.cs @@ -0,0 +1,20 @@ +// 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.Localisation; +using osu.Game.Localisation; + +namespace osu.Game.Rulesets.Mania +{ + public enum ManiaMobilePlayStyle + { + [LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.TouchableColumns))] + TouchableColumns, + + [LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.TouchControls))] + TouchControls, + + [LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.ExtendedColumns))] + ExtendedColumns, + } +} diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs index f87e035ecc..f558d30ee0 100644 --- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs +++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; @@ -46,29 +45,12 @@ namespace osu.Game.Rulesets.Mania LabelText = RulesetSettingsStrings.TimingBasedColouring, Current = config.GetBindable(ManiaRulesetSetting.TimingBasedNoteColouring), }, - new SettingsCheckbox + new SettingsEnumDropdown { - LabelText = RulesetSettingsStrings.ManiaTouchControls, - Current = config.GetBindable(ManiaRulesetSetting.TouchControls), + LabelText = RulesetSettingsStrings.MobilePlayStyle, + Current = config.GetBindable(ManiaRulesetSetting.MobilePlayStyle), }, }; - - if (RuntimeInfo.IsMobile) - { - AddRange(new[] - { - new SettingsCheckbox - { - LabelText = RulesetSettingsStrings.ExtendColumnsOnLandscape, - Current = config.GetBindable(ManiaRulesetSetting.MobileExtendedColumns), - }, - new SettingsCheckbox - { - LabelText = RulesetSettingsStrings.PreferPortraitOnPhone, - Current = config.GetBindable(ManiaRulesetSetting.PreferPortraitOnPhone), - } - }); - } } private partial class ManiaScrollSlider : RoundedSliderBar diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index f9f0c21f0d..ebd8efe124 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Mania.UI public readonly Bindable AccentColour = new Bindable(Color4.Black); - private IBindable touchControls = null!; + private IBindable mobilePlayStyle = null!; public Column(int index, bool isSpecial) { @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Mania.UI RegisterPool(10, 50); if (rulesetConfig != null) - touchControls = rulesetConfig.GetBindable(ManiaRulesetSetting.TouchControls); + mobilePlayStyle = rulesetConfig.GetBindable(ManiaRulesetSetting.MobilePlayStyle); } private void onSourceChanged() @@ -199,8 +199,8 @@ namespace osu.Game.Rulesets.Mania.UI protected override bool OnTouchDown(TouchDownEvent e) { - // if touch controls are enabled, disallow columns from handling touch directly. - if (touchControls.Value) + // if touch controls are selected, disallow columns from handling touch directly. + if (mobilePlayStyle.Value == ManiaMobilePlayStyle.TouchControls) return false; maniaInputManager?.KeyBindingContainer.TriggerPressed(Action.Value); diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs index fcbe9138eb..61c70bcf14 100644 --- a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs +++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs @@ -61,15 +61,15 @@ namespace osu.Game.Rulesets.Mania.UI private ISkinSource currentSkin; - private IBindable mobileExtendedColumns = null!; + private IBindable mobilePlayStyle = null!; [BackgroundDependencyLoader] private void load(ISkinSource skin, ManiaRulesetConfigManager rulesetConfig) { currentSkin = skin; - mobileExtendedColumns = rulesetConfig.GetBindable(ManiaRulesetSetting.MobileExtendedColumns); - mobileExtendedColumns.BindValueChanged(_ => updateMobileSizing()); + mobilePlayStyle = rulesetConfig.GetBindable(ManiaRulesetSetting.MobilePlayStyle); + mobilePlayStyle.BindValueChanged(_ => updateMobileSizing()); skin.SourceChanged += onSkinChanged; onSkinChanged(); @@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Mania.UI private void updateMobileSizing() { - if (!IsLoaded || !RuntimeInfo.IsMobile || !mobileExtendedColumns.Value) + if (!IsLoaded || !RuntimeInfo.IsMobile || mobilePlayStyle.Value != ManiaMobilePlayStyle.ExtendedColumns) return; // GridContainer+CellContainer containing this stage (gets split up for dual stages). diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index c53329599d..2dcbcacf93 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -50,8 +50,9 @@ namespace osu.Game.Rulesets.Mania.UI public IEnumerable BarLines; - public override bool RequiresPortraitOrientation - => Beatmap.Stages.Count == 1 && Config.Get(ManiaRulesetSetting.PreferPortraitOnPhone); + private bool playsWithTouchableColumns => Config.Get(ManiaRulesetSetting.MobilePlayStyle) == ManiaMobilePlayStyle.TouchableColumns; + + public override bool RequiresPortraitOrientation => Beatmap.Stages.Count == 1 && playsWithTouchableColumns; protected override bool RelativeScaleBeatLengths => true; diff --git a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs index bfa8dcab34..48b362b051 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs @@ -97,11 +97,15 @@ namespace osu.Game.Rulesets.Mania.UI }; } + private IBindable mobilePlayStyle; + protected override void LoadComplete() { base.LoadComplete(); - rulesetConfig.BindWith(ManiaRulesetSetting.TouchControls, touchControls); + mobilePlayStyle = rulesetConfig.GetBindable(ManiaRulesetSetting.MobilePlayStyle); + mobilePlayStyle.BindValueChanged(p => touchControls.Value = p.NewValue == ManiaMobilePlayStyle.TouchControls, true); + Opacity.BindValueChanged(o => Alpha = o.NewValue, true); } diff --git a/osu.Game/Localisation/RulesetSettingsStrings.cs b/osu.Game/Localisation/RulesetSettingsStrings.cs index e40d771606..527707b011 100644 --- a/osu.Game/Localisation/RulesetSettingsStrings.cs +++ b/osu.Game/Localisation/RulesetSettingsStrings.cs @@ -90,19 +90,24 @@ namespace osu.Game.Localisation public static LocalisableString TouchControlScheme => new TranslatableString(getKey(@"touch_control_scheme"), @"Touch control scheme"); /// - /// "Prefer portrait mode on mobile phones" + /// "Mobile play style" /// - public static LocalisableString PreferPortraitOnPhone => new TranslatableString(getKey(@"prefer_portrait_on_phone"), @"Prefer portrait mode on mobile phones"); + public static LocalisableString MobilePlayStyle => new TranslatableString(getKey(@"mobile_play_style"), @"Mobile play style"); /// - /// "Extend columns on mobile in landscape mode" + /// "Touchable columns" /// - public static LocalisableString ExtendColumnsOnLandscape => new TranslatableString(getKey(@"extend_columns_on_landscape"), @"Extend columns on mobile in landscape mode"); + public static LocalisableString TouchableColumns => new TranslatableString(getKey(@"touchable_columns"), @"Touchable columns"); /// - /// "Enable touch controls instead of touchable columns" + /// "Touch controls" /// - public static LocalisableString ManiaTouchControls => new TranslatableString(getKey(@"mania_touch_controls"), @"Enable touch controls instead of touchable columns"); + public static LocalisableString TouchControls => new TranslatableString(getKey(@"touch_controls"), @"Touch controls"); + + /// + /// "Extended columns" + /// + public static LocalisableString ExtendedColumns => new TranslatableString(getKey(@"extended_columns"), @"Extended columns"); private static string getKey(string key) => $@"{prefix}:{key}"; } From 25108beae3fb470c123af1d2ffb1cc7fcf808269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Mar 2025 09:47:44 +0100 Subject: [PATCH 714/801] Actually use the proper list --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 17e77f5238..6479956601 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -145,16 +145,12 @@ namespace osu.Game.Screens.Play.HUD private void removePlayersFromMultiplayerRoom() { - if (multiplayerClient.Room == null) - return; - // the multiplayer gameplay leaderboard relies on calling `SpectatorClient.WatchUser()` to get updates on users' total scores. // this has an unfortunate side effect of other players showing up in `SpectatorClient.WatchingUsers`. // // we do not generally wish to display other players in the room as spectators due to that implementation detail, // therefore this code is intended to filter out those players on the client side. - var excludedUserIds = multiplayerClient.Room.Users.Where(u => u.State != MultiplayerUserState.Spectating).Select(u => u.UserID).ToHashSet(); - actualSpectators.RemoveAll(s => excludedUserIds.Contains(s.OnlineID)); + actualSpectators.RemoveAll(s => multiplayerPlayers.Contains(s.OnlineID)); } private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArgs e) From 6b1472b0705486a250fe3d84320ac57f2560e6d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Mar 2025 10:36:19 +0100 Subject: [PATCH 715/801] Pull actual diffcalc out of realm transaction --- .../Database/BackgroundDataStoreProcessor.cs | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs index 5a1c4a4721..4e813fa2c7 100644 --- a/osu.Game/Database/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -179,32 +179,34 @@ namespace osu.Game.Database sleepIfRequired(); - realmAccess.Write(r => + var beatmap = realmAccess.Run(r => r.Find(id)?.Detach()); + + if (beatmap == null) + return; + + try { - var beatmap = r.Find(id); + var working = beatmapManager.GetWorkingBeatmap(beatmap); + var ruleset = getRuleset(working.BeatmapInfo.Ruleset); - if (beatmap == null) - return; + Debug.Assert(ruleset != null); - try + var calculator = ruleset.CreateDifficultyCalculator(working); + + double starRating = calculator.Calculate().StarRating; + realmAccess.Write(r => { - var working = beatmapManager.GetWorkingBeatmap(beatmap); - var ruleset = getRuleset(working.BeatmapInfo.Ruleset); - - Debug.Assert(ruleset != null); - - var calculator = ruleset.CreateDifficultyCalculator(working); - - beatmap.StarRating = calculator.Calculate().StarRating; - ((IWorkingBeatmapCache)beatmapManager).Invalidate(beatmap); - ++processedCount; - } - catch (Exception e) - { - Logger.Log($"Background processing failed on {beatmap}: {e}"); - ++failedCount; - } - }); + if (r.Find(id) is BeatmapInfo liveBeatmapInfo) + liveBeatmapInfo.StarRating = starRating; + }); + ((IWorkingBeatmapCache)beatmapManager).Invalidate(beatmap); + ++processedCount; + } + catch (Exception e) + { + Logger.Log($"Background processing failed on {beatmap}: {e}"); + ++failedCount; + } } completeNotification(notification, processedCount, beatmapIds.Count, failedCount); From 3d4dd8507723fcd3a048442834336486debb9732 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Mar 2025 10:44:22 +0100 Subject: [PATCH 716/801] Move back tag to extra if reached zero votes --- osu.Game/Screens/Ranking/UserTagControl.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index 6b7d22a7c2..b11dc1588b 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -172,7 +172,7 @@ namespace osu.Game.Screens.Ranking var tag = (UserTag)e.NewItems[i]!; var drawableTag = new DrawableUserTag(tag); tagFlow.Insert(tagFlow.Count, drawableTag); - tag.VoteCount.BindValueChanged(sortTags, true); + tag.VoteCount.BindValueChanged(voteCountChanged, true); layout.Invalidate(); } @@ -184,7 +184,7 @@ namespace osu.Game.Screens.Ranking for (int i = 0; i < e.OldItems!.Count; i++) { var tag = (UserTag)e.OldItems[i]!; - tag.VoteCount.ValueChanged -= sortTags; + tag.VoteCount.ValueChanged -= voteCountChanged; tagFlow.Remove(oldItems[e.OldStartingIndex + i], true); } @@ -199,7 +199,18 @@ namespace osu.Game.Screens.Ranking } } - private void sortTags(ValueChangedEvent _) => layout.Invalidate(); + private void voteCountChanged(ValueChangedEvent _) + { + var tagsWithNoVotes = displayedTags.Where(t => t.VoteCount.Value == 0).ToArray(); + + foreach (var tag in tagsWithNoVotes) + { + displayedTags.Remove(tag); + extraTags.Add(tag); + } + + layout.Invalidate(); + } protected override void Update() { From 00127b363d532ed4f51ec03de13f06e0478d5920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Mar 2025 10:52:24 +0100 Subject: [PATCH 717/801] Add search box to user tag control --- osu.Game/Screens/Ranking/UserTagControl.cs | 56 ++++++++++++++++------ 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index b11dc1588b..57b05f078c 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -17,6 +17,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Extensions; @@ -447,6 +448,9 @@ namespace osu.Game.Screens.Ranking private partial class ExtraTagsPopover : OsuPopover { + private SearchTextBox searchBox = null!; + private SearchContainer searchContainer = null!; + public BindableList ExtraTags { get; } = new BindableList(); public Action? OnSelected { get; set; } @@ -457,28 +461,43 @@ namespace osu.Game.Screens.Ranking Child = new OsuScrollContainer { Width = 250, - Height = 200, + Height = 250, ScrollbarOverlapsContent = false, - Child = new FillFlowContainer + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Right = 5 }, - Spacing = new Vector2(10), - ChildrenEnumerable = ExtraTags.Select(tag => new DrawableExtraTag(tag) + searchBox = new SearchTextBox { - Action = () => + RelativeSizeAxes = Axes.X, + }, + searchContainer = new SearchContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Right = 5, Top = 50, }, + Spacing = new Vector2(10), + ChildrenEnumerable = ExtraTags.Select(tag => new DrawableExtraTag(tag) { - OnSelected?.Invoke(tag); - this.HidePopover(); - } - }) - } + Action = () => + { + OnSelected?.Invoke(tag); + this.HidePopover(); + } + }) + } + }, }; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + searchBox.Current.BindValueChanged(_ => searchContainer.SearchTerm = searchBox.Current.Value, true); + } } - private partial class DrawableExtraTag : OsuAnimatedButton + private partial class DrawableExtraTag : OsuAnimatedButton, IFilterable { private readonly UserTag tag; @@ -527,6 +546,15 @@ namespace osu.Game.Screens.Ranking } }); } + + public IEnumerable FilterTerms => [tag.Name, tag.Description]; + + public bool MatchingFilter + { + set => Alpha = value ? 1 : 0; + } + + public bool FilteringActive { set { } } } } From 905afd57140df38b1a473167e0ff691cc50e543e Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 10 Mar 2025 06:49:52 -0400 Subject: [PATCH 718/801] Fix code quality error --- osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs index 48b362b051..7cb6b3b96f 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs @@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Mania.UI }; } - private IBindable mobilePlayStyle; + private IBindable mobilePlayStyle = null!; protected override void LoadComplete() { From afad2cf278cdbc142a8edd56e9b5a69f98cd3acf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Mar 2025 11:52:09 +0100 Subject: [PATCH 719/801] Apply more granular copying from database when retrieving working beatmap --- osu.Game/Beatmaps/WorkingBeatmap.cs | 17 ++++++++++++----- osu.Game/Beatmaps/WorkingBeatmapCache.cs | 19 +++++++++++++++++-- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index fd40097c4e..8df57fd0c8 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -235,11 +235,18 @@ namespace osu.Game.Beatmaps // Todo: Handle cancellation during beatmap parsing var b = GetBeatmap() ?? new Beatmap(); - // The original beatmap version needs to be preserved as the database doesn't contain it - BeatmapInfo.BeatmapVersion = b.BeatmapInfo.BeatmapVersion; - - // Use the database-backed info for more up-to-date values (beatmap id, ranked status, etc) - b.BeatmapInfo = BeatmapInfo; + // Copy across values of key properties for which the database-backed model has data that the decoded beatmap isn't going to. + b.BeatmapInfo.ID = BeatmapInfo.ID; + b.BeatmapInfo.UserSettings = BeatmapInfo.UserSettings; + b.BeatmapInfo.BeatmapSet = BeatmapInfo.BeatmapSet; + b.BeatmapInfo.Status = BeatmapInfo.Status; + b.BeatmapInfo.OnlineID = BeatmapInfo.OnlineID; + b.BeatmapInfo.OnlineMD5Hash = BeatmapInfo.OnlineMD5Hash; + b.BeatmapInfo.LastLocalUpdate = BeatmapInfo.LastLocalUpdate; + b.BeatmapInfo.LastOnlineUpdate = BeatmapInfo.LastOnlineUpdate; + b.BeatmapInfo.LastPlayed = BeatmapInfo.LastPlayed; + b.BeatmapInfo.EditorTimestamp = BeatmapInfo.EditorTimestamp; + b.BeatmapInfo.StarRating = BeatmapInfo.StarRating; // this could be recomputed in the decoding process but it's a bit annoying to do. return b; }, loadCancellationSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index 8af74d11d8..352012106a 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -21,6 +21,7 @@ using osu.Framework.Statistics; using osu.Game.Beatmaps.Formats; using osu.Game.Database; using osu.Game.IO; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Skinning; using osu.Game.Storyboards; @@ -152,14 +153,28 @@ namespace osu.Game.Beatmaps return null; } - if (stream.ComputeMD5Hash() != BeatmapInfo.MD5Hash) + string streamMD5 = stream.ComputeMD5Hash(); + string streamSHA2 = stream.ComputeSHA2Hash(); + + if (streamMD5 != BeatmapInfo.MD5Hash) { Logger.Log($"Beatmap failed to load (file {BeatmapInfo.Path} does not have the expected hash).", level: LogLevel.Error); return null; } using (var reader = new LineBufferedReader(stream)) - return Decoder.GetDecoder(reader).Decode(reader); + { + var beatmap = Decoder.GetDecoder(reader).Decode(reader); + + beatmap.BeatmapInfo.MD5Hash = streamMD5; + beatmap.BeatmapInfo.Hash = streamSHA2; + beatmap.BeatmapInfo.Length = beatmap.CalculatePlayableLength(); + beatmap.BeatmapInfo.BPM = 60000 / beatmap.GetMostCommonBeatLength(); + beatmap.BeatmapInfo.EndTimeObjectCount = beatmap.HitObjects.Count(h => h is IHasDuration); + beatmap.BeatmapInfo.TotalObjectCount = beatmap.HitObjects.Count; + + return beatmap; + } } catch (Exception e) { From a78868712ca0ea25d60826f3e0c9fcb78fb95fd6 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Mon, 10 Mar 2025 16:09:18 -0300 Subject: [PATCH 720/801] Change amount from 0.9f to 0.6f --- .../Compose/Components/Timeline/TimelineBlueprintContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 011ff17b30..0f1d3716e2 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -90,7 +90,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { placementBlueprint = CreateBlueprintFor(obj.NewValue).AsNonNull(); - placementBlueprint.Colour = OsuColour.Gray(0.9f); + placementBlueprint.Colour = OsuColour.Gray(0.6f); // TODO: this is out of order, causing incorrect stacking height. SelectionBlueprints.Add(placementBlueprint); From d5e06102d4879d832c72ee7e6088e33241ae2f0b Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 10 Mar 2025 22:40:24 -0400 Subject: [PATCH 721/801] Fix failing tests --- osu.Game.Rulesets.Mania/UI/ColumnFlow.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs index 61c70bcf14..9f2cc2d10f 100644 --- a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs +++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs @@ -1,9 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; +using JetBrains.Annotations; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -59,16 +58,16 @@ namespace osu.Game.Rulesets.Mania.UI columns.Add(new Container { RelativeSizeAxes = Axes.Y }); } - private ISkinSource currentSkin; + private ISkinSource currentSkin = null!; - private IBindable mobilePlayStyle = null!; + private readonly Bindable mobilePlayStyle = new Bindable(); [BackgroundDependencyLoader] - private void load(ISkinSource skin, ManiaRulesetConfigManager rulesetConfig) + private void load(ISkinSource skin, ManiaRulesetConfigManager? rulesetConfig) { currentSkin = skin; - mobilePlayStyle = rulesetConfig.GetBindable(ManiaRulesetSetting.MobilePlayStyle); + rulesetConfig?.BindWith(ManiaRulesetSetting.MobilePlayStyle, mobilePlayStyle); mobilePlayStyle.BindValueChanged(_ => updateMobileSizing()); skin.SourceChanged += onSkinChanged; From 3e71a969e64e06049290c18189a45099e70ed733 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 11 Mar 2025 15:41:08 +0900 Subject: [PATCH 722/801] Apply commenting adjustments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs index 65c478308f..b52789f535 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs @@ -17,14 +17,14 @@ namespace osu.Game.Screens.OnlinePlay { private OsuScreenDependencies dependencies = null!; - // Note - these are required to be unbound on disposal. + // Note - these bindables must be stored to fields of this component to be correctly unbound on disposal. private Bindable beatmap = null!; private Bindable ruleset = null!; private Bindable> mods = null!; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { - // Bindables are leased by the OnlinePlayScreen, but pulled locally in order ot not rely on screen load timings. + // Bindables are leased by the OnlinePlayScreen, but pulled locally in order to not rely on screen load timings. // They will all be initially enabled while there is no screen in this stack. dependencies = new OsuScreenDependencies(true, parent) { From 75bd101c9e9f822b6dadb9904185516fc8aeab8a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 15:47:45 +0900 Subject: [PATCH 723/801] Ensure realm database file is touched on startup Closes https://github.com/ppy/osu/discussions/32304. --- osu.Game/Database/RealmAccess.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 5cc143f4e2..3212e17b7b 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -315,6 +315,17 @@ namespace osu.Game.Database attemptRecoverFromFile(newerVersionFilename); } + try + { + // Some platforms' realm implementation (including windows) don't update modified time on open. + // Let's do this explicitly as some users may depend on it roughly aligning to usage expectations. + string fullPath = storage.GetFullPath(Filename); + var fi = new FileInfo(fullPath); + if (fi.Exists) + fi.LastWriteTime = DateTime.Now; + } + catch { } + try { return getRealmInstance(); From c962210b4f250fab62c31f082fedf34dbe26a8e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Mar 2025 13:21:27 +0100 Subject: [PATCH 724/801] Fix placement blueprint tests --- .../Editor/CatchPlacementBlueprintTestScene.cs | 2 ++ .../Editor/ManiaPlacementBlueprintTestScene.cs | 2 ++ .../Editor/TestSceneHitCirclePlacementBlueprint.cs | 1 + .../Editor/TestSceneSliderPlacementBlueprint.cs | 2 ++ .../Editor/TestSceneSpinnerPlacementBlueprint.cs | 2 ++ osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs | 4 +++- 6 files changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs index a327e6d4c9..a5713feda3 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs @@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor { public abstract partial class CatchPlacementBlueprintTestScene : PlacementBlueprintTestScene { + protected sealed override Ruleset CreateRuleset() => new CatchRuleset(); + protected const double TIME_SNAP = 100; protected DrawableCatchHitObject LastObject; diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs index 0f913a6a7d..83070c3e29 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs @@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { public abstract partial class ManiaPlacementBlueprintTestScene : PlacementBlueprintTestScene { + protected sealed override Ruleset CreateRuleset() => new ManiaRuleset(); + private readonly Column column; [Cached(typeof(IReadOnlyList))] diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs index a105d860bf..5bce97d7b8 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs @@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { public partial class TestSceneHitCirclePlacementBlueprint : PlacementBlueprintTestScene { + protected sealed override Ruleset CreateRuleset() => new OsuRuleset(); protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableHitCircle((HitCircle)hitObject); protected override HitObjectPlacementBlueprint CreateBlueprint() => new HitCirclePlacementBlueprint(); } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index 5831cc0a8a..8835254c48 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { public partial class TestSceneSliderPlacementBlueprint : PlacementBlueprintTestScene { + protected sealed override Ruleset CreateRuleset() => new OsuRuleset(); + [SetUp] public void Setup() => Schedule(() => { diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs index d7b5cc73be..18834ef847 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs @@ -13,6 +13,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { public partial class TestSceneSpinnerPlacementBlueprint : PlacementBlueprintTestScene { + protected sealed override Ruleset CreateRuleset() => new OsuRuleset(); + protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSpinner((Spinner)hitObject); protected override HitObjectPlacementBlueprint CreateBlueprint() => new SpinnerPlacementBlueprint(); diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs index baf614d1c8..a644936a16 100644 --- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs @@ -51,7 +51,9 @@ namespace osu.Game.Tests.Visual protected virtual IBeatmap GetPlayableBeatmap() { - var playable = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); + var rulesetInfo = CreateRuleset()!.RulesetInfo; + var playable = Beatmap.Value.GetPlayableBeatmap(rulesetInfo); + playable.BeatmapInfo.Ruleset = rulesetInfo; playable.Difficulty.CircleSize = 2; return playable; } From d4f0fc0fdee85accf84de96d6388e0a9ba2ecd0d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 16:12:32 +0900 Subject: [PATCH 725/801] Disallow adjusting slider repeats with more lenient check condition --- .../Compose/Components/Timeline/TimelineHitObjectBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 6c0d5af247..f60d1b023b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -441,7 +441,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline double lengthOfOneRepeat = repeatHitObject.Duration / (repeatHitObject.RepeatCount + 1); int proposedCount = Math.Max(0, (int)Math.Round(proposedDuration / lengthOfOneRepeat) - 1); - if (proposedCount == repeatHitObject.RepeatCount || Precision.AlmostEquals(lengthOfOneRepeat, 0)) + if (proposedCount == repeatHitObject.RepeatCount || Precision.AlmostEquals(lengthOfOneRepeat, 0, 1)) return; repeatHitObject.RepeatCount = proposedCount; From 23891b1994c296d7e6761010deeb334f6c1af103 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 16:17:44 +0900 Subject: [PATCH 726/801] Fix edge case allowing almost-zero-length sliders to be placed during distance snapping --- .../Edit/Blueprints/Components/SelectionEditablePath.cs | 2 +- .../Sliders/Components/PathControlPointVisualiser.cs | 2 +- .../Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs | 2 +- .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 2 +- osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs | 2 +- osu.Game/Rulesets/Objects/SliderPath.cs | 5 ++++- 6 files changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs index 26b26641d3..654ef006a5 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs @@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components { base.UpdateHitObjectFromPath(hitObject); - if (hitObject.Path.ControlPoints.Count <= 1 || !hitObject.Path.HasValidLength) + if (hitObject.Path.ControlPoints.Count <= 1 || !hitObject.Path.HasValidLengthForPlacement) EditorBeatmap?.Remove(hitObject); } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index bc3d27fd68..5ae9b194be 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -484,7 +484,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components // Snap the path to the current beat divisor before checking length validity. hitObject.SnapTo(distanceSnapProvider); - if (!hitObject.Path.HasValidLength) + if (!hitObject.Path.HasValidLengthForPlacement) { for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++) hitObject.Path.ControlPoints[i].Position = oldControlPoints[i]; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index a747d4fce8..d934eb5a9e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 }; - protected override bool IsValidForPlacement => HitObject.Path.HasValidLength; + protected override bool IsValidForPlacement => HitObject.Path.HasValidLengthForPlacement; public SliderPlacementBlueprint() : base(new Slider()) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 9978c46027..d6150f85db 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -476,7 +476,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders HitObject.SnapTo(distanceSnapProvider); // If there are 0 or 1 remaining control points, or the slider has an invalid length, it is in a degenerate form and should be deleted - if (controlPoints.Count <= 1 || !HitObject.Path.HasValidLength) + if (controlPoints.Count <= 1 || !HitObject.Path.HasValidLengthForPlacement) { placementHandler?.Delete(HitObject); return; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 4c3db207f2..9a5d3c3bc1 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -180,7 +180,7 @@ namespace osu.Game.Rulesets.Osu.Edit Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider }); (bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad); - if (xInBounds && yInBounds && slider.Path.HasValidLength) + if (xInBounds && yInBounds && slider.Path.HasValidLengthForPlacement) return; for (int i = 0; i < slider.Path.ControlPoints.Count; i++) diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index 5550815370..eb591ec530 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -31,7 +31,10 @@ namespace osu.Game.Rulesets.Objects /// public readonly Bindable ExpectedDistance = new Bindable(); - public bool HasValidLength => Precision.DefinitelyBigger(Distance, 0); + /// + /// Should be used to check whether placement can continue after a user editor operation. + /// + public bool HasValidLengthForPlacement => Precision.DefinitelyBigger(Distance, 0, 1); /// /// The control points of the path. From 5ef2479e24d8001ee82b32c7bde832347b981747 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 16:29:47 +0900 Subject: [PATCH 727/801] Remove previous version of local cache lookup handling --- .../LocalCachedBeatmapMetadataSource.cs | 41 ------------------- 1 file changed, 41 deletions(-) diff --git a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs index a1744f74b3..1412d3234c 100644 --- a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs +++ b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs @@ -104,11 +104,6 @@ namespace osu.Game.Beatmaps switch (getCacheVersion(db)) { - case 1: - // will eventually become irrelevant due to the monthly recycling of local caches - // can be removed 20250221 - return queryCacheVersion1(db, beatmapInfo, out onlineMetadata); - case 2: return queryCacheVersion2(db, beatmapInfo, out onlineMetadata); } @@ -270,42 +265,6 @@ namespace osu.Game.Beatmaps } } - private bool queryCacheVersion1(SqliteConnection db, BeatmapInfo beatmapInfo, out OnlineBeatmapMetadata? onlineMetadata) - { - Debug.Assert(beatmapInfo.BeatmapSet != null); - - using var cmd = db.CreateCommand(); - - cmd.CommandText = - @"SELECT beatmapset_id, beatmap_id, approved, user_id, checksum, last_update FROM osu_beatmaps WHERE checksum = @MD5Hash OR filename = @Path"; - - cmd.Parameters.Add(new SqliteParameter(@"@MD5Hash", beatmapInfo.MD5Hash)); - cmd.Parameters.Add(new SqliteParameter(@"@Path", beatmapInfo.Path)); - - using var reader = cmd.ExecuteReader(); - - if (reader.Read()) - { - logForModel(beatmapInfo.BeatmapSet, $@"Cached local retrieval for {beatmapInfo} (cache version 1)."); - - onlineMetadata = new OnlineBeatmapMetadata - { - BeatmapSetID = reader.GetInt32(0), - BeatmapID = reader.GetInt32(1), - BeatmapStatus = (BeatmapOnlineStatus)reader.GetByte(2), - BeatmapSetStatus = (BeatmapOnlineStatus)reader.GetByte(2), - AuthorID = reader.GetInt32(3), - MD5Hash = reader.GetString(4), - LastUpdated = reader.GetDateTimeOffset(5), - // TODO: DateSubmitted and DateRanked are not provided by local cache in this version. - }; - return true; - } - - onlineMetadata = null; - return false; - } - private bool queryCacheVersion2(SqliteConnection db, BeatmapInfo beatmapInfo, out OnlineBeatmapMetadata? onlineMetadata) { Debug.Assert(beatmapInfo.BeatmapSet != null); From 8d83dfede7a2c7a2818da4f5cc97165524f4237b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 16:37:46 +0900 Subject: [PATCH 728/801] Ensure only ranked/approved/loved lookups occur on local cached source --- osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs index 1412d3234c..0b4f4f1700 100644 --- a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs +++ b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs @@ -277,7 +277,11 @@ namespace osu.Game.Beatmaps FROM `osu_beatmaps` AS `b` JOIN `osu_beatmapsets` AS `s` ON `s`.`beatmapset_id` = `b`.`beatmapset_id` WHERE `b`.`checksum` = @MD5Hash OR `b`.`filename` = @Path + AND `b`.`approved` in (1, 2, 4) """; + // approved conditional can theoretically be removed as it was fixed in + // https://github.com/ppy/osu-onlinedb-generator/commit/489ac000775c3ff63bc914efb83cad0f6fbde261 + // but it's also safe to leave it (should not affect performance). cmd.Parameters.Add(new SqliteParameter(@"@MD5Hash", beatmapInfo.MD5Hash)); cmd.Parameters.Add(new SqliteParameter(@"@Path", beatmapInfo.Path)); From be5c89c2e40321a1c10d80abb3e523686d7734f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 17:03:06 +0900 Subject: [PATCH 729/801] Add basic helper method to update beatmap statistics --- osu.Game/Beatmaps/BeatmapInfoExtensions.cs | 13 +++++++++++++ osu.Game/Beatmaps/BeatmapUpdater.cs | 7 ++----- osu.Game/Beatmaps/WorkingBeatmapCache.cs | 6 +----- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs index 16b4b04ce4..25f98c812c 100644 --- a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs @@ -1,15 +1,28 @@ // 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.Localisation; using osu.Game.Online.API; using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Screens.Select; namespace osu.Game.Beatmaps { public static class BeatmapInfoExtensions { + /// + /// Given an , update length, BPM and object counts. + /// + public static void UpdateStatisticsFromBeatmap(this BeatmapInfo beatmapInfo, IBeatmap beatmap) + { + beatmapInfo.Length = beatmap.CalculatePlayableLength(); + beatmapInfo.BPM = 60000 / beatmap.GetMostCommonBeatLength(); + beatmapInfo.EndTimeObjectCount = beatmap.HitObjects.Count(h => h is IHasDuration); + beatmapInfo.TotalObjectCount = beatmap.HitObjects.Count; + } + /// /// A user-presentable display title representing this beatmap. /// diff --git a/osu.Game/Beatmaps/BeatmapUpdater.cs b/osu.Game/Beatmaps/BeatmapUpdater.cs index efb432b84e..64ac69bb07 100644 --- a/osu.Game/Beatmaps/BeatmapUpdater.cs +++ b/osu.Game/Beatmaps/BeatmapUpdater.cs @@ -51,7 +51,7 @@ namespace osu.Game.Beatmaps if (lookupScope != MetadataLookupScope.None) metadataLookup.Update(beatmapSet, lookupScope == MetadataLookupScope.OnlineFirst); - foreach (var beatmap in beatmapSet.Beatmaps) + foreach (BeatmapInfo beatmap in beatmapSet.Beatmaps) { difficultyCache.Invalidate(beatmap); @@ -63,10 +63,7 @@ namespace osu.Game.Beatmaps var calculator = ruleset.CreateDifficultyCalculator(working); beatmap.StarRating = calculator.Calculate().StarRating; - beatmap.Length = working.Beatmap.CalculatePlayableLength(); - beatmap.BPM = 60000 / working.Beatmap.GetMostCommonBeatLength(); - beatmap.EndTimeObjectCount = working.Beatmap.HitObjects.Count(h => h is IHasDuration); - beatmap.TotalObjectCount = working.Beatmap.HitObjects.Count; + beatmap.UpdateStatisticsFromBeatmap(working.Beatmap); } // And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required. diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index 352012106a..fdeb840977 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -21,7 +21,6 @@ using osu.Framework.Statistics; using osu.Game.Beatmaps.Formats; using osu.Game.Database; using osu.Game.IO; -using osu.Game.Rulesets.Objects.Types; using osu.Game.Skinning; using osu.Game.Storyboards; @@ -168,10 +167,7 @@ namespace osu.Game.Beatmaps beatmap.BeatmapInfo.MD5Hash = streamMD5; beatmap.BeatmapInfo.Hash = streamSHA2; - beatmap.BeatmapInfo.Length = beatmap.CalculatePlayableLength(); - beatmap.BeatmapInfo.BPM = 60000 / beatmap.GetMostCommonBeatLength(); - beatmap.BeatmapInfo.EndTimeObjectCount = beatmap.HitObjects.Count(h => h is IHasDuration); - beatmap.BeatmapInfo.TotalObjectCount = beatmap.HitObjects.Count; + beatmap.BeatmapInfo.UpdateStatisticsFromBeatmap(beatmap); return beatmap; } From 148fe5ca16e92f6ea783f71cffc12ffe86135486 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 11 Mar 2025 17:08:52 +0900 Subject: [PATCH 730/801] Fix missing base dependencies --- osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs index b52789f535..79baa490ac 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.OnlinePlay { // Bindables are leased by the OnlinePlayScreen, but pulled locally in order to not rely on screen load timings. // They will all be initially enabled while there is no screen in this stack. - dependencies = new OsuScreenDependencies(true, parent) + dependencies = new OsuScreenDependencies(true, base.CreateChildDependencies(parent)) { Beatmap = { Disabled = false }, Ruleset = { Disabled = false }, From 4565c271dd46ab35af1177302e965aa7bf93c920 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 17:23:09 +0900 Subject: [PATCH 731/801] Fix multiple code inspections --- osu.Game.Rulesets.Mania/UI/ColumnFlow.cs | 25 ++++++++++++------------ 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs index 9f2cc2d10f..6a1949e978 100644 --- a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs +++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs @@ -2,10 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; -using JetBrains.Annotations; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Beatmaps; @@ -58,15 +58,14 @@ namespace osu.Game.Rulesets.Mania.UI columns.Add(new Container { RelativeSizeAxes = Axes.Y }); } - private ISkinSource currentSkin = null!; + [Resolved] + private ISkinSource skin { get; set; } = null!; private readonly Bindable mobilePlayStyle = new Bindable(); [BackgroundDependencyLoader] - private void load(ISkinSource skin, ManiaRulesetConfigManager? rulesetConfig) + private void load(ManiaRulesetConfigManager? rulesetConfig) { - currentSkin = skin; - rulesetConfig?.BindWith(ManiaRulesetSetting.MobilePlayStyle, mobilePlayStyle); mobilePlayStyle.BindValueChanged(_ => updateMobileSizing()); @@ -86,16 +85,16 @@ namespace osu.Game.Rulesets.Mania.UI { if (i > 0) { - float spacing = currentSkin.GetConfig( - new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnSpacing, i - 1)) - ?.Value ?? Stage.COLUMN_SPACING; + float spacing = skin.GetConfig( + new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnSpacing, i - 1)) + ?.Value ?? Stage.COLUMN_SPACING; columns[i].Margin = new MarginPadding { Left = spacing }; } - float? width = currentSkin.GetConfig( - new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, i)) - ?.Value; + float? width = skin.GetConfig( + new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, i)) + ?.Value; bool isSpecialColumn = stageDefinition.IsSpecialColumn(i); @@ -146,8 +145,8 @@ namespace osu.Game.Rulesets.Mania.UI { base.Dispose(isDisposing); - if (currentSkin != null) - currentSkin.SourceChanged -= onSkinChanged; + if (skin.IsNotNull()) + skin.SourceChanged -= onSkinChanged; } } } From 338328b9113c3ebbcf67ad01561e0bef31368a77 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 17:56:58 +0900 Subject: [PATCH 732/801] Fix loading layer not showing when closing room --- osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index e5f4b6087a..6aa366dbc5 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -217,10 +217,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge filter.BindValueChanged(_ => { roomListing.Rooms.Clear(); - hasListingResults.Value = false; - listingPoller.PollImmediately(); + RefreshRooms(); }); + updateLoadingLayer(); updateFilter(); } @@ -410,7 +410,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected virtual void OpenNewRoom(Room room) => this.Push(CreateRoomSubScreen(room)); - public void RefreshRooms() => listingPoller.PollImmediately(); + public void RefreshRooms() + { + hasListingResults.Value = false; + listingPoller.PollImmediately(); + } private void updateLoadingLayer() { From 914a230446da83db38d46752381f66e37fe272ed Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 18:17:18 +0900 Subject: [PATCH 733/801] Add brackets to ensure correct lookups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs index 0b4f4f1700..d876ba55b2 100644 --- a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs +++ b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs @@ -276,7 +276,7 @@ namespace osu.Game.Beatmaps SELECT `b`.`beatmapset_id`, `b`.`beatmap_id`, `b`.`approved`, `b`.`user_id`, `b`.`checksum`, `b`.`last_update`, `s`.`submit_date`, `s`.`approved_date` FROM `osu_beatmaps` AS `b` JOIN `osu_beatmapsets` AS `s` ON `s`.`beatmapset_id` = `b`.`beatmapset_id` - WHERE `b`.`checksum` = @MD5Hash OR `b`.`filename` = @Path + WHERE (`b`.`checksum` = @MD5Hash OR `b`.`filename` = @Path) AND `b`.`approved` in (1, 2, 4) """; // approved conditional can theoretically be removed as it was fixed in From 770291b4623f7818ea76c6928de7e69768389ca8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 18:35:10 +0900 Subject: [PATCH 734/801] Show border instead of adjusting dim --- .../Components/Timeline/TimelineBlueprintContainer.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 0f1d3716e2..c149a8f73a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -18,6 +18,7 @@ using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -90,7 +91,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { placementBlueprint = CreateBlueprintFor(obj.NewValue).AsNonNull(); - placementBlueprint.Colour = OsuColour.Gray(0.6f); + // just to show the border. using the selection state doesn't seem to backfire. + // if it does then we'll probably want to just make `new` object above rather than rely on `CreateBlueprintFor`. + placementBlueprint.State = SelectionState.Selected; // TODO: this is out of order, causing incorrect stacking height. SelectionBlueprints.Add(placementBlueprint); From ee723aef6811656fe09aa4f670a98163c648e5fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Mar 2025 10:43:02 +0100 Subject: [PATCH 735/801] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index d4b49e492a..8f219ea426 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index d10a3d649a..8045009621 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From e2e2383c504282a9ef29e7c4803b185d9eb5d2e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Mar 2025 15:02:18 +0100 Subject: [PATCH 736/801] Adjust text flow usages to framework changes --- osu.Game/Graphics/Containers/LinkFlowContainer.cs | 13 +++++++++---- osu.Game/Overlays/BeatmapSet/BeatmapAvailability.cs | 1 - osu.Game/Overlays/Changelog/ChangelogEntry.cs | 1 - osu.Game/Overlays/Chat/ChatLine.cs | 2 +- osu.Game/Overlays/Music/PlaylistItem.cs | 1 - .../Header/Components/PreviousUsernamesDisplay.cs | 1 - 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game/Graphics/Containers/LinkFlowContainer.cs b/osu.Game/Graphics/Containers/LinkFlowContainer.cs index aa72996fff..6022ea6bd6 100644 --- a/osu.Game/Graphics/Containers/LinkFlowContainer.cs +++ b/osu.Game/Graphics/Containers/LinkFlowContainer.cs @@ -134,9 +134,14 @@ namespace osu.Game.Graphics.Containers protected virtual DrawableLinkCompiler CreateLinkCompiler(ITextPart textPart) => new DrawableLinkCompiler(textPart); - // We want the compilers to always be visible no matter where they are, so RelativeSizeAxes is used. - // However due to https://github.com/ppy/osu-framework/issues/2073, it's possible for the compilers to be relative size in the flow's auto-size axes - an unsupported operation. - // Since the compilers don't display any content and don't affect the layout, it's simplest to exclude them from the flow. - public override IEnumerable FlowingChildren => base.FlowingChildren.Where(c => !(c is DrawableLinkCompiler)); + protected override FillFlowContainer CreateFlow() => new LinkFlow(); + + private partial class LinkFlow : InnerFlow + { + // We want the compilers to always be visible no matter where they are, so RelativeSizeAxes is used. + // However due to https://github.com/ppy/osu-framework/issues/2073, it's possible for the compilers to be relative size in the flow's auto-size axes - an unsupported operation. + // Since the compilers don't display any content and don't affect the layout, it's simplest to exclude them from the flow. + public override IEnumerable FlowingChildren => base.FlowingChildren.Where(c => !(c is DrawableLinkCompiler)); + } } } diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapAvailability.cs b/osu.Game/Overlays/BeatmapSet/BeatmapAvailability.cs index d18e1c93c9..c9783d42dc 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapAvailability.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapAvailability.cs @@ -39,7 +39,6 @@ namespace osu.Game.Overlays.BeatmapSet }, textContainer = new LinkFlowContainer(t => t.Font = OsuFont.GetFont(size: 14)) { - Direction = FillDirection.Full, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Padding = new MarginPadding(10), diff --git a/osu.Game/Overlays/Changelog/ChangelogEntry.cs b/osu.Game/Overlays/Changelog/ChangelogEntry.cs index 9c40440778..d6021972c6 100644 --- a/osu.Game/Overlays/Changelog/ChangelogEntry.cs +++ b/osu.Game/Overlays/Changelog/ChangelogEntry.cs @@ -82,7 +82,6 @@ namespace osu.Game.Overlays.Changelog }, title = new LinkFlowContainer { - Direction = FillDirection.Full, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, TextAnchor = Anchor.BottomLeft, diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index e386f2ac09..20c3b26b8b 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -46,7 +46,7 @@ namespace osu.Game.Overlays.Chat } } - public IReadOnlyCollection DrawableContentFlow => drawableContentFlow; + public IEnumerable DrawableContentFlow => drawableContentFlow.Children; private const float font_size = 13; diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 90fdfd0491..01b0472172 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -63,7 +63,6 @@ namespace osu.Game.Overlays.Music { sprite.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold); sprite.Colour = colours.Gray9; - sprite.Padding = new MarginPadding { Top = 1 }; }); SelectedSet.BindValueChanged(set => updateSelectionState(set.NewValue)); diff --git a/osu.Game/Overlays/Profile/Header/Components/PreviousUsernamesDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/PreviousUsernamesDisplay.cs index dce5c84d12..1cd09566fb 100644 --- a/osu.Game/Overlays/Profile/Header/Components/PreviousUsernamesDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/PreviousUsernamesDisplay.cs @@ -85,7 +85,6 @@ namespace osu.Game.Overlays.Profile.Header.Components { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Direction = FillDirection.Full, // Prevents the tooltip of having a sudden size reduction and flickering when the text is being faded out. // Also prevents a potential OnHover/HoverLost feedback loop. AlwaysPresent = true, From 749df665d161fd27b253247980f7e441a528f6ef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 18:47:16 +0900 Subject: [PATCH 737/801] Focus search box immediately --- osu.Game/Screens/Ranking/UserTagControl.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index 57b05f078c..a643bd6206 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -467,6 +467,7 @@ namespace osu.Game.Screens.Ranking { searchBox = new SearchTextBox { + HoldFocus = true, RelativeSizeAxes = Axes.X, }, searchContainer = new SearchContainer From 345f565b90b947eb6d353381a2cc5fc1d7a38a7e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 18:47:28 +0900 Subject: [PATCH 738/801] Allow using `Enter` key to select a single match --- osu.Game/Screens/Ranking/UserTagControl.cs | 39 ++++++++++++++++------ 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index a643bd6206..2e559ff534 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -17,6 +17,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -30,6 +31,7 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osuTK; +using osuTK.Input; namespace osu.Game.Screens.Ranking { @@ -479,32 +481,49 @@ namespace osu.Game.Screens.Ranking Spacing = new Vector2(10), ChildrenEnumerable = ExtraTags.Select(tag => new DrawableExtraTag(tag) { - Action = () => - { - OnSelected?.Invoke(tag); - this.HidePopover(); - } + Action = () => select(tag) }) } }, }; } + private void select(UserTag tag) + { + OnSelected?.Invoke(tag); + this.HidePopover(); + } + protected override void LoadComplete() { base.LoadComplete(); searchBox.Current.BindValueChanged(_ => searchContainer.SearchTerm = searchBox.Current.Value, true); } + + protected override bool OnKeyDown(KeyDownEvent e) + { + var visibleItems = searchContainer.OfType().Where(d => d.IsPresent).ToArray(); + + if (e.Key == Key.Enter) + { + if (visibleItems.Length == 1) + select(visibleItems.Single().Tag); + + return true; + } + + return base.OnKeyDown(e); + } } private partial class DrawableExtraTag : OsuAnimatedButton, IFilterable { - private readonly UserTag tag; + public readonly UserTag Tag; public DrawableExtraTag(UserTag tag) { - this.tag = tag; + Tag = tag; RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -535,20 +554,20 @@ namespace osu.Game.Screens.Ranking { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Text = tag.Name, + Text = Tag.Name, }, new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(size: 14)) { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Text = tag.Description, + Text = Tag.Description, } } } }); } - public IEnumerable FilterTerms => [tag.Name, tag.Description]; + public IEnumerable FilterTerms => [Tag.Name, Tag.Description]; public bool MatchingFilter { From e6fe6206475106d73801c9801498119189566022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Mar 2025 10:51:12 +0100 Subject: [PATCH 739/801] Improve tip threshold for click slider copy & tooltip --- osu.Game/Localisation/TabletSettingsStrings.cs | 5 +++++ osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/TabletSettingsStrings.cs b/osu.Game/Localisation/TabletSettingsStrings.cs index 6c2e3c1f9c..ff0ced457f 100644 --- a/osu.Game/Localisation/TabletSettingsStrings.cs +++ b/osu.Game/Localisation/TabletSettingsStrings.cs @@ -59,6 +59,11 @@ namespace osu.Game.Localisation /// public static LocalisableString LockAspectRatio => new TranslatableString(getKey(@"lock_aspect_ratio"), @"Lock aspect ratio"); + /// + /// "Tip pressure for click" + /// + public static LocalisableString TipPressureForClick => new TranslatableString(getKey(@"tip_pressure_for_click"), "Tip pressure for click"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index 9d70e49659..e104bb7e39 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -215,10 +215,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input Current = sizeY, CanBeShown = { BindTarget = enabled } }, - new SettingsSlider + new SettingsPercentageSlider { TransferValueOnCommit = true, - LabelText = "Tip Threshold", + LabelText = TabletSettingsStrings.TipPressureForClick, Current = pressureThreshold, CanBeShown = { BindTarget = enabled } }, From 61e1234e0aeaa3fc30902d593a283ff786ed0d06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Mar 2025 10:52:03 +0100 Subject: [PATCH 740/801] Fix compile failure --- osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs index 5ca08e0bba..95a134e204 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs @@ -135,6 +135,7 @@ namespace osu.Game.Tests.Visual.Settings public Bindable AreaSize { get; } = new Bindable(); public Bindable Rotation { get; } = new Bindable(); + public BindableFloat PressureThreshold { get; } = new BindableFloat(); public IBindable Tablet => tablet; From bcdc49e248b826b6dce387242d84c4710762dd1c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 18:54:17 +0900 Subject: [PATCH 741/801] Adjust naming and subclassing --- osu.Game/Screens/Ranking/UserTag.cs | 25 ++++ osu.Game/Screens/Ranking/UserTagControl.cs | 144 +++++++++------------ 2 files changed, 87 insertions(+), 82 deletions(-) create mode 100644 osu.Game/Screens/Ranking/UserTag.cs diff --git a/osu.Game/Screens/Ranking/UserTag.cs b/osu.Game/Screens/Ranking/UserTag.cs new file mode 100644 index 0000000000..d44e531330 --- /dev/null +++ b/osu.Game/Screens/Ranking/UserTag.cs @@ -0,0 +1,25 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Screens.Ranking +{ + public record UserTag + { + public long Id { get; } + public string Name { get; } + public string Description { get; } + + public BindableInt VoteCount { get; } = new BindableInt(); + public BindableBool Voted { get; } = new BindableBool(); + + public UserTag(APITag tag) + { + Id = tag.Id; + Name = tag.Name; + Description = tag.Description; + } + } +} diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index 2e559ff534..7600d0aaae 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -79,12 +79,12 @@ namespace osu.Game.Screens.Ranking LayoutEasing = Easing.OutQuint, Spacing = new Vector2(4), }, - new ExtraTagsButton + new AddTagsButton { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, OnTagSelected = onExtraTagSelected, - ExtraTags = { BindTarget = extraTags }, + AvailableTags = { BindTarget = extraTags }, }, }, }, @@ -420,13 +420,13 @@ namespace osu.Game.Screens.Ranking } } - private partial class ExtraTagsButton : GrayButton, IHasPopover + private partial class AddTagsButton : GrayButton, IHasPopover { - public BindableList ExtraTags { get; } = new BindableList(); + public BindableList AvailableTags { get; } = new BindableList(); public Action? OnTagSelected { get; set; } - public ExtraTagsButton() + public AddTagsButton() : base(FontAwesome.Solid.Plus) { Size = new Vector2(30); @@ -438,22 +438,22 @@ namespace osu.Game.Screens.Ranking { base.LoadComplete(); - ExtraTags.BindCollectionChanged((_, _) => Enabled.Value = ExtraTags.Count > 0, true); + AvailableTags.BindCollectionChanged((_, _) => Enabled.Value = AvailableTags.Count > 0, true); } - public Popover GetPopover() => new ExtraTagsPopover + public Popover GetPopover() => new AddTagsPopover { - ExtraTags = { BindTarget = ExtraTags }, + AvailableTags = { BindTarget = AvailableTags }, OnSelected = OnTagSelected, }; } - private partial class ExtraTagsPopover : OsuPopover + private partial class AddTagsPopover : OsuPopover { private SearchTextBox searchBox = null!; private SearchContainer searchContainer = null!; - public BindableList ExtraTags { get; } = new BindableList(); + public BindableList AvailableTags { get; } = new BindableList(); public Action? OnSelected { get; set; } @@ -479,7 +479,7 @@ namespace osu.Game.Screens.Ranking Direction = FillDirection.Vertical, Padding = new MarginPadding { Right = 5, Top = 50, }, Spacing = new Vector2(10), - ChildrenEnumerable = ExtraTags.Select(tag => new DrawableExtraTag(tag) + ChildrenEnumerable = AvailableTags.Select(tag => new DrawableAddableTag(tag) { Action = () => select(tag) }) @@ -488,12 +488,6 @@ namespace osu.Game.Screens.Ranking }; } - private void select(UserTag tag) - { - OnSelected?.Invoke(tag); - this.HidePopover(); - } - protected override void LoadComplete() { base.LoadComplete(); @@ -503,7 +497,7 @@ namespace osu.Game.Screens.Ranking protected override bool OnKeyDown(KeyDownEvent e) { - var visibleItems = searchContainer.OfType().Where(d => d.IsPresent).ToArray(); + var visibleItems = searchContainer.OfType().Where(d => d.IsPresent).ToArray(); if (e.Key == Key.Enter) { @@ -515,82 +509,68 @@ namespace osu.Game.Screens.Ranking return base.OnKeyDown(e); } - } - private partial class DrawableExtraTag : OsuAnimatedButton, IFilterable - { - public readonly UserTag Tag; - - public DrawableExtraTag(UserTag tag) + private void select(UserTag tag) { - Tag = tag; - - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - Anchor = Origin = Anchor.Centre; + OnSelected?.Invoke(tag); + this.HidePopover(); } - [BackgroundDependencyLoader] - private void load(OsuColour colours) + private partial class DrawableAddableTag : OsuAnimatedButton, IFilterable { - Content.AddRange(new Drawable[] + public readonly UserTag Tag; + + public DrawableAddableTag(UserTag tag) { - new Box + Tag = tag; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Anchor = Origin = Anchor.Centre; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Content.AddRange(new Drawable[] { - RelativeSizeAxes = Axes.Both, - Colour = colours.GreySeaFoamDark, - Depth = float.MaxValue, - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(2), - Padding = new MarginPadding(5), - Children = new Drawable[] + new Box { - new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(weight: FontWeight.Bold)) + RelativeSizeAxes = Axes.Both, + Colour = colours.GreySeaFoamDark, + Depth = float.MaxValue, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(2), + Padding = new MarginPadding(5), + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Text = Tag.Name, - }, - new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(size: 14)) - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Text = Tag.Description, + new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(weight: FontWeight.Bold)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = Tag.Name, + }, + new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(size: 14)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = Tag.Description, + } } } - } - }); + }); + } + + public IEnumerable FilterTerms => [Tag.Name, Tag.Description]; + + public bool MatchingFilter { set => Alpha = value ? 1 : 0; } + public bool FilteringActive { set { } } } - - public IEnumerable FilterTerms => [Tag.Name, Tag.Description]; - - public bool MatchingFilter - { - set => Alpha = value ? 1 : 0; - } - - public bool FilteringActive { set { } } - } - } - - public record UserTag - { - public long Id { get; } - public string Name { get; } - public string Description { get; set; } - public BindableInt VoteCount { get; } = new BindableInt(); - public BindableBool Voted { get; } = new BindableBool(); - - public UserTag(APITag tag) - { - Id = tag.Id; - Name = tag.Name; - Description = tag.Description; } } } From 1a8aa861fd4c6157ee35418bc967d1916071f83f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 11 Mar 2025 19:11:41 +0900 Subject: [PATCH 742/801] Use presence in user panel context menu --- osu.Game/Users/UserPanel.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 09a5cb414f..1f72cbccbf 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -22,6 +22,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osu.Game.Resources.Localisation.Web; using osu.Game.Localisation; +using osu.Game.Online.Metadata; using osu.Game.Online.Multiplayer; using osu.Game.Screens; using osu.Game.Screens.Play; @@ -76,6 +77,9 @@ namespace osu.Game.Users [Resolved] private MultiplayerClient? multiplayerClient { get; set; } + [Resolved] + private MetadataClient? metadataClient { get; set; } + [BackgroundDependencyLoader] private void load() { @@ -153,7 +157,7 @@ namespace osu.Game.Users chatOverlay?.Show(); })); - if (User.IsOnline) + if (metadataClient?.GetPresence(User.OnlineID) != null) { items.Add(new OsuMenuItem(ContextMenuStrings.SpectatePlayer, MenuItemType.Standard, () => { From 1f4cfa74dcb7c1b41ee76ab1a368a198cde515c6 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 11 Mar 2025 19:13:10 +0900 Subject: [PATCH 743/801] Initial rework of FriendDisplay to use presence It's at a very simple stage for now: - Tab control is no longer recreated when friends change, counts are updated as bindables. - All async logic removed temporarily. - Sort and filtering happens in realtime without panel reload. - Display modes removed for now. Need to think about this one a bit more, and whether to retry the async path or look for a separate solution. - Real time user presence is now considered. Not considered for sorting by last visit time yet. --- .../Visual/Online/TestSceneFriendDisplay.cs | 2 +- .../TestSceneFriendsOnlineStatusControl.cs | 36 +- .../Changelog/ChangelogUpdateStreamItem.cs | 12 +- .../Dashboard/Friends/FriendDisplay.cs | 312 ++++++++++-------- .../Friends/FriendOnlineStreamControl.cs | 43 ++- .../Dashboard/Friends/FriendStream.cs | 10 +- .../Friends/FriendsOnlineStatusItem.cs | 21 +- osu.Game/Overlays/OverlayStreamItem.cs | 66 +++- 8 files changed, 284 insertions(+), 218 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs index 7925b252b6..010a261d4c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestOffline() { - AddStep("Populate with offline test users", () => display.Users = getUsers()); + // AddStep("Populate with offline test users", () => display.Users = getUsers()); } [Test] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs index c75c2a7877..548f3067a7 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs @@ -1,14 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - -using System.Collections.Generic; -using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Dashboard.Friends; @@ -19,37 +14,14 @@ namespace osu.Game.Tests.Visual.UserInterface [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); - private FriendOnlineStreamControl control; - [SetUp] - public void SetUp() => Schedule(() => Child = control = new FriendOnlineStreamControl + public void SetUp() => Schedule(() => Child = new FriendOnlineStreamControl { Anchor = Anchor.Centre, Origin = Anchor.Centre, + CountAll = { Value = 15 }, + CountOnline = { Value = 10 }, + CountOffline = { Value = 5 } }); - - [Test] - public void Populate() - { - AddStep("Populate", () => control.Populate(new List - { - new APIUser - { - IsOnline = true - }, - new APIUser - { - IsOnline = false - }, - new APIUser - { - IsOnline = false - } - })); - - AddAssert("3 users", () => control.Items.FirstOrDefault(item => item.Status == OnlineStatus.All)?.Count == 3); - AddAssert("1 online user", () => control.Items.FirstOrDefault(item => item.Status == OnlineStatus.Online)?.Count == 1); - AddAssert("2 offline users", () => control.Items.FirstOrDefault(item => item.Status == OnlineStatus.Offline)?.Count == 2); - } } } diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs index 30273d2405..df1ea6c283 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Humanizer; using osu.Framework.Localisation; using osu.Game.Graphics; @@ -18,14 +16,12 @@ namespace osu.Game.Overlays.Changelog { if (stream.IsFeatured) Width *= 2; + + MainText = Value.DisplayName; + AdditionalText = Value.LatestBuild.DisplayVersion; + InfoText = Value.UserCount > 0 ? $"{"user".ToQuantity(Value.UserCount, "N0")} online" : default(LocalisableString); } - protected override LocalisableString MainText => Value.DisplayName; - - protected override LocalisableString AdditionalText => Value.LatestBuild.DisplayVersion; - - protected override LocalisableString InfoText => Value.UserCount > 0 ? $"{"user".ToQuantity(Value.UserCount, "N0")} online" : null; - protected override Color4 GetBarColour(OsuColour colours) => Value.Colour; } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index 3e393ced01..2938be732c 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -1,21 +1,19 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; -using System.Threading; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Metadata; using osu.Game.Resources.Localisation.Web; using osu.Game.Users; using osuTK; @@ -24,32 +22,22 @@ namespace osu.Game.Overlays.Dashboard.Friends { public partial class FriendDisplay : CompositeDrawable { - private List users = new List(); - - public List Users - { - get => users; - set - { - users = value; - onlineStreamControl.Populate(value); - } - } - - private CancellationTokenSource cancellationToken; - - [CanBeNull] - private SearchContainer currentContent; - - private FriendOnlineStreamControl onlineStreamControl; - private Box background; - private Box controlBackground; - private UserListToolbar userListToolbar; - private Container itemsPlaceholder; - private LoadingLayer loading; - private BasicSearchTextBox searchTextBox; - private readonly IBindableList apiFriends = new BindableList(); + private readonly IBindableDictionary friendPresences = new BindableDictionary(); + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private MetadataClient metadataClient { get; set; } = null!; + + private FriendOnlineStreamControl streamControl = null!; + private Box background = null!; + private Box controlBackground = null!; + private UserListToolbar userListToolbar = null!; + private LoadingLayer loading = null!; + private BasicSearchTextBox searchTextBox = null!; + private FriendsSearchContainer panelsContainer = null!; public FriendDisplay() { @@ -57,8 +45,8 @@ namespace osu.Game.Overlays.Dashboard.Friends AutoSizeAxes = Axes.Y; } - [BackgroundDependencyLoader(true)] - private void load(OverlayColourProvider colourProvider, IAPIProvider api) + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) { InternalChild = new FillFlowContainer { @@ -86,7 +74,7 @@ namespace osu.Game.Overlays.Dashboard.Friends Top = 20, Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING - FriendsOnlineStatusItem.PADDING }, - Child = onlineStreamControl = new FriendOnlineStreamControl(), + Child = streamControl = new FriendOnlineStreamControl(), } } }, @@ -157,11 +145,14 @@ namespace osu.Game.Overlays.Dashboard.Friends AutoSizeAxes = Axes.Y, Children = new Drawable[] { - itemsPlaceholder = new Container + panelsContainer = new FriendsSearchContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING } + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING }, + // Todo: Spacing = new Vector2(style == OverlayPanelDisplayStyle.Card ? 10 : 2), + Spacing = new Vector2(10), + SortCriteria = { BindTarget = userListToolbar.SortCriteria } }, loading = new LoadingLayer(true) } @@ -175,127 +166,180 @@ namespace osu.Game.Overlays.Dashboard.Friends background.Colour = colourProvider.Background4; controlBackground.Colour = colourProvider.Background5; - - apiFriends.BindTo(api.Friends); - apiFriends.BindCollectionChanged((_, _) => Schedule(() => Users = apiFriends.Select(f => f.TargetUser).ToList()), true); } protected override void LoadComplete() { base.LoadComplete(); - onlineStreamControl.Current.BindValueChanged(_ => recreatePanels()); - userListToolbar.DisplayStyle.BindValueChanged(_ => recreatePanels()); - userListToolbar.SortCriteria.BindValueChanged(_ => recreatePanels()); - searchTextBox.Current.BindValueChanged(_ => - { - if (currentContent.IsNotNull()) - currentContent.SearchTerm = searchTextBox.Current.Value; - }); + apiFriends.BindTo(api.Friends); + apiFriends.BindCollectionChanged(onFriendsChanged, true); + + friendPresences.BindTo(metadataClient.FriendPresences); + friendPresences.BindCollectionChanged(onFriendPresencesChanged, true); + + searchTextBox.Current.BindValueChanged(onSearchChanged); + streamControl.Current.BindValueChanged(onFriendsStreamChanged); } - private void recreatePanels() + private void onFriendsChanged(object? sender, NotifyCollectionChangedEventArgs e) { - if (!users.Any()) - return; - - cancellationToken?.Cancel(); - - if (itemsPlaceholder.Any()) - loading.Show(); - - var sortedUsers = sortUsers(getUsersInCurrentGroup()); - - LoadComponentAsync(createTable(sortedUsers), addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); - } - - private List getUsersInCurrentGroup() - { - switch (onlineStreamControl.Current.Value?.Status) + switch (e.Action) { - default: - case OnlineStatus.All: - return users; - - case OnlineStatus.Offline: - return users.Where(u => !u.IsOnline).ToList(); - - case OnlineStatus.Online: - return users.Where(u => u.IsOnline).ToList(); - } - } - - private void addContentToPlaceholder(SearchContainer content) - { - loading.Hide(); - - var lastContent = currentContent; - - if (lastContent != null) - { - lastContent.FadeOut(100, Easing.OutQuint).Expire(); - lastContent.Delay(25).Schedule(() => lastContent.BypassAutoSizeAxes = Axes.Y); - } - - itemsPlaceholder.Add(currentContent = content); - currentContent.FadeIn(200, Easing.OutQuint); - } - - private SearchContainer createTable(List users) - { - var style = userListToolbar.DisplayStyle.Value; - - return new SearchContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(style == OverlayPanelDisplayStyle.Card ? 10 : 2), - Children = users.Select(u => createUserPanel(u, style)).ToList(), - SearchTerm = searchTextBox.Current.Value, - }; - } - - private UserPanel createUserPanel(APIUser user, OverlayPanelDisplayStyle style) - { - switch (style) - { - default: - case OverlayPanelDisplayStyle.Card: - return new UserGridPanel(user).With(panel => + case NotifyCollectionChangedAction.Add: + foreach (APIRelation relation in e.NewItems!.OfType()) { - panel.Anchor = Anchor.TopCentre; - panel.Origin = Anchor.TopCentre; - panel.Width = 290; - }); + panelsContainer.Add(new FilterableUserPanel(new UserGridPanel(relation.TargetUser!).With(panel => + { + panel.Anchor = Anchor.TopCentre; + panel.Origin = Anchor.TopCentre; + panel.Width = 290; + }))); + } - case OverlayPanelDisplayStyle.List: - return new UserListPanel(user); + break; - case OverlayPanelDisplayStyle.Brick: - return new UserBrickPanel(user); + case NotifyCollectionChangedAction.Remove: + foreach (APIRelation relation in e.OldItems!.OfType()) + panelsContainer.RemoveAll(panel => panel.User.Equals(relation.TargetUser), true); + + break; } + + updateStatusCounts(); } - private List sortUsers(List unsorted) + private void onFriendPresencesChanged(object? sender, NotifyDictionaryChangedEventArgs e) { - switch (userListToolbar.SortCriteria.Value) + switch (e.Action) { - default: - case UserSortCriteria.LastVisit: - return unsorted.OrderByDescending(u => u.LastVisit).ToList(); - - case UserSortCriteria.Rank: - return unsorted.OrderByDescending(u => u.Statistics.GlobalRank.HasValue).ThenBy(u => u.Statistics.GlobalRank ?? 0).ToList(); - - case UserSortCriteria.Username: - return unsorted.OrderBy(u => u.Username).ToList(); + case NotifyDictionaryChangedAction.Add: + case NotifyDictionaryChangedAction.Remove: + updatePanelVisibilities(); + updateStatusCounts(); + break; } } - protected override void Dispose(bool isDisposing) + private void onFriendsStreamChanged(ValueChangedEvent stream) { - cancellationToken?.Cancel(); - base.Dispose(isDisposing); + updatePanelVisibilities(); + } + + private void onSearchChanged(ValueChangedEvent search) + { + panelsContainer.SearchTerm = search.NewValue; + } + + private void updatePanelVisibilities() + { + foreach (var panel in panelsContainer) + { + switch (streamControl.Current.Value) + { + case OnlineStatus.All: + panel.CanBeShown.Value = true; + break; + + case OnlineStatus.Online: + panel.CanBeShown.Value = friendPresences.ContainsKey(panel.User.OnlineID); + break; + + case OnlineStatus.Offline: + panel.CanBeShown.Value = !friendPresences.ContainsKey(panel.User.OnlineID); + break; + } + } + } + + private void updateStatusCounts() + { + int countOnline = 0; + int countOffline = 0; + + foreach (var user in apiFriends) + { + if (friendPresences.TryGetValue(user.TargetID, out _)) + countOnline++; + else + countOffline++; + } + + streamControl.CountAll.Value = apiFriends.Count; + streamControl.CountOnline.Value = countOnline; + streamControl.CountOffline.Value = countOffline; + } + + private class FriendsSearchContainer : SearchContainer + { + public readonly IBindable SortCriteria = new Bindable(); + + protected override void LoadComplete() + { + base.LoadComplete(); + SortCriteria.BindValueChanged(_ => InvalidateLayout(), true); + } + + public override IEnumerable FlowingChildren + { + get + { + IEnumerable panels = base.FlowingChildren.OfType(); + + switch (SortCriteria.Value) + { + default: + case UserSortCriteria.LastVisit: + return panels.OrderByDescending(panel => panel.User.LastVisit); + + case UserSortCriteria.Rank: + return panels.OrderByDescending(panel => panel.User.Statistics.GlobalRank.HasValue).ThenBy(panel => panel.User.Statistics.GlobalRank ?? 0); + + case UserSortCriteria.Username: + return panels.OrderBy(panel => panel.User.Username); + } + } + } + } + + private class FilterableUserPanel : CompositeDrawable, IConditionalFilterable + { + public readonly Bindable CanBeShown = new Bindable(); + + public APIUser User => panel.User; + + private readonly UserPanel panel; + + public FilterableUserPanel(UserPanel panel) + { + this.panel = panel; + + Anchor = panel.Anchor; + Origin = panel.Origin; + RelativeSizeAxes = panel.RelativeSizeAxes; + AutoSizeAxes = panel.AutoSizeAxes; + Width = panel.Width; + Height = panel.Height; + + InternalChild = panel; + } + + IBindable IConditionalFilterable.CanBeShown => CanBeShown; + + IEnumerable IHasFilterTerms.FilterTerms => panel.FilterTerms; + + bool IFilterable.MatchingFilter + { + set + { + if (value) + Show(); + else + Hide(); + } + } + + bool IFilterable.FilteringActive { set { } } } } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendOnlineStreamControl.cs b/osu.Game/Overlays/Dashboard/Friends/FriendOnlineStreamControl.cs index 9f429c23d8..25b29e8d16 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendOnlineStreamControl.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendOnlineStreamControl.cs @@ -1,30 +1,43 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - -using System.Collections.Generic; -using System.Linq; -using osu.Game.Online.API.Requests.Responses; +using System; +using osu.Framework.Bindables; namespace osu.Game.Overlays.Dashboard.Friends { - public partial class FriendOnlineStreamControl : OverlayStreamControl + public partial class FriendOnlineStreamControl : OverlayStreamControl { - protected override OverlayStreamItem CreateStreamItem(FriendStream value) => new FriendsOnlineStatusItem(value); + public readonly BindableInt CountAll = new BindableInt(); + public readonly BindableInt CountOnline = new BindableInt(); + public readonly BindableInt CountOffline = new BindableInt(); - public void Populate(List users) + public FriendOnlineStreamControl() { - Clear(); + Items = + [ + OnlineStatus.All, + OnlineStatus.Online, + OnlineStatus.Offline + ]; + } - int userCount = users.Count; - int onlineUsersCount = users.Count(user => user.IsOnline); + protected override OverlayStreamItem CreateStreamItem(OnlineStatus value) + { + switch (value) + { + case OnlineStatus.All: + return new FriendsOnlineStatusItem(value) { UserCount = { BindTarget = CountAll } }; - AddItem(new FriendStream(OnlineStatus.All, userCount)); - AddItem(new FriendStream(OnlineStatus.Online, onlineUsersCount)); - AddItem(new FriendStream(OnlineStatus.Offline, userCount - onlineUsersCount)); + case OnlineStatus.Online: + return new FriendsOnlineStatusItem(value) { UserCount = { BindTarget = CountOnline } }; - Current.Value = Items.FirstOrDefault(); + case OnlineStatus.Offline: + return new FriendsOnlineStatusItem(value) { UserCount = { BindTarget = CountOffline } }; + + default: + throw new ArgumentException(nameof(value)); + } } } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendStream.cs b/osu.Game/Overlays/Dashboard/Friends/FriendStream.cs index 4abece9a8d..f791e34c8f 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendStream.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendStream.cs @@ -1,18 +1,18 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Bindables; + namespace osu.Game.Overlays.Dashboard.Friends { public class FriendStream { - public OnlineStatus Status { get; } + public readonly BindableInt UserCount = new BindableInt(); + public readonly OnlineStatus Status; - public int Count { get; } - - public FriendStream(OnlineStatus status, int count) + public FriendStream(OnlineStatus status) { Status = status; - Count = count; } } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs index 2aea631b7c..459592085b 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs @@ -2,27 +2,32 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Bindables; using osu.Framework.Extensions; -using osu.Framework.Localisation; using osu.Game.Graphics; using osuTK.Graphics; namespace osu.Game.Overlays.Dashboard.Friends { - public partial class FriendsOnlineStatusItem : OverlayStreamItem + public partial class FriendsOnlineStatusItem : OverlayStreamItem { - public FriendsOnlineStatusItem(FriendStream value) + public readonly IBindable UserCount = new Bindable(); + + public FriendsOnlineStatusItem(OnlineStatus value) : base(value) { + MainText = value.GetLocalisableDescription(); } - protected override LocalisableString MainText => Value.Status.GetLocalisableDescription(); - - protected override LocalisableString AdditionalText => Value.Count.ToString(); + protected override void LoadComplete() + { + base.LoadComplete(); + UserCount.BindValueChanged(count => AdditionalText = count.NewValue.ToString(), true); + } protected override Color4 GetBarColour(OsuColour colours) { - switch (Value.Status) + switch (Value) { case OnlineStatus.All: return Color4.White; @@ -34,7 +39,7 @@ namespace osu.Game.Overlays.Dashboard.Friends return Color4.Black; default: - throw new ArgumentException($@"{Value.Status} status does not provide a colour in {nameof(GetBarColour)}."); + throw new ArgumentException($@"{Value} status does not provide a colour in {nameof(GetBarColour)}."); } } } diff --git a/osu.Game/Overlays/OverlayStreamItem.cs b/osu.Game/Overlays/OverlayStreamItem.cs index f0ae0b41fc..ec04a130cf 100644 --- a/osu.Game/Overlays/OverlayStreamItem.cs +++ b/osu.Game/Overlays/OverlayStreamItem.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Framework.Graphics.UserInterface; @@ -22,7 +20,9 @@ namespace osu.Game.Overlays { public abstract partial class OverlayStreamItem : TabItem { - public readonly Bindable SelectedItem = new Bindable(); + public const float PADDING = 5; + + public readonly Bindable SelectedItem = new Bindable(); private bool userHoveringArea; @@ -38,10 +38,12 @@ namespace osu.Game.Overlays } } - private FillFlowContainer text; - private ExpandingBar expandingBar; - - public const float PADDING = 5; + private FillFlowContainer text = null!; + private ExpandingBar expandingBar = null!; + private Sample selectSample = null!; + private OsuSpriteText? mainTextPiece; + private OsuSpriteText? additionalTextPiece; + private OsuSpriteText? infoTextPiece; protected OverlayStreamItem(T value) : base(value) @@ -51,8 +53,6 @@ namespace osu.Game.Overlays Margin = new MarginPadding(PADDING); } - private Sample selectSample; - [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, OsuColour colours, AudioManager audio) { @@ -65,17 +65,17 @@ namespace osu.Game.Overlays Margin = new MarginPadding { Top = 6 }, Children = new[] { - new OsuSpriteText + mainTextPiece = new OsuSpriteText { Text = MainText, Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), }, - new OsuSpriteText + additionalTextPiece = new OsuSpriteText { Text = AdditionalText, Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular), }, - new OsuSpriteText + infoTextPiece = new OsuSpriteText { Text = InfoText, Font = OsuFont.GetFont(size: 10), @@ -99,11 +99,47 @@ namespace osu.Game.Overlays SelectedItem.BindValueChanged(_ => updateState(), true); } - protected abstract LocalisableString MainText { get; } + private LocalisableString mainText; - protected abstract LocalisableString AdditionalText { get; } + protected LocalisableString MainText + { + get => mainText; + set + { + mainText = value; - protected virtual LocalisableString InfoText => string.Empty; + if (mainTextPiece != null) + mainTextPiece.Text = value; + } + } + + private LocalisableString additionalText; + + protected LocalisableString AdditionalText + { + get => additionalText; + set + { + additionalText = value; + + if (additionalTextPiece != null) + additionalTextPiece.Text = value; + } + } + + private LocalisableString infoText; + + protected LocalisableString InfoText + { + get => infoText; + set + { + infoText = value; + + if (infoTextPiece != null) + infoTextPiece.Text = value; + } + } protected abstract Color4 GetBarColour(OsuColour colours); From 1a5a47347659a079924a4767f74d5f086d9b11c2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 11 Mar 2025 19:28:59 +0900 Subject: [PATCH 744/801] Update panel visibility when friends are added/removed --- osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index 2938be732c..308be32cf9 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -206,6 +206,7 @@ namespace osu.Game.Overlays.Dashboard.Friends break; } + updatePanelVisibilities(); updateStatusCounts(); } From 6378c8ed754daac6c621cc58061dfd805515f666 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 11 Mar 2025 19:29:10 +0900 Subject: [PATCH 745/801] Simplify condition --- osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index 308be32cf9..4bd79188c8 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -260,7 +260,7 @@ namespace osu.Game.Overlays.Dashboard.Friends foreach (var user in apiFriends) { - if (friendPresences.TryGetValue(user.TargetID, out _)) + if (friendPresences.ContainsKey(user.TargetID)) countOnline++; else countOffline++; From 2af3bebff38b13220f04df04301d8ca1b3d881b1 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 11 Mar 2025 08:26:45 -0400 Subject: [PATCH 746/801] Add draw size invalidation handling to column extension logic Also indirectly makes the setting effective during gameplay as requested. --- osu.Game.Rulesets.Mania/UI/ColumnFlow.cs | 93 ++++++++++++------------ 1 file changed, 48 insertions(+), 45 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs index 6a1949e978..46a7a70f27 100644 --- a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs +++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs @@ -8,6 +8,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Layout; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Skinning; @@ -38,6 +39,8 @@ namespace osu.Game.Rulesets.Mania.UI set => base.Masking = value; } + private readonly LayoutValue columnSizeLayout = new LayoutValue(Invalidation.DrawSize); + public ColumnFlow(StageDefinition stageDefinition) { this.stageDefinition = stageDefinition; @@ -56,6 +59,8 @@ namespace osu.Game.Rulesets.Mania.UI for (int i = 0; i < stageDefinition.Columns; i++) columns.Add(new Container { RelativeSizeAxes = Axes.Y }); + + AddLayout(columnSizeLayout); } [Resolved] @@ -67,20 +72,54 @@ namespace osu.Game.Rulesets.Mania.UI private void load(ManiaRulesetConfigManager? rulesetConfig) { rulesetConfig?.BindWith(ManiaRulesetSetting.MobilePlayStyle, mobilePlayStyle); - mobilePlayStyle.BindValueChanged(_ => updateMobileSizing()); - skin.SourceChanged += onSkinChanged; - onSkinChanged(); + mobilePlayStyle.BindValueChanged(_ => updateColumnSize()); + skin.SourceChanged += updateColumnSize; } - protected override void LoadComplete() + protected override void Update() { - base.LoadComplete(); - updateMobileSizing(); + base.Update(); + + if (!columnSizeLayout.IsValid) + { + updateColumnSize(); + columnSizeLayout.Validate(); + } } - private void onSkinChanged() + /// + /// Sets the content of one of the columns of this . + /// + /// The index of the column to set the content of. + /// The content. + public void SetContentForColumn(int column, TContent content) { + Content[column] = columns[column].Child = content; + } + + private void updateColumnSize() + { + float mobileAdjust = 1f; + + if (mobilePlayStyle.Value == ManiaMobilePlayStyle.ExtendedColumns) + { + // GridContainer+CellContainer containing this stage (gets split up for dual stages). + Vector2? containingCell = this.FindClosestParent()?.Parent?.DrawSize; + + // Will be null in tests. + if (containingCell != null && containingCell.Value.X >= containingCell.Value.Y) + { + float aspectRatio = containingCell.Value.X / containingCell.Value.Y; + + // 2.83 is a mostly arbitrary scale-up (170 / 60, based on original implementation for argon) + mobileAdjust = 2.83f * Math.Min(1, 7f / stageDefinition.Columns); + // 1.92 is a "reference" mobile screen aspect ratio for phones. + // We should scale it back for cases like tablets which aren't so extreme. + mobileAdjust *= aspectRatio / 1.92f; + } + } + for (int i = 0; i < stageDefinition.Columns; i++) { if (i > 0) @@ -101,44 +140,8 @@ namespace osu.Game.Rulesets.Mania.UI // only used by default skin (legacy skins get defaults set in LegacyManiaSkinConfiguration) width ??= isSpecialColumn ? Column.SPECIAL_COLUMN_WIDTH : Column.COLUMN_WIDTH; - columns[i].Width = width.Value; + columns[i].Width = width.Value * mobileAdjust; } - - updateMobileSizing(); - } - - /// - /// Sets the content of one of the columns of this . - /// - /// The index of the column to set the content of. - /// The content. - public void SetContentForColumn(int column, TContent content) - { - Content[column] = columns[column].Child = content; - } - - private void updateMobileSizing() - { - if (!IsLoaded || !RuntimeInfo.IsMobile || mobilePlayStyle.Value != ManiaMobilePlayStyle.ExtendedColumns) - return; - - // GridContainer+CellContainer containing this stage (gets split up for dual stages). - Vector2? containingCell = this.FindClosestParent()?.Parent?.DrawSize; - - // Will be null in tests. - if (containingCell == null || containingCell.Value.X < containingCell.Value.Y) - return; - - float aspectRatio = containingCell.Value.X / containingCell.Value.Y; - - // 2.83 is a mostly arbitrary scale-up (170 / 60, based on original implementation for argon) - float mobileAdjust = 2.83f * Math.Min(1, 7f / stageDefinition.Columns); - // 1.92 is a "reference" mobile screen aspect ratio for phones. - // We should scale it back for cases like tablets which aren't so extreme. - mobileAdjust *= aspectRatio / 1.92f; - - for (int i = 0; i < stageDefinition.Columns; i++) - columns[i].Width *= mobileAdjust; } protected override void Dispose(bool isDisposing) @@ -146,7 +149,7 @@ namespace osu.Game.Rulesets.Mania.UI base.Dispose(isDisposing); if (skin.IsNotNull()) - skin.SourceChanged -= onSkinChanged; + skin.SourceChanged -= updateColumnSize; } } } From c99448939258b8d7ec7c39b3d1f17b71d5dec38b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 21:28:35 +0900 Subject: [PATCH 747/801] Fix silly test failures --- osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs index 95a134e204..9f0dc75f84 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs @@ -135,7 +135,13 @@ namespace osu.Game.Tests.Visual.Settings public Bindable AreaSize { get; } = new Bindable(); public Bindable Rotation { get; } = new Bindable(); - public BindableFloat PressureThreshold { get; } = new BindableFloat(); + + public BindableFloat PressureThreshold { get; } = new BindableFloat + { + MinValue = 0f, + MaxValue = 1f, + Precision = 0.005f, + }; public IBindable Tablet => tablet; From b64e69d581a08d0be9d5e705f9bb1078faf3050f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 11 Mar 2025 09:04:07 -0400 Subject: [PATCH 748/801] Apply suggested renames --- .../TestSceneManiaTouchInput.cs | 2 +- .../Configuration/ManiaRulesetConfigManager.cs | 4 ++-- ...iaMobilePlayStyle.cs => ManiaMobileLayout.cs} | 14 +++++++------- .../ManiaSettingsSubsection.cs | 15 ++++++++++----- osu.Game.Rulesets.Mania/UI/Column.cs | 8 ++++---- osu.Game.Rulesets.Mania/UI/ColumnFlow.cs | 8 ++++---- .../UI/DrawableManiaRuleset.cs | 2 +- .../UI/ManiaTouchInputArea.cs | 6 +++--- osu.Game/Localisation/RulesetSettingsStrings.cs | 16 ++++++++-------- 9 files changed, 40 insertions(+), 35 deletions(-) rename osu.Game.Rulesets.Mania/{ManiaMobilePlayStyle.cs => ManiaMobileLayout.cs} (62%) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs index b33c74d4e9..fc495a5ab0 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs @@ -137,7 +137,7 @@ namespace osu.Game.Rulesets.Mania.Tests private void toggleTouchControls(bool enabled) { var maniaConfig = (ManiaRulesetConfigManager)RulesetConfigs.GetConfigFor(CreatePlayerRuleset())!; - maniaConfig.SetValue(ManiaRulesetSetting.MobilePlayStyle, enabled ? ManiaMobilePlayStyle.TouchControls : ManiaMobilePlayStyle.TouchableColumns); + maniaConfig.SetValue(ManiaRulesetSetting.MobileLayout, enabled ? ManiaMobileLayout.LandscapeWithOverlay : ManiaMobileLayout.Portrait); } private ManiaTouchInputArea? getTouchOverlay() => this.ChildrenOfType().SingleOrDefault(); diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs index 10a3236178..5242b6685c 100644 --- a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Configuration SetDefault(ManiaRulesetSetting.ScrollSpeed, 8.0, 1.0, 40.0, 0.1); SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down); SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false); - SetDefault(ManiaRulesetSetting.MobilePlayStyle, ManiaMobilePlayStyle.TouchableColumns); + SetDefault(ManiaRulesetSetting.MobileLayout, ManiaMobileLayout.Portrait); #pragma warning disable CS0618 // Although obsolete, this is still required to populate the bindable from the database in case migration is required. @@ -57,6 +57,6 @@ namespace osu.Game.Rulesets.Mania.Configuration ScrollSpeed, ScrollDirection, TimingBasedNoteColouring, - MobilePlayStyle, + MobileLayout, } } diff --git a/osu.Game.Rulesets.Mania/ManiaMobilePlayStyle.cs b/osu.Game.Rulesets.Mania/ManiaMobileLayout.cs similarity index 62% rename from osu.Game.Rulesets.Mania/ManiaMobilePlayStyle.cs rename to osu.Game.Rulesets.Mania/ManiaMobileLayout.cs index e6b1224fd3..7d70dba092 100644 --- a/osu.Game.Rulesets.Mania/ManiaMobilePlayStyle.cs +++ b/osu.Game.Rulesets.Mania/ManiaMobileLayout.cs @@ -6,15 +6,15 @@ using osu.Game.Localisation; namespace osu.Game.Rulesets.Mania { - public enum ManiaMobilePlayStyle + public enum ManiaMobileLayout { - [LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.TouchableColumns))] - TouchableColumns, + [LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.PortraitExpandedColumns))] + Portrait, - [LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.TouchControls))] - TouchControls, + [LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.LandscapeExpandedColumns))] + Landscape, - [LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.ExtendedColumns))] - ExtendedColumns, + [LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.LandscapeTouchOverlay))] + LandscapeWithOverlay, } } diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs index f558d30ee0..5ae7ec9480 100644 --- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs +++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; @@ -45,12 +46,16 @@ namespace osu.Game.Rulesets.Mania LabelText = RulesetSettingsStrings.TimingBasedColouring, Current = config.GetBindable(ManiaRulesetSetting.TimingBasedNoteColouring), }, - new SettingsEnumDropdown - { - LabelText = RulesetSettingsStrings.MobilePlayStyle, - Current = config.GetBindable(ManiaRulesetSetting.MobilePlayStyle), - }, }; + + if (RuntimeInfo.IsMobile) + { + Add(new SettingsEnumDropdown + { + LabelText = RulesetSettingsStrings.MobileLayout, + Current = config.GetBindable(ManiaRulesetSetting.MobileLayout), + }); + } } private partial class ManiaScrollSlider : RoundedSliderBar diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index ebd8efe124..cb825761d1 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Mania.UI public readonly Bindable AccentColour = new Bindable(Color4.Black); - private IBindable mobilePlayStyle = null!; + private IBindable mobilePlayStyle = null!; public Column(int index, bool isSpecial) { @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Mania.UI RegisterPool(10, 50); if (rulesetConfig != null) - mobilePlayStyle = rulesetConfig.GetBindable(ManiaRulesetSetting.MobilePlayStyle); + mobilePlayStyle = rulesetConfig.GetBindable(ManiaRulesetSetting.MobileLayout); } private void onSourceChanged() @@ -199,8 +199,8 @@ namespace osu.Game.Rulesets.Mania.UI protected override bool OnTouchDown(TouchDownEvent e) { - // if touch controls are selected, disallow columns from handling touch directly. - if (mobilePlayStyle.Value == ManiaMobilePlayStyle.TouchControls) + // if touch overlay is visible, disallow columns from handling touch directly. + if (mobilePlayStyle.Value == ManiaMobileLayout.LandscapeWithOverlay) return false; maniaInputManager?.KeyBindingContainer.TriggerPressed(Action.Value); diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs index 46a7a70f27..3b19e90b60 100644 --- a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs +++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs @@ -66,14 +66,14 @@ namespace osu.Game.Rulesets.Mania.UI [Resolved] private ISkinSource skin { get; set; } = null!; - private readonly Bindable mobilePlayStyle = new Bindable(); + private readonly Bindable mobileLayout = new Bindable(); [BackgroundDependencyLoader] private void load(ManiaRulesetConfigManager? rulesetConfig) { - rulesetConfig?.BindWith(ManiaRulesetSetting.MobilePlayStyle, mobilePlayStyle); + rulesetConfig?.BindWith(ManiaRulesetSetting.MobileLayout, mobileLayout); - mobilePlayStyle.BindValueChanged(_ => updateColumnSize()); + mobileLayout.BindValueChanged(_ => updateColumnSize()); skin.SourceChanged += updateColumnSize; } @@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Mania.UI { float mobileAdjust = 1f; - if (mobilePlayStyle.Value == ManiaMobilePlayStyle.ExtendedColumns) + if (RuntimeInfo.IsMobile && mobileLayout.Value == ManiaMobileLayout.Landscape) { // GridContainer+CellContainer containing this stage (gets split up for dual stages). Vector2? containingCell = this.FindClosestParent()?.Parent?.DrawSize; diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index 2dcbcacf93..bac62d2b66 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Mania.UI public IEnumerable BarLines; - private bool playsWithTouchableColumns => Config.Get(ManiaRulesetSetting.MobilePlayStyle) == ManiaMobilePlayStyle.TouchableColumns; + private bool playsWithTouchableColumns => Config.Get(ManiaRulesetSetting.MobileLayout) == ManiaMobileLayout.Portrait; public override bool RequiresPortraitOrientation => Beatmap.Stages.Count == 1 && playsWithTouchableColumns; diff --git a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs index 7cb6b3b96f..e9489d4c06 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs @@ -97,14 +97,14 @@ namespace osu.Game.Rulesets.Mania.UI }; } - private IBindable mobilePlayStyle = null!; + private IBindable mobilePlayStyle = null!; protected override void LoadComplete() { base.LoadComplete(); - mobilePlayStyle = rulesetConfig.GetBindable(ManiaRulesetSetting.MobilePlayStyle); - mobilePlayStyle.BindValueChanged(p => touchControls.Value = p.NewValue == ManiaMobilePlayStyle.TouchControls, true); + mobilePlayStyle = rulesetConfig.GetBindable(ManiaRulesetSetting.MobileLayout); + mobilePlayStyle.BindValueChanged(p => touchControls.Value = p.NewValue == ManiaMobileLayout.LandscapeWithOverlay, true); Opacity.BindValueChanged(o => Alpha = o.NewValue, true); } diff --git a/osu.Game/Localisation/RulesetSettingsStrings.cs b/osu.Game/Localisation/RulesetSettingsStrings.cs index 527707b011..fc4fb58e26 100644 --- a/osu.Game/Localisation/RulesetSettingsStrings.cs +++ b/osu.Game/Localisation/RulesetSettingsStrings.cs @@ -90,24 +90,24 @@ namespace osu.Game.Localisation public static LocalisableString TouchControlScheme => new TranslatableString(getKey(@"touch_control_scheme"), @"Touch control scheme"); /// - /// "Mobile play style" + /// "Mobile layout" /// - public static LocalisableString MobilePlayStyle => new TranslatableString(getKey(@"mobile_play_style"), @"Mobile play style"); + public static LocalisableString MobileLayout => new TranslatableString(getKey(@"mobile_layout"), @"Mobile layout"); /// - /// "Touchable columns" + /// "Portrait (expanded columns)" /// - public static LocalisableString TouchableColumns => new TranslatableString(getKey(@"touchable_columns"), @"Touchable columns"); + public static LocalisableString PortraitExpandedColumns => new TranslatableString(getKey(@"portrait_expanded_columns"), @"Portrait (expanded columns)"); /// - /// "Touch controls" + /// "Landscape (expanded columns)" /// - public static LocalisableString TouchControls => new TranslatableString(getKey(@"touch_controls"), @"Touch controls"); + public static LocalisableString LandscapeExpandedColumns => new TranslatableString(getKey(@"landscape_expanded_columns"), @"Landscape (expanded columns)"); /// - /// "Extended columns" + /// "Landscape (touch overlay)" /// - public static LocalisableString ExtendedColumns => new TranslatableString(getKey(@"extended_columns"), @"Extended columns"); + public static LocalisableString LandscapeTouchOverlay => new TranslatableString(getKey(@"landscape_touch_overlay"), @"Landscape (touch overlay)"); private static string getKey(string key) => $@"{prefix}:{key}"; } From b3ea63598e892ba860d21992f3dcceb4f2244712 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 11 Mar 2025 09:17:19 -0400 Subject: [PATCH 749/801] Fix touch input area not handling settings changes --- .../UI/DrawableManiaRuleset.cs | 27 +++++++++++++++--- .../UI/ManiaTouchInputArea.cs | 28 +++---------------- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index bac62d2b66..66400b0a55 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -50,9 +50,7 @@ namespace osu.Game.Rulesets.Mania.UI public IEnumerable BarLines; - private bool playsWithTouchableColumns => Config.Get(ManiaRulesetSetting.MobileLayout) == ManiaMobileLayout.Portrait; - - public override bool RequiresPortraitOrientation => Beatmap.Stages.Count == 1 && playsWithTouchableColumns; + public override bool RequiresPortraitOrientation => Beatmap.Stages.Count == 1 && mobileLayout.Value == ManiaMobileLayout.Portrait; protected override bool RelativeScaleBeatLengths => true; @@ -60,6 +58,7 @@ namespace osu.Game.Rulesets.Mania.UI private readonly Bindable configDirection = new Bindable(); private readonly BindableDouble configScrollSpeed = new BindableDouble(); + private readonly Bindable mobileLayout = new Bindable(); private double currentTimeRange; protected double TargetTimeRange; @@ -114,7 +113,27 @@ namespace osu.Game.Rulesets.Mania.UI TimeRange.Value = TargetTimeRange = currentTimeRange = ComputeScrollTime(configScrollSpeed.Value); - KeyBindingInputManager.Add(new ManiaTouchInputArea(this)); + Config.BindWith(ManiaRulesetSetting.MobileLayout, mobileLayout); + mobileLayout.BindValueChanged(_ => updateMobileLayout(), true); + } + + private ManiaTouchInputArea? touchInputArea; + + private void updateMobileLayout() + { + switch (mobileLayout.Value) + { + case ManiaMobileLayout.LandscapeWithOverlay: + KeyBindingInputManager.Add(touchInputArea = new ManiaTouchInputArea(this)); + break; + + default: + if (touchInputArea != null) + KeyBindingInputManager.Remove(touchInputArea, true); + + touchInputArea = null; + break; + } } protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount; diff --git a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs index e9489d4c06..1df05bf350 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs @@ -46,8 +46,6 @@ namespace osu.Game.Rulesets.Mania.UI private GridContainer gridContainer = null!; - private readonly BindableBool touchControls = new BindableBool(); - public ManiaTouchInputArea(DrawableManiaRuleset drawableRuleset) { this.drawableRuleset = drawableRuleset; @@ -80,7 +78,6 @@ namespace osu.Game.Rulesets.Mania.UI receptorGridContent.Add(new ColumnInputReceptor { Action = { BindTarget = column.Action }, - Enabled = { BindTarget = touchControls }, }); receptorGridDimensions.Add(new Dimension()); @@ -97,15 +94,9 @@ namespace osu.Game.Rulesets.Mania.UI }; } - private IBindable mobilePlayStyle = null!; - protected override void LoadComplete() { base.LoadComplete(); - - mobilePlayStyle = rulesetConfig.GetBindable(ManiaRulesetSetting.MobileLayout); - mobilePlayStyle.BindValueChanged(p => touchControls.Value = p.NewValue == ManiaMobileLayout.LandscapeWithOverlay, true); - Opacity.BindValueChanged(o => Alpha = o.NewValue, true); } @@ -118,13 +109,8 @@ namespace osu.Game.Rulesets.Mania.UI protected override bool OnTouchDown(TouchDownEvent e) { - if (touchControls.Value) - { - Show(); - return true; - } - - return false; + Show(); + return true; } protected override void PopIn() @@ -140,7 +126,6 @@ namespace osu.Game.Rulesets.Mania.UI public partial class ColumnInputReceptor : CompositeDrawable { public readonly IBindable Action = new Bindable(); - public readonly IBindable Enabled = new BindableBool(); private readonly Box highlightOverlay; @@ -180,13 +165,8 @@ namespace osu.Game.Rulesets.Mania.UI protected override bool OnTouchDown(TouchDownEvent e) { - if (Enabled.Value) - { - updateButton(true); - return false; // handled by parent container to show overlay. - } - - return false; + updateButton(true); + return false; // handled by parent container to show overlay. } protected override void OnTouchUp(TouchUpEvent e) From 54d7a91cabc04e63873e53ec8ccefd69662e36f1 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 12 Mar 2025 00:36:28 -0400 Subject: [PATCH 750/801] Fix osu!taiko mobile scaling not accurate --- .../UI/TaikoPlayfieldAdjustmentContainer.cs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs index 6a9e5789de..07fda13c8c 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -60,19 +59,7 @@ namespace osu.Game.Rulesets.Taiko.UI // Limit the maximum relative height of the playfield to one-third of available area to avoid it masking out on extreme resolutions. relativeHeight = Math.Min(relativeHeight, 1f / 3f); - Scale = new Vector2(Math.Max((Parent!.ChildSize.Y / 768f) * (relativeHeight / base_relative_height), 1f)); - - // on mobile platforms where the base aspect ratio is wider, the taiko playfield - // needs to be scaled down to remain playable. - if (RuntimeInfo.IsMobile && osuGame != null) - { - const float base_aspect_ratio = 1024f / 768f; - float gameAspectRatio = osuGame.ScalingContainerTargetDrawSize.X / osuGame.ScalingContainerTargetDrawSize.Y; - // this magic scale is unexplainable, but required so the playfield doesn't become too zoomed out as the aspect ratio increases. - const float magic_scale = 1.25f; - Scale *= magic_scale * new Vector2(base_aspect_ratio / gameAspectRatio); - } - + Scale = new Vector2(Parent!.ChildSize.Y / 768f * (relativeHeight / base_relative_height)); Width = 1 / Scale.X; } From 65cdcb469603ca22ec36872d20e07ad8f9fd563f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Mar 2025 14:01:11 +0900 Subject: [PATCH 751/801] Fix default beatmap not being correctly set after aborting new beatmap creation Closes https://github.com/ppy/osu/issues/32337. --- osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs | 2 ++ osu.Game/Beatmaps/WorkingBeatmapCache.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 996e87ff8a..2758954907 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -94,6 +94,8 @@ namespace osu.Game.Tests.Visual.Editing AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == editorBeatmap.BeatmapInfo.BeatmapSet.AsNonNull().ID)?.Value.DeletePending == true); + + AddUntilStep("wait for default beatmap", () => Editor.Beatmap.Value is DummyWorkingBeatmap); } [Test] diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index fdeb840977..bd125deddf 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -89,7 +89,7 @@ namespace osu.Game.Beatmaps public virtual WorkingBeatmap GetWorkingBeatmap([CanBeNull] BeatmapInfo beatmapInfo) { - if (beatmapInfo?.BeatmapSet == null) + if (beatmapInfo?.ID == DefaultBeatmap.BeatmapInfo.ID || beatmapInfo?.BeatmapSet == null) return DefaultBeatmap; lock (workingCache) From 7f4f92dedf35e6933a2a3f242484eb81c2279e88 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 12 Mar 2025 01:14:31 -0400 Subject: [PATCH 752/801] Remove unnecessary DI property --- .../UI/TaikoPlayfieldAdjustmentContainer.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs index 07fda13c8c..9f821ee93d 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Taiko.Beatmaps; @@ -20,9 +19,6 @@ namespace osu.Game.Rulesets.Taiko.UI public readonly IBindable LockPlayfieldAspectRange = new BindableBool(true); - [Resolved] - private OsuGame? osuGame { get; set; } - public TaikoPlayfieldAdjustmentContainer() { RelativeSizeAxes = Axes.X; From 72854d0ae64c0c867b9b9bf2a601bfba6c115a1e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Mar 2025 14:20:13 +0900 Subject: [PATCH 753/801] Fix storyboard letterbox hiding HUD elements Addresses https://github.com/ppy/osu/discussions/29788. --- osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs | 2 +- osu.Game/Screens/Play/BreakOverlay.cs | 8 +------- osu.Game/Screens/Play/Player.cs | 9 ++++++++- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs index 21b6495865..9fc1ce3027 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs @@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.Gameplay RelativeSizeAxes = Axes.Both, }, breakTracker = new TestBreakTracker(), - breakOverlay = new BreakOverlay(true, new ScoreProcessor(new OsuRuleset())) + breakOverlay = new BreakOverlay(new ScoreProcessor(new OsuRuleset())) { ProcessCustomClock = false, BreakTracker = breakTracker, diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index 550d29965f..49b7067c8d 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -46,7 +46,7 @@ namespace osu.Game.Screens.Play private readonly IBindable currentPeriod = new Bindable(); - public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor) + public BreakOverlay(ScoreProcessor scoreProcessor) { this.scoreProcessor = scoreProcessor; RelativeSizeAxes = Axes.Both; @@ -63,12 +63,6 @@ namespace osu.Game.Screens.Play RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - new LetterboxOverlay - { - Alpha = letterboxing ? 1 : 0, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, new CircularContainer { Anchor = Anchor.Centre, diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 92c483b24a..29b54c8699 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -34,6 +34,7 @@ using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; +using osu.Game.Screens.Play.Break; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Ranking; using osu.Game.Skinning; @@ -450,6 +451,12 @@ namespace osu.Game.Screens.Play Children = new[] { DimmableStoryboard.OverlayLayerContainer.CreateProxy(), + new LetterboxOverlay + { + Alpha = working.Beatmap.LetterboxInBreaks ? 1 : 0, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods, Configuration.AlwaysShowLeaderboard) { HoldToQuit = @@ -468,7 +475,7 @@ namespace osu.Game.Screens.Play Anchor = Anchor.Centre, Origin = Anchor.Centre }, - BreakOverlay = new BreakOverlay(working.Beatmap.LetterboxInBreaks, ScoreProcessor) + BreakOverlay = new BreakOverlay(ScoreProcessor) { Clock = DrawableRuleset.FrameStableClock, ProcessCustomClock = false, From 5a5246a40710491fc1d29ee095cd54a20ac4d40e Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 12 Mar 2025 03:04:06 -0400 Subject: [PATCH 754/801] Improve handling for top-anchored judgement positions in osu!mania --- .../Skinning/Argon/ArgonJudgementPiece.cs | 6 ++++- .../Legacy/LegacyManiaJudgementPiece.cs | 15 ++++++++---- .../UI/DefaultManiaJudgementPiece.cs | 6 ++++- .../UI/DrawableManiaJudgement.cs | 23 +++++++------------ 4 files changed, 29 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs index 6098459f6b..bef8625ea2 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs @@ -53,7 +53,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon } } - private void onDirectionChanged() => Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position; + private void onDirectionChanged() + { + Anchor = direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; + Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position; + } protected override SpriteText CreateJudgementText() => new OsuSpriteText diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs index 3752c5f27a..5ece5df66f 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs @@ -24,7 +24,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy this.result = result; this.animation = animation; - Anchor = Anchor.BottomCentre; Origin = Anchor.Centre; AutoSizeAxes = Axes.Both; @@ -53,10 +52,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy float hitPosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? 0; float scorePosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value ?? 0; - float absoluteHitPosition = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition; - float finalPosition = scorePosition - absoluteHitPosition; + float hitPositionFromTop = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition; - Y = direction.Value == ScrollingDirection.Up ? -finalPosition : finalPosition; + if (scorePosition > hitPositionFromTop / 2f) + { + Anchor = direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; + Y = direction.Value == ScrollingDirection.Up ? hitPositionFromTop - scorePosition : scorePosition - hitPositionFromTop; + } + else + { + Anchor = direction.Value == ScrollingDirection.Up ? Anchor.BottomCentre : Anchor.TopCentre; + Y = direction.Value == ScrollingDirection.Up ? -scorePosition : scorePosition; + } } public void PlayAnimation() diff --git a/osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.cs b/osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.cs index f0af6085d0..4872fe5049 100644 --- a/osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.cs @@ -29,7 +29,11 @@ namespace osu.Game.Rulesets.Mania.UI direction.BindValueChanged(_ => onDirectionChanged(), true); } - private void onDirectionChanged() => Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position; + private void onDirectionChanged() + { + Anchor = direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; + Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position; + } protected override void LoadComplete() { diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index 40fef1a56a..20248ab6bc 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -3,30 +3,23 @@ #nullable disable -using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; -using osu.Game.Rulesets.UI.Scrolling; +using osuTK; namespace osu.Game.Rulesets.Mania.UI { public partial class DrawableManiaJudgement : DrawableJudgement { - private IBindable direction; - - [BackgroundDependencyLoader] - private void load(IScrollingInfo scrollingInfo) + public DrawableManiaJudgement() { - direction = scrollingInfo.Direction.GetBoundCopy(); - direction.BindValueChanged(_ => onDirectionChanged(), true); - } - - private void onDirectionChanged() - { - Anchor = direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; - Origin = Anchor.Centre; + // Extend the dimensions of this drawable to the entire parenting container. + // This allows skin implementations (i.e. LegacyManiaJudgementPiece) to freely choose the anchor based on skin settings. + Anchor = Anchor.TopLeft; + Origin = Anchor.TopLeft; + RelativeSizeAxes = Axes.Both; + Size = new Vector2(1f); } protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result); From a492e070fbf939b9b70022c8ed501a5cf7180a9c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Mar 2025 15:57:53 +0900 Subject: [PATCH 755/801] Add failing test cases that came up in review --- .../Editing/TestScenePlacementBlueprint.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs index 37caccfa0d..ae20f5e5cf 100644 --- a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs @@ -73,8 +73,11 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("new combo false", () => this.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(TernaryState.False)); AddStep("click right mouse", () => InputManager.Click(MouseButton.Right)); AddAssert("new combo true", () => this.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(TernaryState.True)); + AddAssert("context menu not visible", () => !Editor.ChildrenOfType().Any(c => c.IsPresent)); + AddStep("click right mouse", () => InputManager.Click(MouseButton.Right)); AddAssert("new combo false", () => this.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(TernaryState.False)); + AddAssert("context menu not visible", () => !Editor.ChildrenOfType().Any(c => c.IsPresent)); } [Test] @@ -89,6 +92,8 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("circle removed", () => EditorBeatmap.HitObjects, () => Has.Exactly(0).Items); AddAssert("circle not selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Exactly(0).Items); + AddAssert("context menu not visible", () => !Editor.ChildrenOfType().Any(c => c.IsPresent)); + AddAssert("new combo false", () => this.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(TernaryState.False)); } [Test] @@ -97,13 +102,22 @@ namespace osu.Game.Tests.Visual.Editing AddStep("select circle placement tool", () => InputManager.Key(Key.Number2)); AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); AddStep("place circle", () => InputManager.Click(MouseButton.Left)); - AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items); + + // ensure the circle we're selecting is not a new combo so we can assert + // new combo doesn't happen to get toggled by right click. + AddStep("seek forward", () => EditorClock.Seek(1000)); + AddStep("place second circle", () => InputManager.Click(MouseButton.Left)); + + AddAssert("two circles added", () => EditorBeatmap.HitObjects, () => Has.Exactly(2).Items); + AddAssert("context menu not visible", () => !Editor.ChildrenOfType().Any(c => c.IsPresent)); AddStep("select selection tool", () => InputManager.Key(Key.Number1)); AddStep("click right mouse", () => InputManager.Click(MouseButton.Right)); - AddAssert("circle not removed", () => EditorBeatmap.HitObjects, () => Has.One.Items); + AddAssert("circle not removed", () => EditorBeatmap.HitObjects, () => Has.Exactly(2).Items); AddAssert("circle selected", () => EditorBeatmap.SelectedHitObjects, () => Has.One.Items); + AddAssert("context menu visible", () => Editor.ChildrenOfType().Any(c => c.IsPresent)); + AddAssert("new combo false", () => this.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(TernaryState.False)); } [Test] From db7d1f32d7801099caef27aa92272a2715657d38 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Mar 2025 16:42:49 +0900 Subject: [PATCH 756/801] Fix quick delete still propagating right click mouse input upwards --- .../Edit/Compose/Components/BlueprintContainer.cs | 14 ++++++++------ .../Edit/Compose/Components/SelectionHandler.cs | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index dc04561242..872c4c2465 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -115,19 +115,21 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override bool OnMouseDown(MouseDownEvent e) { - bool selectionPerformed = performMouseDownActions(e); + var selectionBefore = SelectionHandler.SelectedItems.ToArray(); + bool handled = performMouseDownActions(e); + bool selectionChanged = !SelectionHandler.SelectedItems.SequenceEqual(selectionBefore); bool movementPossible = prepareSelectionMovement(e); - // check if selection has occurred - if (selectionPerformed) + if (selectionChanged) { - // only unmodified right click should show context menu + // if the selection changed and there are no modifiers pressed, don't block so the context menu still shows. bool shouldShowContextMenu = e.Button == MouseButton.Right && !e.ShiftPressed && !e.AltPressed && !e.SuperPressed; - - // stop propagation if not showing context menu return !shouldShowContextMenu; } + if (handled) + return true; + // even if a selection didn't occur, a drag event may still move the selection. return e.Button == MouseButton.Left && movementPossible; } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 59b64ad192..758b712fef 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -262,7 +262,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// The blueprint. /// The mouse event responsible for selection. - /// Whether a selection was performed. + /// Whether an action was performed. internal virtual bool MouseDownSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e) { if (ShouldQuickDelete(e)) From 6a7a83415199b17a1371a9c6c791246e075f7221 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Mar 2025 16:52:51 +0900 Subject: [PATCH 757/801] Fix new combo being toggled when selection is made --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index ee386aa366..8917a68efa 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -358,8 +358,12 @@ namespace osu.Game.Rulesets.Osu.Edit { var osuSelectionHandler = (OsuSelectionHandler)BlueprintContainer.SelectionHandler; - osuSelectionHandler.SelectionNewComboState.Value = - osuSelectionHandler.SelectionNewComboState.Value == TernaryState.False ? TernaryState.True : TernaryState.False; + if (!osuSelectionHandler.SelectedItems.Any()) + { + osuSelectionHandler.SelectionNewComboState.Value = + osuSelectionHandler.SelectionNewComboState.Value == TernaryState.False ? TernaryState.True : TernaryState.False; + return true; + } } return base.OnMouseDown(e); From c2de43f6777126e57034a4e02f07408e0aa93d1d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Mar 2025 16:56:16 +0900 Subject: [PATCH 758/801] Add explanation of why the logic is in such a bad place --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 8917a68efa..ed3fc34d94 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -354,6 +354,18 @@ namespace osu.Game.Rulesets.Osu.Edit protected override bool OnMouseDown(MouseDownEvent e) { + // Why is this logic here and not in `OsuSelectionHandler`? + // Because we only want to handle this toggle after all other right-click handling completes. + // + // Consider that input is handled from the most nested child first: + // + // ComposeScreen + // |- OsuContextMenuContainer // right click for context + // |- TimelineBlueprintContainer + // |- TimelineSelectionHandler + // |- (Osu)HitObjectComposer // right click for toggle new combo + // |- (Osu)EditorBlueprintContainer // right click for select + // |- (Osu)EditorSelectionHandler // right click for delete if (e.Button == MouseButton.Right) { var osuSelectionHandler = (OsuSelectionHandler)BlueprintContainer.SelectionHandler; From aeb55ee25da10960e7aa98f60ea7a7d0438299c6 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 12 Mar 2025 17:07:23 +0900 Subject: [PATCH 759/801] Don't use `is` for null-checks --- .../Playlists/PlaylistsRoomSubScreen.cs | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index f8dd9cd3d9..31d5c7ea33 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -526,19 +526,19 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// /// Responds to changes in to validate the user style and update the global gameplay state. /// - private void onSelectedItemChanged(ValueChangedEvent e) + private void onSelectedItemChanged(ValueChangedEvent item) { - if (e.NewValue is not PlaylistItem item) + if (item.NewValue == null) return; // Always resetting the user beatmap style when a new item is selected is most intuitive. UserBeatmap.Value = null; - if (item.Freestyle) + if (item.NewValue.Freestyle) { // If freestyle is active, attempt to preserve the user ruleset style but only if the online item is from the osu! ruleset // (i.e. the beatmap is generally always convertible to the current ruleset, excluding custom rulesets). - if (item.RulesetID > 0) + if (item.NewValue.RulesetID > 0) UserRuleset.Value = null; } else @@ -554,9 +554,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// private Mod[] listAllowedMods() { - if (SelectedItem.Value is not PlaylistItem item) + if (SelectedItem.Value == null) return []; + PlaylistItem item = SelectedItem.Value; + RulesetInfo gameplayRuleset = UserRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; Ruleset rulesetInstance = gameplayRuleset.CreateInstance(); @@ -580,9 +582,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// private void updateGameplayState() { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) + if (!this.IsCurrentScreen() || SelectedItem.Value == null) return; + PlaylistItem item = SelectedItem.Value; + IBeatmapInfo gameplayBeatmap = UserBeatmap.Value ?? item.Beatmap; RulesetInfo gameplayRuleset = UserRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; Ruleset rulesetInstance = gameplayRuleset.CreateInstance(); @@ -638,9 +642,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// private void startPlay() { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) + if (!this.IsCurrentScreen() || SelectedItem.Value == null) return; + PlaylistItem item = SelectedItem.Value; + // Required for validation inside the player. RulesetInfo gameplayRuleset = UserRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; IBeatmapInfo gameplayBeatmap = UserBeatmap.Value ?? item.Beatmap; @@ -661,7 +667,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// private void showUserModSelect() { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) + if (!this.IsCurrentScreen() || SelectedItem.Value == null) return; userModsSelectOverlay.Show(); @@ -672,10 +678,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// private void showUserStyleSelect() { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) + if (!this.IsCurrentScreen() || SelectedItem.Value == null) return; - this.Push(new PlaylistsRoomFreestyleSelect(room, item) + this.Push(new PlaylistsRoomFreestyleSelect(room, SelectedItem.Value) { Beatmap = { BindTarget = UserBeatmap }, Ruleset = { BindTarget = UserRuleset } From 16afd5f1179f02a791949e3e35dafa8773c1e9fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Mar 2025 17:18:30 +0900 Subject: [PATCH 760/801] Use reference check rather than `Guid` comparison --- osu.Game/Beatmaps/WorkingBeatmapCache.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index bd125deddf..30bbbbc1fe 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -89,7 +89,7 @@ namespace osu.Game.Beatmaps public virtual WorkingBeatmap GetWorkingBeatmap([CanBeNull] BeatmapInfo beatmapInfo) { - if (beatmapInfo?.ID == DefaultBeatmap.BeatmapInfo.ID || beatmapInfo?.BeatmapSet == null) + if (beatmapInfo == null || ReferenceEquals(beatmapInfo, DefaultBeatmap.BeatmapInfo)) return DefaultBeatmap; lock (workingCache) From 77aac2922f5135b9a9eac5a163bb79aae1c95391 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Mar 2025 17:31:27 +0900 Subject: [PATCH 761/801] Fix `LetterboxOverlay` not handling its own visibility --- .../Visual/Gameplay/TestSceneBreakTracker.cs | 7 +- .../Gameplay/TestSceneLetterboxOverlay.cs | 24 ------ .../Screens/Play/Break/LetterboxOverlay.cs | 42 ---------- osu.Game/Screens/Play/BreakOverlay.cs | 4 +- osu.Game/Screens/Play/LetterboxOverlay.cs | 82 +++++++++++++++++++ osu.Game/Screens/Play/Player.cs | 6 +- 6 files changed, 93 insertions(+), 72 deletions(-) delete mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneLetterboxOverlay.cs delete mode 100644 osu.Game/Screens/Play/Break/LetterboxOverlay.cs create mode 100644 osu.Game/Screens/Play/LetterboxOverlay.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs index 9fc1ce3027..844f5cba01 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs @@ -44,7 +44,12 @@ namespace osu.Game.Tests.Visual.Gameplay { ProcessCustomClock = false, BreakTracker = breakTracker, - } + }, + new LetterboxOverlay + { + ProcessCustomClock = false, + BreakTracker = breakTracker, + }, }; } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLetterboxOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLetterboxOverlay.cs deleted file mode 100644 index ce93837925..0000000000 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneLetterboxOverlay.cs +++ /dev/null @@ -1,24 +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.Graphics; -using osu.Framework.Graphics.Shapes; -using osu.Game.Screens.Play.Break; - -namespace osu.Game.Tests.Visual.Gameplay -{ - public partial class TestSceneLetterboxOverlay : OsuTestScene - { - public TestSceneLetterboxOverlay() - { - AddRange(new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both - }, - new LetterboxOverlay() - }); - } - } -} diff --git a/osu.Game/Screens/Play/Break/LetterboxOverlay.cs b/osu.Game/Screens/Play/Break/LetterboxOverlay.cs deleted file mode 100644 index 9308a02b07..0000000000 --- a/osu.Game/Screens/Play/Break/LetterboxOverlay.cs +++ /dev/null @@ -1,42 +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.Graphics; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osuTK.Graphics; - -namespace osu.Game.Screens.Play.Break -{ - public partial class LetterboxOverlay : CompositeDrawable - { - private static readonly Color4 transparent_black = new Color4(0, 0, 0, 0); - - public LetterboxOverlay() - { - const int height = 150; - - RelativeSizeAxes = Axes.Both; - InternalChildren = new Drawable[] - { - new Box - { - Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft, - RelativeSizeAxes = Axes.X, - Height = height, - Colour = ColourInfo.GradientVertical(Color4.Black, transparent_black), - }, - new Box - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - Height = height, - Colour = ColourInfo.GradientVertical(transparent_black, Color4.Black), - } - }; - } - } -} diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index 49b7067c8d..2ae66a6dc4 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -35,7 +35,7 @@ namespace osu.Game.Screens.Play public override bool RemoveCompletedTransforms => false; - public BreakTracker BreakTracker { get; init; } = null!; + public required BreakTracker BreakTracker { get; init; } private readonly Container remainingTimeAdjustmentBox; private readonly Container remainingTimeBox; @@ -159,7 +159,7 @@ namespace osu.Game.Screens.Play if (currentPeriod.Value == null) return; - float timeBoxTargetWidth = (float)Math.Max(0, (remainingTimeForCurrentPeriod - timingPoint.BeatLength / currentPeriod.Value.Value.Duration)); + float timeBoxTargetWidth = (float)Math.Max(0, remainingTimeForCurrentPeriod - timingPoint.BeatLength / currentPeriod.Value.Value.Duration); remainingTimeBox.ResizeWidthTo(timeBoxTargetWidth, timingPoint.BeatLength * 3.5, Easing.OutQuint); } diff --git a/osu.Game/Screens/Play/LetterboxOverlay.cs b/osu.Game/Screens/Play/LetterboxOverlay.cs new file mode 100644 index 0000000000..168c707c3b --- /dev/null +++ b/osu.Game/Screens/Play/LetterboxOverlay.cs @@ -0,0 +1,82 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Utils; +using osuTK.Graphics; + +namespace osu.Game.Screens.Play +{ + public partial class LetterboxOverlay : CompositeDrawable + { + public required BreakTracker BreakTracker { get; init; } + + private readonly Container fadeContainer; + + private readonly IBindable currentPeriod = new Bindable(); + + private static readonly Color4 transparent_black = new Color4(0, 0, 0, 0); + + public LetterboxOverlay() + { + const int letterbox_height = 150; + + RelativeSizeAxes = Axes.Both; + + InternalChild = fadeContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + Children = new Drawable[] + { + new Box + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + RelativeSizeAxes = Axes.X, + Height = letterbox_height, + Colour = ColourInfo.GradientVertical(Color4.Black, transparent_black), + }, + new Box + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = letterbox_height, + Colour = ColourInfo.GradientVertical(transparent_black, Color4.Black), + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + currentPeriod.BindTo(BreakTracker.CurrentPeriod); + currentPeriod.BindValueChanged(updateDisplay, true); + } + + private void updateDisplay(ValueChangedEvent period) + { + FinishTransforms(true); + Scheduler.CancelDelayedTasks(); + + if (period.NewValue == null) + return; + + var b = period.NewValue.Value; + + using (BeginAbsoluteSequence(b.Start)) + { + fadeContainer.FadeIn(BreakOverlay.BREAK_FADE_DURATION); + using (BeginDelayedSequence(b.Duration - BreakOverlay.BREAK_FADE_DURATION)) + fadeContainer.FadeOut(BreakOverlay.BREAK_FADE_DURATION); + } + } + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 29b54c8699..b27e0b7477 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -34,7 +34,6 @@ using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; -using osu.Game.Screens.Play.Break; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Ranking; using osu.Game.Skinning; @@ -453,9 +452,10 @@ namespace osu.Game.Screens.Play DimmableStoryboard.OverlayLayerContainer.CreateProxy(), new LetterboxOverlay { + Clock = DrawableRuleset.FrameStableClock, + ProcessCustomClock = false, + BreakTracker = breakTracker, Alpha = working.Beatmap.LetterboxInBreaks ? 1 : 0, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, }, HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods, Configuration.AlwaysShowLeaderboard) { From 8ef5a01bc15e5ec88c43020cdd5cf78a32e699bf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Mar 2025 17:46:05 +0900 Subject: [PATCH 762/801] Adjust visuals to match stable Was never a huge fan of the gradient we had. --- osu.Game/Screens/Play/LetterboxOverlay.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Play/LetterboxOverlay.cs b/osu.Game/Screens/Play/LetterboxOverlay.cs index 168c707c3b..4c934f56cd 100644 --- a/osu.Game/Screens/Play/LetterboxOverlay.cs +++ b/osu.Game/Screens/Play/LetterboxOverlay.cs @@ -3,7 +3,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Utils; @@ -23,9 +22,8 @@ namespace osu.Game.Screens.Play public LetterboxOverlay() { - const int letterbox_height = 150; - RelativeSizeAxes = Axes.Both; + const float letterbox_height = 0.125f; InternalChild = fadeContainer = new Container { @@ -37,17 +35,17 @@ namespace osu.Game.Screens.Play { Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, - RelativeSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Both, Height = letterbox_height, - Colour = ColourInfo.GradientVertical(Color4.Black, transparent_black), + Colour = Color4.Black, }, new Box { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Both, Height = letterbox_height, - Colour = ColourInfo.GradientVertical(transparent_black, Color4.Black), + Colour = Color4.Black, } } }; From 5b911ad8d0d8a72d58d01dedf052b9378c0f6271 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Mar 2025 18:25:56 +0900 Subject: [PATCH 763/801] Fix context menu not working when selection hasn't changed --- .../Screens/Edit/Compose/Components/BlueprintContainer.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 872c4c2465..b49dee279e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -115,14 +115,12 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override bool OnMouseDown(MouseDownEvent e) { - var selectionBefore = SelectionHandler.SelectedItems.ToArray(); bool handled = performMouseDownActions(e); - bool selectionChanged = !SelectionHandler.SelectedItems.SequenceEqual(selectionBefore); bool movementPossible = prepareSelectionMovement(e); - if (selectionChanged) + if (SelectedItems.Any()) { - // if the selection changed and there are no modifiers pressed, don't block so the context menu still shows. + // if there is a selection and there are no modifiers pressed, don't block so the context menu still shows. bool shouldShowContextMenu = e.Button == MouseButton.Right && !e.ShiftPressed && !e.AltPressed && !e.SuperPressed; return !shouldShowContextMenu; } From 70693c8bb80a3830512f2e4a964155d01fd4a398 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Mar 2025 18:33:20 +0900 Subject: [PATCH 764/801] Fix weird implementation of layout validation --- osu.Game.Rulesets.Mania/UI/ColumnFlow.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs index 3b19e90b60..cee43b300a 100644 --- a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs +++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Mania.UI set => base.Masking = value; } - private readonly LayoutValue columnSizeLayout = new LayoutValue(Invalidation.DrawSize); + private readonly LayoutValue layout = new LayoutValue(Invalidation.DrawSize); public ColumnFlow(StageDefinition stageDefinition) { @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Mania.UI for (int i = 0; i < stageDefinition.Columns; i++) columns.Add(new Container { RelativeSizeAxes = Axes.Y }); - AddLayout(columnSizeLayout); + AddLayout(layout); } [Resolved] @@ -73,18 +73,18 @@ namespace osu.Game.Rulesets.Mania.UI { rulesetConfig?.BindWith(ManiaRulesetSetting.MobileLayout, mobileLayout); - mobileLayout.BindValueChanged(_ => updateColumnSize()); - skin.SourceChanged += updateColumnSize; + mobileLayout.BindValueChanged(_ => invalidateLayout()); + skin.SourceChanged += invalidateLayout; } protected override void Update() { base.Update(); - if (!columnSizeLayout.IsValid) + if (!layout.IsValid) { updateColumnSize(); - columnSizeLayout.Validate(); + layout.Validate(); } } @@ -98,6 +98,8 @@ namespace osu.Game.Rulesets.Mania.UI Content[column] = columns[column].Child = content; } + private void invalidateLayout() => layout.Invalidate(); + private void updateColumnSize() { float mobileAdjust = 1f; @@ -149,7 +151,7 @@ namespace osu.Game.Rulesets.Mania.UI base.Dispose(isDisposing); if (skin.IsNotNull()) - skin.SourceChanged -= updateColumnSize; + skin.SourceChanged -= invalidateLayout; } } } From 92d374a5bb61aa8d0f2a4f3248c12b4439c15d53 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Mar 2025 18:36:02 +0900 Subject: [PATCH 765/801] Remove unused thing --- osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs index 1df05bf350..2a2faf0cf7 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Configuration; -using osu.Game.Rulesets.Mania.Configuration; using osuTK; namespace osu.Game.Rulesets.Mania.UI @@ -41,9 +40,6 @@ namespace osu.Game.Rulesets.Mania.UI MaxValue = 1 }; - [Resolved] - private ManiaRulesetConfigManager rulesetConfig { get; set; } = null!; - private GridContainer gridContainer = null!; public ManiaTouchInputArea(DrawableManiaRuleset drawableRuleset) From 9c0f3f9bef31fb3e541eb37c4b0fc0e290bf55c3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 12 Mar 2025 19:34:34 +0900 Subject: [PATCH 766/801] Describe special case of user mod validation --- .../Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 31d5c7ea33..ae31e55da5 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -458,12 +458,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateGameplayState()); UserBeatmap.BindValueChanged(_ => updateGameplayState()); + UserMods.BindValueChanged(_ => updateGameplayState()); UserRuleset.BindValueChanged(_ => { + // The user mod selection overlay is separate from the beatmap/ruleset style selection screen, + // and so the validity of mods has to be confirmed separately after the ruleset is changed. validateUserMods(); updateGameplayState(); }); - UserMods.BindValueChanged(_ => updateGameplayState()); updateSetupState(); updateGameplayState(); @@ -545,7 +547,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists UserRuleset.Value = null; validateUserMods(); - updateGameplayState(); } From 0ffb23379629176aac71a2ae7e9e7ea59a4e815f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Mar 2025 11:29:31 +0100 Subject: [PATCH 767/801] Rewrite `OverallRanking` to interface directly with `UserStatisticsWatcher` --- .../Visual/Ranking/TestSceneOverallRanking.cs | 18 ++++++--- .../Ranking/TestSceneStatisticsPanel.cs | 38 ------------------- .../Ranking/Statistics/User/OverallRanking.cs | 38 ++++++++++++++----- .../Ranking/Statistics/UserStatisticsPanel.cs | 25 +----------- 4 files changed, 42 insertions(+), 77 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs b/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs index b406ea369f..f96d272e40 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Online; using osu.Game.Scoring; @@ -12,7 +13,7 @@ namespace osu.Game.Tests.Visual.Ranking { public partial class TestSceneOverallRanking : OsuTestScene { - private OverallRanking overallRanking = null!; + private readonly Bindable statisticsUpdate = new Bindable(); [Test] public void TestUpdatePending() @@ -104,14 +105,19 @@ namespace osu.Game.Tests.Visual.Ranking displayUpdate(statistics, statistics); } - private void createDisplay() => AddStep("create display", () => Child = overallRanking = new OverallRanking + private void createDisplay() => AddStep("create display", () => { - Width = 400, - Anchor = Anchor.Centre, - Origin = Anchor.Centre + statisticsUpdate.Value = null; + Child = new OverallRanking(new ScoreInfo()) + { + Width = 400, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + DisplayedUpdate = { BindTarget = statisticsUpdate } + }; }); private void displayUpdate(UserStatistics before, UserStatistics after) => - AddStep("display update", () => overallRanking.StatisticsUpdate.Value = new ScoreBasedUserStatisticsUpdate(new ScoreInfo(), before, after)); + AddStep("display update", () => statisticsUpdate.Value = new ScoreBasedUserStatisticsUpdate(new ScoreInfo(), before, after)); } } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index c12b9d29bc..c075e75c87 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -155,44 +155,6 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both, State = { Value = Visibility.Visible }, Score = { Value = score }, - DisplayedUserStatisticsUpdate = - { - Value = new ScoreBasedUserStatisticsUpdate(score, new UserStatistics - { - Level = new UserStatistics.LevelInfo - { - Current = 5, - Progress = 20, - }, - GlobalRank = 38000, - CountryRank = 12006, - PP = 2134, - RankedScore = 21123849, - Accuracy = 0.985, - PlayCount = 13375, - PlayTime = 354490, - TotalScore = 128749597, - TotalHits = 0, - MaxCombo = 1233, - }, new UserStatistics - { - Level = new UserStatistics.LevelInfo - { - Current = 5, - Progress = 30, - }, - GlobalRank = 36000, - CountryRank = 12000, - PP = (decimal)2134.5, - RankedScore = 23897015, - Accuracy = 0.984, - PlayCount = 13376, - PlayTime = 35789, - TotalScore = 132218497, - TotalHits = 0, - MaxCombo = 1233, - }) - } }; }); diff --git a/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs b/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs index 9f5afea6f0..9d0a511f5a 100644 --- a/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs +++ b/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs @@ -5,8 +5,10 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Extensions; using osu.Game.Graphics.UserInterface; using osu.Game.Online; +using osu.Game.Scoring; namespace osu.Game.Screens.Ranking.Statistics.User { @@ -14,13 +16,21 @@ namespace osu.Game.Screens.Ranking.Statistics.User { private const float transition_duration = 300; - public Bindable StatisticsUpdate { get; } = new Bindable(); + public Bindable DisplayedUpdate { get; } = new Bindable(); + private readonly IBindable latestGlobalStatisticsUpdate = new Bindable(); + + private readonly ScoreInfo scoreInfo; private LoadingLayer loadingLayer = null!; private GridContainer content = null!; + public OverallRanking(ScoreInfo scoreInfo) + { + this.scoreInfo = scoreInfo; + } + [BackgroundDependencyLoader] - private void load() + private void load(UserStatisticsWatcher? userStatisticsWatcher) { AutoSizeAxes = Axes.Y; AutoSizeEasing = Easing.OutQuint; @@ -55,34 +65,44 @@ namespace osu.Game.Screens.Ranking.Statistics.User { new Drawable[] { - new GlobalRankChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + new GlobalRankChangeRow { StatisticsUpdate = { BindTarget = DisplayedUpdate } }, new SimpleStatisticTable.Spacer(), - new PerformancePointsChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + new PerformancePointsChangeRow { StatisticsUpdate = { BindTarget = DisplayedUpdate } }, }, [], new Drawable[] { - new MaximumComboChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + new MaximumComboChangeRow { StatisticsUpdate = { BindTarget = DisplayedUpdate } }, new SimpleStatisticTable.Spacer(), - new AccuracyChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + new AccuracyChangeRow { StatisticsUpdate = { BindTarget = DisplayedUpdate } }, }, [], new Drawable[] { - new RankedScoreChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + new RankedScoreChangeRow { StatisticsUpdate = { BindTarget = DisplayedUpdate } }, new SimpleStatisticTable.Spacer(), - new TotalScoreChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + new TotalScoreChangeRow { StatisticsUpdate = { BindTarget = DisplayedUpdate } }, } } } }; + + if (userStatisticsWatcher != null) + { + latestGlobalStatisticsUpdate.BindTo(userStatisticsWatcher.LatestUpdate); + latestGlobalStatisticsUpdate.BindValueChanged(update => + { + if (update.NewValue?.Score.MatchesOnlineID(scoreInfo) == true) + DisplayedUpdate.Value = update.NewValue; + }, true); + } } protected override void LoadComplete() { base.LoadComplete(); - StatisticsUpdate.BindValueChanged(onUpdateReceived, true); + DisplayedUpdate.BindValueChanged(onUpdateReceived, true); FinishTransforms(true); } diff --git a/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs index 86fed4a9bb..de31c234c4 100644 --- a/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs @@ -3,12 +3,8 @@ using System.Collections.Generic; using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps; -using osu.Game.Extensions; -using osu.Game.Online; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics.User; @@ -18,29 +14,11 @@ namespace osu.Game.Screens.Ranking.Statistics { private readonly ScoreInfo achievedScore; - internal readonly Bindable DisplayedUserStatisticsUpdate = new Bindable(); - - private IBindable latestGlobalStatisticsUpdate = null!; - public UserStatisticsPanel(ScoreInfo achievedScore) { this.achievedScore = achievedScore; } - [BackgroundDependencyLoader] - private void load(UserStatisticsWatcher? userStatisticsWatcher) - { - if (userStatisticsWatcher != null) - { - latestGlobalStatisticsUpdate = userStatisticsWatcher.LatestUpdate.GetBoundCopy(); - latestGlobalStatisticsUpdate.BindValueChanged(update => - { - if (update.NewValue?.Score.MatchesOnlineID(achievedScore) == true) - DisplayedUserStatisticsUpdate.Value = update.NewValue; - }, true); - } - } - protected override ICollection CreateStatisticItems(ScoreInfo newScore, IBeatmap playableBeatmap) { var items = base.CreateStatisticItems(newScore, playableBeatmap); @@ -50,12 +28,11 @@ namespace osu.Game.Screens.Ranking.Statistics && newScore.OnlineID > 0 && newScore.OnlineID == achievedScore.OnlineID) { - items = items.Append(new StatisticItem("Overall Ranking", () => new OverallRanking + items = items.Append(new StatisticItem("Overall Ranking", () => new OverallRanking(newScore) { RelativeSizeAxes = Axes.X, Anchor = Anchor.Centre, Origin = Anchor.Centre, - StatisticsUpdate = { BindTarget = DisplayedUserStatisticsUpdate } })).ToArray(); } From 5f0451eb791c83af12b5896a05f8b3d5c93f14ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Mar 2025 11:39:07 +0100 Subject: [PATCH 768/801] Remove inheritance in `StatisticsPanel` --- .../Ranking/TestSceneStatisticsPanel.cs | 8 ++-- osu.Game/Screens/Ranking/ResultsScreen.cs | 15 ++----- .../Ranking/Statistics/StatisticsPanel.cs | 31 ++++++++++++-- .../Ranking/Statistics/UserStatisticsPanel.cs | 42 ------------------- 4 files changed, 37 insertions(+), 59 deletions(-) delete mode 100644 osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index c075e75c87..b117e90260 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -137,11 +137,12 @@ namespace osu.Game.Tests.Visual.Ranking { CachedDependencies = [(typeof(UserStatisticsWatcher), userStatisticsWatcher)], RelativeSizeAxes = Axes.Both, - Child = new UserStatisticsPanel(score) + Child = new StatisticsPanel { RelativeSizeAxes = Axes.Both, State = { Value = Visibility.Visible }, - Score = { Value = score, } + Score = { Value = score, }, + AchievedScore = score, } }); AddUntilStep("overall ranking present", () => this.ChildrenOfType().Any()); @@ -150,11 +151,12 @@ namespace osu.Game.Tests.Visual.Ranking private void loadPanel(ScoreInfo score) => AddStep("load panel", () => { - Child = new UserStatisticsPanel(score) + Child = new StatisticsPanel { RelativeSizeAxes = Axes.Both, State = { Value = Visibility.Visible }, Score = { Value = score }, + AchievedScore = score, }; }); diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 24b40968d6..6f9bbd0cfb 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -121,7 +121,10 @@ namespace osu.Game.Screens.Ranking Children = new Drawable[] { new GlobalScrollAdjustsVolume(), - StatisticsPanel = createStatisticsPanel().With(panel => + StatisticsPanel = new StatisticsPanel + { + AchievedScore = ShowUserStatistics && Score != null ? Score : null + }.With(panel => { panel.RelativeSizeAxes = Axes.Both; panel.Score.BindTarget = SelectedScore; @@ -353,16 +356,6 @@ namespace osu.Game.Screens.Ranking /// The fetch direction. -1 to fetch scores greater than the current start of the list, and 1 to fetch scores lower than the current end of the list. protected virtual Task FetchNextPage(int direction) => Task.FromResult([]); - /// - /// Creates the to be used to display extended information about scores. - /// - private StatisticsPanel createStatisticsPanel() - { - return ShowUserStatistics && Score != null - ? new UserStatisticsPanel(Score) - : new StatisticsPanel(); - } - private Task addScores(ScoreInfo[] scores) { var tcs = new TaskCompletionSource(); diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index f9f5254bc2..639600391f 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -18,6 +18,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Placeholders; using osu.Game.Scoring; +using osu.Game.Screens.Ranking.Statistics.User; using osuTK; namespace osu.Game.Screens.Ranking.Statistics @@ -28,6 +29,13 @@ namespace osu.Game.Screens.Ranking.Statistics public readonly Bindable Score = new Bindable(); + /// + /// The score which was achieved by the local user. + /// If this is set to a non-null score, an component will be displayed showing changes to the local user's ranking & statistics + /// when a statistics update related to this score is received from spectator server. + /// + public ScoreInfo? AchievedScore { get; init; } + protected override bool StartHidden => true; [Resolved] @@ -97,7 +105,7 @@ namespace osu.Game.Screens.Ranking.Statistics bool hitEventsAvailable = newScore.HitEvents.Count != 0; Container container; - var statisticItems = CreateStatisticItems(newScore, task.GetResultSafely()); + var statisticItems = CreateStatisticItems(newScore, task.GetResultSafely()).ToArray(); if (!hitEventsAvailable && statisticItems.All(c => c.RequiresHitEvents)) { @@ -199,8 +207,25 @@ namespace osu.Game.Screens.Ranking.Statistics /// /// The score to create the rows for. /// The beatmap on which the score was set. - protected virtual ICollection CreateStatisticItems(ScoreInfo newScore, IBeatmap playableBeatmap) - => newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap); + protected virtual IEnumerable CreateStatisticItems(ScoreInfo newScore, IBeatmap playableBeatmap) + { + foreach (var statistic in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap)) + yield return statistic; + + if (AchievedScore != null + && newScore.UserID > 1 + && newScore.UserID == AchievedScore.UserID + && newScore.OnlineID > 0 + && newScore.OnlineID == AchievedScore.OnlineID) + { + yield return new StatisticItem("Overall Ranking", () => new OverallRanking(newScore) + { + RelativeSizeAxes = Axes.X, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + } + } protected override bool OnClick(ClickEvent e) { diff --git a/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs deleted file mode 100644 index de31c234c4..0000000000 --- a/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs +++ /dev/null @@ -1,42 +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 osu.Framework.Graphics; -using osu.Game.Beatmaps; -using osu.Game.Scoring; -using osu.Game.Screens.Ranking.Statistics.User; - -namespace osu.Game.Screens.Ranking.Statistics -{ - public partial class UserStatisticsPanel : StatisticsPanel - { - private readonly ScoreInfo achievedScore; - - public UserStatisticsPanel(ScoreInfo achievedScore) - { - this.achievedScore = achievedScore; - } - - protected override ICollection CreateStatisticItems(ScoreInfo newScore, IBeatmap playableBeatmap) - { - var items = base.CreateStatisticItems(newScore, playableBeatmap); - - if (newScore.UserID > 1 - && newScore.UserID == achievedScore.UserID - && newScore.OnlineID > 0 - && newScore.OnlineID == achievedScore.OnlineID) - { - items = items.Append(new StatisticItem("Overall Ranking", () => new OverallRanking(newScore) - { - RelativeSizeAxes = Axes.X, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - })).ToArray(); - } - - return items; - } - } -} From d1ce64b3f62113e8f5597c81b10e48cbb7a9d9c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Mar 2025 11:43:23 +0100 Subject: [PATCH 769/801] Add user tag control to results screen's statistics panel --- .../Visual/Ranking/TestSceneUserTagControl.cs | 2 +- osu.Game/Screens/Ranking/ResultsScreen.cs | 13 ++++++------ .../Ranking/Statistics/StatisticsPanel.cs | 21 +++++++++++++++++++ osu.Game/Screens/Ranking/UserTagControl.cs | 14 ++++++++----- 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs b/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs index ebfd553815..d622df8d76 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs @@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.Ranking Child = new PopoverContainer { RelativeSizeAxes = Axes.Both, - Child = new UserTagControl + Child = new UserTagControl(Beatmap.Value.BeatmapInfo) { Width = 500, Anchor = Anchor.Centre, diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 6f9bbd0cfb..fcf90a3e28 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -85,6 +85,8 @@ namespace osu.Game.Screens.Ranking /// public bool ShowUserStatistics { get; init; } + public bool ShowUserTagControl { get; init; } + private Sample? popInSample; protected ResultsScreen(ScoreInfo? score) @@ -123,12 +125,11 @@ namespace osu.Game.Screens.Ranking new GlobalScrollAdjustsVolume(), StatisticsPanel = new StatisticsPanel { - AchievedScore = ShowUserStatistics && Score != null ? Score : null - }.With(panel => - { - panel.RelativeSizeAxes = Axes.Both; - panel.Score.BindTarget = SelectedScore; - }), + RelativeSizeAxes = Axes.Both, + Score = { BindTarget = SelectedScore }, + AchievedScore = ShowUserStatistics && Score != null ? Score : null, + ShowUserTagControl = ShowUserTagControl, + }, ScorePanelList = new ScorePanelList { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 639600391f..b974b2f515 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -16,6 +16,7 @@ using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Online.Placeholders; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics.User; @@ -36,11 +37,19 @@ namespace osu.Game.Screens.Ranking.Statistics /// public ScoreInfo? AchievedScore { get; init; } + /// + /// Whether to show a control that allows to assign user tags to the played beatmap. + /// + public bool ShowUserTagControl { get; init; } + protected override bool StartHidden => true; [Resolved] private BeatmapManager beatmapManager { get; set; } = null!; + [Resolved] + private IAPIProvider api { get; set; } = null!; + private readonly Container content; private readonly LoadingSpinner spinner; @@ -225,6 +234,18 @@ namespace osu.Game.Screens.Ranking.Statistics Origin = Anchor.Centre, }); } + + if (ShowUserTagControl + && newScore.BeatmapInfo!.OnlineID > 0 + && api.IsLoggedIn) + { + yield return new StatisticItem("Tag the beatmap!", () => new UserTagControl(newScore.BeatmapInfo) + { + RelativeSizeAxes = Axes.X, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + } } protected override bool OnClick(ClickEvent e) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index 7600d0aaae..3817f662eb 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -37,6 +37,8 @@ namespace osu.Game.Screens.Ranking { public partial class UserTagControl : CompositeDrawable { + private readonly BeatmapInfo beatmapInfo; + public override bool HandlePositionalInput => true; private readonly Cached layout = new Cached(); @@ -53,8 +55,10 @@ namespace osu.Game.Screens.Ranking [Resolved] private IAPIProvider api { get; set; } = null!; - [Resolved] - private Bindable beatmap { get; set; } = null!; + public UserTagControl(BeatmapInfo beatmapInfo) + { + this.beatmapInfo = beatmapInfo; + } [BackgroundDependencyLoader] private void load(SessionStatics sessionStatics) @@ -104,8 +108,8 @@ namespace osu.Game.Screens.Ranking api.Queue(listTagsRequest); } - var getBeatmapSetRequest = new GetBeatmapSetRequest(beatmap.Value.BeatmapInfo.BeatmapSet!.OnlineID); - getBeatmapSetRequest.Success += set => apiBeatmap.Value = set.Beatmaps.SingleOrDefault(b => b.MatchesOnlineID(beatmap.Value.BeatmapInfo)); + var getBeatmapSetRequest = new GetBeatmapSetRequest(beatmapInfo.BeatmapSet!.OnlineID); + getBeatmapSetRequest.Success += set => apiBeatmap.Value = set.Beatmaps.SingleOrDefault(b => b.MatchesOnlineID(beatmapInfo)); api.Queue(getBeatmapSetRequest); } @@ -114,7 +118,7 @@ namespace osu.Game.Screens.Ranking loadingLayer.Show(); extraTags.Remove(tag); - var req = new AddBeatmapTagRequest(beatmap.Value.BeatmapInfo.OnlineID, tag.Id); + var req = new AddBeatmapTagRequest(beatmapInfo.OnlineID, tag.Id); req.Success += () => { tag.Voted.Value = true; From e70ad146472e3bfeeaa150b03503a2fac36c9554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Mar 2025 11:49:31 +0100 Subject: [PATCH 770/801] Show user tag control only following local user gameplay --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 5 +++++ .../Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs | 4 +++- osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs | 1 + osu.Game/Screens/Play/Player.cs | 6 +----- osu.Game/Screens/Play/SubmittingPlayer.cs | 8 ++++++++ 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 4377cc6219..60cb3ba07c 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -13,7 +14,9 @@ using osu.Game.Overlays; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Scoring; using osu.Game.Screens.Play; +using osu.Game.Screens.Ranking; using osu.Game.Users; namespace osu.Game.Screens.Edit.GameplayTest @@ -228,5 +231,7 @@ namespace osu.Game.Screens.Edit.GameplayTest editor.RestoreState(editorState); return base.OnExiting(e); } + + protected override ResultsScreen CreateResults(ScoreInfo score) => throw new NotSupportedException(); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 111b453adb..67a67cf271 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -199,10 +199,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer ? new MultiplayerTeamResultsScreen(score, Room.RoomID.Value, PlaylistItem, multiplayerLeaderboard.TeamScores) { ShowUserStatistics = true, + ShowUserTagControl = true, } : new MultiplayerResultsScreen(score, Room.RoomID.Value, PlaylistItem) { - ShowUserStatistics = true + ShowUserStatistics = true, + ShowUserTagControl = true, }; } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index b82c2404ab..80b378bdcf 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -60,6 +60,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { AllowRetry = true, ShowUserStatistics = true, + ShowUserTagControl = true, }; } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b27e0b7477..a738a40993 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1276,11 +1276,7 @@ namespace osu.Game.Screens.Play /// /// The to be displayed in the results screen. /// The . - protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score) - { - AllowRetry = true, - ShowUserStatistics = true, - }; + protected abstract ResultsScreen CreateResults(ScoreInfo score); private void fadeOut() { diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index b667963a70..04cf473173 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -21,6 +21,7 @@ using osu.Game.Online.Rooms; using osu.Game.Online.Spectator; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Screens.Ranking; namespace osu.Game.Screens.Play { @@ -323,5 +324,12 @@ namespace osu.Game.Screens.Play api.Queue(request); return scoreSubmissionSource.Task; } + + protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score) + { + AllowRetry = true, + ShowUserStatistics = true, + ShowUserTagControl = true, + }; } } From 266682d5854d31b0f176d62205477cd13195ef45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Mar 2025 12:10:48 +0100 Subject: [PATCH 771/801] Adjust colour of user tag popover control For some reason in actual gameplay there seems to be an `OverlayColourProvider` cached that's nowhere to be seen in tests, and without this change things look bad. Dunno, not looking for it. --- osu.Game/Screens/Ranking/UserTagControl.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index 3817f662eb..3ae6501b36 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -30,6 +30,7 @@ using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; using osuTK; using osuTK.Input; @@ -534,14 +535,14 @@ namespace osu.Game.Screens.Ranking } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, OverlayColourProvider? colourProvider) { Content.AddRange(new Drawable[] { new Box { RelativeSizeAxes = Axes.Both, - Colour = colours.GreySeaFoamDark, + Colour = colourProvider?.Background3 ?? colours.GreySeaFoamDark, Depth = float.MaxValue, }, new FillFlowContainer From 6f74d8ad507a182a5e91863c556c93a88897555e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Mar 2025 12:16:54 +0100 Subject: [PATCH 772/801] Add visual test coverage of user tag control on results --- .../Ranking/TestSceneStatisticsPanel.cs | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index b117e90260..a64fd488bc 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -11,6 +11,7 @@ using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; using osu.Game.Beatmaps; @@ -18,6 +19,9 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; @@ -36,6 +40,8 @@ namespace osu.Game.Tests.Visual.Ranking { public partial class TestSceneStatisticsPanel : OsuTestScene { + private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; + [Test] public void TestScoreWithPositionStatistics() { @@ -149,6 +155,71 @@ namespace osu.Game.Tests.Visual.Ranking AddUntilStep("loading spinner not visible", () => this.ChildrenOfType().All(l => l.State.Value == Visibility.Hidden)); } + [Test] + public void TestTagging() + { + var score = TestResources.CreateTestScoreInfo(); + + AddStep("set up network requests", () => + { + dummyAPI.HandleRequest = request => + { + switch (request) + { + case ListTagsRequest listTagsRequest: + { + Scheduler.AddDelayed(() => listTagsRequest.TriggerSuccess(new APITagCollection + { + Tags = + [ + new APITag { Id = 1, Name = "tech", Description = "Tests uncommon skills.", }, + new APITag { Id = 2, Name = "alt", Description = "Colloquial term for maps which use rhythms that encourage the player to alternate notes. Typically distinct from burst or stream maps.", }, + new APITag { Id = 3, Name = "aim", Description = "Category for difficulty relating to cursor movement.", }, + new APITag { Id = 4, Name = "tap", Description = "Category for difficulty relating to tapping input.", }, + ] + }), 500); + return true; + } + + case GetBeatmapSetRequest getBeatmapSetRequest: + { + var beatmapSet = CreateAPIBeatmapSet(score.BeatmapInfo); + beatmapSet.Beatmaps.Single().TopTags = + [ + new APIBeatmapTag { TagId = 3, VoteCount = 9 }, + ]; + Scheduler.AddDelayed(() => getBeatmapSetRequest.TriggerSuccess(beatmapSet), 500); + return true; + } + + case AddBeatmapTagRequest: + case RemoveBeatmapTagRequest: + { + Scheduler.AddDelayed(request.TriggerSuccess, 500); + return true; + } + } + + return false; + }; + }); + AddStep("load panel", () => + { + Child = new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Child = new StatisticsPanel + { + RelativeSizeAxes = Axes.Both, + State = { Value = Visibility.Visible }, + Score = { Value = score }, + AchievedScore = score, + ShowUserTagControl = true, + } + }; + }); + } + private void loadPanel(ScoreInfo score) => AddStep("load panel", () => { Child = new StatisticsPanel From 73fba15adf1cf2a5d8be1d48fb0cff6de322d1d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Mar 2025 12:38:55 +0100 Subject: [PATCH 773/801] Fix xmldoc --- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index b974b2f515..16de00fcf1 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// /// The score which was achieved by the local user. - /// If this is set to a non-null score, an component will be displayed showing changes to the local user's ranking & statistics + /// If this is set to a non-null score, an component will be displayed showing changes to the local user's ranking and statistics /// when a statistics update related to this score is received from spectator server. /// public ScoreInfo? AchievedScore { get; init; } From 17f964dc6bcf31edae1dd40cb6dc2e8401a47d0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Mar 2025 13:21:04 +0100 Subject: [PATCH 774/801] Fix `OsuTextFlowContainer.AddArbitraryDrawable()` not aligning the drawable correctly Closes https://github.com/ppy/osu/issues/32348. --- osu.Game/Graphics/Containers/OsuTextFlowContainer.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/OsuTextFlowContainer.cs b/osu.Game/Graphics/Containers/OsuTextFlowContainer.cs index d3bbc2e80b..d5cce1a10a 100644 --- a/osu.Game/Graphics/Containers/OsuTextFlowContainer.cs +++ b/osu.Game/Graphics/Containers/OsuTextFlowContainer.cs @@ -21,8 +21,18 @@ namespace osu.Game.Graphics.Containers protected override SpriteText CreateSpriteText() => new OsuSpriteText(); - public ITextPart AddArbitraryDrawable(Drawable drawable) => AddPart(new TextPartManual(drawable.Yield())); + public ITextPart AddArbitraryDrawable(Drawable drawable) => AddPart(new TextPartManual(new ArbitraryDrawableWrapper { Child = drawable }.Yield())); public ITextPart AddIcon(IconUsage icon, Action creationParameters = null) => AddText(icon.Icon.ToString(), creationParameters); + + private partial class ArbitraryDrawableWrapper : Container, IHasLineBaseHeight + { + public float LineBaseHeight => DrawHeight; + + public ArbitraryDrawableWrapper() + { + AutoSizeAxes = Axes.Both; + } + } } } From 0906983f6fda3c43d23e17542d72e9d3532ca614 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Mar 2025 00:45:57 +0900 Subject: [PATCH 775/801] Add reload on display mode change --- .../Dashboard/Friends/FriendDisplay.cs | 161 +++---------- .../Overlays/Dashboard/Friends/FriendsList.cs | 212 ++++++++++++++++++ 2 files changed, 243 insertions(+), 130 deletions(-) create mode 100644 osu.Game/Overlays/Dashboard/Friends/FriendsList.cs diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index 4bd79188c8..6cc56d5915 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -1,22 +1,20 @@ // 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.Collections.Specialized; using System.Linq; +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Metadata; using osu.Game.Resources.Localisation.Web; using osu.Game.Users; -using osuTK; namespace osu.Game.Overlays.Dashboard.Friends { @@ -35,9 +33,11 @@ namespace osu.Game.Overlays.Dashboard.Friends private Box background = null!; private Box controlBackground = null!; private UserListToolbar userListToolbar = null!; + private Container listContainer = null!; private LoadingLayer loading = null!; private BasicSearchTextBox searchTextBox = null!; - private FriendsSearchContainer panelsContainer = null!; + + private CancellationTokenSource? listLoadCancellation; public FriendDisplay() { @@ -145,14 +145,11 @@ namespace osu.Game.Overlays.Dashboard.Friends AutoSizeAxes = Axes.Y, Children = new Drawable[] { - panelsContainer = new FriendsSearchContainer + listContainer = new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING }, - // Todo: Spacing = new Vector2(style == OverlayPanelDisplayStyle.Card ? 10 : 2), - Spacing = new Vector2(10), - SortCriteria = { BindTarget = userListToolbar.SortCriteria } + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING } }, loading = new LoadingLayer(true) } @@ -178,35 +175,12 @@ namespace osu.Game.Overlays.Dashboard.Friends friendPresences.BindTo(metadataClient.FriendPresences); friendPresences.BindCollectionChanged(onFriendPresencesChanged, true); - searchTextBox.Current.BindValueChanged(onSearchChanged); - streamControl.Current.BindValueChanged(onFriendsStreamChanged); + userListToolbar.DisplayStyle.BindValueChanged(_ => reloadList()); } private void onFriendsChanged(object? sender, NotifyCollectionChangedEventArgs e) { - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - foreach (APIRelation relation in e.NewItems!.OfType()) - { - panelsContainer.Add(new FilterableUserPanel(new UserGridPanel(relation.TargetUser!).With(panel => - { - panel.Anchor = Anchor.TopCentre; - panel.Origin = Anchor.TopCentre; - panel.Width = 290; - }))); - } - - break; - - case NotifyCollectionChangedAction.Remove: - foreach (APIRelation relation in e.OldItems!.OfType()) - panelsContainer.RemoveAll(panel => panel.User.Equals(relation.TargetUser), true); - - break; - } - - updatePanelVisibilities(); + reloadList(); updateStatusCounts(); } @@ -216,40 +190,39 @@ namespace osu.Game.Overlays.Dashboard.Friends { case NotifyDictionaryChangedAction.Add: case NotifyDictionaryChangedAction.Remove: - updatePanelVisibilities(); updateStatusCounts(); break; } } - private void onFriendsStreamChanged(ValueChangedEvent stream) + private void reloadList() { - updatePanelVisibilities(); - } + listLoadCancellation?.Cancel(); + var cancellationSource = listLoadCancellation = new CancellationTokenSource(); - private void onSearchChanged(ValueChangedEvent search) - { - panelsContainer.SearchTerm = search.NewValue; - } - - private void updatePanelVisibilities() - { - foreach (var panel in panelsContainer) + FriendsList? currentList = listContainer.SingleOrDefault(); + FriendsList newList = new FriendsList(userListToolbar.DisplayStyle.Value, apiFriends.Select(f => f.TargetUser!).ToArray()) { - switch (streamControl.Current.Value) + OnlineStream = { BindTarget = streamControl.Current }, + SortCriteria = { BindTarget = userListToolbar.SortCriteria }, + SearchText = { BindTarget = searchTextBox.Current } + }; + + loading.Show(); + LoadComponentAsync(newList, finishLoad, cancellationSource.Token); + + void finishLoad(FriendsList list) + { + loading.Hide(); + + if (currentList != null) { - case OnlineStatus.All: - panel.CanBeShown.Value = true; - break; - - case OnlineStatus.Online: - panel.CanBeShown.Value = friendPresences.ContainsKey(panel.User.OnlineID); - break; - - case OnlineStatus.Offline: - panel.CanBeShown.Value = !friendPresences.ContainsKey(panel.User.OnlineID); - break; + currentList.FadeOut(100, Easing.OutQuint).Expire(); + currentList.Delay(25).Schedule(() => currentList.BypassAutoSizeAxes = Axes.Y); } + + listContainer.Add(newList); + newList.FadeIn(200, Easing.OutQuint); } } @@ -270,77 +243,5 @@ namespace osu.Game.Overlays.Dashboard.Friends streamControl.CountOnline.Value = countOnline; streamControl.CountOffline.Value = countOffline; } - - private class FriendsSearchContainer : SearchContainer - { - public readonly IBindable SortCriteria = new Bindable(); - - protected override void LoadComplete() - { - base.LoadComplete(); - SortCriteria.BindValueChanged(_ => InvalidateLayout(), true); - } - - public override IEnumerable FlowingChildren - { - get - { - IEnumerable panels = base.FlowingChildren.OfType(); - - switch (SortCriteria.Value) - { - default: - case UserSortCriteria.LastVisit: - return panels.OrderByDescending(panel => panel.User.LastVisit); - - case UserSortCriteria.Rank: - return panels.OrderByDescending(panel => panel.User.Statistics.GlobalRank.HasValue).ThenBy(panel => panel.User.Statistics.GlobalRank ?? 0); - - case UserSortCriteria.Username: - return panels.OrderBy(panel => panel.User.Username); - } - } - } - } - - private class FilterableUserPanel : CompositeDrawable, IConditionalFilterable - { - public readonly Bindable CanBeShown = new Bindable(); - - public APIUser User => panel.User; - - private readonly UserPanel panel; - - public FilterableUserPanel(UserPanel panel) - { - this.panel = panel; - - Anchor = panel.Anchor; - Origin = panel.Origin; - RelativeSizeAxes = panel.RelativeSizeAxes; - AutoSizeAxes = panel.AutoSizeAxes; - Width = panel.Width; - Height = panel.Height; - - InternalChild = panel; - } - - IBindable IConditionalFilterable.CanBeShown => CanBeShown; - - IEnumerable IHasFilterTerms.FilterTerms => panel.FilterTerms; - - bool IFilterable.MatchingFilter - { - set - { - if (value) - Show(); - else - Hide(); - } - } - - bool IFilterable.FilteringActive { set { } } - } } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs new file mode 100644 index 0000000000..ed87a58ff4 --- /dev/null +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs @@ -0,0 +1,212 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.EnumExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Metadata; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Overlays.Dashboard.Friends +{ + public class FriendsList : CompositeDrawable + { + public readonly IBindable OnlineStream = new Bindable(); + public readonly IBindable SortCriteria = new Bindable(); + public readonly IBindable SearchText = new Bindable(); + + [Resolved] + private MetadataClient metadataClient { get; set; } = null!; + + private readonly IBindableDictionary friendPresences = new BindableDictionary(); + private readonly OverlayPanelDisplayStyle style; + private readonly APIUser[] friends; + + private FriendsSearchContainer searchContainer = null!; + + public FriendsList(OverlayPanelDisplayStyle style, APIUser[] friends) + { + this.style = style; + this.friends = friends; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = searchContainer = new FriendsSearchContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING }, + Spacing = new Vector2(style == OverlayPanelDisplayStyle.Card ? 10 : 2), + SortCriteria = { BindTarget = SortCriteria }, + ChildrenEnumerable = friends.Select(createUserPanel) + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + friendPresences.BindTo(metadataClient.FriendPresences); + friendPresences.BindCollectionChanged(onFriendPresencesChanged, true); + + SearchText.BindValueChanged(onSearchTextChanged, true); + OnlineStream.BindValueChanged(onFriendsStreamChanged, true); + } + + private void onFriendPresencesChanged(object? sender, NotifyDictionaryChangedEventArgs e) + { + switch (e.Action) + { + case NotifyDictionaryChangedAction.Add: + case NotifyDictionaryChangedAction.Remove: + updatePanelVisibilities(); + break; + } + } + + private void onSearchTextChanged(ValueChangedEvent search) + { + searchContainer.SearchTerm = search.NewValue; + } + + private void onFriendsStreamChanged(ValueChangedEvent stream) + { + updatePanelVisibilities(); + } + + private void updatePanelVisibilities() + { + foreach (var panel in searchContainer) + { + switch (OnlineStream.Value) + { + case OnlineStatus.All: + panel.CanBeShown.Value = true; + break; + + case OnlineStatus.Online: + panel.CanBeShown.Value = friendPresences.ContainsKey(panel.User.OnlineID); + break; + + case OnlineStatus.Offline: + panel.CanBeShown.Value = !friendPresences.ContainsKey(panel.User.OnlineID); + break; + } + } + } + + private FilterableUserPanel createUserPanel(APIUser user) + { + UserPanel panel; + + switch (style) + { + default: + case OverlayPanelDisplayStyle.Card: + panel = new UserGridPanel(user); + panel.Anchor = Anchor.TopCentre; + panel.Origin = Anchor.TopCentre; + panel.Width = 290; + break; + + case OverlayPanelDisplayStyle.List: + panel = new UserListPanel(user); + break; + + case OverlayPanelDisplayStyle.Brick: + panel = new UserBrickPanel(user); + break; + } + + return new FilterableUserPanel(panel); + } + + private class FriendsSearchContainer : SearchContainer + { + public readonly IBindable SortCriteria = new Bindable(); + + protected override void LoadComplete() + { + base.LoadComplete(); + SortCriteria.BindValueChanged(_ => InvalidateLayout(), true); + } + + public override IEnumerable FlowingChildren + { + get + { + IEnumerable panels = base.FlowingChildren.OfType(); + + switch (SortCriteria.Value) + { + default: + case UserSortCriteria.LastVisit: + return panels.OrderByDescending(panel => panel.User.LastVisit); + + case UserSortCriteria.Rank: + return panels.OrderByDescending(panel => panel.User.Statistics.GlobalRank.HasValue).ThenBy(panel => panel.User.Statistics.GlobalRank ?? 0); + + case UserSortCriteria.Username: + return panels.OrderBy(panel => panel.User.Username); + } + } + } + } + + private class FilterableUserPanel : CompositeDrawable, IConditionalFilterable + { + public readonly Bindable CanBeShown = new Bindable(); + + public APIUser User => panel.User; + + private readonly UserPanel panel; + + public FilterableUserPanel(UserPanel panel) + { + this.panel = panel; + + Anchor = panel.Anchor; + Origin = panel.Origin; + RelativeSizeAxes = panel.RelativeSizeAxes; + AutoSizeAxes = panel.AutoSizeAxes; + + if (!AutoSizeAxes.HasFlagFast(Axes.X)) + Width = panel.Width; + + if (!AutoSizeAxes.HasFlagFast(Axes.Y)) + Height = panel.Height; + + InternalChild = panel; + } + + IBindable IConditionalFilterable.CanBeShown => CanBeShown; + + IEnumerable IHasFilterTerms.FilterTerms => panel.FilterTerms; + + bool IFilterable.MatchingFilter + { + set + { + if (value) + Show(); + else + Hide(); + } + } + + bool IFilterable.FilteringActive { set { } } + } + } +} From 9ff6c44559a6455ea91e363f4da9bb726203098e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Mar 2025 01:36:24 +0900 Subject: [PATCH 776/801] Handle stream counts internally --- .../Dashboard/Friends/FriendDisplay.cs | 47 +------------- .../Friends/FriendOnlineStreamControl.cs | 64 +++++++++++++++++-- 2 files changed, 59 insertions(+), 52 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index 6cc56d5915..223bcdf2d9 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Specialized; using System.Linq; using System.Threading; using osu.Framework.Allocation; @@ -12,23 +11,17 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Metadata; using osu.Game.Resources.Localisation.Web; -using osu.Game.Users; namespace osu.Game.Overlays.Dashboard.Friends { public partial class FriendDisplay : CompositeDrawable { private readonly IBindableList apiFriends = new BindableList(); - private readonly IBindableDictionary friendPresences = new BindableDictionary(); [Resolved] private IAPIProvider api { get; set; } = null!; - [Resolved] - private MetadataClient metadataClient { get; set; } = null!; - private FriendOnlineStreamControl streamControl = null!; private Box background = null!; private Box controlBackground = null!; @@ -170,31 +163,11 @@ namespace osu.Game.Overlays.Dashboard.Friends base.LoadComplete(); apiFriends.BindTo(api.Friends); - apiFriends.BindCollectionChanged(onFriendsChanged, true); - - friendPresences.BindTo(metadataClient.FriendPresences); - friendPresences.BindCollectionChanged(onFriendPresencesChanged, true); + apiFriends.BindCollectionChanged((_, _) => reloadList()); userListToolbar.DisplayStyle.BindValueChanged(_ => reloadList()); } - private void onFriendsChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - reloadList(); - updateStatusCounts(); - } - - private void onFriendPresencesChanged(object? sender, NotifyDictionaryChangedEventArgs e) - { - switch (e.Action) - { - case NotifyDictionaryChangedAction.Add: - case NotifyDictionaryChangedAction.Remove: - updateStatusCounts(); - break; - } - } - private void reloadList() { listLoadCancellation?.Cancel(); @@ -225,23 +198,5 @@ namespace osu.Game.Overlays.Dashboard.Friends newList.FadeIn(200, Easing.OutQuint); } } - - private void updateStatusCounts() - { - int countOnline = 0; - int countOffline = 0; - - foreach (var user in apiFriends) - { - if (friendPresences.ContainsKey(user.TargetID)) - countOnline++; - else - countOffline++; - } - - streamControl.CountAll.Value = apiFriends.Count; - streamControl.CountOnline.Value = countOnline; - streamControl.CountOffline.Value = countOffline; - } } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendOnlineStreamControl.cs b/osu.Game/Overlays/Dashboard/Friends/FriendOnlineStreamControl.cs index 25b29e8d16..763571f605 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendOnlineStreamControl.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendOnlineStreamControl.cs @@ -2,15 +2,28 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Metadata; +using osu.Game.Users; namespace osu.Game.Overlays.Dashboard.Friends { public partial class FriendOnlineStreamControl : OverlayStreamControl { - public readonly BindableInt CountAll = new BindableInt(); - public readonly BindableInt CountOnline = new BindableInt(); - public readonly BindableInt CountOffline = new BindableInt(); + private readonly IBindableDictionary friendPresences = new BindableDictionary(); + private readonly IBindableList apiFriends = new BindableList(); + private readonly BindableInt countAll = new BindableInt(); + private readonly BindableInt countOnline = new BindableInt(); + private readonly BindableInt countOffline = new BindableInt(); + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private MetadataClient metadataClient { get; set; } = null!; public FriendOnlineStreamControl() { @@ -22,18 +35,57 @@ namespace osu.Game.Overlays.Dashboard.Friends ]; } + protected override void LoadComplete() + { + base.LoadComplete(); + + apiFriends.BindTo(api.Friends); + apiFriends.BindCollectionChanged((_, _) => updateCounts()); + + friendPresences.BindTo(metadataClient.FriendPresences); + friendPresences.BindCollectionChanged(onFriendPresencesChanged); + + updateCounts(); + } + + private void onFriendPresencesChanged(object? sender, NotifyDictionaryChangedEventArgs e) + { + switch (e.Action) + { + case NotifyDictionaryChangedAction.Add: + case NotifyDictionaryChangedAction.Remove: + updateCounts(); + break; + } + } + + private void updateCounts() + { + countAll.Value = apiFriends.Count; + countOnline.Value = 0; + countOffline.Value = 0; + + foreach (var user in apiFriends) + { + if (friendPresences.ContainsKey(user.TargetID)) + countOnline.Value++; + else + countOffline.Value++; + } + } + protected override OverlayStreamItem CreateStreamItem(OnlineStatus value) { switch (value) { case OnlineStatus.All: - return new FriendsOnlineStatusItem(value) { UserCount = { BindTarget = CountAll } }; + return new FriendsOnlineStatusItem(value) { UserCount = { BindTarget = countAll } }; case OnlineStatus.Online: - return new FriendsOnlineStatusItem(value) { UserCount = { BindTarget = CountOnline } }; + return new FriendsOnlineStatusItem(value) { UserCount = { BindTarget = countOnline } }; case OnlineStatus.Offline: - return new FriendsOnlineStatusItem(value) { UserCount = { BindTarget = CountOffline } }; + return new FriendsOnlineStatusItem(value) { UserCount = { BindTarget = countOffline } }; default: throw new ArgumentException(nameof(value)); From 4b54b8c0d843855305c25dc0ad1e33ba8e60978f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Mar 2025 01:36:32 +0900 Subject: [PATCH 777/801] Update tests --- .../Visual/Online/TestSceneFriendDisplay.cs | 151 ++++++++++++++++-- .../TestSceneFriendsOnlineStatusControl.cs | 96 ++++++++++- .../Overlays/Dashboard/Friends/FriendsList.cs | 2 +- 3 files changed, 233 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs index 010a261d4c..2e0f86c622 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs @@ -5,46 +5,179 @@ using System; using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Metadata; using osu.Game.Overlays; using osu.Game.Overlays.Dashboard.Friends; +using osu.Game.Tests.Visual.Metadata; using osu.Game.Users; namespace osu.Game.Tests.Visual.Online { public partial class TestSceneFriendDisplay : OsuTestScene { - protected override bool UseOnlineAPI => true; - [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - private FriendDisplay display; + private TestMetadataClient metadataClient; [SetUp] public void Setup() => Schedule(() => { - Child = new BasicScrollContainer + Child = new DependencyProvidingContainer { RelativeSizeAxes = Axes.Both, - Child = display = new FriendDisplay() + CachedDependencies = + [ + (typeof(MetadataClient), metadataClient = new TestMetadataClient()) + ], + Children = new Drawable[] + { + metadataClient, + new BasicScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = new FriendDisplay() + } + } }; }); [Test] - public void TestOffline() + public void TestAddAndRemoveFriends() { - // AddStep("Populate with offline test users", () => display.Users = getUsers()); + AddStep("set friends", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + api.Friends.Clear(); + api.Friends.AddRange(getUsers().Select(u => new APIRelation + { + RelationType = RelationType.Friend, + TargetID = u.OnlineID, + TargetUser = u + })); + }); + + waitForLoad(3); + + AddStep("remove one friend", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + api.Friends.RemoveAt(0); + }); + + waitForLoad(2); + + AddStep("add one friend", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + api.Friends.AddRange(getUsers().Take(1).Select(u => new APIRelation + { + RelationType = RelationType.Friend, + TargetID = u.OnlineID, + TargetUser = u + })); + }); + + waitForLoad(3); + + void waitForLoad(int expectedPanels) + { + AddUntilStep("wait for friends to load", () => this.ChildrenOfType().LastOrDefault()?.IsLoaded == true); + AddAssert($"{expectedPanels} panels in list", () => this.ChildrenOfType().Last().ChildrenOfType().Count(), () => Is.EqualTo(expectedPanels)); + } } [Test] - public void TestOnline() + public void TestChangeDisplayStyle() { - // No need to do anything, fetch is performed automatically. + AddStep("set friends", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + api.Friends.Clear(); + api.Friends.AddRange(getUsers().Select(u => new APIRelation + { + RelationType = RelationType.Friend, + TargetID = u.OnlineID, + TargetUser = u + })); + }); + + waitForLoad(); + + AddStep("set list style", () => this.ChildrenOfType().Single().DisplayStyle.Value = OverlayPanelDisplayStyle.List); + waitForLoad(); + + AddStep("set brick style", () => this.ChildrenOfType().Single().DisplayStyle.Value = OverlayPanelDisplayStyle.Brick); + waitForLoad(); + + void waitForLoad() + { + AddUntilStep("wait for friends to load", () => this.ChildrenOfType().LastOrDefault()?.IsLoaded == true); + AddAssert($"3 {typeof(T).ReadableName()} in list", () => this.ChildrenOfType().Last().ChildrenOfType().Count(), () => Is.EqualTo(3)); + } + } + + [Test] + public void TestOnlinePresence() + { + AddStep("set friends", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + api.Friends.Clear(); + api.Friends.AddRange(getUsers().Select(u => new APIRelation + { + RelationType = RelationType.Friend, + TargetID = u.OnlineID, + TargetUser = u + })); + }); + + AddUntilStep("wait for friends to load", () => this.ChildrenOfType().LastOrDefault()?.IsLoaded == true); + assertVisible(3); + + AddStep("change to online stream", () => this.ChildrenOfType().Single().Current.Value = OnlineStatus.Online); + assertVisible(0); + + AddStep("bring a friend online", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + metadataClient.FriendPresenceUpdated(api.Friends[0].TargetID, new UserPresence { Status = UserStatus.Online }); + }); + + assertVisible(1); + + AddStep("change to offline stream", () => this.ChildrenOfType().Single().Current.Value = OnlineStatus.Offline); + assertVisible(2); + + AddStep("bring a friend online", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + metadataClient.FriendPresenceUpdated(api.Friends[1].TargetID, new UserPresence { Status = UserStatus.Online }); + }); + + assertVisible(1); + + AddStep("change to online stream", () => this.ChildrenOfType().Single().Current.Value = OnlineStatus.Online); + assertVisible(2); + + AddStep("change to all stream", () => this.ChildrenOfType().Single().Current.Value = OnlineStatus.All); + assertVisible(3); + + void assertVisible(int expectedPanels) + { + AddAssert($"{expectedPanels} panels visible", + () => this.ChildrenOfType().Count(p => p.Alpha > 0), + () => Is.EqualTo(expectedPanels)); + } } private List getUsers() => new List diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs index 548f3067a7..c08e7b7b0f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs @@ -1,11 +1,18 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Metadata; using osu.Game.Overlays; using osu.Game.Overlays.Dashboard.Friends; +using osu.Game.Tests.Visual.Metadata; +using osu.Game.Users; namespace osu.Game.Tests.Visual.UserInterface { @@ -14,14 +21,91 @@ namespace osu.Game.Tests.Visual.UserInterface [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); + private TestMetadataClient metadataClient = null!; + [SetUp] - public void SetUp() => Schedule(() => Child = new FriendOnlineStreamControl + public void SetUp() => Schedule(() => { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - CountAll = { Value = 15 }, - CountOnline = { Value = 10 }, - CountOffline = { Value = 5 } + Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = + [ + (typeof(MetadataClient), metadataClient = new TestMetadataClient()) + ], + Children = new Drawable[] + { + metadataClient, + new FriendOnlineStreamControl + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + } + }; }); + + [Test] + public void TestChangeFriends() + { + AddStep("set 10 friends", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + api.Friends.Clear(); + api.Friends.AddRange(Enumerable.Range(1, 10).Select(i => new APIRelation + { + RelationType = RelationType.Friend, + TargetID = i, + TargetUser = new APIUser { Id = i }, + })); + }); + + AddStep("set 20 friends", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + api.Friends.Clear(); + api.Friends.AddRange(Enumerable.Range(1, 20).Select(i => new APIRelation + { + RelationType = RelationType.Friend, + TargetID = i, + TargetUser = new APIUser { Id = i }, + })); + }); + } + + [Solo] + [Test] + public void TestChangeOnlineStates() + { + AddStep("set 10 friends", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + api.Friends.Clear(); + api.Friends.AddRange(Enumerable.Range(1, 10).Select(i => new APIRelation + { + RelationType = RelationType.Friend, + TargetID = i, + TargetUser = new APIUser { Id = i }, + })); + }); + + AddStep("make users 1-5 online", () => + { + for (int i = 1; i <= 5; i++) + metadataClient.FriendPresenceUpdated(i, new UserPresence { Status = UserStatus.Online }); + }); + + AddStep("make users 1-5 DnD", () => + { + for (int i = 1; i <= 5; i++) + metadataClient.FriendPresenceUpdated(i, new UserPresence { Status = UserStatus.DoNotDisturb }); + }); + + AddStep("make users 1-5 offline", () => + { + for (int i = 1; i <= 5; i++) + metadataClient.FriendPresenceUpdated(i, null); + }); + } } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs index ed87a58ff4..d1ae4b2f46 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs @@ -165,7 +165,7 @@ namespace osu.Game.Overlays.Dashboard.Friends } } - private class FilterableUserPanel : CompositeDrawable, IConditionalFilterable + public class FilterableUserPanel : CompositeDrawable, IConditionalFilterable { public readonly Bindable CanBeShown = new Bindable(); From e4ade7acd1b4c807702350cbb3a5f579aef2e15e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Mar 2025 01:44:20 +0900 Subject: [PATCH 778/801] Prevent spectate/invite callbacks on invalid states --- osu.Game/Users/UserPanel.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 1f72cbccbf..1010234e1f 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -157,20 +157,28 @@ namespace osu.Game.Users chatOverlay?.Show(); })); - if (metadataClient?.GetPresence(User.OnlineID) != null) + if (isUserOnline()) { items.Add(new OsuMenuItem(ContextMenuStrings.SpectatePlayer, MenuItemType.Standard, () => { - performer?.PerformFromScreen(s => s.Push(new SoloSpectatorScreen(User))); + if (isUserOnline()) + performer?.PerformFromScreen(s => s.Push(new SoloSpectatorScreen(User))); })); - if (multiplayerClient?.Room?.Users.All(u => u.UserID != User.Id) == true) + if (canInviteUser()) { - items.Add(new OsuMenuItem(ContextMenuStrings.InvitePlayer, MenuItemType.Standard, () => multiplayerClient.InvitePlayer(User.Id))); + items.Add(new OsuMenuItem(ContextMenuStrings.InvitePlayer, MenuItemType.Standard, () => + { + if (canInviteUser()) + multiplayerClient!.InvitePlayer(User.Id); + })); } } return items.ToArray(); + + bool isUserOnline() => metadataClient?.GetPresence(User.OnlineID) != null; + bool canInviteUser() => isUserOnline() && multiplayerClient?.Room?.Users.All(u => u.UserID != User.Id) == true; } } From bfdc7c1688d30a8c53f4860dfe9289b9dd1af948 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Mar 2025 01:54:36 +0900 Subject: [PATCH 779/801] Fix no display if friends already loaded --- .../Visual/Online/TestSceneFriendDisplay.cs | 40 +++++++++++++++++++ .../Dashboard/Friends/FriendDisplay.cs | 2 +- .../Overlays/Dashboard/Friends/FriendsList.cs | 2 +- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs index 2e0f86c622..06e51f97fc 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs @@ -180,6 +180,46 @@ namespace osu.Game.Tests.Visual.Online } } + [Test] + public void TestLoadFriendsBeforeDisplay() + { + AddStep("set friends", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + api.Friends.Clear(); + api.Friends.AddRange(getUsers().Select(u => new APIRelation + { + RelationType = RelationType.Friend, + TargetID = u.OnlineID, + TargetUser = u + })); + }); + + AddStep("load new display", () => + { + Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = + [ + (typeof(MetadataClient), metadataClient = new TestMetadataClient()) + ], + Children = new Drawable[] + { + metadataClient, + new BasicScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = new FriendDisplay() + } + } + }; + }); + + AddUntilStep("wait for friends to load", () => this.ChildrenOfType().LastOrDefault()?.IsLoaded == true); + AddAssert("3 panels in list", () => this.ChildrenOfType().Last().ChildrenOfType().Count(), () => Is.EqualTo(3)); + } + private List getUsers() => new List { new APIUser diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index 223bcdf2d9..19fcb44be7 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -165,7 +165,7 @@ namespace osu.Game.Overlays.Dashboard.Friends apiFriends.BindTo(api.Friends); apiFriends.BindCollectionChanged((_, _) => reloadList()); - userListToolbar.DisplayStyle.BindValueChanged(_ => reloadList()); + userListToolbar.DisplayStyle.BindValueChanged(_ => reloadList(), true); } private void reloadList() diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs index d1ae4b2f46..826db945c0 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs @@ -59,7 +59,7 @@ namespace osu.Game.Overlays.Dashboard.Friends base.LoadComplete(); friendPresences.BindTo(metadataClient.FriendPresences); - friendPresences.BindCollectionChanged(onFriendPresencesChanged, true); + friendPresences.BindCollectionChanged(onFriendPresencesChanged); SearchText.BindValueChanged(onSearchTextChanged, true); OnlineStream.BindValueChanged(onFriendsStreamChanged, true); From 09c8cf9a83fdce08ba5343c280e555dc7662d9ea Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Mar 2025 02:17:23 +0900 Subject: [PATCH 780/801] Partial classes --- osu.Game/Overlays/Dashboard/Friends/FriendsList.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs index 826db945c0..4107bdfb73 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs @@ -16,7 +16,7 @@ using osuTK; namespace osu.Game.Overlays.Dashboard.Friends { - public class FriendsList : CompositeDrawable + public partial class FriendsList : CompositeDrawable { public readonly IBindable OnlineStream = new Bindable(); public readonly IBindable SortCriteria = new Bindable(); @@ -133,7 +133,7 @@ namespace osu.Game.Overlays.Dashboard.Friends return new FilterableUserPanel(panel); } - private class FriendsSearchContainer : SearchContainer + private partial class FriendsSearchContainer : SearchContainer { public readonly IBindable SortCriteria = new Bindable(); @@ -165,7 +165,7 @@ namespace osu.Game.Overlays.Dashboard.Friends } } - public class FilterableUserPanel : CompositeDrawable, IConditionalFilterable + public partial class FilterableUserPanel : CompositeDrawable, IConditionalFilterable { public readonly Bindable CanBeShown = new Bindable(); From 04bb1c13f858b0ff51eb1df02ae48b72dc7eb14e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Mar 2025 02:23:24 +0900 Subject: [PATCH 781/801] Cancel + dispose CTS --- osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index 19fcb44be7..941d293d9d 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -198,5 +198,13 @@ namespace osu.Game.Overlays.Dashboard.Friends newList.FadeIn(200, Easing.OutQuint); } } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + listLoadCancellation?.Cancel(); + listLoadCancellation?.Dispose(); + } } } From 22e4527118c9c30685ac988611465ba2fc036427 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Mar 2025 02:24:30 +0900 Subject: [PATCH 782/801] Fix doubled padding --- osu.Game/Overlays/Dashboard/Friends/FriendsList.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs index 4107bdfb73..91256cae17 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs @@ -47,7 +47,6 @@ namespace osu.Game.Overlays.Dashboard.Friends { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING }, Spacing = new Vector2(style == OverlayPanelDisplayStyle.Card ? 10 : 2), SortCriteria = { BindTarget = SortCriteria }, ChildrenEnumerable = friends.Select(createUserPanel) From db0676d6158e55b44c1b99f6cef4ccad96a17eff Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Wed, 12 Mar 2025 19:10:41 +0000 Subject: [PATCH 783/801] Remove JSON property attributes from non-databased taiko difficulty attributes --- .../Difficulty/TaikoDifficultyAttributes.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index b43468ab18..b8051054e7 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -13,25 +13,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// /// The difficulty corresponding to the rhythm skill. /// - [JsonProperty("rhythm_difficulty")] public double RhythmDifficulty { get; set; } /// /// The difficulty corresponding to the reading skill. /// - [JsonProperty("reading_difficulty")] public double ReadingDifficulty { get; set; } /// /// The difficulty corresponding to the colour skill. /// - [JsonProperty("colour_difficulty")] public double ColourDifficulty { get; set; } /// /// The difficulty corresponding to the stamina skill. /// - [JsonProperty("stamina_difficulty")] public double StaminaDifficulty { get; set; } /// @@ -40,13 +36,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("mono_stamina_factor")] public double MonoStaminaFactor { get; set; } - [JsonProperty("rhythm_difficult_strains")] public double RhythmTopStrains { get; set; } - [JsonProperty("colour_difficult_strains")] public double ColourTopStrains { get; set; } - [JsonProperty("stamina_difficult_strains")] public double StaminaTopStrains { get; set; } public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() From a075a5641aaa0eb44f2c9cc455a8d8e20d4bf84b Mon Sep 17 00:00:00 2001 From: Nuno <146981906+Nunolin@users.noreply.github.com> Date: Thu, 13 Mar 2025 01:02:34 +0000 Subject: [PATCH 784/801] Remove end note conversion in mania invert mod --- osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs index ef9154d180..d1912e3690 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs @@ -42,8 +42,7 @@ namespace osu.Game.Rulesets.Mania.Mods var locations = column.OfType().Select(n => (startTime: n.StartTime, samples: n.Samples)) .Concat(column.OfType().SelectMany(h => new[] { - (startTime: h.StartTime, samples: h.GetNodeSamples(0)), - (startTime: h.EndTime, samples: h.GetNodeSamples(1)) + (startTime: h.StartTime, samples: h.GetNodeSamples(0)) })) .OrderBy(h => h.startTime).ToList(); From f00d2cbfa282ee630340fb89928e6c1895c51e78 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Mar 2025 12:55:00 +0900 Subject: [PATCH 785/801] Fix tests --- .../Visual/Online/TestSceneFriendDisplay.cs | 69 ++++++++++--------- .../TestSceneFriendsOnlineStatusControl.cs | 2 - 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs index 06e51f97fc..8ed61fe028 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs @@ -12,6 +12,7 @@ using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Metadata; @@ -66,7 +67,8 @@ namespace osu.Game.Tests.Visual.Online })); }); - waitForLoad(3); + waitForLoad(); + assertPanels(3); AddStep("remove one friend", () => { @@ -74,7 +76,8 @@ namespace osu.Game.Tests.Visual.Online api.Friends.RemoveAt(0); }); - waitForLoad(2); + waitForLoad(); + assertPanels(2); AddStep("add one friend", () => { @@ -87,13 +90,8 @@ namespace osu.Game.Tests.Visual.Online })); }); - waitForLoad(3); - - void waitForLoad(int expectedPanels) - { - AddUntilStep("wait for friends to load", () => this.ChildrenOfType().LastOrDefault()?.IsLoaded == true); - AddAssert($"{expectedPanels} panels in list", () => this.ChildrenOfType().Last().ChildrenOfType().Count(), () => Is.EqualTo(expectedPanels)); - } + waitForLoad(); + assertPanels(3); } [Test] @@ -111,19 +109,18 @@ namespace osu.Game.Tests.Visual.Online })); }); - waitForLoad(); + waitForLoad(); + assertPanels(3); AddStep("set list style", () => this.ChildrenOfType().Single().DisplayStyle.Value = OverlayPanelDisplayStyle.List); - waitForLoad(); + + waitForLoad(); + assertPanels(3); AddStep("set brick style", () => this.ChildrenOfType().Single().DisplayStyle.Value = OverlayPanelDisplayStyle.Brick); - waitForLoad(); - void waitForLoad() - { - AddUntilStep("wait for friends to load", () => this.ChildrenOfType().LastOrDefault()?.IsLoaded == true); - AddAssert($"3 {typeof(T).ReadableName()} in list", () => this.ChildrenOfType().Last().ChildrenOfType().Count(), () => Is.EqualTo(3)); - } + waitForLoad(); + assertPanels(3); } [Test] @@ -141,11 +138,11 @@ namespace osu.Game.Tests.Visual.Online })); }); - AddUntilStep("wait for friends to load", () => this.ChildrenOfType().LastOrDefault()?.IsLoaded == true); - assertVisible(3); + waitForLoad(); + assertPanels(3); AddStep("change to online stream", () => this.ChildrenOfType().Single().Current.Value = OnlineStatus.Online); - assertVisible(0); + assertPanels(0); AddStep("bring a friend online", () => { @@ -153,10 +150,10 @@ namespace osu.Game.Tests.Visual.Online metadataClient.FriendPresenceUpdated(api.Friends[0].TargetID, new UserPresence { Status = UserStatus.Online }); }); - assertVisible(1); + assertPanels(1); AddStep("change to offline stream", () => this.ChildrenOfType().Single().Current.Value = OnlineStatus.Offline); - assertVisible(2); + assertPanels(2); AddStep("bring a friend online", () => { @@ -164,20 +161,13 @@ namespace osu.Game.Tests.Visual.Online metadataClient.FriendPresenceUpdated(api.Friends[1].TargetID, new UserPresence { Status = UserStatus.Online }); }); - assertVisible(1); + assertPanels(1); AddStep("change to online stream", () => this.ChildrenOfType().Single().Current.Value = OnlineStatus.Online); - assertVisible(2); + assertPanels(2); AddStep("change to all stream", () => this.ChildrenOfType().Single().Current.Value = OnlineStatus.All); - assertVisible(3); - - void assertVisible(int expectedPanels) - { - AddAssert($"{expectedPanels} panels visible", - () => this.ChildrenOfType().Count(p => p.Alpha > 0), - () => Is.EqualTo(expectedPanels)); - } + assertPanels(3); } [Test] @@ -216,8 +206,19 @@ namespace osu.Game.Tests.Visual.Online }; }); - AddUntilStep("wait for friends to load", () => this.ChildrenOfType().LastOrDefault()?.IsLoaded == true); - AddAssert("3 panels in list", () => this.ChildrenOfType().Last().ChildrenOfType().Count(), () => Is.EqualTo(3)); + waitForLoad(); + assertPanels(3); + } + + private void waitForLoad() + => AddUntilStep("wait for panels to load", () => this.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Hidden)); + + private void assertPanels(int expectedVisible) + where T : UserPanel + { + AddAssert($"{typeof(T).ReadableName()}s in list", () => this.ChildrenOfType().Last().ChildrenOfType().All(p => p is T)); + AddAssert($"{expectedVisible} panels visible", () => this.ChildrenOfType().Last().ChildrenOfType().Count(p => p.IsPresent), + () => Is.EqualTo(expectedVisible)); } private List getUsers() => new List diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs index c08e7b7b0f..c7e2a0ed4b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs @@ -5,7 +5,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Testing; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Metadata; @@ -73,7 +72,6 @@ namespace osu.Game.Tests.Visual.UserInterface }); } - [Solo] [Test] public void TestChangeOnlineStates() { From 6b786a6ab476f6c3fdcdef92727dc2022df799ff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Mar 2025 13:45:02 +0900 Subject: [PATCH 786/801] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 8f219ea426..b6ab7dc712 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 8045009621..486979487b 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From da71e7a3633ebdd363c577a0584d004ff1b44e08 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Mar 2025 14:24:10 +0900 Subject: [PATCH 787/801] Fix enter to select tag not working in results scren --- osu.Game/Screens/Ranking/UserTagControl.cs | 35 ++++++++++++++++++---- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index 3ae6501b36..ae4a918ae5 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -27,6 +27,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Input.Bindings; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; @@ -500,21 +501,45 @@ namespace osu.Game.Screens.Ranking searchBox.Current.BindValueChanged(_ => searchContainer.SearchTerm = searchBox.Current.Value, true); } + public override bool OnPressed(KeyBindingPressEvent e) + { + if (base.OnPressed(e)) + return true; + + if (e.Repeat) + return false; + + if (State.Value == Visibility.Hidden) + return false; + + if (e.Action == GlobalAction.Select) + { + attemptSelect(); + return true; + } + + return false; + } + protected override bool OnKeyDown(KeyDownEvent e) { - var visibleItems = searchContainer.OfType().Where(d => d.IsPresent).ToArray(); - if (e.Key == Key.Enter) { - if (visibleItems.Length == 1) - select(visibleItems.Single().Tag); - + attemptSelect(); return true; } return base.OnKeyDown(e); } + private void attemptSelect() + { + var visibleItems = searchContainer.OfType().Where(d => d.IsPresent).ToArray(); + + if (visibleItems.Length == 1) + select(visibleItems.Single().Tag); + } + private void select(UserTag tag) { OnSelected?.Invoke(tag); From 829dfedfdc58866ed3c6628d4bf4362bca295607 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Mar 2025 15:28:04 +0900 Subject: [PATCH 788/801] Add test coverage of friend going offline --- .../Visual/Online/TestSceneFriendDisplay.cs | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs index 8ed61fe028..f7fd95a6e1 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs @@ -68,7 +68,7 @@ namespace osu.Game.Tests.Visual.Online }); waitForLoad(); - assertPanels(3); + assertVisiblePanelCount(3); AddStep("remove one friend", () => { @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Online }); waitForLoad(); - assertPanels(2); + assertVisiblePanelCount(2); AddStep("add one friend", () => { @@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.Online }); waitForLoad(); - assertPanels(3); + assertVisiblePanelCount(3); } [Test] @@ -110,17 +110,17 @@ namespace osu.Game.Tests.Visual.Online }); waitForLoad(); - assertPanels(3); + assertVisiblePanelCount(3); AddStep("set list style", () => this.ChildrenOfType().Single().DisplayStyle.Value = OverlayPanelDisplayStyle.List); waitForLoad(); - assertPanels(3); + assertVisiblePanelCount(3); AddStep("set brick style", () => this.ChildrenOfType().Single().DisplayStyle.Value = OverlayPanelDisplayStyle.Brick); waitForLoad(); - assertPanels(3); + assertVisiblePanelCount(3); } [Test] @@ -139,10 +139,10 @@ namespace osu.Game.Tests.Visual.Online }); waitForLoad(); - assertPanels(3); + assertVisiblePanelCount(3); AddStep("change to online stream", () => this.ChildrenOfType().Single().Current.Value = OnlineStatus.Online); - assertPanels(0); + assertVisiblePanelCount(0); AddStep("bring a friend online", () => { @@ -150,10 +150,10 @@ namespace osu.Game.Tests.Visual.Online metadataClient.FriendPresenceUpdated(api.Friends[0].TargetID, new UserPresence { Status = UserStatus.Online }); }); - assertPanels(1); + assertVisiblePanelCount(1); AddStep("change to offline stream", () => this.ChildrenOfType().Single().Current.Value = OnlineStatus.Offline); - assertPanels(2); + assertVisiblePanelCount(2); AddStep("bring a friend online", () => { @@ -161,13 +161,20 @@ namespace osu.Game.Tests.Visual.Online metadataClient.FriendPresenceUpdated(api.Friends[1].TargetID, new UserPresence { Status = UserStatus.Online }); }); - assertPanels(1); + assertVisiblePanelCount(1); AddStep("change to online stream", () => this.ChildrenOfType().Single().Current.Value = OnlineStatus.Online); - assertPanels(2); + assertVisiblePanelCount(2); + + AddStep("take friend offline", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + metadataClient.FriendPresenceUpdated(api.Friends[1].TargetID, null); + }); + assertVisiblePanelCount(1); AddStep("change to all stream", () => this.ChildrenOfType().Single().Current.Value = OnlineStatus.All); - assertPanels(3); + assertVisiblePanelCount(3); } [Test] @@ -207,13 +214,13 @@ namespace osu.Game.Tests.Visual.Online }); waitForLoad(); - assertPanels(3); + assertVisiblePanelCount(3); } private void waitForLoad() => AddUntilStep("wait for panels to load", () => this.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Hidden)); - private void assertPanels(int expectedVisible) + private void assertVisiblePanelCount(int expectedVisible) where T : UserPanel { AddAssert($"{typeof(T).ReadableName()}s in list", () => this.ChildrenOfType().Last().ChildrenOfType().All(p => p is T)); From d74dd262f532f2922a6fcd90655c35a9a4947fe0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Mar 2025 16:04:27 +0900 Subject: [PATCH 789/801] Add inline TODOs regarding sorting modes --- osu.Game/Overlays/Dashboard/Friends/FriendsList.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs index 91256cae17..955c2c046e 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs @@ -152,9 +152,11 @@ namespace osu.Game.Overlays.Dashboard.Friends { default: case UserSortCriteria.LastVisit: + // Todo: Last visit time is not currently updated according to realtime user presence. return panels.OrderByDescending(panel => panel.User.LastVisit); case UserSortCriteria.Rank: + // Todo: Statistics are not currently updated according to realtime user statistics, but it's also not currently displayed in the panels. return panels.OrderByDescending(panel => panel.User.Statistics.GlobalRank.HasValue).ThenBy(panel => panel.User.Statistics.GlobalRank ?? 0); case UserSortCriteria.Username: From 3e71d04bd076926d794c0a066c900515c05fcbf9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Mar 2025 15:07:15 +0900 Subject: [PATCH 790/801] Standardise sizing of placeholder messages on beatmap scores overlay --- .../Overlays/BeatmapSet/Scores/NotSupporterPlaceholder.cs | 6 ++---- osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/NotSupporterPlaceholder.cs b/osu.Game/Overlays/BeatmapSet/Scores/NotSupporterPlaceholder.cs index 7cb119bf32..36f71be606 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/NotSupporterPlaceholder.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/NotSupporterPlaceholder.cs @@ -3,11 +3,10 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osuTK; -using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; using osu.Game.Resources.Localisation.Web; +using osuTK; namespace osu.Game.Overlays.BeatmapSet.Scores { @@ -30,7 +29,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Text = BeatmapsetsStrings.ShowScoreboardSupporterOnly, - Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold), }, text = new LinkFlowContainer(t => t.Font = t.Font.With(size: 11)) { diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index b53b7826f3..bd61992dbf 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -158,6 +158,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, + Margin = new MarginPadding { Vertical = 10 }, Alpha = 0, }, new FillFlowContainer From 9af3c8351d27c743d623268a836b13d8225b2b14 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Mar 2025 15:08:12 +0900 Subject: [PATCH 791/801] Add helper methods to check whether beatmap leaderboard scope requires supporter --- osu.Game/Extensions/ModelExtensions.cs | 16 ++++++++++++++++ .../BeatmapSet/Scores/ScoresContainer.cs | 3 ++- .../Select/Leaderboards/BeatmapLeaderboard.cs | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/osu.Game/Extensions/ModelExtensions.cs b/osu.Game/Extensions/ModelExtensions.cs index eef9b63b62..ec6b5ac6de 100644 --- a/osu.Game/Extensions/ModelExtensions.cs +++ b/osu.Game/Extensions/ModelExtensions.cs @@ -9,6 +9,7 @@ using osu.Game.IO; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Scoring; +using osu.Game.Screens.Select.Leaderboards; using osu.Game.Users; namespace osu.Game.Extensions @@ -164,5 +165,20 @@ namespace osu.Game.Extensions /// that function does not have per-platform considerations (and is only made to work on windows). /// public static string GetValidFilename(this string filename) => invalid_filename_chars.Replace(filename, "_"); + + public static bool RequiresSupporter(this BeatmapLeaderboardScope scope, bool filterMods) + { + switch (scope) + { + case BeatmapLeaderboardScope.Local: + return false; + + case BeatmapLeaderboardScope.Country: + case BeatmapLeaderboardScope.Friend: + return true; + } + + return filterMods; + } } } diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index bd61992dbf..b54750c5c3 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; +using osu.Game.Extensions; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests; @@ -248,7 +249,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores return; } - if ((scope.Value != BeatmapLeaderboardScope.Global || modSelector.SelectedMods.Count > 0) && !userIsSupporter) + if (scope.Value.RequiresSupporter(modSelector.SelectedMods.Count > 0) && !userIsSupporter) { Scores = null; notSupporterPlaceholder.Show(); diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 57fe22aa59..9bf517cb77 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -138,7 +138,7 @@ namespace osu.Game.Screens.Select.Leaderboards return null; } - if (!api.LocalUser.Value.IsSupporter && (Scope != BeatmapLeaderboardScope.Global || filterMods)) + if (Scope.RequiresSupporter(filterMods) && !api.LocalUser.Value.IsSupporter) { SetErrorState(LeaderboardState.NotSupporter); return null; From 249bbd0b5975510725637529c86c76325d4aab27 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Mar 2025 16:26:54 +0900 Subject: [PATCH 792/801] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 21a3ddad0e..5b5482b3c7 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From b0cf5e8bff79e25333783ffa53f91041ca476576 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Mar 2025 15:08:23 +0900 Subject: [PATCH 793/801] Add support for team beatmap leaderboards --- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 1 + osu.Game/Localisation/LeaderboardStrings.cs | 5 +++ osu.Game/Online/Leaderboards/Leaderboard.cs | 3 ++ .../Online/Leaderboards/LeaderboardState.cs | 1 + .../BeatmapSet/LeaderboardScopeSelector.cs | 1 + .../BeatmapSet/Scores/NoScoresPlaceholder.cs | 4 +++ .../BeatmapSet/Scores/NoTeamPlaceholder.cs | 34 +++++++++++++++++++ .../BeatmapSet/Scores/ScoresContainer.cs | 16 +++++++++ .../Select/Leaderboards/BeatmapLeaderboard.cs | 7 ++++ .../Leaderboards/BeatmapLeaderboardScope.cs | 4 +++ .../Screens/Select/PlayBeatmapDetailArea.cs | 10 +++++- 11 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Overlays/BeatmapSet/Scores/NoTeamPlaceholder.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 62ca8bf831..474d2ee6e3 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -201,6 +201,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("ensure no scores displayed", () => leaderboard.SetScores(null)); AddStep(@"Network failure", () => leaderboard.SetErrorState(LeaderboardState.NetworkFailure)); + AddStep(@"No team", () => leaderboard.SetErrorState(LeaderboardState.NoTeam)); AddStep(@"No supporter", () => leaderboard.SetErrorState(LeaderboardState.NotSupporter)); AddStep(@"Not logged in", () => leaderboard.SetErrorState(LeaderboardState.NotLoggedIn)); AddStep(@"Ruleset unavailable", () => leaderboard.SetErrorState(LeaderboardState.RulesetUnavailable)); diff --git a/osu.Game/Localisation/LeaderboardStrings.cs b/osu.Game/Localisation/LeaderboardStrings.cs index 8e53f8e88c..816406bf31 100644 --- a/osu.Game/Localisation/LeaderboardStrings.cs +++ b/osu.Game/Localisation/LeaderboardStrings.cs @@ -44,6 +44,11 @@ namespace osu.Game.Localisation /// public static LocalisableString PleaseInvestInAnOsuSupporterTagToViewThisLeaderboard => new TranslatableString(getKey(@"please_invest_in_an_osu_supporter_tag_to_view_this_leaderboard"), @"Please invest in an osu!supporter tag to view this leaderboard!"); + /// + /// "You are not on a team. Maybe you should join one!" + /// + public static LocalisableString NoTeam => new TranslatableString(getKey(@"no_team"), @"You are not on a team. Maybe you should join one!"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 3c25d6f789..021a2b3959 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -356,6 +356,9 @@ namespace osu.Game.Online.Leaderboards case LeaderboardState.NotSupporter: return new MessagePlaceholder(LeaderboardStrings.PleaseInvestInAnOsuSupporterTagToViewThisLeaderboard); + case LeaderboardState.NoTeam: + return new MessagePlaceholder(LeaderboardStrings.NoTeam); + case LeaderboardState.Retrieving: return null; diff --git a/osu.Game/Online/Leaderboards/LeaderboardState.cs b/osu.Game/Online/Leaderboards/LeaderboardState.cs index 6b07500a98..dbd982acf2 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardState.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardState.cs @@ -14,5 +14,6 @@ namespace osu.Game.Online.Leaderboards NoScores, NotLoggedIn, NotSupporter, + NoTeam } } diff --git a/osu.Game/Overlays/BeatmapSet/LeaderboardScopeSelector.cs b/osu.Game/Overlays/BeatmapSet/LeaderboardScopeSelector.cs index 5cfe4a35b3..12fbc4c790 100644 --- a/osu.Game/Overlays/BeatmapSet/LeaderboardScopeSelector.cs +++ b/osu.Game/Overlays/BeatmapSet/LeaderboardScopeSelector.cs @@ -22,6 +22,7 @@ namespace osu.Game.Overlays.BeatmapSet AddItem(BeatmapLeaderboardScope.Global); AddItem(BeatmapLeaderboardScope.Country); AddItem(BeatmapLeaderboardScope.Friend); + AddItem(BeatmapLeaderboardScope.Team); } [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/BeatmapSet/Scores/NoScoresPlaceholder.cs b/osu.Game/Overlays/BeatmapSet/Scores/NoScoresPlaceholder.cs index 29a696593d..b161ee49c6 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/NoScoresPlaceholder.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/NoScoresPlaceholder.cs @@ -41,6 +41,10 @@ namespace osu.Game.Overlays.BeatmapSet.Scores case BeatmapLeaderboardScope.Country: text.Text = BeatmapsetsStrings.ShowScoreboardNoScoresCountry; break; + + case BeatmapLeaderboardScope.Team: + text.Text = BeatmapsetsStrings.ShowScoreboardNoScoresTeam; + break; } } } diff --git a/osu.Game/Overlays/BeatmapSet/Scores/NoTeamPlaceholder.cs b/osu.Game/Overlays/BeatmapSet/Scores/NoTeamPlaceholder.cs new file mode 100644 index 0000000000..0bd4a1334f --- /dev/null +++ b/osu.Game/Overlays/BeatmapSet/Scores/NoTeamPlaceholder.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Localisation; +using osuTK; + +namespace osu.Game.Overlays.BeatmapSet.Scores +{ + public partial class NoTeamPlaceholder : Container + { + public NoTeamPlaceholder() + { + AutoSizeAxes = Axes.Both; + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 20), + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = LeaderboardStrings.NoTeam, + }, + } + }; + } + } +} diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index b54750c5c3..9b9661f83d 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -41,6 +41,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores private readonly LeaderboardModSelector modSelector; private readonly NoScoresPlaceholder noScoresPlaceholder; private readonly NotSupporterPlaceholder notSupporterPlaceholder; + private readonly NoTeamPlaceholder noTeamPlaceholder; [Resolved] private IAPIProvider api { get; set; } @@ -155,6 +156,13 @@ namespace osu.Game.Overlays.BeatmapSet.Scores AlwaysPresent = true, Margin = new MarginPadding { Vertical = 10 } }, + noTeamPlaceholder = new NoTeamPlaceholder + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Margin = new MarginPadding { Vertical = 10 }, + Alpha = 0, + }, notSupporterPlaceholder = new NotSupporterPlaceholder { Anchor = Anchor.TopCentre, @@ -249,6 +257,13 @@ namespace osu.Game.Overlays.BeatmapSet.Scores return; } + if ((scope.Value == BeatmapLeaderboardScope.Team) && user.Value.Team == null) + { + Scores = null; + noTeamPlaceholder.Show(); + return; + } + if (scope.Value.RequiresSupporter(modSelector.SelectedMods.Count > 0) && !userIsSupporter) { Scores = null; @@ -256,6 +271,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores return; } + noTeamPlaceholder.Hide(); notSupporterPlaceholder.Hide(); Show(); diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 9bf517cb77..ec1ef33387 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -9,6 +9,7 @@ using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Extensions; using osu.Game.Online.API; @@ -144,6 +145,12 @@ namespace osu.Game.Screens.Select.Leaderboards return null; } + if (Scope == BeatmapLeaderboardScope.Team && api.LocalUser.Value.Team == null) + { + SetErrorState(LeaderboardState.NoTeam); + return null; + } + IReadOnlyList? requestMods = null; if (filterMods && !mods.Value.Any()) diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs index e2e3404877..aec22cc007 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs @@ -20,5 +20,9 @@ namespace osu.Game.Screens.Select.Leaderboards [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowScoreboardFriend))] Friend, + + // TODO: localise once localisations are updated + [Description("Team Ranking")] + Team, } } diff --git a/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs b/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs index deb1100dfc..5b62d5e8d7 100644 --- a/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs +++ b/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs @@ -83,6 +83,7 @@ namespace osu.Game.Screens.Select new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Global), new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Country), new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Friend), + new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Team), }).ToArray(); private BeatmapDetailAreaTabItem getTabItemFromTabType(TabType type) @@ -104,6 +105,9 @@ namespace osu.Game.Screens.Select case TabType.Friends: return new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Friend); + case TabType.Team: + return new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Team); + default: throw new ArgumentOutOfRangeException(nameof(type)); } @@ -131,6 +135,9 @@ namespace osu.Game.Screens.Select case BeatmapLeaderboardScope.Friend: return TabType.Friends; + case BeatmapLeaderboardScope.Team: + return TabType.Team; + default: throw new ArgumentOutOfRangeException(nameof(item)); } @@ -146,7 +153,8 @@ namespace osu.Game.Screens.Select Local, Country, Global, - Friends + Friends, + Team } } } From 3515f9a8f8c95b577d8ac79e6802df4547b1d0c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Mar 2025 08:36:19 +0100 Subject: [PATCH 794/801] Remove unused using --- osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index ec1ef33387..46705aaa28 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -9,7 +9,6 @@ using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Beatmaps; -using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Extensions; using osu.Game.Online.API; From c18764d764f2f18abfad5a4f3e98f1c0b6357055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Mar 2025 08:37:02 +0100 Subject: [PATCH 795/801] Use localisable string --- .../Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs index aec22cc007..a3687d9586 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs @@ -21,8 +21,7 @@ namespace osu.Game.Screens.Select.Leaderboards [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowScoreboardFriend))] Friend, - // TODO: localise once localisations are updated - [Description("Team Ranking")] + [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowScoreboardTeam))] Team, } } From f78e371b1b5194cd7ee74e79ef54926d6ac7f3a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Mar 2025 10:10:09 +0100 Subject: [PATCH 796/801] Simplify boolean flags --- .../Visual/Ranking/TestSceneResultsScreen.cs | 2 +- .../Visual/Ranking/TestSceneStatisticsPanel.cs | 1 - .../OnlinePlay/Multiplayer/MultiplayerPlayer.cs | 6 ++---- .../Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs | 3 +-- osu.Game/Screens/Play/SubmittingPlayer.cs | 3 +-- osu.Game/Screens/Ranking/ResultsScreen.cs | 12 ++++-------- .../Screens/Ranking/Statistics/StatisticsPanel.cs | 7 +------ 7 files changed, 10 insertions(+), 24 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index b19288fd99..4758b70526 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -404,7 +404,7 @@ namespace osu.Game.Tests.Visual.Ranking : base(score) { AllowRetry = true; - ShowUserStatistics = true; + IsLocalPlay = true; } protected override void LoadComplete() diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index a64fd488bc..02d30d12e6 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -214,7 +214,6 @@ namespace osu.Game.Tests.Visual.Ranking State = { Value = Visibility.Visible }, Score = { Value = score }, AchievedScore = score, - ShowUserTagControl = true, } }; }); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 67a67cf271..3d4b46f49e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -198,13 +198,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return multiplayerLeaderboard.TeamScores.Count == 2 ? new MultiplayerTeamResultsScreen(score, Room.RoomID.Value, PlaylistItem, multiplayerLeaderboard.TeamScores) { - ShowUserStatistics = true, - ShowUserTagControl = true, + IsLocalPlay = true, } : new MultiplayerResultsScreen(score, Room.RoomID.Value, PlaylistItem) { - ShowUserStatistics = true, - ShowUserTagControl = true, + IsLocalPlay = true, }; } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index 80b378bdcf..dc4078cb1f 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -59,8 +59,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return new PlaylistItemScoreResultsScreen(score, Room.RoomID.Value, PlaylistItem) { AllowRetry = true, - ShowUserStatistics = true, - ShowUserTagControl = true, + IsLocalPlay = true, }; } diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 04cf473173..dc3e5f08ac 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -328,8 +328,7 @@ namespace osu.Game.Screens.Play protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score) { AllowRetry = true, - ShowUserStatistics = true, - ShowUserTagControl = true, + IsLocalPlay = true, }; } } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index fcf90a3e28..6da731588f 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -79,13 +79,10 @@ namespace osu.Game.Screens.Ranking public bool AllowWatchingReplay { get; init; } = true; /// - /// Whether the user's personal statistics should be shown on the extended statistics panel - /// after clicking the score panel associated with the being presented. - /// Requires to be present. + /// Whether the provided score is for a local user's play. + /// This will trigger elements like the user's ranking to display. /// - public bool ShowUserStatistics { get; init; } - - public bool ShowUserTagControl { get; init; } + public bool IsLocalPlay { get; init; } private Sample? popInSample; @@ -127,8 +124,7 @@ namespace osu.Game.Screens.Ranking { RelativeSizeAxes = Axes.Both, Score = { BindTarget = SelectedScore }, - AchievedScore = ShowUserStatistics && Score != null ? Score : null, - ShowUserTagControl = ShowUserTagControl, + AchievedScore = IsLocalPlay && Score != null ? Score : null, }, ScorePanelList = new ScorePanelList { diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 16de00fcf1..0cee01cf77 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -37,11 +37,6 @@ namespace osu.Game.Screens.Ranking.Statistics /// public ScoreInfo? AchievedScore { get; init; } - /// - /// Whether to show a control that allows to assign user tags to the played beatmap. - /// - public bool ShowUserTagControl { get; init; } - protected override bool StartHidden => true; [Resolved] @@ -235,7 +230,7 @@ namespace osu.Game.Screens.Ranking.Statistics }); } - if (ShowUserTagControl + if (AchievedScore != null && newScore.BeatmapInfo!.OnlineID > 0 && api.IsLoggedIn) { From 5b1a4c8ed14bbf17baa11c87325226540a5fe170 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Mar 2025 10:13:22 +0100 Subject: [PATCH 797/801] Require high enough score rank for tagging beatmap --- .../Ranking/TestSceneStatisticsPanel.cs | 22 +++++++++++++++ .../Ranking/Statistics/StatisticsPanel.cs | 27 +++++++++++++++---- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index 02d30d12e6..168410fe4a 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -219,6 +219,28 @@ namespace osu.Game.Tests.Visual.Ranking }); } + [Test] + public void TestTaggingWhenRankTooLow() + { + var score = TestResources.CreateTestScoreInfo(); + score.Rank = ScoreRank.D; + + AddStep("load panel", () => + { + Child = new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Child = new StatisticsPanel + { + RelativeSizeAxes = Axes.Both, + State = { Value = Visibility.Visible }, + Score = { Value = score }, + AchievedScore = score, + } + }; + }); + } + private void loadPanel(ScoreInfo score) => AddStep("load panel", () => { Child = new StatisticsPanel diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 0cee01cf77..758eabcf2e 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Beatmaps; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; @@ -234,12 +235,28 @@ namespace osu.Game.Screens.Ranking.Statistics && newScore.BeatmapInfo!.OnlineID > 0 && api.IsLoggedIn) { - yield return new StatisticItem("Tag the beatmap!", () => new UserTagControl(newScore.BeatmapInfo) + if ( + // We may want to iterate on this condition + AchievedScore.Rank >= ScoreRank.C + ) { - RelativeSizeAxes = Axes.X, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }); + yield return new StatisticItem("Tag the beatmap!", () => new UserTagControl(newScore.BeatmapInfo) + { + RelativeSizeAxes = Axes.X, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + } + else + { + yield return new StatisticItem("Tag the beatmap!", () => new OsuTextFlowContainer(cp => cp.Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE, weight: FontWeight.SemiBold)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + TextAnchor = Anchor.Centre, + Text = "Set a better score to contribute to beatmap tags!", + }); + } } } From 723a22130d61061b3f0b9062ba49b89e4ff4c0c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Mar 2025 12:06:27 +0100 Subject: [PATCH 798/801] Fix test --- osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index 168410fe4a..df65023303 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -152,7 +152,9 @@ namespace osu.Game.Tests.Visual.Ranking } }); AddUntilStep("overall ranking present", () => this.ChildrenOfType().Any()); - AddUntilStep("loading spinner not visible", () => this.ChildrenOfType().All(l => l.State.Value == Visibility.Hidden)); + AddUntilStep("loading spinner not visible", + () => this.ChildrenOfType().Single() + .ChildrenOfType().All(l => l.State.Value == Visibility.Hidden)); } [Test] From 607c83abd69c09c8d5313f18ec67734151bc576a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Mar 2025 12:07:19 +0100 Subject: [PATCH 799/801] Switch add beatmap tag request to using path paramx See https://github.com/ppy/osu-web/pull/11999. --- osu.Game/Online/API/Requests/AddBeatmapTagRequest.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Online/API/Requests/AddBeatmapTagRequest.cs b/osu.Game/Online/API/Requests/AddBeatmapTagRequest.cs index 4fa02dc569..2dd9303841 100644 --- a/osu.Game/Online/API/Requests/AddBeatmapTagRequest.cs +++ b/osu.Game/Online/API/Requests/AddBeatmapTagRequest.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Globalization; using System.Net.Http; using osu.Framework.IO.Network; @@ -22,10 +21,9 @@ namespace osu.Game.Online.API.Requests { var req = base.CreateWebRequest(); req.Method = HttpMethod.Post; - req.AddParameter(@"tag_id", TagID.ToString(CultureInfo.InvariantCulture), RequestParameterType.Query); return req; } - protected override string Target => $@"beatmaps/{BeatmapID}/tags"; + protected override string Target => $@"beatmaps/{BeatmapID}/tags/{TagID}"; } } From 7715a3dd9f65049a140e450e59dbdec0560ae152 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 14 Mar 2025 19:11:15 +0900 Subject: [PATCH 800/801] Add failing test --- .../Visual/Multiplayer/TestSceneRoomListing.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs index 27c5758afa..45f1ff1acb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs @@ -11,6 +11,7 @@ using osu.Framework.Testing; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Taiko; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Tests.Visual.OnlinePlay; @@ -198,6 +199,22 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3, withPassword: true))); } + [Test] + public void TestFreestyleBypassesRulesetFilter() + { + AddStep("apply taiko filter", () => container.Filter.Value = new FilterCriteria { Ruleset = new TaikoRuleset().RulesetInfo }); + + AddStep("add osu + freestyle room", () => + { + var room = GenerateRooms(1, new OsuRuleset().RulesetInfo)[0]; + room.Playlist[0].Freestyle = true; + room.CurrentPlaylistItem = room.Playlist[0]; + rooms.Add(room); + }); + + AddAssert("room visible", () => container.DrawableRooms.Any()); + } + private bool checkRoomSelected(Room? room) => selectedRoom.Value == room; private Room? getRoomInFlow(int index) => From 145cff50911e5c48cc1fb7d11f5e92751f0faa66 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 14 Mar 2025 19:11:32 +0900 Subject: [PATCH 801/801] Filter freestyle rooms against all rulesets --- osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs index 0276601656..9835802fae 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs @@ -99,7 +99,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { bool matchingFilter = true; - matchingFilter &= criteria.Ruleset == null || r.Room.PlaylistItemStats?.RulesetIDs.Any(id => id == criteria.Ruleset.OnlineID) != false; + matchingFilter &= criteria.Ruleset == null || r.Room.CurrentPlaylistItem?.Freestyle == true || r.Room.PlaylistItemStats?.RulesetIDs.Any(id => id == criteria.Ruleset.OnlineID) != false; matchingFilter &= matchPermissions(r, criteria.Permissions); // Room name isn't translatable, so ToString() is used here for simplicity.