From 2bae93d7add0d6d24758040b46d3542200a40480 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 9 Dec 2024 01:59:16 -0500 Subject: [PATCH 01/93] 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 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 02/93] 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 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 03/93] 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 04/93] 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 05/93] 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 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 06/93] 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 07/93] 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 08/93] 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 zcmWIWW@Zs#U|`^2aIGwA+QJoLveOyo?d2NrfSbdu0sw2tiOMEEe~b! z{36g&5>^(Lr7R|$zqq}8chOFPKW5zaC*uv0%nc6L#Jyj2=-;snQx@4hF@=FAbtIWf z7uaPld803KNcf42wTXYmb^iyyzMQVD$v9PPa-}zjbuGV+h~(A$R~CvzYVI?=if?Ty zSX5xPd*h1#i#3<7J|)r`8)5A%c=hMC&C(b3!i_h-{OlXt+kD5B-O$}{ZurW?(_4Qs z1)%t(Q1NR`8PFGfK+FroAfIIBrRD2o73b%zeijrE@X7m(Z|J6=fK&QM^;d3;T#mN-<+=?)V1{?zSo%jxr`&!0|zR(Le8 z@~CQOrRvO1)tNsl=T&C@3~2s(r*YZ~*7WpePg9>QeQnwFhT&t0NWvtqn5_(`4%D6) zUf#ycz#t&Uz#s;6pl@Pko=2#QbAE0?eqL%`Nio=^Z)e8l-!c$sd;eQosf;_&&O>2x z)y0Dnm(6&4%l58Vb)v=jQDRPtb#CO<|90vZrZw#D)a5P7+Nn|b_J-xh#h>M7-&Q`n zqLy9HVDZ-NZL#Oq?UspMEZEpza%f7$YLlhMT6zTnuZ#cOy7AKIZI_SAgn0LMUis6I zmcXaX=g?}YkdgT9{H|x5{gSrrg*?nWf>(NfuPaS9{kt*s+5BA>Jv)6YWr96a-IwI? zToOEZQ9WYLCd~`UvxI_H+n=*bU^<=3(0_i(7im?KJvk>zOD7(EIrCa`*>HN!IgjuT6|kH(ywD!7<6 z?Y`%+Dy5TeTzo7&(^@KK+&TX~Z{L~_hZBt}%8OVR9$&cPWaN8EjrKxp4_<^E(@|-T4S4m+v%NED|3-6Sk z>k@b>aLbBY6Qs^DKARrPyYhkU?{bxsm)BaU$KNT~W740~qP0MZVZMQ#vq=-vzQem5V`-ETaI@+_vWsbT--K?QaW#i?K>4Xd!x2#e8&|zzM~6X zPrIqwUeNX<Vnlt-C&Rn#q6td_{k2+H#?H?N(@v~2Px$nhITCA(> z&SmU~3;V;mHupo#f7hM6UT14N{a=6b_v5JN|MjQu-E?*B-_!m&vS!j7SAAz*_wLA^ zJFVrW-0!DFpRv#J0y}g}w z`}3NF{>sHqXRckA8hP)afk|RW-c=LM1sk<0uI*9d*mZaAp0t_ueVg*j_oL<9Q`;Xm z7z1;xB@pugaX?ODWm;xxP70!I;quniI(O!L@TMSxD~umwyM#^)Pc90w{e0 From f59762f0cb4f199e4e00c034807e1084a3237edc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jan 2025 17:11:40 +0900 Subject: [PATCH 09/93] `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 10/93] 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 b2150739573b3e3f8ca27577b19b724c66722661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 Jan 2025 10:26:59 +0100 Subject: [PATCH 11/93] Add completion marker to daily challenge profile counter --- .../TestSceneUserProfileDailyChallenge.cs | 4 + .../Components/DailyChallengeStatsDisplay.cs | 120 +++++++++++++----- 2 files changed, 92 insertions(+), 32 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs index 0477d39193..ce62a3255d 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs @@ -38,6 +38,10 @@ namespace osu.Game.Tests.Visual.Online AddSliderStep("top 10%", 0, 999, 0, v => update(s => s.Top10PercentPlacements = v)); AddSliderStep("top 50%", 0, 999, 0, v => update(s => s.Top50PercentPlacements = v)); AddSliderStep("playcount", 0, 1500, 1, v => update(s => s.PlayCount = v)); + AddStep("user played today", () => update(s => s.LastUpdate = DateTimeOffset.UtcNow.Date)); + AddStep("user played yesterday", () => update(s => s.LastUpdate = DateTimeOffset.UtcNow.Date.AddDays(-1))); + AddStep("user is local user", () => update(s => s.UserID = API.LocalUser.Value.Id)); + AddStep("user is not local user", () => update(s => s.UserID = API.LocalUser.Value.Id + 1000)); AddStep("create", () => { Clear(); diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs index 3e86b2268f..ad64f7d7ac 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.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.LocalisationExtensions; @@ -8,11 +9,14 @@ 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.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Localisation; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osuTK; namespace osu.Game.Overlays.Profile.Header.Components { @@ -23,6 +27,11 @@ namespace osu.Game.Overlays.Profile.Header.Components public DailyChallengeTooltipData? TooltipContent { get; private set; } private OsuSpriteText dailyPlayCount = null!; + private Container content = null!; + private CircularContainer completionMark = null!; + + [Resolved] + private IAPIProvider api { get; set; } [Resolved] private OsuColour colours { get; set; } = null!; @@ -34,58 +43,91 @@ namespace osu.Game.Overlays.Profile.Header.Components private void load() { AutoSizeAxes = Axes.Both; - CornerRadius = 5; - Masking = true; InternalChildren = new Drawable[] { - new Box + content = new Container { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background4, - }, - new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Padding = new MarginPadding(5f), AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, + CornerRadius = 6, + BorderThickness = 2, + BorderColour = colourProvider.Background4, + Masking = true, Children = new Drawable[] { - new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12)) + new Box { - AutoSizeAxes = Axes.Both, - // can't use this because osu-web does weird stuff with \\n. - // Text = UsersStrings.ShowDailyChallengeTitle., - Text = "Daily\nChallenge", - Margin = new MarginPadding { Horizontal = 5f, Bottom = 2f }, + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, }, - new Container + new FillFlowContainer { - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - CornerRadius = 5f, - Masking = true, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding(3f), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, Children = new Drawable[] { - new Box + new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12)) { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background6, + AutoSizeAxes = Axes.Both, + // can't use this because osu-web does weird stuff with \\n. + // Text = UsersStrings.ShowDailyChallengeTitle., + Text = "Daily\nChallenge", + Margin = new MarginPadding { Horizontal = 5f, Bottom = 2f }, }, - dailyPlayCount = new OsuSpriteText + new Container { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - UseFullGlyphHeight = false, - Colour = colourProvider.Content2, - Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f }, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + CornerRadius = 3, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6, + }, + dailyPlayCount = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + UseFullGlyphHeight = false, + Colour = colourProvider.Content2, + Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f }, + }, + } }, } }, } }, + completionMark = new CircularContainer + { + Alpha = 0, + Size = new Vector2(16), + Anchor = Anchor.TopRight, + Origin = Anchor.Centre, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Lime1, + }, + new SpriteIcon + { + Size = new Vector2(8), + Colour = colourProvider.Background6, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.Check, + } + } + }, }; } @@ -114,6 +156,20 @@ namespace osu.Game.Overlays.Profile.Header.Components dailyPlayCount.Text = DailyChallengeStatsDisplayStrings.UnitDay(stats.PlayCount.ToLocalisableString("N0")); dailyPlayCount.Colour = colours.ForRankingTier(DailyChallengeStatsTooltip.TierForPlayCount(stats.PlayCount)); + bool playedToday = stats.LastUpdate?.Date == DateTimeOffset.UtcNow.Date; + bool userIsOnOwnProfile = stats.UserID == api.LocalUser.Value.Id; + + if (playedToday && userIsOnOwnProfile) + { + completionMark.Alpha = 1; + content.BorderColour = colours.Lime1; + } + else + { + completionMark.Alpha = 0; + content.BorderColour = colourProvider.Background4; + } + TooltipContent = new DailyChallengeTooltipData(colourProvider, stats); Show(); From a67a68c5969e61349a8d5866dd9c946bbf39c823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 Jan 2025 10:40:26 +0100 Subject: [PATCH 12/93] Remove unnecessary masking spec It was clipping the daily challenge completion checkmark, and it originates in some veeeeery old code where the profile overlay looked and behaved very differently (0fa02718786a0eefa063cce18e9e5351f509ab59). --- osu.Game/Overlays/Profile/Header/Components/MainDetails.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs index 4bdd5425c0..10bb69f0f5 100644 --- a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs +++ b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs @@ -41,7 +41,6 @@ namespace osu.Game.Overlays.Profile.Header.Components AutoSizeAxes = Axes.Y, AutoSizeDuration = 200, AutoSizeEasing = Easing.OutQuint, - Masking = true, Direction = FillDirection.Vertical, Spacing = new Vector2(0, 15), Children = new Drawable[] 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 13/93] 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 b79e937d2dbf0d9363833c7d725a5ab4c5d9f28d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 Jan 2025 13:34:16 +0100 Subject: [PATCH 14/93] Fix code quality --- .../Profile/Header/Components/DailyChallengeStatsDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs index ad64f7d7ac..a9d982e17f 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs @@ -31,7 +31,7 @@ namespace osu.Game.Overlays.Profile.Header.Components private CircularContainer completionMark = null!; [Resolved] - private IAPIProvider api { get; set; } + private IAPIProvider api { get; set; } = null!; [Resolved] private OsuColour colours { get; set; } = null!; From ebca2e4b4ffc2bee95016e4fac4063dc5bc78405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 Jan 2025 13:33:59 +0100 Subject: [PATCH 15/93] Implement precise movement tool As mentioned in one of the points in https://github.com/ppy/osu/discussions/31263. --- .../Edit/PreciseMovementPopover.cs | 190 ++++++++++++++++++ .../Edit/TransformToolboxGroup.cs | 25 ++- .../UserInterfaceV2/SliderWithTextBoxInput.cs | 5 + .../Input/Bindings/GlobalActionContainer.cs | 6 +- .../GlobalActionKeyBindingStrings.cs | 5 + 5 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs new file mode 100644 index 0000000000..151ca31ac0 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs @@ -0,0 +1,190 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Input.Events; +using osu.Game.Extensions; +using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; +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.Screens.Edit; +using osu.Game.Utils; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Edit +{ + public partial class PreciseMovementPopover : OsuPopover + { + [Resolved] + private EditorBeatmap editorBeatmap { get; set; } = null!; + + private readonly Dictionary initialPositions = new Dictionary(); + private RectangleF initialSurroundingQuad; + + private BindableNumber xBindable = null!; + private BindableNumber yBindable = null!; + + private SliderWithTextBoxInput xInput = null!; + private OsuCheckbox relativeCheckbox = null!; + + public PreciseMovementPopover() + { + AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight }; + } + + [BackgroundDependencyLoader] + private void load() + { + Child = new FillFlowContainer + { + Width = 220, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(20), + Children = new Drawable[] + { + xInput = new SliderWithTextBoxInput("X:") + { + Current = xBindable = new BindableNumber + { + Precision = 1, + }, + Instantaneous = true, + TabbableContentContainer = this, + }, + new SliderWithTextBoxInput("Y:") + { + Current = yBindable = new BindableNumber + { + Precision = 1, + }, + Instantaneous = true, + TabbableContentContainer = this, + }, + relativeCheckbox = new OsuCheckbox(false) + { + RelativeSizeAxes = Axes.X, + LabelText = "Relative movement", + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ScheduleAfterChildren(() => + { + xInput.TakeFocus(); + xInput.SelectAll(); + }); + } + + protected override void PopIn() + { + base.PopIn(); + editorBeatmap.BeginChange(); + initialPositions.AddRange(editorBeatmap.SelectedHitObjects.Where(ho => ho is not Spinner).Select(ho => new KeyValuePair(ho, ((IHasPosition)ho).Position))); + initialSurroundingQuad = GeometryUtils.GetSurroundingQuad(initialPositions.Keys.Cast()).AABBFloat; + + Debug.Assert(initialPositions.Count > 0); + + if (initialPositions.Count > 1) + { + relativeCheckbox.Current.Value = true; + relativeCheckbox.Current.Disabled = true; + } + + relativeCheckbox.Current.BindValueChanged(_ => relativeChanged(), true); + xBindable.BindValueChanged(_ => applyPosition()); + yBindable.BindValueChanged(_ => applyPosition()); + } + + protected override void PopOut() + { + base.PopOut(); + if (IsLoaded) editorBeatmap.EndChange(); + } + + private void relativeChanged() + { + // reset bindable bounds to something that is guaranteed to be larger than any previous value. + // this prevents crashes that can happen in the middle of changing the bounds, as updating both bound ends at the same is not atomic - + // if the old and new bounds are disjoint, assigning X first can produce a situation where MinValue > MaxValue. + (xBindable.MinValue, xBindable.MaxValue) = (float.MinValue, float.MaxValue); + (yBindable.MinValue, yBindable.MaxValue) = (float.MinValue, float.MaxValue); + + float previousX = xBindable.Value; + float previousY = yBindable.Value; + + 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.Default = yBindable.Default = 0; + + if (initialPositions.Count == 1) + { + var initialPosition = initialPositions.Single().Value; + xBindable.Value = previousX - initialPosition.X; + yBindable.Value = previousY - initialPosition.Y; + } + } + else + { + Debug.Assert(initialPositions.Count == 1); + var initialPosition = initialPositions.Single().Value; + + 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); + + xBindable.Default = initialPosition.X; + yBindable.Default = initialPosition.Y; + + xBindable.Value = xBindable.Default + previousX; + yBindable.Value = yBindable.Default + previousY; + } + } + + private void applyPosition() + { + editorBeatmap.PerformOnSelection(ho => + { + if (!initialPositions.TryGetValue(ho, out var initialPosition)) + return; + + var pos = new Vector2(xBindable.Value, yBindable.Value); + if (relativeCheckbox.Current.Value) + ((IHasPosition)ho).Position = initialPosition + pos; + else + ((IHasPosition)ho).Position = pos; + }); + } + + public override bool OnPressed(KeyBindingPressEvent e) + { + if (e.Action == GlobalAction.Select && !e.Repeat) + { + this.HidePopover(); + return true; + } + + return base.OnPressed(e); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs index a41412cbe3..440e06598d 100644 --- a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -10,6 +11,9 @@ using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -18,9 +22,12 @@ namespace osu.Game.Rulesets.Osu.Edit { public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler { + private readonly BindableList selectedHitObjects = new BindableList(); + private readonly BindableBool canMove = new BindableBool(); private readonly AggregateBindable canRotate = new AggregateBindable((x, y) => x || y); private readonly AggregateBindable canScale = new AggregateBindable((x, y) => x || y); + private EditorToolButton moveButton = null!; private EditorToolButton rotateButton = null!; private EditorToolButton scaleButton = null!; @@ -35,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Edit } [BackgroundDependencyLoader] - private void load() + private void load(EditorBeatmap editorBeatmap) { Child = new FillFlowContainer { @@ -44,20 +51,27 @@ namespace osu.Game.Rulesets.Osu.Edit Spacing = new Vector2(5), Children = new Drawable[] { + moveButton = new EditorToolButton("Move", + () => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt }, + () => new PreciseMovementPopover()), rotateButton = new EditorToolButton("Rotate", () => new SpriteIcon { Icon = FontAwesome.Solid.Undo }, () => new PreciseRotationPopover(RotationHandler, GridToolbox)), scaleButton = new EditorToolButton("Scale", - () => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt }, + () => new SpriteIcon { Icon = FontAwesome.Solid.ExpandArrowsAlt }, () => new PreciseScalePopover(ScaleHandler, GridToolbox)) } }; + + selectedHitObjects.BindTo(editorBeatmap.SelectedHitObjects); } protected override void LoadComplete() { base.LoadComplete(); + selectedHitObjects.BindCollectionChanged((_, _) => canMove.Value = selectedHitObjects.Any(ho => ho is not Spinner), true); + canRotate.AddSource(RotationHandler.CanRotateAroundPlayfieldOrigin); canRotate.AddSource(RotationHandler.CanRotateAroundSelectionOrigin); @@ -67,6 +81,7 @@ namespace osu.Game.Rulesets.Osu.Edit // bindings to `Enabled` on the buttons are decoupled on purpose // due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set. + canMove.BindValueChanged(move => moveButton.Enabled.Value = move.NewValue, true); canRotate.Result.BindValueChanged(rotate => rotateButton.Enabled.Value = rotate.NewValue, true); canScale.Result.BindValueChanged(scale => scaleButton.Enabled.Value = scale.NewValue, true); } @@ -77,6 +92,12 @@ namespace osu.Game.Rulesets.Osu.Edit switch (e.Action) { + case GlobalAction.EditorToggleMoveControl: + { + moveButton.TriggerClick(); + return true; + } + case GlobalAction.EditorToggleRotateControl: { if (!RotationHandler.OperationInProgress.Value || rotateButton.Selected.Value) diff --git a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs index 50d8d763e1..c16a6c612d 100644 --- a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs +++ b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs @@ -32,6 +32,11 @@ namespace osu.Game.Graphics.UserInterfaceV2 set => slider.Current = value; } + public CompositeDrawable TabbableContentContainer + { + set => textBox.TabbableContentContainer = value; + } + private bool instantaneous; /// diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 5e509d2035..6c130ff309 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -144,6 +144,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelUp }, GlobalAction.EditorIncreaseDistanceSpacing), new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelDown }, GlobalAction.EditorCyclePreviousBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelUp }, GlobalAction.EditorCycleNextBeatSnapDivisor), + new KeyBinding(InputKey.None, GlobalAction.EditorToggleMoveControl), new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl), new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl), new KeyBinding(new[] { InputKey.Control, InputKey.Left }, GlobalAction.EditorSeekToPreviousHitObject), @@ -493,7 +494,10 @@ namespace osu.Game.Input.Bindings EditorSeekToNextBookmark, [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.AbsoluteScrollSongList))] - AbsoluteScrollSongList + AbsoluteScrollSongList, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleMoveControl))] + EditorToggleMoveControl, } public enum GlobalActionCategory diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 436a2be648..5713df57c9 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -454,6 +454,11 @@ namespace osu.Game.Localisation /// public static LocalisableString AbsoluteScrollSongList => new TranslatableString(getKey(@"absolute_scroll_song_list"), @"Absolute scroll song list"); + /// + /// "Toggle movement control" + /// + public static LocalisableString EditorToggleMoveControl => new TranslatableString(getKey(@"editor_toggle_move_control"), @"Toggle movement control"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } 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 16/93] 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 17/93] 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 18/93] 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 19/93] 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 20/93] 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 b5b407fe7ca888ae1a9a8297767646e3bb60b2c0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jan 2025 15:40:38 +0900 Subject: [PATCH 21/93] Knock some sense into daily challenge profile test scene --- .../TestSceneUserProfileDailyChallenge.cs | 53 ++++++++++++------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs index ce62a3255d..2be9c1ab14 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Profile; @@ -20,28 +21,16 @@ namespace osu.Game.Tests.Visual.Online public partial class TestSceneUserProfileDailyChallenge : OsuManualInputManagerTestScene { [Cached] - public readonly Bindable User = new Bindable(new UserProfileData(new APIUser(), new OsuRuleset().RulesetInfo)); + private readonly Bindable userProfileData = new Bindable(new UserProfileData(new APIUser(), new OsuRuleset().RulesetInfo)); [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); - protected override void LoadComplete() + private DailyChallengeStatsDisplay display = null!; + + [SetUpSteps] + public void SetUpSteps() { - base.LoadComplete(); - - DailyChallengeStatsDisplay display = null!; - - AddSliderStep("daily", 0, 999, 2, v => update(s => s.DailyStreakCurrent = v)); - AddSliderStep("daily best", 0, 999, 2, v => update(s => s.DailyStreakBest = v)); - AddSliderStep("weekly", 0, 250, 1, v => update(s => s.WeeklyStreakCurrent = v)); - AddSliderStep("weekly best", 0, 250, 1, v => update(s => s.WeeklyStreakBest = v)); - AddSliderStep("top 10%", 0, 999, 0, v => update(s => s.Top10PercentPlacements = v)); - AddSliderStep("top 50%", 0, 999, 0, v => update(s => s.Top50PercentPlacements = v)); - AddSliderStep("playcount", 0, 1500, 1, v => update(s => s.PlayCount = v)); - AddStep("user played today", () => update(s => s.LastUpdate = DateTimeOffset.UtcNow.Date)); - AddStep("user played yesterday", () => update(s => s.LastUpdate = DateTimeOffset.UtcNow.Date.AddDays(-1))); - AddStep("user is local user", () => update(s => s.UserID = API.LocalUser.Value.Id)); - AddStep("user is not local user", () => update(s => s.UserID = API.LocalUser.Value.Id + 1000)); AddStep("create", () => { Clear(); @@ -55,16 +44,40 @@ namespace osu.Game.Tests.Visual.Online Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(1f), - User = { BindTarget = User }, + User = { BindTarget = userProfileData }, }); }); + + AddStep("set local user", () => update(s => s.UserID = API.LocalUser.Value.Id)); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + AddSliderStep("daily", 0, 999, 2, v => update(s => s.DailyStreakCurrent = v)); + AddSliderStep("daily best", 0, 999, 2, v => update(s => s.DailyStreakBest = v)); + AddSliderStep("weekly", 0, 250, 1, v => update(s => s.WeeklyStreakCurrent = v)); + AddSliderStep("weekly best", 0, 250, 1, v => update(s => s.WeeklyStreakBest = v)); + AddSliderStep("top 10%", 0, 999, 0, v => update(s => s.Top10PercentPlacements = v)); + AddSliderStep("top 50%", 0, 999, 0, v => update(s => s.Top50PercentPlacements = v)); + AddSliderStep("playcount", 0, 1500, 1, v => update(s => s.PlayCount = v)); + } + + [Test] + public void TestStates() + { + AddStep("played today", () => update(s => s.LastUpdate = DateTimeOffset.UtcNow.Date)); + AddStep("played yesterday", () => update(s => s.LastUpdate = DateTimeOffset.UtcNow.Date.AddDays(-1))); + AddStep("change to non-local user", () => update(s => s.UserID = API.LocalUser.Value.Id + 1000)); + AddStep("hover", () => InputManager.MoveMouseTo(display)); } private void update(Action change) { - change.Invoke(User.Value!.User.DailyChallengeStatistics); - User.Value = new UserProfileData(User.Value.User, User.Value.Ruleset); + change.Invoke(userProfileData.Value!.User.DailyChallengeStatistics); + userProfileData.Value = new UserProfileData(userProfileData.Value.User, userProfileData.Value.Ruleset); } [Test] From 04ba686be5f3abbe93ddfc7e59395f1a0b2d9f11 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jan 2025 15:47:47 +0900 Subject: [PATCH 22/93] Add basic animation --- .../Header/Components/DailyChallengeStatsDisplay.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs index a9d982e17f..a3dce89ad4 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs @@ -161,12 +161,21 @@ namespace osu.Game.Overlays.Profile.Header.Components if (playedToday && userIsOnOwnProfile) { - completionMark.Alpha = 1; + if (completionMark.Alpha > 0.8f) + { + completionMark.ScaleTo(1.2f).ScaleTo(1, 800, Easing.OutElastic); + } + else + { + completionMark.FadeIn(500, Easing.OutExpo); + completionMark.ScaleTo(1.6f).ScaleTo(1, 500, Easing.OutExpo); + } + content.BorderColour = colours.Lime1; } else { - completionMark.Alpha = 0; + completionMark.FadeOut(50); content.BorderColour = colourProvider.Background4; } From dcdb8d13a998b049a377b93a2deed8d92e42562c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jan 2025 16:17:39 +0900 Subject: [PATCH 23/93] Always select text when an editor slider-textbox is focused --- osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs | 6 +----- osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs | 6 +----- osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs | 6 +----- osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs | 3 +-- 4 files changed, 4 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs index 151ca31ac0..f2cb8794b5 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs @@ -85,11 +85,7 @@ namespace osu.Game.Rulesets.Osu.Edit { base.LoadComplete(); - ScheduleAfterChildren(() => - { - xInput.TakeFocus(); - xInput.SelectAll(); - }); + ScheduleAfterChildren(() => xInput.TakeFocus()); } protected override void PopIn() diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index 477d3b4e57..ae8ad2c01b 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -96,11 +96,7 @@ namespace osu.Game.Rulesets.Osu.Edit { base.LoadComplete(); - ScheduleAfterChildren(() => - { - angleInput.TakeFocus(); - angleInput.SelectAll(); - }); + ScheduleAfterChildren(() => angleInput.TakeFocus()); angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue }); rotationHandler.CanRotateAroundSelectionOrigin.BindValueChanged(e => diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index e728290289..ac6d9fbb19 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -139,11 +139,7 @@ namespace osu.Game.Rulesets.Osu.Edit { base.LoadComplete(); - ScheduleAfterChildren(() => - { - scaleInput.TakeFocus(); - scaleInput.SelectAll(); - }); + ScheduleAfterChildren(() => scaleInput.TakeFocus()); scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue }); xCheckBox.Current.BindValueChanged(_ => diff --git a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs index c16a6c612d..2fbe3ae89b 100644 --- a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs +++ b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs @@ -74,6 +74,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 textBox = new LabelledTextBox { Label = labelText, + SelectAllOnFocus = true, }, slider = new SettingsSlider { @@ -92,8 +93,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 public bool TakeFocus() => GetContainingFocusManager()?.ChangeFocus(textBox) == true; - public bool SelectAll() => textBox.SelectAll(); - private bool updatingFromTextBox; private void textChanged(ValueChangedEvent change) From 89586d5ab25cb7108ed71d7c516debf9950f60cf Mon Sep 17 00:00:00 2001 From: Rudi Herouard Date: Mon, 20 Jan 2025 13:43:45 +0100 Subject: [PATCH 24/93] 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 25/93] 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 46ff9d1aad2d70616114a6b6075b1bdbe6a8f0f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jan 2025 14:13:42 +0900 Subject: [PATCH 26/93] 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 27/93] 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 28/93] 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 29/93] 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 30/93] 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 31/93] 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 32/93] 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 33/93] 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 34/93] 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 35/93] 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 36/93] 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 37/93] 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 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 38/93] 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 2a5a2738e152e4d23835e0c618873792ed57f148 Mon Sep 17 00:00:00 2001 From: Layendan Date: Tue, 21 Jan 2025 12:45:23 -0700 Subject: [PATCH 39/93] 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 40/93] 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 41/93] 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 42/93] 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 43/93] 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 44/93] 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 45/93] 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 46/93] 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 47/93] 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 f673d16a1f97d153f74cfbd6e8549886552910cb Mon Sep 17 00:00:00 2001 From: Layendan Date: Wed, 22 Jan 2025 11:42:11 -0700 Subject: [PATCH 48/93] 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 49/93] 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 50/93] 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 51/93] 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 52/93] `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 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 53/93] 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 c67c0a7fc02d29a093821d00b95a249e73f01a4a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 16:07:18 +0900 Subject: [PATCH 54/93] 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 55/93] 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 56/93] 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 57/93] 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 58/93] 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 59/93] 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 60/93] 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 2feab314267ae017cce1334ed8e83ba4fdfc0ec7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 22:41:20 +0900 Subject: [PATCH 61/93] 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 62/93] `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 63/93] 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 64/93] 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 65/93] 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 66/93] 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 67/93] 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 68/93] 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 69/93] 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 70/93] 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 71/93] 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 72/93] 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 73/93] 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 74/93] 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 75/93] 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 92429b2ed9e8f7a658196659656aeb9ec7dcd14d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 18:34:04 +0900 Subject: [PATCH 76/93] 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 77/93] 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 0164a2e4dca86fed1f3ea016eb9b1e4084eebba1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 19:02:31 +0900 Subject: [PATCH 78/93] 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 79/93] 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 80/93] 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 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 81/93] 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 82/93] 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 83/93] 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 354126b7f7684a052389fff61715118a6fe3d885 Mon Sep 17 00:00:00 2001 From: ThePooN Date: Fri, 24 Jan 2025 18:14:55 +0100 Subject: [PATCH 84/93] =?UTF-8?q?=F0=9F=94=A7=20Specify=20we're=20not=20us?= =?UTF-8?q?ing=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 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 85/93] 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 86/93] 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 87/93] 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 88/93] 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 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 89/93] 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 1aa1137b09cc649b1e99d2f0eb18b846feb249ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Jan 2025 21:22:51 +0900 Subject: [PATCH 90/93] 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 91/93] 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 92/93] 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 fd1d90cbd93ffc0cc9be5c3d18035e78613e0d06 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 11:55:35 +0900 Subject: [PATCH 93/93] 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 - +