From 4d09e94367ef308c031750aa1e96d1ace1ad1df4 Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Tue, 10 Sep 2024 11:46:34 -0400 Subject: [PATCH 001/370] Initial implementation --- .../Database/RealmArchiveModelImporter.cs | 4 +- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 88 +++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmArchiveModelImporter.cs b/osu.Game/Database/RealmArchiveModelImporter.cs index cf0625c51c..901782238c 100644 --- a/osu.Game/Database/RealmArchiveModelImporter.cs +++ b/osu.Game/Database/RealmArchiveModelImporter.cs @@ -188,7 +188,9 @@ namespace osu.Game.Database Directory.CreateDirectory(mountedPath); - foreach (var realmFile in model.Files) + // Detach files from the model to avoid realm contention when copying to the external location. + // This is safe as we are not modifying the model in any way. + foreach (var realmFile in model.Files.Detach()) { string sourcePath = Files.Storage.GetFullPath(realmFile.File.GetStoragePath()); string destinationPath = Path.Join(mountedPath, realmFile.Filename); diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 6f7781ee9c..1e2dbf2ffd 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -11,6 +11,7 @@ using Newtonsoft.Json; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; @@ -18,6 +19,8 @@ using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Framework.Logging; +using osu.Framework.Platform; using Web = osu.Game.Resources.Localisation.Web; using osu.Framework.Testing; using osu.Game.Database; @@ -58,6 +61,9 @@ namespace osu.Game.Overlays.SkinEditor [Resolved] private OsuGame? game { get; set; } + [Resolved] + private GameHost host { get; set; } = null!; + [Resolved] private SkinManager skins { get; set; } = null!; @@ -84,6 +90,7 @@ namespace osu.Game.Overlays.SkinEditor private SkinEditorChangeHandler? changeHandler; + private EditorMenuItem mountMenuItem = null!; private EditorMenuItem undoMenuItem = null!; private EditorMenuItem redoMenuItem = null!; @@ -157,6 +164,7 @@ namespace osu.Game.Overlays.SkinEditor { new EditorMenuItem(Web.CommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()), new EditorMenuItem(CommonStrings.Export, MenuItemType.Standard, () => skins.ExportCurrentSkin()) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, + mountMenuItem = new EditorMenuItem("Edit externally", MenuItemType.Standard, () => _ = editExternally()) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, new OsuMenuItemSpacer(), new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, () => dialogOverlay?.Push(new RevertConfirmDialog(revert))), new OsuMenuItemSpacer(), @@ -274,6 +282,86 @@ namespace osu.Game.Overlays.SkinEditor selectedTarget.BindValueChanged(targetChanged, true); } + private ExternalEditOperation? externalEditOperation; + + private async Task editExternally() + { + var skin = currentSkin.Value.SkinInfo.PerformRead(s => s.Detach()); + + try + { + externalEditOperation = await skins.BeginExternalEditing(skin).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.Log($"Failed to initialize external edit operation: {ex}", LoggingTarget.Database, LogLevel.Error); + } + + if (externalEditOperation == null) + return; + + host.OpenFileExternally(externalEditOperation.MountedPath.TrimDirectorySeparator() + Path.DirectorySeparatorChar); + + mountMenuItem.Text.Value = "Finish external edit"; + mountMenuItem.Action.Value = () => _ = finishExternalEdit(); + } + + private async Task finishExternalEdit() + { + if (externalEditOperation == null || !externalEditOperation.IsMounted) + return; + + // TODO: The cache is not being invalidated, resulting in there being no visual change after the skin is updated. I don't know how to work with the cache, so I'm leaving it like this for now. + await Task.Run(() => + { + currentSkin.Value.SkinInfo.PerformWrite(skinInfo => + { + var filesInSkin = skinInfo.Files.Select(f => f.Filename).ToHashSet(); + var filesInMounted = Directory.EnumerateFiles(externalEditOperation.MountedPath, "*.*", SearchOption.AllDirectories).Select(f => Path.GetRelativePath(externalEditOperation.MountedPath, f)).ToHashSet(); + + // Enumerate over every file in the skin. If it's not in the mounted directory, it was deleted and should be removed from the skin. + var filesToDelete = filesInSkin.Except(filesInMounted).ToList(); + + foreach (string file in filesToDelete) + { + var fileToDelete = skinInfo.Files.First(f => f.Filename == file); + skins.DeleteFile(skinInfo, fileToDelete); + } + + // Enumerate over every file in the mounted directory. If the file is not in the skin, it should be added. If it is, the hashes should be compared, and the file should be updated if necessary. + var filesToAddOrUpdate = filesInMounted.Except(filesInSkin).ToList(); + var filesToUpdate = filesInMounted.Intersect(filesInSkin).ToList(); + + foreach (string file in filesToAddOrUpdate) + { + using var stream = File.OpenRead(Path.Combine(externalEditOperation.MountedPath, file)); + skins.AddFile(skinInfo, stream, file); + } + + foreach (string newFile in filesToUpdate) + { + var existingFile = skinInfo.Files.First(f => f.Filename == newFile); + string newFileAbsolutePath = Path.Combine(externalEditOperation.MountedPath, newFile); + string? hash = File.ReadAllText(newFileAbsolutePath).ComputeSHA2Hash(); + + if (hash == existingFile.File.Hash) continue; + + using var stream = File.OpenRead(newFileAbsolutePath); + skins.AddFile(skinInfo, stream, existingFile.Filename); + } + }); + }).ConfigureAwait(false); + + try + { + Directory.Delete(externalEditOperation.MountedPath, true); + } + catch { } + + mountMenuItem.Text.Value = "Edit externally"; + mountMenuItem.Action.Value = () => _ = editExternally(); + } + public bool OnPressed(KeyBindingPressEvent e) { switch (e.Action) From 9de248f5d718d58830d8bb08dafe7d05bbd73a02 Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Thu, 12 Sep 2024 23:04:19 -0400 Subject: [PATCH 002/370] Fix changes not being applied instantly, Improve import performance, lay down framework for abstracting logic --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 73 +++++++++++----------- osu.Game/Skinning/SkinImporter.cs | 7 +++ 2 files changed, 44 insertions(+), 36 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 1e2dbf2ffd..a2f64d0a9c 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -286,6 +286,7 @@ namespace osu.Game.Overlays.SkinEditor private async Task editExternally() { + mountMenuItem.Action.Disabled = true; var skin = currentSkin.Value.SkinInfo.PerformRead(s => s.Detach()); try @@ -302,8 +303,12 @@ namespace osu.Game.Overlays.SkinEditor host.OpenFileExternally(externalEditOperation.MountedPath.TrimDirectorySeparator() + Path.DirectorySeparatorChar); - mountMenuItem.Text.Value = "Finish external edit"; - mountMenuItem.Action.Value = () => _ = finishExternalEdit(); + Schedule(() => + { + mountMenuItem.Action.Disabled = false; + mountMenuItem.Text.Value = "Finish external edit"; + mountMenuItem.Action.Value = () => _ = finishExternalEdit(); + }); } private async Task finishExternalEdit() @@ -311,55 +316,51 @@ namespace osu.Game.Overlays.SkinEditor if (externalEditOperation == null || !externalEditOperation.IsMounted) return; - // TODO: The cache is not being invalidated, resulting in there being no visual change after the skin is updated. I don't know how to work with the cache, so I'm leaving it like this for now. + mountMenuItem.Action.Disabled = true; + await Task.Run(() => { currentSkin.Value.SkinInfo.PerformWrite(skinInfo => { - var filesInSkin = skinInfo.Files.Select(f => f.Filename).ToHashSet(); - var filesInMounted = Directory.EnumerateFiles(externalEditOperation.MountedPath, "*.*", SearchOption.AllDirectories).Select(f => Path.GetRelativePath(externalEditOperation.MountedPath, f)).ToHashSet(); + // Clear files in the skin + skinInfo.Files.Clear(); - // Enumerate over every file in the skin. If it's not in the mounted directory, it was deleted and should be removed from the skin. - var filesToDelete = filesInSkin.Except(filesInMounted).ToList(); + // Get all the files in the mounted directory and add them to the skin + string[] filesInMounted = Directory.EnumerateFiles(externalEditOperation.MountedPath, "*.*", SearchOption.AllDirectories).Select(f => Path.GetRelativePath(externalEditOperation.MountedPath, f)).ToArray(); - foreach (string file in filesToDelete) - { - var fileToDelete = skinInfo.Files.First(f => f.Filename == file); - skins.DeleteFile(skinInfo, fileToDelete); - } - - // Enumerate over every file in the mounted directory. If the file is not in the skin, it should be added. If it is, the hashes should be compared, and the file should be updated if necessary. - var filesToAddOrUpdate = filesInMounted.Except(filesInSkin).ToList(); - var filesToUpdate = filesInMounted.Intersect(filesInSkin).ToList(); - - foreach (string file in filesToAddOrUpdate) + foreach (string file in filesInMounted) { using var stream = File.OpenRead(Path.Combine(externalEditOperation.MountedPath, file)); + + // The GetFile call in this method is really expensive, and we are certain that the file does not exist in the skin yet. + // Consider adding a method to add a file without checking if it exists. Or add the file directly to the skin. skins.AddFile(skinInfo, stream, file); } - - foreach (string newFile in filesToUpdate) - { - var existingFile = skinInfo.Files.First(f => f.Filename == newFile); - string newFileAbsolutePath = Path.Combine(externalEditOperation.MountedPath, newFile); - string? hash = File.ReadAllText(newFileAbsolutePath).ComputeSHA2Hash(); - - if (hash == existingFile.File.Hash) continue; - - using var stream = File.OpenRead(newFileAbsolutePath); - skins.AddFile(skinInfo, stream, existingFile.Filename); - } }); + + try + { + Directory.Delete(externalEditOperation.MountedPath, true); + } + catch { } }).ConfigureAwait(false); - try + Schedule(() => { - Directory.Delete(externalEditOperation.MountedPath, true); - } - catch { } + var oldskin = currentSkin.Value; + var newSkinInfo = oldskin.SkinInfo.PerformRead(s => s); - mountMenuItem.Text.Value = "Edit externally"; - mountMenuItem.Action.Value = () => _ = editExternally(); + // Create a new skin instance to ensure the skin is reloaded + // If there's a better way to reload the skin, this should be replaced with it. + currentSkin.Value = newSkinInfo.CreateInstance(skins); + + // Dispose the old skin to ensure it's no longer used + oldskin.Dispose(); + + mountMenuItem.Action.Disabled = false; + mountMenuItem.Text.Value = "Edit externally"; + mountMenuItem.Action.Value = () => _ = editExternally(); + }); } public bool OnPressed(KeyBindingPressEvent e) diff --git a/osu.Game/Skinning/SkinImporter.cs b/osu.Game/Skinning/SkinImporter.cs index 59c7f0ba26..9d9b197ac2 100644 --- a/osu.Game/Skinning/SkinImporter.cs +++ b/osu.Game/Skinning/SkinImporter.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Text; using System.Threading; +using System.Threading.Tasks; using Newtonsoft.Json; using osu.Framework.Platform; using osu.Game.Beatmaps; @@ -13,6 +14,7 @@ using osu.Game.Database; using osu.Game.Extensions; using osu.Game.IO; using osu.Game.IO.Archives; +using osu.Game.Overlays.Notifications; using Realms; namespace osu.Game.Skinning @@ -43,6 +45,11 @@ namespace osu.Game.Skinning private const string unknown_creator_string = @"Unknown"; + public override async Task?> ImportAsUpdate(ProgressNotification notification, ImportTask task, SkinInfo original) + { + throw new NotImplementedException(); + } + protected override void Populate(SkinInfo model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) { var skinInfoFile = model.GetFile(skin_info_file); From a20bd5cc3d3b07ec3dfb461a3b0c6b33c2a25d74 Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Mon, 23 Sep 2024 09:47:33 -0400 Subject: [PATCH 003/370] Abstract out logic to SkinImporter --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 28 +------------------- osu.Game/Skinning/SkinImporter.cs | 30 ++++++++++++++++++++-- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index a2f64d0a9c..56529de88d 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -318,32 +318,7 @@ namespace osu.Game.Overlays.SkinEditor mountMenuItem.Action.Disabled = true; - await Task.Run(() => - { - currentSkin.Value.SkinInfo.PerformWrite(skinInfo => - { - // Clear files in the skin - skinInfo.Files.Clear(); - - // Get all the files in the mounted directory and add them to the skin - string[] filesInMounted = Directory.EnumerateFiles(externalEditOperation.MountedPath, "*.*", SearchOption.AllDirectories).Select(f => Path.GetRelativePath(externalEditOperation.MountedPath, f)).ToArray(); - - foreach (string file in filesInMounted) - { - using var stream = File.OpenRead(Path.Combine(externalEditOperation.MountedPath, file)); - - // The GetFile call in this method is really expensive, and we are certain that the file does not exist in the skin yet. - // Consider adding a method to add a file without checking if it exists. Or add the file directly to the skin. - skins.AddFile(skinInfo, stream, file); - } - }); - - try - { - Directory.Delete(externalEditOperation.MountedPath, true); - } - catch { } - }).ConfigureAwait(false); + await externalEditOperation.Finish().ConfigureAwait(false); Schedule(() => { @@ -354,7 +329,6 @@ namespace osu.Game.Overlays.SkinEditor // If there's a better way to reload the skin, this should be replaced with it. currentSkin.Value = newSkinInfo.CreateInstance(skins); - // Dispose the old skin to ensure it's no longer used oldskin.Dispose(); mountMenuItem.Action.Disabled = false; diff --git a/osu.Game/Skinning/SkinImporter.cs b/osu.Game/Skinning/SkinImporter.cs index 9d9b197ac2..8147287eec 100644 --- a/osu.Game/Skinning/SkinImporter.cs +++ b/osu.Game/Skinning/SkinImporter.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -45,9 +46,34 @@ namespace osu.Game.Skinning private const string unknown_creator_string = @"Unknown"; - public override async Task?> ImportAsUpdate(ProgressNotification notification, ImportTask task, SkinInfo original) + /// + /// Update an existing skin with the contents of a path + /// + /// The progress notification + /// The to update the with + /// The to update + /// + public override Task?> ImportAsUpdate(ProgressNotification notification, ImportTask task, SkinInfo original) { - throw new NotImplementedException(); + var skinInfoLive = original.ToLive(Realm); + + skinInfoLive.PerformWrite(skinInfo => + { + skinInfo.Files.Clear(); + + string[] filesInMountedDirectory = Directory.EnumerateFiles(task.Path, "*.*", SearchOption.AllDirectories).Select(f => Path.GetRelativePath(task.Path, f)).ToArray(); + + foreach (string file in filesInMountedDirectory) + { + using var stream = File.OpenRead(Path.Combine(task.Path, file)); + + // The GetFile call in this method is *really* expensive, and we are certain that the file does not exist in the skin yet. + // Consider adding a method to add a file without checking if it already exists. Or add the file directly to the skin. + modelManager.AddFile(original, stream, file); + } + }); + + return Task.FromResult(skinInfoLive)!; } protected override void Populate(SkinInfo model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) From b7883f18be12d304fddb57269c5bcb123e0763b2 Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Fri, 11 Oct 2024 13:46:17 -0400 Subject: [PATCH 004/370] Add a toggle for checking overwriting The GetFile method in AddFile has a huge overhead, given we're doing this in a loop. Since we clear the files in the skin, we already know there won't be any existing files, so we can skip all of that logic --- osu.Game/Database/ModelManager.cs | 17 ++++++++++------- osu.Game/Skinning/SkinImporter.cs | 5 ++--- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/osu.Game/Database/ModelManager.cs b/osu.Game/Database/ModelManager.cs index 7a5fb5efbf..5ecf1e0080 100644 --- a/osu.Game/Database/ModelManager.cs +++ b/osu.Game/Database/ModelManager.cs @@ -81,16 +81,19 @@ namespace osu.Game.Database } /// - /// Add a file from within an ongoing realm transaction. If the file already exists, it is overwritten. + /// Add a file from within an ongoing realm transaction. If the file already exists, it is overwritten so long as is true. /// - public void AddFile(TModel item, Stream contents, string filename, Realm realm) + public void AddFile(TModel item, Stream contents, string filename, Realm realm, bool overwrite = true) { - var existing = item.GetFile(filename); - - if (existing != null) + if (overwrite) { - ReplaceFile(existing, contents, realm); - return; + var existing = item.GetFile(filename); + + if (existing != null) + { + ReplaceFile(existing, contents, realm); + return; + } } var file = realmFileStore.Add(contents, realm); diff --git a/osu.Game/Skinning/SkinImporter.cs b/osu.Game/Skinning/SkinImporter.cs index 8147287eec..aba14efb2f 100644 --- a/osu.Game/Skinning/SkinImporter.cs +++ b/osu.Game/Skinning/SkinImporter.cs @@ -59,6 +59,7 @@ namespace osu.Game.Skinning skinInfoLive.PerformWrite(skinInfo => { + // Not sure if this deletes the files from the storage or just the database. skinInfo.Files.Clear(); string[] filesInMountedDirectory = Directory.EnumerateFiles(task.Path, "*.*", SearchOption.AllDirectories).Select(f => Path.GetRelativePath(task.Path, f)).ToArray(); @@ -67,9 +68,7 @@ namespace osu.Game.Skinning { using var stream = File.OpenRead(Path.Combine(task.Path, file)); - // The GetFile call in this method is *really* expensive, and we are certain that the file does not exist in the skin yet. - // Consider adding a method to add a file without checking if it already exists. Or add the file directly to the skin. - modelManager.AddFile(original, stream, file); + modelManager.AddFile(original, stream, file, Realm.Realm, false); } }); From ee16c964dce4e3ecff95c6b8688c0673a2f5c2f5 Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Fri, 11 Oct 2024 14:06:43 -0400 Subject: [PATCH 005/370] Make everything translatable --- osu.Game/Localisation/EditorStrings.cs | 10 ++++++++++ osu.Game/Overlays/SkinEditor/SkinEditor.cs | 6 +++--- osu.Game/Screens/Edit/Editor.cs | 2 +- osu.Game/Screens/Edit/ExternalEditScreen.cs | 3 ++- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index bcffc18d4d..6aaab1f653 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -134,6 +134,16 @@ namespace osu.Game.Localisation /// public static LocalisableString TimelineShowTimingChanges => new TranslatableString(getKey(@"timeline_show_timing_changes"), @"Show timing changes"); + /// + /// "Edit externally" + /// + public static LocalisableString EditExternally => new TranslatableString(getKey(@"edit_externally"), @"Edit externally"); + + /// + /// "Finish editing and import changes" + /// + public static LocalisableString FinishEditingExternally => new TranslatableString(getKey(@"Finish editing and import changes"), @"Finish editing and import changes"); + /// /// "Show ticks" /// diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 56529de88d..ef76cd0378 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -164,7 +164,7 @@ namespace osu.Game.Overlays.SkinEditor { new EditorMenuItem(Web.CommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()), new EditorMenuItem(CommonStrings.Export, MenuItemType.Standard, () => skins.ExportCurrentSkin()) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, - mountMenuItem = new EditorMenuItem("Edit externally", MenuItemType.Standard, () => _ = editExternally()) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, + mountMenuItem = new EditorMenuItem(EditorStrings.EditExternally, MenuItemType.Standard, () => _ = editExternally()) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, new OsuMenuItemSpacer(), new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, () => dialogOverlay?.Push(new RevertConfirmDialog(revert))), new OsuMenuItemSpacer(), @@ -306,7 +306,7 @@ namespace osu.Game.Overlays.SkinEditor Schedule(() => { mountMenuItem.Action.Disabled = false; - mountMenuItem.Text.Value = "Finish external edit"; + mountMenuItem.Text.Value = EditorStrings.FinishEditingExternally; mountMenuItem.Action.Value = () => _ = finishExternalEdit(); }); } @@ -332,7 +332,7 @@ namespace osu.Game.Overlays.SkinEditor oldskin.Dispose(); mountMenuItem.Action.Disabled = false; - mountMenuItem.Text.Value = "Edit externally"; + mountMenuItem.Text.Value = EditorStrings.EditExternally; mountMenuItem.Action.Value = () => _ = editExternally(); }); } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index e9bcd3050b..5a5716b056 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1210,7 +1210,7 @@ namespace osu.Game.Screens.Edit saveRelatedMenuItems.AddRange(export.Items); yield return export; - var externalEdit = new EditorMenuItem("Edit externally", MenuItemType.Standard, editExternally); + var externalEdit = new EditorMenuItem(EditorStrings.EditExternally, MenuItemType.Standard, editExternally); saveRelatedMenuItems.Add(externalEdit); yield return externalEdit; } diff --git a/osu.Game/Screens/Edit/ExternalEditScreen.cs b/osu.Game/Screens/Edit/ExternalEditScreen.cs index 8a97e3dcb2..e906d74855 100644 --- a/osu.Game/Screens/Edit/ExternalEditScreen.cs +++ b/osu.Game/Screens/Edit/ExternalEditScreen.cs @@ -21,6 +21,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; using osu.Game.Online.Multiplayer; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Match.Components; @@ -156,7 +157,7 @@ namespace osu.Game.Screens.Edit }, new DangerousRoundedButton { - Text = "Finish editing and import changes", + Text = EditorStrings.FinishEditingExternally, Width = 350, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, From 2b7eb5626cfb321b54ae0be7a7b9a3049a20f2c2 Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Fri, 11 Oct 2024 14:14:06 -0400 Subject: [PATCH 006/370] Rename oldskin --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index ef76cd0378..3d287d04d3 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -322,14 +322,14 @@ namespace osu.Game.Overlays.SkinEditor Schedule(() => { - var oldskin = currentSkin.Value; - var newSkinInfo = oldskin.SkinInfo.PerformRead(s => s); + var oldSkin = currentSkin.Value; + var newSkinInfo = oldSkin.SkinInfo.PerformRead(s => s); // Create a new skin instance to ensure the skin is reloaded // If there's a better way to reload the skin, this should be replaced with it. currentSkin.Value = newSkinInfo.CreateInstance(skins); - oldskin.Dispose(); + oldSkin.Dispose(); mountMenuItem.Action.Disabled = false; mountMenuItem.Text.Value = EditorStrings.EditExternally; From 0dc77a70e11de02cc4257922b5a139dd88810715 Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Fri, 11 Oct 2024 16:15:57 -0400 Subject: [PATCH 007/370] Revert "Add a toggle for checking overwriting" This reverts commit b7883f18be12d304fddb57269c5bcb123e0763b2. --- osu.Game/Database/ModelManager.cs | 17 +++++++---------- osu.Game/Skinning/SkinImporter.cs | 5 +++-- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/osu.Game/Database/ModelManager.cs b/osu.Game/Database/ModelManager.cs index 5ecf1e0080..7a5fb5efbf 100644 --- a/osu.Game/Database/ModelManager.cs +++ b/osu.Game/Database/ModelManager.cs @@ -81,19 +81,16 @@ namespace osu.Game.Database } /// - /// Add a file from within an ongoing realm transaction. If the file already exists, it is overwritten so long as is true. + /// Add a file from within an ongoing realm transaction. If the file already exists, it is overwritten. /// - public void AddFile(TModel item, Stream contents, string filename, Realm realm, bool overwrite = true) + public void AddFile(TModel item, Stream contents, string filename, Realm realm) { - if (overwrite) - { - var existing = item.GetFile(filename); + var existing = item.GetFile(filename); - if (existing != null) - { - ReplaceFile(existing, contents, realm); - return; - } + if (existing != null) + { + ReplaceFile(existing, contents, realm); + return; } var file = realmFileStore.Add(contents, realm); diff --git a/osu.Game/Skinning/SkinImporter.cs b/osu.Game/Skinning/SkinImporter.cs index aba14efb2f..8147287eec 100644 --- a/osu.Game/Skinning/SkinImporter.cs +++ b/osu.Game/Skinning/SkinImporter.cs @@ -59,7 +59,6 @@ namespace osu.Game.Skinning skinInfoLive.PerformWrite(skinInfo => { - // Not sure if this deletes the files from the storage or just the database. skinInfo.Files.Clear(); string[] filesInMountedDirectory = Directory.EnumerateFiles(task.Path, "*.*", SearchOption.AllDirectories).Select(f => Path.GetRelativePath(task.Path, f)).ToArray(); @@ -68,7 +67,9 @@ namespace osu.Game.Skinning { using var stream = File.OpenRead(Path.Combine(task.Path, file)); - modelManager.AddFile(original, stream, file, Realm.Realm, false); + // The GetFile call in this method is *really* expensive, and we are certain that the file does not exist in the skin yet. + // Consider adding a method to add a file without checking if it already exists. Or add the file directly to the skin. + modelManager.AddFile(original, stream, file); } }); From 3d16e45d79a8788fa3feec9b9b7777c2b101d878 Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Sat, 12 Oct 2024 17:15:48 -0400 Subject: [PATCH 008/370] Remove a comment --- osu.Game/Skinning/SkinImporter.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Skinning/SkinImporter.cs b/osu.Game/Skinning/SkinImporter.cs index 8147287eec..4b024f7138 100644 --- a/osu.Game/Skinning/SkinImporter.cs +++ b/osu.Game/Skinning/SkinImporter.cs @@ -67,8 +67,6 @@ namespace osu.Game.Skinning { using var stream = File.OpenRead(Path.Combine(task.Path, file)); - // The GetFile call in this method is *really* expensive, and we are certain that the file does not exist in the skin yet. - // Consider adding a method to add a file without checking if it already exists. Or add the file directly to the skin. modelManager.AddFile(original, stream, file); } }); From 44fe7f2a4484d92738cc27c851cd938c4d4bf771 Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Sat, 12 Oct 2024 20:39:33 -0400 Subject: [PATCH 009/370] Switch to using an overlay interface for skin mounting --- osu.Game/OsuGame.cs | 1 + osu.Game/Overlays/ExternalEditOverlay.cs | 268 +++++++++++++++++++++ osu.Game/Overlays/SkinEditor/SkinEditor.cs | 62 +---- 3 files changed, 274 insertions(+), 57 deletions(-) create mode 100644 osu.Game/Overlays/ExternalEditOverlay.cs diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index dce24c6ee7..91378b2bbc 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1119,6 +1119,7 @@ namespace osu.Game loadComponentSingleFile(beatmapSetOverlay = new BeatmapSetOverlay(), overlayContent.Add, true); loadComponentSingleFile(wikiOverlay = new WikiOverlay(), overlayContent.Add, true); loadComponentSingleFile(skinEditor = new SkinEditorOverlay(ScreenContainer), overlayContent.Add, true); + loadComponentSingleFile(new ExternalEditOverlay(), overlayContent.Add, true); loadComponentSingleFile(new LoginOverlay { diff --git a/osu.Game/Overlays/ExternalEditOverlay.cs b/osu.Game/Overlays/ExternalEditOverlay.cs new file mode 100644 index 0000000000..a107392882 --- /dev/null +++ b/osu.Game/Overlays/ExternalEditOverlay.cs @@ -0,0 +1,268 @@ +// 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.IO; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Testing; +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.Graphics.UserInterfaceV2; +using osu.Game.Input.Bindings; +using osu.Game.Localisation; +using osu.Game.Online.Multiplayer; +using osu.Game.Screens.OnlinePlay.Match.Components; +using osu.Game.Skinning; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Overlays +{ + public partial class ExternalEditOverlay : OsuFocusedOverlayContainer + { + private const double transition_duration = 300; + private FillFlowContainer flow = null!; + + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); + + [Resolved] + private GameHost gameHost { get; set; } = null!; + + private ExternalEditOperation? editOperation; + + private Bindable? skinBindable; + private SkinManager? skinManager; + + protected override bool DimMainContent => false; + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.Both; + InternalChild = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + // Since we're drawing this overlay on top of another overlay (SkinEditor), the dimming effect isn't applied. So we need to add a dimming effect manually. + new Box + { + Colour = Color4.Black.Opacity(0.5f), + RelativeSizeAxes = Axes.Both, + }, + new Container + { + Masking = true, + CornerRadius = 20, + AutoSizeAxes = Axes.Both, + AutoSizeDuration = 500, + AutoSizeEasing = Easing.OutQuint, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new Box + { + Colour = colourProvider.Background5, + RelativeSizeAxes = Axes.Both, + }, + flow = new FillFlowContainer + { + Margin = new MarginPadding(20), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Spacing = new Vector2(15), + } + } + } + } + }; + } + + public async Task Begin(SkinInfo skinInfo, Bindable skinBindable, SkinManager skinManager) + { + Show(); + showSpinner("Mounting external skin..."); + + await Task.Delay(500).ConfigureAwait(true); + + try + { + editOperation = await skinManager.BeginExternalEditing(skinInfo).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.Log($"Failed to initialize external edit operation: {ex}", LoggingTarget.Database, LogLevel.Error); + Schedule(() => showSpinner("Export failed!")); + await Task.Delay(1000).ConfigureAwait(true); + Hide(); + } + + this.skinBindable = skinBindable; + this.skinManager = skinManager; + + Schedule(() => + { + flow.Children = new Drawable[] + { + new OsuSpriteText + { + Text = "Skin is mounted externally", + Font = OsuFont.Default.With(size: 30), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + new OsuTextFlowContainer + { + Padding = new MarginPadding(5), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Width = 350, + AutoSizeAxes = Axes.Y, + Text = "Any changes made to the exported folder will be imported to the game, including file additions, modifications and deletions.", + }, + new PurpleRoundedButton + { + Text = "Open folder", + Width = 350, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Action = openDirectory, + Enabled = { Value = false } + }, + new DangerousRoundedButton + { + Text = EditorStrings.FinishEditingExternally, + Width = 350, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Action = () => finish().FireAndForget(), + Enabled = { Value = false } + } + }; + }); + + Scheduler.AddDelayed(() => + { + foreach (var b in flow.ChildrenOfType()) + b.Enabled.Value = true; + openDirectory(); + }, 1000); + } + + private void openDirectory() + { + if (editOperation == null) + return; + + gameHost.OpenFileExternally(editOperation.MountedPath.TrimDirectorySeparator() + Path.DirectorySeparatorChar); + } + + private async Task finish() + { + showSpinner("Cleaning up..."); + await Task.Delay(500).ConfigureAwait(true); + + try + { + await editOperation!.Finish().ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.Log($"Failed to finish external edit operation: {ex}", LoggingTarget.Database, LogLevel.Error); + showSpinner("Import failed!"); + await Task.Delay(1000).ConfigureAwait(true); + Hide(); + } + + Schedule(() => + { + var oldSkin = skinBindable!.Value; + var newSkinInfo = oldSkin.SkinInfo.PerformRead(s => s); + + // Create a new skin instance to ensure the skin is reloaded + // If there's a better way to reload the skin, this should be replaced with it. + skinBindable.Value = newSkinInfo.CreateInstance(skinManager!); + + oldSkin.Dispose(); + + Hide(); + }); + } + + protected override void PopIn() + { + this.FadeIn(transition_duration, Easing.OutQuint); + } + + protected override void PopOut() + { + this.FadeOut(transition_duration, Easing.OutQuint).Finally(_ => + { + // Set everything to a clean state + editOperation = null; + skinManager = null; + skinBindable = null; + flow.Children = Array.Empty(); + }); + } + + public override bool OnPressed(KeyBindingPressEvent e) + { + if (e.Repeat) + return false; + + switch (e.Action) + { + case GlobalAction.Back: + case GlobalAction.Select: + if (editOperation == null) return base.OnPressed(e); + + finish().FireAndForget(); + return true; + } + + return base.OnPressed(e); + } + + private void showSpinner(string text) + { + foreach (var b in flow.ChildrenOfType()) + b.Enabled.Value = false; + + flow.Children = new Drawable[] + { + new OsuSpriteText + { + Text = text, + Font = OsuFont.Default.With(size: 30), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + new LoadingSpinner + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + State = { Value = Visibility.Visible } + }, + }; + } + } +} diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 3d287d04d3..8010f66eaa 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -11,7 +11,6 @@ using Newtonsoft.Json; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; @@ -19,8 +18,6 @@ using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Localisation; -using osu.Framework.Logging; -using osu.Framework.Platform; using Web = osu.Game.Resources.Localisation.Web; using osu.Framework.Testing; using osu.Game.Database; @@ -61,9 +58,6 @@ namespace osu.Game.Overlays.SkinEditor [Resolved] private OsuGame? game { get; set; } - [Resolved] - private GameHost host { get; set; } = null!; - [Resolved] private SkinManager skins { get; set; } = null!; @@ -90,7 +84,6 @@ namespace osu.Game.Overlays.SkinEditor private SkinEditorChangeHandler? changeHandler; - private EditorMenuItem mountMenuItem = null!; private EditorMenuItem undoMenuItem = null!; private EditorMenuItem redoMenuItem = null!; @@ -109,6 +102,9 @@ namespace osu.Game.Overlays.SkinEditor [Resolved] private IDialogOverlay? dialogOverlay { get; set; } + [Resolved] + private ExternalEditOverlay? externalEditOverlay { get; set; } + public SkinEditor() { } @@ -164,7 +160,7 @@ namespace osu.Game.Overlays.SkinEditor { new EditorMenuItem(Web.CommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()), new EditorMenuItem(CommonStrings.Export, MenuItemType.Standard, () => skins.ExportCurrentSkin()) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, - mountMenuItem = new EditorMenuItem(EditorStrings.EditExternally, MenuItemType.Standard, () => _ = editExternally()) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, + new EditorMenuItem(EditorStrings.EditExternally, MenuItemType.Standard, () => _ = editExternally()) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, new OsuMenuItemSpacer(), new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, () => dialogOverlay?.Push(new RevertConfirmDialog(revert))), new OsuMenuItemSpacer(), @@ -282,59 +278,11 @@ namespace osu.Game.Overlays.SkinEditor selectedTarget.BindValueChanged(targetChanged, true); } - private ExternalEditOperation? externalEditOperation; - private async Task editExternally() { - mountMenuItem.Action.Disabled = true; var skin = currentSkin.Value.SkinInfo.PerformRead(s => s.Detach()); - try - { - externalEditOperation = await skins.BeginExternalEditing(skin).ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.Log($"Failed to initialize external edit operation: {ex}", LoggingTarget.Database, LogLevel.Error); - } - - if (externalEditOperation == null) - return; - - host.OpenFileExternally(externalEditOperation.MountedPath.TrimDirectorySeparator() + Path.DirectorySeparatorChar); - - Schedule(() => - { - mountMenuItem.Action.Disabled = false; - mountMenuItem.Text.Value = EditorStrings.FinishEditingExternally; - mountMenuItem.Action.Value = () => _ = finishExternalEdit(); - }); - } - - private async Task finishExternalEdit() - { - if (externalEditOperation == null || !externalEditOperation.IsMounted) - return; - - mountMenuItem.Action.Disabled = true; - - await externalEditOperation.Finish().ConfigureAwait(false); - - Schedule(() => - { - var oldSkin = currentSkin.Value; - var newSkinInfo = oldSkin.SkinInfo.PerformRead(s => s); - - // Create a new skin instance to ensure the skin is reloaded - // If there's a better way to reload the skin, this should be replaced with it. - currentSkin.Value = newSkinInfo.CreateInstance(skins); - - oldSkin.Dispose(); - - mountMenuItem.Action.Disabled = false; - mountMenuItem.Text.Value = EditorStrings.EditExternally; - mountMenuItem.Action.Value = () => _ = editExternally(); - }); + await externalEditOverlay!.Begin(skin, currentSkin, skins).ConfigureAwait(false); } public bool OnPressed(KeyBindingPressEvent e) From ca5a74df26e2b25ef981db277ff2c9f662b47986 Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Sat, 12 Oct 2024 21:06:25 -0400 Subject: [PATCH 010/370] Switch overlay colour scheme to purple Figured this made sense since we're using purple buttons. It doesn't really seem to change anything visually though --- osu.Game/Overlays/ExternalEditOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/ExternalEditOverlay.cs b/osu.Game/Overlays/ExternalEditOverlay.cs index a107392882..7ba1517be5 100644 --- a/osu.Game/Overlays/ExternalEditOverlay.cs +++ b/osu.Game/Overlays/ExternalEditOverlay.cs @@ -37,7 +37,7 @@ namespace osu.Game.Overlays private FillFlowContainer flow = null!; [Cached] - private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); [Resolved] private GameHost gameHost { get; set; } = null!; From 15c7a12174687d5bcb9d058bcbb72a35e3a42b35 Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Sun, 13 Oct 2024 19:40:06 -0400 Subject: [PATCH 011/370] Switch colour scheme to blue --- osu.Game/Overlays/ExternalEditOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/ExternalEditOverlay.cs b/osu.Game/Overlays/ExternalEditOverlay.cs index 7ba1517be5..e9b3590626 100644 --- a/osu.Game/Overlays/ExternalEditOverlay.cs +++ b/osu.Game/Overlays/ExternalEditOverlay.cs @@ -37,7 +37,7 @@ namespace osu.Game.Overlays private FillFlowContainer flow = null!; [Cached] - private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); [Resolved] private GameHost gameHost { get; set; } = null!; From 165afe357f5a52050f2337e54478f87739f125fb Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Wed, 11 Dec 2024 10:19:10 -0500 Subject: [PATCH 012/370] Rename SkinInfo when it is changed in skin.ini Peppy spoke about using a shortcut and/or hashes to determine if the skin.ini is changed, and if so, then to rename the skin. In my opinion, hashing and doing numerous comparisons is probably less efficient than just syncing the SkinInfo's name during the update. This is an easy solution that does what it needs to. --- osu.Game/Skinning/SkinImporter.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game/Skinning/SkinImporter.cs b/osu.Game/Skinning/SkinImporter.cs index 4b024f7138..087c0f0dee 100644 --- a/osu.Game/Skinning/SkinImporter.cs +++ b/osu.Game/Skinning/SkinImporter.cs @@ -69,6 +69,23 @@ namespace osu.Game.Skinning modelManager.AddFile(original, stream, file); } + + string skinIniPath = Path.Combine(task.Path, "skin.ini"); + + if (!File.Exists(skinIniPath)) + return; + + using (var stream = File.OpenRead(skinIniPath)) + using (var lineReader = new LineBufferedReader(stream)) + { + var decodedSkinIni = new LegacySkinDecoder().Decode(lineReader); + + if (!string.IsNullOrEmpty(decodedSkinIni.SkinInfo.Name)) + skinInfo.Name = decodedSkinIni.SkinInfo.Name; + + if (!string.IsNullOrEmpty(decodedSkinIni.SkinInfo.Creator)) + skinInfo.Creator = decodedSkinIni.SkinInfo.Creator; + } }); return Task.FromResult(skinInfoLive)!; From 7a5e613cf68484876bbf0c4f6580b29127adb83b Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Mon, 16 Dec 2024 11:33:45 -0500 Subject: [PATCH 013/370] Disallow opening settings menu when external edit ovelay is open Also disallows using the random skin keybind when the external edit overlay is open. SkinEditor should already be disabling it, but I figured we might as well add this in for redundancy --- osu.Game/OsuGame.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 39c5fd842c..fda6c553ac 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -127,6 +127,8 @@ namespace osu.Game private SkinEditorOverlay skinEditor; + private ExternalEditOverlay externalEditOverlay; + private Container overlayContent; private Container rightFloatingOverlayContent; @@ -1125,7 +1127,7 @@ namespace osu.Game loadComponentSingleFile(beatmapSetOverlay = new BeatmapSetOverlay(), overlayContent.Add, true); loadComponentSingleFile(wikiOverlay = new WikiOverlay(), overlayContent.Add, true); loadComponentSingleFile(skinEditor = new SkinEditorOverlay(ScreenContainer), overlayContent.Add, true); - loadComponentSingleFile(new ExternalEditOverlay(), overlayContent.Add, true); + loadComponentSingleFile(externalEditOverlay = new ExternalEditOverlay(), overlayContent.Add, true); loadComponentSingleFile(new LoginOverlay { @@ -1175,6 +1177,17 @@ namespace osu.Game }; } + Settings.State.ValueChanged += state => + { + if (state.NewValue == Visibility.Hidden) + return; + + if (externalEditOverlay.State.Value == Visibility.Visible) + { + Scheduler.Add(() => Settings.Hide()); + } + }; + // ensure only one of these overlays are open at once. var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, news, dashboard, beatmapListing, changelogOverlay, rankingsOverlay, wikiOverlay }; @@ -1462,7 +1475,7 @@ namespace osu.Game // Don't allow random skin selection while in the skin editor. // This is mainly to stop many "osu! default (modified)" skins being created via the SkinManager.EnsureMutableSkin() path. // If people want this to work we can potentially avoid selecting default skins when the editor is open, or allow a maximum of one mutable skin somehow. - if (skinEditor.State.Value == Visibility.Visible) + if (skinEditor.State.Value == Visibility.Visible || externalEditOverlay.State.Value == Visibility.Visible) return false; SkinManager.SelectRandomSkin(); From 13ace770aefad0df750b526144f0ad1ce6f971c7 Mon Sep 17 00:00:00 2001 From: Chris Ehmann Date: Wed, 28 May 2025 17:02:18 -0700 Subject: [PATCH 014/370] Add check to see if MouseUp event was the left button --- osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index b49dee279e..ce0411a027 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -166,6 +166,9 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override void OnMouseUp(MouseUpEvent e) { + if (e.Button != MouseButton.Left) + return; + // Special case for when a drag happened instead of a click Schedule(() => { From 59045c8bca134f132316d69253ad8d7ba9a07ba0 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 30 May 2025 08:23:09 +0300 Subject: [PATCH 015/370] Duck music and dim background when "no results" placeholder is visible --- osu.Game/Screens/SelectV2/SongSelect.cs | 106 ++++++++++++++++++++---- 1 file changed, 88 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index f4ba68cbd5..e2d5092b81 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -22,6 +22,7 @@ using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Database; +using osu.Game.Graphics; using osu.Game.Graphics.Carousel; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; @@ -368,6 +369,65 @@ namespace osu.Game.Screens.SelectV2 private void ensureTrackLooping(IWorkingBeatmap beatmap, TrackChangeDirection changeDirection) => beatmap.PrepareTrackForPreview(true); + private IDisposable? trackDuck; + + private void attachTrackDuckingIfShould() + { + bool shouldDuck = noResultsPlaceholder.State.Value == Visibility.Visible; + + if (shouldDuck && trackDuck == null) + trackDuck = music.Duck(new DuckParameters { DuckVolumeTo = 1, DuckCutoffTo = 500 }); + } + + private void detachTrackDucking() + { + trackDuck?.Dispose(); + trackDuck = null; + } + + #endregion + + #region Background + + private bool gradientDimApplied; + + private void updateScreenBackground() + { + var beatmap = Beatmap.Value; + + ApplyToBackground(backgroundModeBeatmap => + { + backgroundModeBeatmap.BlurAmount.Value = 0; + backgroundModeBeatmap.Beatmap = beatmap; + backgroundModeBeatmap.IgnoreUserSettings.Value = true; + backgroundModeBeatmap.DimWhenUserSettingsIgnored.Value = 0.1f; + + ColourInfo targetColour = gradientDimApplied + ? ColourInfo.GradientHorizontal(OsuColour.Gray(0.8f), OsuColour.Gray(0.4f)) + : Color4.White; + + backgroundModeBeatmap.FadeColour(targetColour, 300, Easing.OutQuint); + }); + } + + private void applyGradientDimToBackground() + { + gradientDimApplied = true; + + // If not current, background will be updated later by OnEntering/OnResuming events. + if (this.IsCurrentScreen()) + updateScreenBackground(); + } + + private void removeGradientDimFromBackground() + { + gradientDimApplied = false; + + // If not current, background will be updated later by OnEntering/OnResuming events. + if (this.IsCurrentScreen()) + updateScreenBackground(); + } + #endregion #region Selection handling @@ -405,20 +465,11 @@ namespace osu.Game.Screens.SelectV2 carousel.CurrentSelection = beatmap.BeatmapInfo; - if (this.IsCurrentScreen()) - ensurePlayingSelected(); - // If not the current screen, this will be applied in OnResuming. if (this.IsCurrentScreen()) { - ApplyToBackground(backgroundModeBeatmap => - { - backgroundModeBeatmap.BlurAmount.Value = 0; - backgroundModeBeatmap.Beatmap = beatmap; - backgroundModeBeatmap.IgnoreUserSettings.Value = true; - backgroundModeBeatmap.DimWhenUserSettingsIgnored.Value = 0.1f; - backgroundModeBeatmap.FadeColour(Color4.White, 250); - }); + ensurePlayingSelected(); + updateScreenBackground(); } }); @@ -440,6 +491,7 @@ namespace osu.Game.Screens.SelectV2 modSelectOverlay.SelectedMods.BindTo(Mods); beginLooping(); + attachTrackDuckingIfShould(); // force reselection if entering song select with a protected beatmap if (Beatmap.Value.BeatmapInfo.BeatmapSet!.Protected) @@ -470,6 +522,7 @@ namespace osu.Game.Screens.SelectV2 modSelectOverlay.SelectedMods.BindTo(Mods); beginLooping(); + attachTrackDuckingIfShould(); if (Beatmap.Value.BeatmapInfo.BeatmapSet!.Protected) Beatmap.SetDefault(); @@ -491,6 +544,7 @@ namespace osu.Game.Screens.SelectV2 carousel.VisuallyFocusSelected = true; endLooping(); + detachTrackDucking(); base.OnSuspending(e); } @@ -504,6 +558,7 @@ namespace osu.Game.Screens.SelectV2 filterControl.Hide(); endLooping(); + detachTrackDucking(); return base.OnExiting(e); } @@ -577,13 +632,7 @@ namespace osu.Game.Screens.SelectV2 int count = carousel.MatchedBeatmapsCount; - if (count == 0) - { - noResultsPlaceholder.Show(); - noResultsPlaceholder.Filter = carousel.Criteria; - } - else - noResultsPlaceholder.Hide(); + updateNoResultsPlaceholder(); // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 // but also in this case we want support for formatting a number within a string). @@ -606,6 +655,27 @@ namespace osu.Game.Screens.SelectV2 carousel.NextRandom(); } + private void updateNoResultsPlaceholder() + { + int count = carousel.MatchedBeatmapsCount; + + if (count == 0) + { + noResultsPlaceholder.Show(); + noResultsPlaceholder.Filter = carousel.Criteria!; + + attachTrackDuckingIfShould(); + applyGradientDimToBackground(); + } + else + { + noResultsPlaceholder.Hide(); + + detachTrackDucking(); + removeGradientDimFromBackground(); + } + } + #endregion #region Hotkeys From 914abd1c25791cca6cd8af21023f8a9f1435c2c5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 31 May 2025 14:00:19 +0900 Subject: [PATCH 016/370] Fix footer buttons handling input in non-sheared space --- osu.Game/Screens/Footer/ScreenFooterButton.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Footer/ScreenFooterButton.cs b/osu.Game/Screens/Footer/ScreenFooterButton.cs index d0532273bc..2b23560c26 100644 --- a/osu.Game/Screens/Footer/ScreenFooterButton.cs +++ b/osu.Game/Screens/Footer/ScreenFooterButton.cs @@ -58,6 +58,8 @@ namespace osu.Game.Screens.Footer set => text.Text = value; } + private readonly Container shearedContent; + private readonly SpriteText text; private readonly SpriteIcon icon; @@ -77,7 +79,7 @@ namespace osu.Game.Screens.Footer Children = new Drawable[] { - new Container + shearedContent = new Container { EdgeEffect = new EdgeEffectParameters { @@ -170,8 +172,8 @@ namespace osu.Game.Screens.Footer FinishTransforms(true); } - // use Content for tracking input as some buttons might be temporarily hidden with DisappearToBottom, and they become hidden by moving Content away from screen. - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Content.ReceivePositionalInputAt(screenSpacePos); + // account for shear and buttons temporarily hidden with DisappearToBottom. + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => shearedContent.ReceivePositionalInputAt(screenSpacePos); public GlobalAction? Hotkey; From b650309706f571a5b7aa00257dd44cd40774a61d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 31 May 2025 14:06:56 +0900 Subject: [PATCH 017/370] Fix clicking song select search links not working --- .../Screens/SelectV2/BeatmapMetadataWedge.cs | 2 +- .../SelectV2/BeatmapMetadataWedge_TagsLine.cs | 2 +- .../Screens/SelectV2/BeatmapTitleWedge.cs | 2 +- osu.Game/Screens/SelectV2/ISongSelect.cs | 8 +++- osu.Game/Screens/SelectV2/SongSelect.cs | 40 +++++++++---------- 5 files changed, 28 insertions(+), 26 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs index ffd1418796..5a0222ec20 100644 --- a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs @@ -54,7 +54,7 @@ namespace osu.Game.Screens.SelectV2 private ILinkHandler? linkHandler { get; set; } [Resolved] - private SongSelect? songSelect { get; set; } + private ISongSelect? songSelect { get; set; } [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_TagsLine.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_TagsLine.cs index bd3bb4dabb..8df1596720 100644 --- a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_TagsLine.cs +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_TagsLine.cs @@ -194,7 +194,7 @@ namespace osu.Game.Screens.SelectV2 public partial class TagsOverflowPopover : OsuPopover { private readonly string[] tags; - private readonly SongSelect? songSelect; + private readonly ISongSelect? songSelect; public TagsOverflowPopover(string[] tags, SongSelect? songSelect) { diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs index a73fc78771..0fb4616db2 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs @@ -64,7 +64,7 @@ namespace osu.Game.Screens.SelectV2 private Statistic bpmStatistic = null!; [Resolved] - private SongSelect? songSelect { get; set; } + private ISongSelect? songSelect { get; set; } [Resolved] private LocalisationManager localisation { get; set; } = null!; diff --git a/osu.Game/Screens/SelectV2/ISongSelect.cs b/osu.Game/Screens/SelectV2/ISongSelect.cs index 1a80548380..e39f74c018 100644 --- a/osu.Game/Screens/SelectV2/ISongSelect.cs +++ b/osu.Game/Screens/SelectV2/ISongSelect.cs @@ -29,10 +29,16 @@ namespace osu.Game.Screens.SelectV2 void ManageCollections(); /// - /// Present the provided score at the results screen. + /// Opens results screen with the given score. + /// This assumes active beatmap and ruleset selection matches the score. /// void PresentScore(ScoreInfo score); + /// + /// Set the current filter text query to the provided string. + /// + void Search(string query); + /// /// Gets relevant actionable items for beatmap context menus, based on the type of song select. /// diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index c753dd77cf..486bbd9f4e 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -372,6 +372,18 @@ namespace osu.Game.Screens.SelectV2 #region Selection handling + /// + /// Finalises selection on the given . + /// + public void SelectAndStart(BeatmapInfo beatmap) + { + if (!this.IsCurrentScreen()) + return; + + Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap); + OnStart(); + } + /// /// Immediately flush any pending selection. Should be run before performing final actions such as leaving the screen. /// @@ -558,12 +570,6 @@ namespace osu.Game.Screens.SelectV2 private ScheduledDelegate? filterDebounce; - /// - /// Set the query to the search text box. - /// - /// The string to search. - public void Search(string query) => filterControl.Search(query); - private void criteriaChanged(FilterCriteria criteria) { // The first filter needs to be applied immediately as this triggers the initial carousel load. @@ -660,11 +666,11 @@ namespace osu.Game.Screens.SelectV2 #endregion - /// - /// Opens results screen with the given score. - /// This assumes active beatmap and ruleset selection matches the score. - /// - public void PresentScore(ScoreInfo score) + #region Implementation of ISongSelect + + void ISongSelect.Search(string query) => filterControl.Search(query); + + void ISongSelect.PresentScore(ScoreInfo score) { Debug.Assert(Beatmap.Value.BeatmapInfo.Equals(score.BeatmapInfo)); Debug.Assert(Ruleset.Value.Equals(score.Ruleset)); @@ -672,17 +678,7 @@ namespace osu.Game.Screens.SelectV2 this.Push(new SoloResultsScreen(score)); } - /// - /// Finalises selection on the given . - /// - public void SelectAndStart(BeatmapInfo beatmap) - { - if (!this.IsCurrentScreen()) - return; - - Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap); - OnStart(); - } + #endregion #region Beatmap management From 0fc129fbacb7d0cce565aba12d726719ead4ee1a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 1 Jun 2025 00:51:52 +0900 Subject: [PATCH 018/370] Add failing test showing random selecting filtered-away difficulties --- .../TestSceneBeatmapCarouselRandom.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs index 6e9b30e25d..4d864e4dec 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs @@ -19,6 +19,24 @@ namespace osu.Game.Tests.Visual.SongSelectV2 CreateCarousel(); } + [Test] + public void TestRandomObeysFiltering() + { + AddBeatmaps(2, 10, true); + + ApplyToFilter("filter", c => c.SearchText = BeatmapSets[0].Beatmaps.Last().DifficultyName); + WaitForFiltering(); + + CheckDisplayedBeatmapSetsCount(1); + CheckDisplayedBeatmapsCount(1); + + for (int i = 0; i < 10; i++) + { + nextRandom(); + WaitForSelection(0, 9); + } + } + /// /// Test random non-repeating algorithm /// From 4d33602ccd0d4422aa3657b97d6d998cc0ab0f87 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 31 May 2025 17:35:55 +0900 Subject: [PATCH 019/370] Fix random selection potentially selecting a filtered-away beatmap --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 20 +++++++++++-------- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 13 +++++++----- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index d14002181c..c2dd4302e6 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -219,13 +219,7 @@ namespace osu.Game.Screens.SelectV2 return; case BeatmapSetInfo setInfo: - // Selecting a set isn't valid – let's re-select the first visible difficulty. - if (grouping.SetItems.TryGetValue(setInfo, out var items)) - { - var beatmaps = items.Select(i => i.Model).OfType(); - RequestRecommendedSelection(beatmaps); - } - + selectRecommendedDifficultyForBeatmapSet(setInfo); return; case BeatmapInfo beatmapInfo: @@ -284,6 +278,16 @@ namespace osu.Game.Screens.SelectV2 setExpandedGroup(groupForReselection); } + private void selectRecommendedDifficultyForBeatmapSet(BeatmapSetInfo beatmapSet) + { + // Selecting a set isn't valid – let's re-select the first visible difficulty. + if (grouping.SetItems.TryGetValue(beatmapSet, out var items)) + { + var beatmaps = items.Select(i => i.Model).OfType(); + RequestRecommendedSelection(beatmaps); + } + } + /// /// If we don't have a selection and there's a single beatmap set returned, select it for the user. /// @@ -644,7 +648,7 @@ namespace osu.Game.Screens.SelectV2 if (CurrentSelectionItem != null) playSpinSample(distanceBetween(carouselItems.First(i => !ReferenceEquals(i.Model, set)), CurrentSelectionItem), visibleSets.Count); - RequestRecommendedSelection(set.Beatmaps.Where(b => !b.Hidden)); + selectRecommendedDifficultyForBeatmapSet(set); return true; } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 8720378ad6..926349d6cc 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -76,15 +76,18 @@ namespace osu.Game.Screens.SelectV2 { var beatmap = (BeatmapInfo)item.Model; + bool newBeatmapSet = lastBeatmap?.BeatmapSet!.ID != beatmap.BeatmapSet!.ID; + + if (newBeatmapSet) + { + if (!setMap.TryGetValue(beatmap.BeatmapSet!, out currentSetItems)) + setMap[beatmap.BeatmapSet!] = currentSetItems = new HashSet(); + } + if (BeatmapSetsGroupedTogether) { - bool newBeatmapSet = lastBeatmap?.BeatmapSet!.ID != beatmap.BeatmapSet!.ID; - if (newBeatmapSet) { - if (!setMap.TryGetValue(beatmap.BeatmapSet!, out currentSetItems)) - setMap[beatmap.BeatmapSet!] = currentSetItems = new HashSet(); - if (groupItem != null) groupItem.NestedItemCount++; From e1ebb7ccca9254674e10529091bf433a7468c2ed Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 1 Jun 2025 15:12:05 +0900 Subject: [PATCH 020/370] Fix accent colour not always propagating to statistics display --- .../BeatmapTitleWedge_DifficultyStatisticsDisplay.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyStatisticsDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyStatisticsDisplay.cs index a185448f36..571fc82fc1 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyStatisticsDisplay.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyStatisticsDisplay.cs @@ -179,7 +179,11 @@ namespace osu.Game.Screens.SelectV2 } else { - statisticsFlow.ChildrenEnumerable = statistics.Select(d => new StatisticDifficulty { Value = d }); + statisticsFlow.ChildrenEnumerable = statistics.Select(d => new StatisticDifficulty + { + AccentColour = accentColour, + Value = d + }); updateStatisticsSizing(); } } From b56fd5b4b420210c1d7e3ee08398a43324ce4db7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Jun 2025 15:52:49 +0900 Subject: [PATCH 021/370] Don't touch beatmap background --- osu.Game/Screens/SelectV2/SongSelect.cs | 68 +++++++------------------ 1 file changed, 17 insertions(+), 51 deletions(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index e2d5092b81..e2a9edc198 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -22,7 +22,6 @@ using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Database; -using osu.Game.Graphics; using osu.Game.Graphics.Carousel; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; @@ -93,6 +92,7 @@ namespace osu.Game.Screens.SelectV2 private BeatmapTitleWedge titleWedge = null!; private BeatmapDetailsArea detailsArea = null!; private FillFlowContainer wedgesContainer = null!; + private Box rightGradientBackground = null!; private NoResultsPlaceholder noResultsPlaceholder = null!; @@ -133,8 +133,8 @@ namespace osu.Game.Screens.SelectV2 new Box { RelativeSizeAxes = Axes.Both, - Width = 0.5f, - Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.5f), Color4.Black.Opacity(0f)), + Width = 0.6f, + Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.3f), Color4.Black.Opacity(0f)), }, new Container { @@ -205,8 +205,10 @@ namespace osu.Game.Screens.SelectV2 RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - new Box + rightGradientBackground = new Box { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.0f), Color4.Black.Opacity(0.5f)), RelativeSizeAxes = Axes.Both, }, @@ -387,49 +389,6 @@ namespace osu.Game.Screens.SelectV2 #endregion - #region Background - - private bool gradientDimApplied; - - private void updateScreenBackground() - { - var beatmap = Beatmap.Value; - - ApplyToBackground(backgroundModeBeatmap => - { - backgroundModeBeatmap.BlurAmount.Value = 0; - backgroundModeBeatmap.Beatmap = beatmap; - backgroundModeBeatmap.IgnoreUserSettings.Value = true; - backgroundModeBeatmap.DimWhenUserSettingsIgnored.Value = 0.1f; - - ColourInfo targetColour = gradientDimApplied - ? ColourInfo.GradientHorizontal(OsuColour.Gray(0.8f), OsuColour.Gray(0.4f)) - : Color4.White; - - backgroundModeBeatmap.FadeColour(targetColour, 300, Easing.OutQuint); - }); - } - - private void applyGradientDimToBackground() - { - gradientDimApplied = true; - - // If not current, background will be updated later by OnEntering/OnResuming events. - if (this.IsCurrentScreen()) - updateScreenBackground(); - } - - private void removeGradientDimFromBackground() - { - gradientDimApplied = false; - - // If not current, background will be updated later by OnEntering/OnResuming events. - if (this.IsCurrentScreen()) - updateScreenBackground(); - } - - #endregion - #region Selection handling /// @@ -465,11 +424,18 @@ namespace osu.Game.Screens.SelectV2 carousel.CurrentSelection = beatmap.BeatmapInfo; - // If not the current screen, this will be applied in OnResuming. if (this.IsCurrentScreen()) { + // If not the current screen, this will be applied in OnResuming. ensurePlayingSelected(); - updateScreenBackground(); + + ApplyToBackground(backgroundModeBeatmap => + { + backgroundModeBeatmap.BlurAmount.Value = 0; + backgroundModeBeatmap.Beatmap = beatmap; + backgroundModeBeatmap.IgnoreUserSettings.Value = true; + backgroundModeBeatmap.DimWhenUserSettingsIgnored.Value = 0.1f; + }); } }); @@ -665,14 +631,14 @@ namespace osu.Game.Screens.SelectV2 noResultsPlaceholder.Filter = carousel.Criteria!; attachTrackDuckingIfShould(); - applyGradientDimToBackground(); + rightGradientBackground.ResizeWidthTo(3, 1000, Easing.OutQuint); } else { noResultsPlaceholder.Hide(); detachTrackDucking(); - removeGradientDimFromBackground(); + rightGradientBackground.ResizeWidthTo(1, 500, Easing.OutQuint); } } From b7eba1bdc8204b41e72a123a9d1ac64187cb90e9 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 2 Jun 2025 00:27:39 -0700 Subject: [PATCH 022/370] Fix directory breadcrumb buttons playing clicking sounds twice --- .../OsuDirectorySelectorBreadcrumbDisplay.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FileSelection/OsuDirectorySelectorBreadcrumbDisplay.cs b/osu.Game/Graphics/UserInterfaceV2/FileSelection/OsuDirectorySelectorBreadcrumbDisplay.cs index aeeda82bfb..efeebb2fc1 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FileSelection/OsuDirectorySelectorBreadcrumbDisplay.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FileSelection/OsuDirectorySelectorBreadcrumbDisplay.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; @@ -78,13 +77,9 @@ namespace osu.Game.Graphics.UserInterfaceV2.FileSelection Flow.Height = 25; Flow.Margin = new MarginPadding { Horizontal = 10, }; - AddRangeInternal(new Drawable[] + AddInternal(new BackgroundLayer(0.5f) { - new BackgroundLayer(0.5f) - { - Depth = 1 - }, - new HoverClickSounds(), + Depth = 1 }); Flow.Add(new SpriteIcon From a0eb39d26dd4c1e7208192173a610da374bce4c8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Jun 2025 16:55:21 +0900 Subject: [PATCH 023/370] Disable broken tests temporarily --- .../SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs | 1 + .../SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs | 1 + .../Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs | 1 + 3 files changed, 3 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs index 0ce13c6963..dad987ab60 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs @@ -177,6 +177,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } [Test] + [Ignore("broken")] public void TestInputHandlingWithinGaps() { AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs index 8f7c901c37..61ecf38637 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs @@ -170,6 +170,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } [Test] + [Ignore("broken")] public void TestInputHandlingWithinGaps() { AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs index 800cde8c50..84e7d38239 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs @@ -259,6 +259,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } [Test] + [Ignore("broken")] public void TestInputHandlingWithinGaps() { AddBeatmaps(2, 5); From ea2eded6e1c0775619d0e30bd4d6c134a4648c60 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Jun 2025 18:10:55 +0900 Subject: [PATCH 024/370] Fix multiple incorrect behaviours due to reliance on dictionary init state --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 4 +++- osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index c2dd4302e6..740ed14e1e 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -255,7 +255,9 @@ namespace osu.Game.Screens.SelectV2 if (containingGroup != null) setExpandedGroup(containingGroup); - setExpandedSet(beatmapInfo); + + if (grouping.BeatmapSetsGroupedTogether) + setExpandedSet(beatmapInfo); break; } } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 926349d6cc..c68f377fbb 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -117,7 +117,7 @@ namespace osu.Game.Screens.SelectV2 currentGroupItems?.Add(i); currentSetItems?.Add(i); - i.IsVisible = i.Model is GroupDefinition || (group == null && (i.Model is BeatmapSetInfo || currentSetItems == null)); + i.IsVisible = i.Model is GroupDefinition || (group == null && (i.Model is BeatmapSetInfo || !BeatmapSetsGroupedTogether)); } } From 90b9fb0809f18a2ca5c6c1f0670bab57712af5cd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Jun 2025 19:10:06 +0900 Subject: [PATCH 025/370] Store input padding adjustments in `CarouselItem` to allow more reliable inflation --- .../TestSceneBeatmapCarouselArtistGrouping.cs | 5 ++--- ...estSceneBeatmapCarouselDifficultyGrouping.cs | 1 - .../TestSceneBeatmapCarouselNoGrouping.cs | 12 ++++++------ osu.Game/Graphics/Carousel/Carousel.cs | 4 ++++ osu.Game/Graphics/Carousel/CarouselItem.cs | 17 +++++++++++++++++ osu.Game/Screens/SelectV2/Panel.cs | 17 +++++++++++++---- osu.Game/Screens/SelectV2/PanelBeatmap.cs | 13 ------------- osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 13 ------------- .../Screens/SelectV2/PanelBeatmapStandalone.cs | 16 ---------------- 9 files changed, 42 insertions(+), 56 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs index dad987ab60..15ae35ad28 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs @@ -177,7 +177,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } [Test] - [Ignore("broken")] public void TestInputHandlingWithinGaps() { AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); @@ -204,10 +203,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ClickVisiblePanelWithOffset(0, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1))); WaitForGroupSelection(0, 1); - ClickVisiblePanelWithOffset(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + ClickVisiblePanelWithOffset(1, new Vector2(0, CarouselItem.DEFAULT_HEIGHT / 2 + 1)); WaitForGroupSelection(0, 2); - ClickVisiblePanelWithOffset(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + ClickVisiblePanelWithOffset(1, new Vector2(0, CarouselItem.DEFAULT_HEIGHT / 2 + 1)); WaitForGroupSelection(0, 5); } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs index 61ecf38637..8f7c901c37 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs @@ -170,7 +170,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } [Test] - [Ignore("broken")] public void TestInputHandlingWithinGaps() { AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs index 84e7d38239..8a173d3e71 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs @@ -5,7 +5,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Graphics.Carousel; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; using osuTK; @@ -259,7 +258,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } [Test] - [Ignore("broken")] public void TestInputHandlingWithinGaps() { AddBeatmaps(2, 5); @@ -278,14 +276,16 @@ namespace osu.Game.Tests.Visual.SongSelectV2 WaitForSelection(0, 0); // Beatmap panels expand their selection area to cover holes from spacing. - ClickVisiblePanelWithOffset(1, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + ClickVisiblePanelWithOffset(0, new Vector2(0, -(PanelBeatmap.HEIGHT / 2 + 1))); WaitForSelection(0, 0); - // Panels with higher depth will handle clicks in the gutters for simplicity. - ClickVisiblePanelWithOffset(2, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + ClickVisiblePanelWithOffset(2, new Vector2(0, (PanelBeatmap.HEIGHT / 2 + 1))); WaitForSelection(0, 2); - ClickVisiblePanelWithOffset(3, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + ClickVisiblePanelWithOffset(2, new Vector2(0, -(PanelBeatmap.HEIGHT / 2 + 1))); + WaitForSelection(0, 2); + + ClickVisiblePanelWithOffset(3, new Vector2(0, (PanelBeatmap.HEIGHT / 2 + 1))); WaitForSelection(0, 3); } diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index 8db0c683c2..2b1124eef1 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -395,6 +395,10 @@ namespace osu.Game.Graphics.Carousel offset += spacing; item.CarouselYPosition = offset; + item.CarouselInputLenienceAbove = spacing / 2; + if (previousVisible != null) + previousVisible.CarouselInputLenienceBelow = item.CarouselInputLenienceAbove; + if (item.IsVisible) { offset += item.DrawHeight; diff --git a/osu.Game/Graphics/Carousel/CarouselItem.cs b/osu.Game/Graphics/Carousel/CarouselItem.cs index 741e4d32fc..e1e93dd036 100644 --- a/osu.Game/Graphics/Carousel/CarouselItem.cs +++ b/osu.Game/Graphics/Carousel/CarouselItem.cs @@ -20,10 +20,27 @@ namespace osu.Game.Graphics.Carousel /// /// The current Y position in the carousel. + /// /// This is managed by and should not be set manually. /// public double CarouselYPosition { get; set; } + /// + /// The amount of input padding/lenience to be added to the area above this panel. + /// Calculated as half of the calculated spacing between this panel and the panel above it. + /// + /// This is managed by and should not be set manually. + /// + public float CarouselInputLenienceAbove { get; set; } + + /// + /// The amount of input padding/lenience to be added to the area below this panel. + /// Calculated as half of the calculated spacing between this panel and the panel below it. + /// + /// This is managed by and should not be set manually. + /// + public float CarouselInputLenienceBelow { get; set; } + /// /// The height this item will take when displayed. Defaults to . /// diff --git a/osu.Game/Screens/SelectV2/Panel.cs b/osu.Game/Screens/SelectV2/Panel.cs index f17567f9ba..f30c895a3b 100644 --- a/osu.Game/Screens/SelectV2/Panel.cs +++ b/osu.Game/Screens/SelectV2/Panel.cs @@ -68,10 +68,19 @@ namespace osu.Game.Screens.SelectV2 } } - // content is offset by PanelXOffset, make sure we only handle input at the actual visible - // offset region. - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => - TopLevelContent.ReceivePositionalInputAt(screenSpacePos); + public sealed override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + var inputRectangle = TopLevelContent.DrawRectangle; + + // Cover the gaps introduced by the spacing between panels so that user mis-aims don't result in no-ops. + inputRectangle = inputRectangle.Inflate(new MarginPadding + { + Top = item!.CarouselInputLenienceAbove, + Bottom = item!.CarouselInputLenienceBelow, + }); + + return inputRectangle.Contains(TopLevelContent.ToLocalSpace(screenSpacePos)); + } [Resolved] private BeatmapCarousel? carousel { get; set; } diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index e785448c9a..19ff8a0676 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -72,19 +72,6 @@ namespace osu.Game.Screens.SelectV2 PanelXOffset = 60; } - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) - { - var inputRectangle = TopLevelContent.DrawRectangle; - - // Cover the gaps introduced by the spacing between BeatmapPanels so that clicks will not fall through the carousel. - // - // Caveat is that for simplicity, we are covering the full spacing, so panels with frontmost depth will have a slightly - // larger hit target. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING }); - - return inputRectangle.Contains(TopLevelContent.ToLocalSpace(screenSpacePos)); - } - [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index e34e822e2d..425ca02e5a 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -67,19 +67,6 @@ namespace osu.Game.Screens.SelectV2 PanelXOffset = 20f; } - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) - { - var inputRectangle = TopLevelContent.DrawRectangle; - - // Cover the gaps introduced by the spacing between BeatmapPanels so that clicks will not fall through the carousel. - // - // Caveat is that for simplicity, we are covering the full spacing, so panels with frontmost depth will have a slightly - // larger hit target. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING }); - - return inputRectangle.Contains(TopLevelContent.ToLocalSpace(screenSpacePos)); - } - [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs index d461653dcb..287af444ee 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs @@ -71,22 +71,6 @@ namespace osu.Game.Screens.SelectV2 private OsuSpriteText authorText = null!; private FillFlowContainer mainFill = null!; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) - { - var inputRectangle = TopLevelContent.DrawRectangle; - - if (Selected.Value) - { - // Cover the gaps introduced by the spacing between BeatmapPanels so that clicks will not fall through the carousel. - // - // Caveat is that for simplicity, we are covering the full spacing, so panels with frontmost depth will have a slightly - // larger hit target. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING * 2 }); - } - - return inputRectangle.Contains(TopLevelContent.ToLocalSpace(screenSpacePos)); - } - public PanelBeatmapStandalone() { PanelXOffset = 20; From 7ffead6878059d35329b0cdeedb9a30f083fab53 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Jun 2025 23:19:22 +0900 Subject: [PATCH 026/370] SongSelectV2: Fix backgrounds taking too long to load due to model backed drawable --- .../Screens/SelectV2/PanelSetBackground.cs | 136 +++++++++--------- 1 file changed, 71 insertions(+), 65 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelSetBackground.cs b/osu.Game/Screens/SelectV2/PanelSetBackground.cs index dd07be0410..eeac9c4f89 100644 --- a/osu.Game/Screens/SelectV2/PanelSetBackground.cs +++ b/osu.Game/Screens/SelectV2/PanelSetBackground.cs @@ -9,94 +9,100 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; -using osu.Game.Overlays; using osuTK; using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public partial class PanelSetBackground : ModelBackedDrawable + public partial class PanelSetBackground : CompositeDrawable { - protected override double TransformDuration => 400; + private Sprite? sprite; + + private WorkingBeatmap? working; public WorkingBeatmap? Beatmap { - get => Model; - set => Model = value; + get => working; + set + { + working = value; + loadNextBackground(); + } } - protected override Drawable CreateDrawable(WorkingBeatmap? model) => new BackgroundSprite(model); - - private partial class BackgroundSprite : CompositeDrawable + public PanelSetBackground() { - private readonly WorkingBeatmap? working; + RelativeSizeAxes = Axes.Both; + } - public BackgroundSprite(WorkingBeatmap? working) + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] { - this.working = working; - - RelativeSizeAxes = Axes.Both; - } - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - var texture = working?.GetPanelBackground(); - - if (texture != null) + new FillFlowContainer { - InternalChildren = new Drawable[] + Depth = -1, + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + // This makes the gradient not be perfectly horizontal, but diagonal at a ~40° angle + Shear = new Vector2(0.8f, 0), + Children = new[] { - new Sprite + // The left half with no gradient applied + new Box { RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - FillMode = FillMode.Fill, - Texture = texture, + Colour = Color4.Black.Opacity(0.5f), + Width = 0.4f, }, - new FillFlowContainer + new Box { - Depth = -1, RelativeSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - // This makes the gradient not be perfectly horizontal, but diagonal at a ~40° angle - Shear = new Vector2(0.8f, 0), - Children = new[] - { - // The left half with no gradient applied - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black.Opacity(0.5f), - Width = 0.4f, - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.5f), Color4.Black.Opacity(0.3f)), - Width = 0.2f, - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.3f), Color4.Black.Opacity(0.2f)), - // Slightly more than 1.0 in total to account for shear. - Width = 0.45f, - }, - } + Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.5f), Color4.Black.Opacity(0.3f)), + Width = 0.2f, }, - }; - } - else - { - InternalChild = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background6, - }; - } + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.3f), Color4.Black.Opacity(0.2f)), + // Slightly more than 1.0 in total to account for shear. + Width = 0.45f, + }, + } + }, + }; + } + + private void loadNextBackground() + { + const double transition_duration = 500; + + var texture = working?.GetPanelBackground(); + + if (texture == null) + { + sprite?.FadeOut(transition_duration, Easing.OutQuint); + sprite = null; + return; } + + LoadComponentAsync(new Sprite + { + Depth = float.MaxValue, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fill, + Texture = texture, + }, s => + { + sprite?.Delay(transition_duration) + .FadeOut(); + + AddInternal(sprite = s); + sprite.FadeInFromZero(transition_duration, Easing.OutQuint); + }); } } } From 920eec2c589299616274d5e78b8e6dae8b453839 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Jun 2025 00:31:16 +0900 Subject: [PATCH 027/370] Add basic delay before beginning background load to avoid load on large scroll --- .../Screens/SelectV2/PanelSetBackground.cs | 72 +++++++++++++++---- 1 file changed, 59 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelSetBackground.cs b/osu.Game/Screens/SelectV2/PanelSetBackground.cs index eeac9c4f89..743d0d489a 100644 --- a/osu.Game/Screens/SelectV2/PanelSetBackground.cs +++ b/osu.Game/Screens/SelectV2/PanelSetBackground.cs @@ -1,11 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; @@ -14,27 +17,52 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public partial class PanelSetBackground : CompositeDrawable + public partial class PanelSetBackground : BufferedContainer { + [Resolved] + private BeatmapCarousel? beatmapCarousel { get; set; } + private Sprite? sprite; private WorkingBeatmap? working; + private CancellationTokenSource? loadCancellation; + + private double timeSinceUnpool; + public WorkingBeatmap? Beatmap { get => working; set { + if (value == working) + return; + working = value; - loadNextBackground(); + + loadCancellation?.Cancel(); + loadCancellation = null; + + sprite?.Expire(); + sprite = null; + + timeSinceUnpool = 0; } } public PanelSetBackground() + // : base(cachedFrameBuffer: true) { RelativeSizeAxes = Axes.Both; } + protected override void Update() + { + base.Update(); + + loadContentIfRequired(); + } + [BackgroundDependencyLoader] private void load() { @@ -74,18 +102,39 @@ namespace osu.Game.Screens.SelectV2 }; } - private void loadNextBackground() + private void loadContentIfRequired() { - const double transition_duration = 500; + // A load is already in progress if the cancellation token is non-null. + if (loadCancellation != null) + return; + + if (beatmapCarousel != null) + { + Quad containingSsdq = beatmapCarousel.ScreenSpaceDrawQuad; + + // Using DelayedLoadWrappers would only allow us to load content when on screen, but we want to preload while off-screen + // to provide a better user experience. + + // This is tracking time that this drawable is updating since the last pool. + // This is intended to provide a debounce so very fast scrolls (from one end to the other of the carousel) + // don't cause huge overheads. + // + // We increase the delay based on distance from centre, so the beatmaps the user is currently looking at load first. + float timeUpdatingBeforeLoad = 50 + Math.Abs(containingSsdq.Centre.Y - ScreenSpaceDrawQuad.Centre.Y) / containingSsdq.Height * 100; + + timeSinceUnpool += Time.Elapsed; + + // We only trigger a load after this set has been in an updating state for a set amount of time. + if (timeSinceUnpool <= timeUpdatingBeforeLoad) + return; + } + + loadCancellation = new CancellationTokenSource(); var texture = working?.GetPanelBackground(); if (texture == null) - { - sprite?.FadeOut(transition_duration, Easing.OutQuint); - sprite = null; return; - } LoadComponentAsync(new Sprite { @@ -97,12 +146,9 @@ namespace osu.Game.Screens.SelectV2 Texture = texture, }, s => { - sprite?.Delay(transition_duration) - .FadeOut(); - AddInternal(sprite = s); - sprite.FadeInFromZero(transition_duration, Easing.OutQuint); - }); + sprite.FadeInFromZero(200, Easing.OutQuint); + }, loadCancellation.Token); } } } From e2bbe49ca08c565644889c42999450e610c9bd1d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Jun 2025 00:29:49 +0900 Subject: [PATCH 028/370] Fix unstable y positions when panels are displayed on scroll --- osu.Game/Graphics/Carousel/Carousel.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index 8db0c683c2..6df78fe0cc 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -13,6 +13,7 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Framework.Development; +using osu.Framework.Extensions.PolygonExtensions; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -845,6 +846,8 @@ namespace osu.Game.Graphics.Carousel throw new InvalidOperationException($"Carousel panel drawables must implement {typeof(ICarouselPanel)}"); carouselPanel.Item = item; + carouselPanel.DrawYPosition = item.CarouselYPosition; + Scroll.Add(drawable); } @@ -853,6 +856,7 @@ namespace osu.Game.Graphics.Carousel // To make transitions of items appearing in the flow look good, do a pass and make sure newly added items spawn from // just beneath the *current interpolated position* of the previous panel. var orderedPanels = Scroll.Panels + .Where(p => Scroll.ScreenSpaceDrawQuad.Intersects(p.ScreenSpaceDrawQuad)) .OfType() .Where(p => p.Item != null) .OrderBy(p => p.Item!.CarouselYPosition) @@ -868,8 +872,6 @@ namespace osu.Game.Graphics.Carousel // It's usually off-screen anyway. if (i > 0 && i < orderedPanels.Count - 1) panel.DrawYPosition = orderedPanels[i - 1].DrawYPosition; - else - panel.DrawYPosition = panel.Item!.CarouselYPosition; } } } From 13cf9279222f2a37829c745d3d75bd23f6fd79f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Jun 2025 00:53:20 +0900 Subject: [PATCH 029/370] Fix unstable x positions when scrolling carousel --- osu.Game/Screens/SelectV2/Panel.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Panel.cs b/osu.Game/Screens/SelectV2/Panel.cs index f17567f9ba..ae832375ce 100644 --- a/osu.Game/Screens/SelectV2/Panel.cs +++ b/osu.Game/Screens/SelectV2/Panel.cs @@ -212,7 +212,9 @@ namespace osu.Game.Screens.SelectV2 base.PrepareForUse(); updateAccentColour(); - updateXOffset(); + + updateXOffset(animated: false); + updateSelectedState(animated: false); this.FadeIn(DURATION, Easing.OutQuint); } @@ -257,7 +259,7 @@ namespace osu.Game.Screens.SelectV2 selectionLayer.FadeOut(200, Easing.OutQuint); } - private void updateXOffset() + private void updateXOffset(bool animated = true) { float x = PanelXOffset + corner_radius; @@ -272,7 +274,7 @@ namespace osu.Game.Screens.SelectV2 if (!KeyboardSelected.Value) x += active_x_offset; - TopLevelContent.MoveToX(x, DURATION, Easing.OutQuint); + TopLevelContent.MoveToX(x, animated ? DURATION : 0, Easing.OutQuint); } protected override bool OnHover(HoverEvent e) From fa4c72887f07dd18d61959447e1bb91a2d2725a2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Jun 2025 01:14:18 +0900 Subject: [PATCH 030/370] Bring back missing logic to avoid stutters when scrolling Again, I don't know why the new implementation didn't just draw from the old which was known to work. This mostly matches what was there in v1. --- .../Screens/SelectV2/PanelSetBackground.cs | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelSetBackground.cs b/osu.Game/Screens/SelectV2/PanelSetBackground.cs index 743d0d489a..ae7c7d3138 100644 --- a/osu.Game/Screens/SelectV2/PanelSetBackground.cs +++ b/osu.Game/Screens/SelectV2/PanelSetBackground.cs @@ -5,6 +5,7 @@ using System; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.PolygonExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -51,7 +52,9 @@ namespace osu.Game.Screens.SelectV2 } public PanelSetBackground() - // : base(cachedFrameBuffer: true) + // TODO: for performance reasons we probably want this to be true + // for it to work we will need to move transforms accordingly. + : base(cachedFrameBuffer: false) { RelativeSizeAxes = Axes.Both; } @@ -105,7 +108,7 @@ namespace osu.Game.Screens.SelectV2 private void loadContentIfRequired() { // A load is already in progress if the cancellation token is non-null. - if (loadCancellation != null) + if (loadCancellation != null || working == null) return; if (beatmapCarousel != null) @@ -131,24 +134,37 @@ namespace osu.Game.Screens.SelectV2 loadCancellation = new CancellationTokenSource(); - var texture = working?.GetPanelBackground(); - - if (texture == null) - return; - - LoadComponentAsync(new Sprite + LoadComponentAsync(new PanelBeatmapBackground(working) { Depth = float.MaxValue, RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, FillMode = FillMode.Fill, - Texture = texture, }, s => { AddInternal(sprite = s); - sprite.FadeInFromZero(200, Easing.OutQuint); + bool spriteOnScreen = beatmapCarousel?.ScreenSpaceDrawQuad.Intersects(sprite.ScreenSpaceDrawQuad) != false; + sprite.FadeInFromZero(spriteOnScreen ? 400 : 0, Easing.OutQuint); }, loadCancellation.Token); } + + public partial class PanelBeatmapBackground : Sprite + { + private readonly IWorkingBeatmap working; + + public PanelBeatmapBackground(IWorkingBeatmap working) + { + ArgumentNullException.ThrowIfNull(working); + + this.working = working; + } + + [BackgroundDependencyLoader] + private void load() + { + Texture = working.GetPanelBackground(); + } + } } } From d2452ca25f7e8e6adf22375d481c961a3f9e2a4c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Jun 2025 02:06:20 +0900 Subject: [PATCH 031/370] SongSelectV2: Add padding to avoid overlap between mods button and personal best --- osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs index e3d52adef5..ff70596b2f 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs @@ -75,7 +75,7 @@ namespace osu.Game.Screens.SelectV2 private readonly IBindable fetchedScores = new Bindable(); - private const float personal_best_height = 100; + private const float personal_best_height = 112; [BackgroundDependencyLoader] private void load() From 8a129355778c327c85d20a711a5324ecd2c35cf7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Jun 2025 14:45:57 +0900 Subject: [PATCH 032/370] Fix null reference in some test scenes --- osu.Game/Screens/SelectV2/Panel.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Panel.cs b/osu.Game/Screens/SelectV2/Panel.cs index f30c895a3b..ddc9c1beb4 100644 --- a/osu.Game/Screens/SelectV2/Panel.cs +++ b/osu.Game/Screens/SelectV2/Panel.cs @@ -70,13 +70,16 @@ namespace osu.Game.Screens.SelectV2 public sealed override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { + if (item == null) + return TopLevelContent.ReceivePositionalInputAt(screenSpacePos); + var inputRectangle = TopLevelContent.DrawRectangle; // Cover the gaps introduced by the spacing between panels so that user mis-aims don't result in no-ops. inputRectangle = inputRectangle.Inflate(new MarginPadding { - Top = item!.CarouselInputLenienceAbove, - Bottom = item!.CarouselInputLenienceBelow, + Top = item.CarouselInputLenienceAbove, + Bottom = item.CarouselInputLenienceBelow, }); return inputRectangle.Contains(TopLevelContent.ToLocalSpace(screenSpacePos)); From 7af6acb17f0d0f1c2e0f006cb9acc12388d6d2db Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Jun 2025 15:17:13 +0900 Subject: [PATCH 033/370] De-duplicate logic which is applies when arriving/leaving song select MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Standardises all the logic which is applied in both directions – entering/resuming and suspending/exiting. --- osu.Game/Screens/SelectV2/SongSelect.cs | 112 +++++++++++------------- 1 file changed, 53 insertions(+), 59 deletions(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 252a0fc763..7d9f3bbde9 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -433,6 +433,18 @@ namespace osu.Game.Screens.SelectV2 selectionDebounce = Scheduler.AddDelayed(() => Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap), SELECTION_DEBOUNCE); } + private void ensureValidSelection() + { + // force reselection if entering song select with a protected beatmap + if (Beatmap.Value.BeatmapInfo.BeatmapSet!.Protected) + { + if (!carousel.NextRandom()) + Beatmap.SetDefault(); + } + else + updateSelection(); + } + private void updateSelection() => Scheduler.AddOnce(() => { var beatmap = Beatmap.Value; @@ -444,13 +456,7 @@ namespace osu.Game.Screens.SelectV2 // If not the current screen, this will be applied in OnResuming. ensurePlayingSelected(); - ApplyToBackground(backgroundModeBeatmap => - { - backgroundModeBeatmap.BlurAmount.Value = 0; - backgroundModeBeatmap.Beatmap = beatmap; - backgroundModeBeatmap.IgnoreUserSettings.Value = true; - backgroundModeBeatmap.DimWhenUserSettingsIgnored.Value = 0.1f; - }); + updateBackgroundDim(); } }); @@ -463,25 +469,7 @@ namespace osu.Game.Screens.SelectV2 base.OnEntering(e); this.FadeIn(); - - titleWedge.Show(); - detailsArea.Show(); - filterControl.Show(); - - modSelectOverlay.Beatmap.BindTo(Beatmap); - modSelectOverlay.SelectedMods.BindTo(Mods); - - beginLooping(); - attachTrackDuckingIfShould(); - - // force reselection if entering song select with a protected beatmap - if (Beatmap.Value.BeatmapInfo.BeatmapSet!.Protected) - { - if (!carousel.NextRandom()) - Beatmap.SetDefault(); - } - else - updateSelection(); + onArrivingAtScreen(); } public override void OnResuming(ScreenTransitionEvent e) @@ -489,43 +477,15 @@ namespace osu.Game.Screens.SelectV2 base.OnResuming(e); this.FadeIn(fade_duration, Easing.OutQuint); - - carousel.VisuallyFocusSelected = false; - - titleWedge.Show(); - detailsArea.Show(); - filterControl.Show(); - - modSelectOverlay.Beatmap.BindTo(Beatmap); - - // required due to https://github.com/ppy/osu-framework/issues/3218 - modSelectOverlay.SelectedMods.Disabled = false; - modSelectOverlay.SelectedMods.BindTo(Mods); - - beginLooping(); - attachTrackDuckingIfShould(); - - if (Beatmap.Value.BeatmapInfo.BeatmapSet!.Protected) - Beatmap.SetDefault(); - else - updateSelection(); + onArrivingAtScreen(); } public override void OnSuspending(ScreenTransitionEvent e) { - this.FadeOut(fade_duration, Easing.OutQuint); - - modSelectOverlay.SelectedMods.UnbindFrom(Mods); - modSelectOverlay.Beatmap.UnbindFrom(Beatmap); - - titleWedge.Hide(); - detailsArea.Hide(); - filterControl.Hide(); - carousel.VisuallyFocusSelected = true; - endLooping(); - detachTrackDucking(); + this.FadeOut(fade_duration, Easing.OutQuint); + onLeavingScreen(); base.OnSuspending(e); } @@ -533,6 +493,34 @@ namespace osu.Game.Screens.SelectV2 public override bool OnExiting(ScreenExitEvent e) { this.FadeOut(fade_duration, Easing.OutQuint); + onLeavingScreen(); + + return base.OnExiting(e); + } + + private void onArrivingAtScreen() + { + modSelectOverlay.Beatmap.BindTo(Beatmap); + // required due to https://github.com/ppy/osu-framework/issues/3218 + modSelectOverlay.SelectedMods.Disabled = false; + modSelectOverlay.SelectedMods.BindTo(Mods); + + carousel.VisuallyFocusSelected = false; + + titleWedge.Show(); + detailsArea.Show(); + filterControl.Show(); + + beginLooping(); + attachTrackDuckingIfShould(); + + ensureValidSelection(); + } + + private void onLeavingScreen() + { + modSelectOverlay.SelectedMods.UnbindFrom(Mods); + modSelectOverlay.Beatmap.UnbindFrom(Beatmap); titleWedge.Hide(); detailsArea.Hide(); @@ -540,8 +528,6 @@ namespace osu.Game.Screens.SelectV2 endLooping(); detachTrackDucking(); - - return base.OnExiting(e); } protected override void LogoArriving(OsuLogo logo, bool resuming) @@ -583,6 +569,14 @@ namespace osu.Game.Screens.SelectV2 logo.FadeOut(120, Easing.Out); } + private void updateBackgroundDim() => ApplyToBackground(backgroundModeBeatmap => + { + backgroundModeBeatmap.BlurAmount.Value = 0; + backgroundModeBeatmap.Beatmap = Beatmap.Value; + backgroundModeBeatmap.IgnoreUserSettings.Value = true; + backgroundModeBeatmap.DimWhenUserSettingsIgnored.Value = 0.1f; + }); + #endregion #region Filtering From 21c06a7fbd8db658b59510e64b2e9d229747041c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Jun 2025 15:20:07 +0900 Subject: [PATCH 034/370] SongSelectV2: Fix background dim not being applied correctly when returning to screen --- osu.Game/Screens/SelectV2/SongSelect.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 7d9f3bbde9..340413da67 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -575,6 +575,10 @@ namespace osu.Game.Screens.SelectV2 backgroundModeBeatmap.Beatmap = Beatmap.Value; backgroundModeBeatmap.IgnoreUserSettings.Value = true; backgroundModeBeatmap.DimWhenUserSettingsIgnored.Value = 0.1f; + + // Required to undo results screen dimming the background. + // Probably needs more thought because this needs to be in every `ApplyToBackground` currently to restore sane defaults. + backgroundModeBeatmap.FadeColour(Color4.White, 250); }); #endregion From 097d02e7506a22573d95c0798f5e2769942044f1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Jun 2025 15:32:07 +0900 Subject: [PATCH 035/370] Fix `PanelBeatmapStandalone` having too much horizontal offset on selection --- osu.Game/Screens/SelectV2/Panel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/Panel.cs b/osu.Game/Screens/SelectV2/Panel.cs index f17567f9ba..d5c6d994cf 100644 --- a/osu.Game/Screens/SelectV2/Panel.cs +++ b/osu.Game/Screens/SelectV2/Panel.cs @@ -263,7 +263,7 @@ namespace osu.Game.Screens.SelectV2 if (!Expanded.Value && !Selected.Value) { - if (this is PanelBeatmap) + if (this is PanelBeatmap || this is PanelBeatmapStandalone) x += active_x_offset * 2; else x += active_x_offset * 4; From 429c9d42c1e4d515d15dfd4d2bdb0a4eea8d79f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Jun 2025 15:57:54 +0900 Subject: [PATCH 036/370] Update inline comments to add clarity to implementation details --- .../Screens/SelectV2/PanelSetBackground.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelSetBackground.cs b/osu.Game/Screens/SelectV2/PanelSetBackground.cs index ae7c7d3138..d81a6007d8 100644 --- a/osu.Game/Screens/SelectV2/PanelSetBackground.cs +++ b/osu.Game/Screens/SelectV2/PanelSetBackground.cs @@ -52,8 +52,9 @@ namespace osu.Game.Screens.SelectV2 } public PanelSetBackground() - // TODO: for performance reasons we probably want this to be true - // for it to work we will need to move transforms accordingly. + // TODO: for performance reasons we may want this to be true. + // Setting to true will require that the buffered portion is moved to a child such that `FadeIn`/`FadeOut` transforms + // still work. : base(cachedFrameBuffer: false) { RelativeSizeAxes = Axes.Both; @@ -115,14 +116,14 @@ namespace osu.Game.Screens.SelectV2 { Quad containingSsdq = beatmapCarousel.ScreenSpaceDrawQuad; - // Using DelayedLoadWrappers would only allow us to load content when on screen, but we want to preload while off-screen - // to provide a better user experience. - - // This is tracking time that this drawable is updating since the last pool. - // This is intended to provide a debounce so very fast scrolls (from one end to the other of the carousel) - // don't cause huge overheads. + // One may ask why we are not using `DelayedLoadWrapper` for this delayed load logic. // - // We increase the delay based on distance from centre, so the beatmaps the user is currently looking at load first. + // - Using `DelayedLoadWrapper` would only allow us to load content when on screen, but we want to preload while panels are off-screen. + // This allows a more seamless experience when a user is scrolling at a moderate speed, as we are loading in backgrounds before they + // enter the visible viewport. + // - By using a slightly customised formula to decide when to start the load, we can coerce the loading of backgrounds into an order that + // prioritises panels which are closest to the centre of the screen. Basically, we want to load backgrounds "outwards" from the visual + // centre to give the user the best experience possible. float timeUpdatingBeforeLoad = 50 + Math.Abs(containingSsdq.Centre.Y - ScreenSpaceDrawQuad.Centre.Y) / containingSsdq.Height * 100; timeSinceUnpool += Time.Elapsed; From ef29eda3e0010c775413b934d9f2397da261d8f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Jun 2025 16:23:31 +0900 Subject: [PATCH 037/370] Mark some more recent flaky tests --- osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs | 2 ++ .../Visual/Gameplay/TestSceneGameplaySamplePlayback.cs | 1 + osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs | 4 +++- .../Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs | 1 + 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs index 75bcd809c8..4a72690da2 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs @@ -22,6 +22,7 @@ using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Storyboards; +using osu.Game.Tests; using osu.Game.Tests.Visual; using osuTK; @@ -107,6 +108,7 @@ namespace osu.Game.Rulesets.Osu.Tests } [Test] + [FlakyTest] public void TestVibrateWithoutSpinningOnCentreWithDoubleTime() { List frames = new List(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs index 21c83d521c..2334b1c6d6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs @@ -21,6 +21,7 @@ namespace osu.Game.Tests.Visual.Gameplay private bool seek; [Test] + [FlakyTest] public void TestAllSamplesStopDuringSeek() { DrawableSlider? slider = null; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs index 8b1a8307ca..276a0c3410 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs @@ -69,6 +69,7 @@ namespace osu.Game.Tests.Visual.Gameplay } [Test] + [FlakyTest] public void TestFadeOnIdle() { createTest(); @@ -144,7 +145,8 @@ namespace osu.Game.Tests.Visual.Gameplay } [Test] - public void TestDoesntFadeOnMouseDown() + [FlakyTest] + public void TestDoesNotFadeOnMouseDown() { createTest(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index bd66694cd9..faf8f35a8e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -303,6 +303,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } [Test] + [FlakyTest] public void TestMostInSyncUserIsAudioSource() { start(new[] { PLAYER_1_ID, PLAYER_2_ID }); From b152b786a5d6d8f2846a06bd0154ffbe62e68a80 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 3 Jun 2025 18:02:37 +0900 Subject: [PATCH 038/370] Attempt to fix flaky tests by removing finaliser --- osu.Game/Tests/Visual/OsuTestScene.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 09cfe5ecad..1cb7b2c840 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -341,8 +341,6 @@ namespace osu.Game.Tests.Visual { private readonly Track track; - private readonly TrackVirtualStore store; - /// /// Create an instance which creates a for the provided ruleset when requested. /// @@ -372,7 +370,7 @@ namespace osu.Game.Tests.Visual if (referenceClock != null) { - store = new TrackVirtualStore(referenceClock); + var store = new TrackVirtualStore(referenceClock); audio.AddItem(store); track = store.GetVirtual(trackLength); } @@ -385,12 +383,6 @@ namespace osu.Game.Tests.Visual LoadTrack(); } - ~ClockBackedTestWorkingBeatmap() - { - // Remove the track store from the audio manager - store?.Dispose(); - } - protected override Track GetBeatmapTrack() => track; public override bool TryTransferTrack(WorkingBeatmap target) From 88fb08851f882027e3cc23a58f7827627e8c8448 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Jun 2025 18:28:38 +0900 Subject: [PATCH 039/370] Fix non-matching corner radii --- osu.Game/Screens/SelectV2/FilterControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/FilterControl.cs b/osu.Game/Screens/SelectV2/FilterControl.cs index 05429c2c12..4700842a96 100644 --- a/osu.Game/Screens/SelectV2/FilterControl.cs +++ b/osu.Game/Screens/SelectV2/FilterControl.cs @@ -31,7 +31,7 @@ namespace osu.Game.Screens.SelectV2 // taken from draw visualiser. used for carousel alignment purposes. public const float HEIGHT_FROM_SCREEN_TOP = 141 - corner_radius; - private const float corner_radius = 8; + private const float corner_radius = 10; private SongSelectSearchTextBox searchTextBox = null!; private ShearedToggleButton showConvertedBeatmapsButton = null!; From b957fab10b4305bdcfa4c5cdd3679b7ded66395f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 3 Jun 2025 19:15:12 +0900 Subject: [PATCH 040/370] Isolate EditorBeatmap instance to fix flaky tests --- ...tSceneHitObjectComposerDistanceSnapping.cs | 64 ++++++++++--------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index 408db39d54..c081671a48 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs @@ -3,9 +3,7 @@ using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Testing; using osu.Framework.Utils; @@ -27,33 +25,34 @@ namespace osu.Game.Tests.Editing public partial class TestSceneHitObjectComposerDistanceSnapping : EditorClockTestScene { private TestHitObjectComposer composer = null!; - - [Cached(typeof(EditorBeatmap))] - [Cached(typeof(IBeatSnapProvider))] - private readonly EditorBeatmap editorBeatmap; - - protected override Container Content { get; } = new PopoverContainer { RelativeSizeAxes = Axes.Both }; - - public TestSceneHitObjectComposerDistanceSnapping() - { - base.Content.Add(new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - editorBeatmap = new EditorBeatmap(new OsuBeatmap - { - BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, - }), - Content - }, - }); - } + private EditorBeatmap editorBeatmap = null!; [SetUp] public void Setup() => Schedule(() => { - Child = composer = new TestHitObjectComposer(); + editorBeatmap = new EditorBeatmap(new OsuBeatmap + { + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, + }); + + Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = + [ + (typeof(EditorBeatmap), editorBeatmap), + (typeof(IBeatSnapProvider), editorBeatmap) + ], + Children = new Drawable[] + { + editorBeatmap, + new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Child = composer = new TestHitObjectComposer() + } + } + }; BeatDivisor.Value = 1; @@ -247,16 +246,23 @@ namespace osu.Game.Tests.Editing } private void assertSnapDistance(float expectedDistance, IHasSliderVelocity? hasSliderVelocity = null) - => AddAssert($"distance is {expectedDistance}", () => composer.DistanceSnapProvider.GetBeatSnapDistance(hasSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); + => AddAssert($"distance is {expectedDistance}", () => composer.DistanceSnapProvider.GetBeatSnapDistance(hasSliderVelocity), + () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); private void assertDurationToDistance(double duration, float expectedDistance, HitObject? referenceObject = null) - => AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DistanceSnapProvider.DurationToDistance(duration, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); + => AddAssert($"duration = {duration} -> distance = {expectedDistance}", + () => composer.DistanceSnapProvider.DurationToDistance(duration, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity), + () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); private void assertDistanceToDuration(float distance, double expectedDuration, HitObject? referenceObject = null) - => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceSnapProvider.DistanceToDuration(distance, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON)); + => AddAssert($"distance = {distance} -> duration = {expectedDuration}", + () => composer.DistanceSnapProvider.DistanceToDuration(distance, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity), + () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON)); private void assertSnappedDistance(float distance, float expectedDistance, HitObject? referenceObject = null) - => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDistance(distance, referenceObject?.GetEndTime() ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); + => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", + () => composer.DistanceSnapProvider.FindSnappedDistance(distance, referenceObject?.GetEndTime() ?? 0, referenceObject as IHasSliderVelocity), + () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); private partial class TestHitObjectComposer : OsuHitObjectComposer { From bea3653c520b00f46305232a6704dc078179d5b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 3 Jun 2025 12:46:09 +0200 Subject: [PATCH 041/370] Fix argon & triangles skins reading legacy slider colour overrides from beatmap skins Closes https://github.com/ppy/osu/issues/33383. --- osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs | 7 ++++--- osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBody.cs | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs index bda1e6cf41..7b43886057 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs @@ -44,10 +44,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default SnakingOut.BindTo(configSnakingOut); - BorderColour = skin.GetConfig(OsuSkinColour.SliderBorder)?.Value ?? Color4.White; + BorderColour = GetBorderColour(skin); } - protected virtual Color4 GetBodyAccentColour(ISkinSource skin, Color4 hitObjectAccentColour) => - skin.GetConfig(OsuSkinColour.SliderTrackOverride)?.Value ?? hitObjectAccentColour; + protected virtual Color4 GetBorderColour(ISkinSource skin) => Color4.White; + + protected virtual Color4 GetBodyAccentColour(ISkinSource skin, Color4 hitObjectAccentColour) => hitObjectAccentColour; } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBody.cs index b54bb44f94..43b7260e2c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBody.cs @@ -15,11 +15,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { protected override DrawableSliderPath CreateSliderPath() => new LegacyDrawableSliderPath(); + protected override Color4 GetBorderColour(ISkinSource skin) + => skin.GetConfig(OsuSkinColour.SliderBorder)?.Value ?? Color4.White; + protected override Color4 GetBodyAccentColour(ISkinSource skin, Color4 hitObjectAccentColour) - { // legacy skins use a constant value for slider track alpha, regardless of the source colour. - return base.GetBodyAccentColour(skin, hitObjectAccentColour).Opacity(0.7f); - } + => (skin.GetConfig(OsuSkinColour.SliderTrackOverride)?.Value ?? hitObjectAccentColour).Opacity(0.7f); private partial class LegacyDrawableSliderPath : DrawableSliderPath { From f08743302ba09edcca326808454f0acd0b3c5f77 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 3 Jun 2025 20:48:36 +0900 Subject: [PATCH 042/370] Remove another similar finaliser --- osu.Game.Tests/WaveformTestBeatmap.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/osu.Game.Tests/WaveformTestBeatmap.cs b/osu.Game.Tests/WaveformTestBeatmap.cs index 12660ed2e1..2da54eb055 100644 --- a/osu.Game.Tests/WaveformTestBeatmap.cs +++ b/osu.Game.Tests/WaveformTestBeatmap.cs @@ -39,12 +39,6 @@ namespace osu.Game.Tests trackStore = audioManager.GetTrackStore(getZipReader()); } - ~WaveformTestBeatmap() - { - // Remove the track store from the audio manager - trackStore?.Dispose(); - } - private static Stream getStream() => TestResources.GetTestBeatmapStream(); private static ZipArchiveReader getZipReader() => new ZipArchiveReader(getStream()); From fe325ba8619d0ff24767459e12a6c512d50557c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 3 Jun 2025 13:52:12 +0200 Subject: [PATCH 043/370] Use UTF-8 encoding when exporting skin archives Closes https://github.com/ppy/osu/issues/33337. See also: https://github.com/ppy/osu/pull/28034#issuecomment-2084450285 --- osu.Game/Database/LegacySkinExporter.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Database/LegacySkinExporter.cs b/osu.Game/Database/LegacySkinExporter.cs index 14a3907916..98c4c5dbea 100644 --- a/osu.Game/Database/LegacySkinExporter.cs +++ b/osu.Game/Database/LegacySkinExporter.cs @@ -13,6 +13,8 @@ namespace osu.Game.Database { } + protected override bool UseFixedEncoding => false; + protected override string FileExtension => @".osk"; } } From ad251d701e617b5503e7e9a0e1c9b784acda71ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 3 Jun 2025 14:28:41 +0200 Subject: [PATCH 044/370] Fix negative input lenience being applied to overlapping panels --- osu.Game/Graphics/Carousel/Carousel.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index 2b1124eef1..bcac4a90c0 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -395,9 +395,14 @@ namespace osu.Game.Graphics.Carousel offset += spacing; item.CarouselYPosition = offset; - item.CarouselInputLenienceAbove = spacing / 2; - if (previousVisible != null) - previousVisible.CarouselInputLenienceBelow = item.CarouselInputLenienceAbove; + // ensure there are no input gaps where clicking will fall through the carousel. + // notably, only do this where there's positive spacing between panels (negative spacing means they overlap already and there is no gap to fill). + if (spacing > 0) + { + item.CarouselInputLenienceAbove = spacing / 2; + if (previousVisible != null) + previousVisible.CarouselInputLenienceBelow = item.CarouselInputLenienceAbove; + } if (item.IsVisible) { From 511c1d835bc41e648f8aa8f3e3778c9e24c0952a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 3 Jun 2025 14:50:35 +0200 Subject: [PATCH 045/370] Fix track not looping if specified preview point exceeds duration of track By falling back to the default of 40% of track duration in such cases. Also added a safety for the restart point exceeding acceptable bounds in case of a non-zero offset. Closes https://github.com/ppy/osu/issues/33308. --- osu.Game/Beatmaps/WorkingBeatmap.cs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index b0f6082406..4ea26b46f8 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -124,18 +124,16 @@ namespace osu.Game.Beatmaps Track.Looping = looping; Track.RestartPoint = Metadata.PreviewTime; - if (Track.RestartPoint == -1) + if (!Track.IsLoaded) { - if (!Track.IsLoaded) - { - // force length to be populated (https://github.com/ppy/osu-framework/issues/4202) - Track.Seek(Track.CurrentTime); - } - - Track.RestartPoint = 0.4f * Track.Length; + // force length to be populated (https://github.com/ppy/osu-framework/issues/4202) + Track.Seek(Track.CurrentTime); } - Track.RestartPoint += offsetFromPreviewPoint; + if (Track.RestartPoint < 0 || Track.RestartPoint > Track.Length) + Track.RestartPoint = 0.4f * Track.Length; + + Track.RestartPoint = Math.Clamp(Track.RestartPoint + offsetFromPreviewPoint, 0, Track.Length); } /// From 4a4991e3485e48d4cebce979030390ef86b88e1b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Jun 2025 20:13:36 +0900 Subject: [PATCH 046/370] Remove local manifestation of beatmap sets now that set items is always populated Since 4d33602. --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 740ed14e1e..41b45df443 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -612,13 +612,6 @@ namespace osu.Game.Screens.SelectV2 // If set grouping is available, this is the fastest way to retrieve sets for randomisation. ICollection visibleSets = grouping.SetItems.Keys; - // If not, we need to do an expensive copy. - // - // There's probably a more efficient way to handle this. Maybe the grouping filter should always expose grouped sets regardless - // as that process is done asynchronously. - if (!visibleSets.Any()) - visibleSets = carouselItems.Select(i => i.Model).OfType().Select(b => b.BeatmapSet!).Distinct().ToList(); - if (CurrentSelection is BeatmapInfo beatmapInfo) { randomSelectedBeatmaps.Add(beatmapInfo); From 367b6727cd0c8087070d0adf6ed32cfb8f16d5f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 3 Jun 2025 15:14:56 +0200 Subject: [PATCH 047/370] Update one more inline comment --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 41b45df443..19333a97b5 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -609,7 +609,7 @@ namespace osu.Game.Screens.SelectV2 if (carouselItems?.Any() != true) return false; - // If set grouping is available, this is the fastest way to retrieve sets for randomisation. + // This is the fastest way to retrieve sets for randomisation. ICollection visibleSets = grouping.SetItems.Keys; if (CurrentSelection is BeatmapInfo beatmapInfo) From b42b8ba0de4bcaaeb3108e842105276f357d0caa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Jun 2025 13:03:34 +0900 Subject: [PATCH 048/370] Add failing test coverage showing bad selection logic --- .../SongSelectV2/TestSceneSongSelect.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs index dcd745395b..294a33c7e5 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Online.API; using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Mods; @@ -105,6 +106,39 @@ namespace osu.Game.Tests.Visual.SongSelectV2 void onScreenPushed(IScreen lastScreen, IScreen newScreen) => screensPushed.Add(lastScreen); } + [Test] + public void TestInvalidRulesetDoesNotEnterGameplay() + { + var screensPushed = new List(); + + ImportBeatmapForRuleset(0); + ImportBeatmapForRuleset(1); + + LoadSongSelect(); + AddStep("subscribe to screen pushed", () => Stack.ScreenPushed += onScreenPushed); + + AddStep("change ruleset to taiko", () => Ruleset.Value = Rulesets.AvailableRulesets.Single(r => r.OnlineID == 1)); + + AddStep("disable converts", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, false)); + + AddUntilStep("wait for taiko beatmap selected", () => Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, () => Is.EqualTo(1)); + + AddStep("change ruleset back and start gameplay immediately", () => + { + Ruleset.Value = Rulesets.AvailableRulesets.Single(r => r.OnlineID == 0); + + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("no screens pushed", () => screensPushed, () => Is.Empty); + AddStep("unsubscribe from screen pushed", () => Stack.ScreenPushed -= onScreenPushed); + + AddUntilStep("wait for osu beatmap selected", () => Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, () => Is.EqualTo(0)); + + void onScreenPushed(IScreen lastScreen, IScreen newScreen) => screensPushed.Add(lastScreen); + } + #region Hotkeys [Test] From 12d3952905b0970f067ae86be4f838a453d2e204 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Jun 2025 15:53:37 +0900 Subject: [PATCH 049/370] SongSelectV2: Simplify and standardise selection logic Before attempting to fix issues with invalid selections reaching gameplay, I needed to do a pass of the song select class as selection logic was already more complex than I'd hope. All operations now go through a single flow (`SelectAndRun`) when leaving the song select screen in a "success" state. --- osu.Game/Screens/SelectV2/SoloSongSelect.cs | 20 ++--- osu.Game/Screens/SelectV2/SongSelect.cs | 99 +++++++++++---------- 2 files changed, 59 insertions(+), 60 deletions(-) diff --git a/osu.Game/Screens/SelectV2/SoloSongSelect.cs b/osu.Game/Screens/SelectV2/SoloSongSelect.cs index 2c1eabc5fb..6d0a2b3b62 100644 --- a/osu.Game/Screens/SelectV2/SoloSongSelect.cs +++ b/osu.Game/Screens/SelectV2/SoloSongSelect.cs @@ -58,7 +58,7 @@ namespace osu.Game.Screens.SelectV2 public override IEnumerable GetForwardActions(BeatmapInfo beatmap) { - yield return new OsuMenuItem(ButtonSystemStrings.Play.ToSentence(), MenuItemType.Highlighted, () => SelectAndStart(beatmap)) { Icon = FontAwesome.Solid.Check }; + yield return new OsuMenuItem(ButtonSystemStrings.Play.ToSentence(), MenuItemType.Highlighted, () => SelectAndRun(beatmap, OnStart)) { Icon = FontAwesome.Solid.Check }; yield return new OsuMenuItem(ButtonSystemStrings.Edit.ToSentence(), MenuItemType.Standard, () => edit(beatmap)) { Icon = FontAwesome.Solid.PencilAlt }; yield return new OsuMenuItemSpacer(); @@ -85,13 +85,9 @@ namespace osu.Game.Screens.SelectV2 yield return new OsuMenuItem(WebCommonStrings.ButtonsHide.ToSentence(), MenuItemType.Destructive, () => beatmaps.Hide(beatmap)); } - protected override bool OnStart() + protected override void OnStart() { - if (playerLoader != null) return false; - if (!this.IsCurrentScreen()) return false; - if (Beatmap.IsDefault) return false; - - FinaliseSelection(); + if (playerLoader != null) return; modsAtGameplayStart = Mods.Value; @@ -106,7 +102,7 @@ namespace osu.Game.Screens.SelectV2 { Text = NotificationsStrings.NoAutoplayMod }); - return false; + return; } var mods = Mods.Value.Append(autoInstance).ToArray(); @@ -120,7 +116,6 @@ namespace osu.Game.Screens.SelectV2 sampleConfirmSelection?.Play(); this.Push(playerLoader = new PlayerLoader(createPlayer)); - return true; Player createPlayer() { @@ -146,12 +141,7 @@ namespace osu.Game.Screens.SelectV2 if (!this.IsCurrentScreen()) return; - FinaliseSelection(); - - // Forced refetch is important here to guarantee correct invalidation across all difficulties. - Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap, true); - - this.Push(new EditorLoader()); + SelectAndRun(beatmap, () => this.Push(new EditorLoader())); } public override void OnResuming(ScreenTransitionEvent e) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 340413da67..a732f1447b 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -227,7 +227,7 @@ namespace osu.Game.Screens.SelectV2 BleedTop = FilterControl.HEIGHT_FROM_SCREEN_TOP + 5, BleedBottom = ScreenFooter.HEIGHT + 5, RelativeSizeAxes = Axes.Both, - RequestPresentBeatmap = _ => OnStart(), + RequestPresentBeatmap = b => SelectAndRun(b, OnStart), RequestSelection = selectBeatmap, RequestRecommendedSelection = selectRecommendedBeatmap, NewItemsPresented = newItemsPresented, @@ -263,10 +263,11 @@ namespace osu.Game.Screens.SelectV2 } /// - /// Called when a selection is made. + /// Called when a selection is made to progress away from the song select screen. + /// + /// This is the default action which should be provided to . /// - /// If a resultant action occurred that takes the user away from SongSelect. - protected abstract bool OnStart(); + protected abstract void OnStart(); public override IReadOnlyList CreateFooterButtons() => new ScreenFooterButton[] { @@ -302,7 +303,7 @@ namespace osu.Game.Screens.SelectV2 .FadeTo(v.NewValue == Visibility.Visible ? 0f : 1f, 200, Easing.OutQuint); }); - Beatmap.BindValueChanged(_ => updateSelection()); + Beatmap.BindValueChanged(_ => EnsureValidSelection()); } protected override void Update() @@ -391,28 +392,36 @@ namespace osu.Game.Screens.SelectV2 #region Selection handling + private ScheduledDelegate? selectionDebounce; + /// - /// Finalises selection on the given . + /// Finalises selection on the given and runs the provided action if possible. /// - public void SelectAndStart(BeatmapInfo beatmap) + /// The beatmap which should be selected. If not provided, the current globally selected beatmap will be used. + /// The action to perform if conditions are met to be able to proceed. May not be invoked if in an invalid state. + public void SelectAndRun(BeatmapInfo beatmap, Action startAction) { + selectionDebounce?.Cancel(); + if (!this.IsCurrentScreen()) return; - Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap); - OnStart(); - } + // Forced refetch is important here to guarantee correct invalidation across all difficulties (editor specific). + Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap, true); - /// - /// Immediately flush any pending selection. Should be run before performing final actions such as leaving the screen. - /// - protected void FinaliseSelection() - { - if (selectionDebounce?.State == ScheduledDelegate.RunState.Waiting) - selectionDebounce.RunTask(); - } + if (Beatmap.IsDefault) + return; - private ScheduledDelegate? selectionDebounce; + // EnsureValidSelection also performs these checks, but it will change the active selection on fail. + // We want no-op for such an edge case, so early return. + if (beatmap.BeatmapSet!.Protected || beatmap.BeatmapSet!.DeletePending) + return; + + if (!EnsureValidSelection()) + return; + + startAction(); + } private void selectRecommendedBeatmap(IEnumerable beatmaps) { @@ -424,42 +433,42 @@ namespace osu.Game.Screens.SelectV2 if (!this.IsCurrentScreen()) return; - if (beatmap.BeatmapSet!.Protected) - return; - carousel.CurrentSelection = beatmap; + // Debounce consideration is to avoid beatmap churn on key repeat selection. selectionDebounce?.Cancel(); selectionDebounce = Scheduler.AddDelayed(() => Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap), SELECTION_DEBOUNCE); } - private void ensureValidSelection() + protected bool EnsureValidSelection() { - // force reselection if entering song select with a protected beatmap - if (Beatmap.Value.BeatmapInfo.BeatmapSet!.Protected) + if (!this.IsCurrentScreen()) + return false; + + bool validSelection = true; + + if (Beatmap.Value.BeatmapSetInfo.Protected || Beatmap.Value.BeatmapSetInfo.DeletePending) { if (!carousel.NextRandom()) + { Beatmap.SetDefault(); + validSelection = false; + } } - else - updateSelection(); + + carousel.CurrentSelection = Beatmap.Value.BeatmapInfo; + + ensurePlayingSelected(); + updateBackgroundDim(); + + if (!validSelection) + return false; + + // TODO: Add things here like ruleset validation. Or maybe a forced carousel filter. + + return validSelection; } - private void updateSelection() => Scheduler.AddOnce(() => - { - var beatmap = Beatmap.Value; - - carousel.CurrentSelection = beatmap.BeatmapInfo; - - if (this.IsCurrentScreen()) - { - // If not the current screen, this will be applied in OnResuming. - ensurePlayingSelected(); - - updateBackgroundDim(); - } - }); - #endregion #region Transitions @@ -514,7 +523,7 @@ namespace osu.Game.Screens.SelectV2 beginLooping(); attachTrackDuckingIfShould(); - ensureValidSelection(); + EnsureValidSelection(); } private void onLeavingScreen() @@ -548,7 +557,7 @@ namespace osu.Game.Screens.SelectV2 logo.Action = () => { - OnStart(); + SelectAndRun(Beatmap.Value.BeatmapInfo, OnStart); return false; }; } @@ -724,7 +733,7 @@ namespace osu.Game.Screens.SelectV2 public virtual IEnumerable GetForwardActions(BeatmapInfo beatmap) { - yield return new OsuMenuItem("Select", MenuItemType.Highlighted, () => SelectAndStart(beatmap)) + yield return new OsuMenuItem("Select", MenuItemType.Highlighted, () => SelectAndRun(beatmap, OnStart)) { Icon = FontAwesome.Solid.Check }; From c73ef15ebf64a495dde528175fd454c3d1472623 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Jun 2025 18:24:43 +0900 Subject: [PATCH 050/370] Ensure valid ruleset for gameplay --- osu.Game/Beatmaps/BeatmapInfoExtensions.cs | 14 +++ osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 8 +- .../SelectV2/BeatmapCarouselFilterMatching.cs | 4 +- osu.Game/Screens/SelectV2/SongSelect.cs | 92 +++++++++++-------- 4 files changed, 74 insertions(+), 44 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs index 25f98c812c..d25a171023 100644 --- a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs @@ -64,6 +64,20 @@ namespace osu.Game.Beatmaps private static string getVersionString(IBeatmapInfo beatmapInfo) => string.IsNullOrEmpty(beatmapInfo.DifficultyName) ? string.Empty : $"[{beatmapInfo.DifficultyName}]"; + /// + /// Whether gameplay is allowed for this beatmap with the provided ruleset (via conversion or direct compatibility). + /// + public static bool AllowGameplayWithRuleset(this IBeatmapInfo beatmap, RulesetInfo ruleset, bool allowConversion) + { + if (beatmap.Ruleset.ShortName == ruleset.ShortName) + return true; + + if (allowConversion && beatmap.Ruleset.OnlineID == 0 && ruleset.OnlineID != 0) + return true; + + return false; + } + /// /// Get the beatmap info page URL, or null if unavailable. /// diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 19333a97b5..cc40921562 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -13,6 +13,7 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Framework.Threading; using osu.Framework.Utils; @@ -495,7 +496,7 @@ namespace osu.Game.Screens.SelectV2 private ScheduledDelegate? loadingDebounce; - public void Filter(FilterCriteria criteria) + public void Filter(FilterCriteria criteria, bool showLoadingImmediately = false) { bool resetDisplay = grouping.BeatmapSetsGroupedTogether != BeatmapCarouselFilterGrouping.ShouldGroupBeatmapsTogether(criteria); @@ -503,9 +504,12 @@ namespace osu.Game.Screens.SelectV2 loadingDebounce ??= Scheduler.AddDelayed(() => { + if (loading.State.Value == Visibility.Visible) + return; + Scroll.FadeColour(OsuColour.Gray(0.5f), 1000, Easing.OutQuint); loading.Show(); - }, 250); + }, showLoadingImmediately ? 0 : 250); FilterAsync(resetDisplay).ContinueWith(_ => Schedule(() => { diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs index 545fbbd5fd..a776b2f796 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs @@ -56,9 +56,7 @@ namespace osu.Game.Screens.SelectV2 private static bool checkCriteriaMatch(BeatmapInfo beatmap, FilterCriteria criteria) { - bool match = criteria.Ruleset == null || - beatmap.Ruleset.ShortName == criteria.Ruleset.ShortName || - (beatmap.Ruleset.OnlineID == 0 && criteria.Ruleset.OnlineID != 0 && criteria.AllowConvertedBeatmaps); + bool match = criteria.Ruleset == null || beatmap.AllowGameplayWithRuleset(criteria.Ruleset!, criteria.AllowConvertedBeatmaps); if (beatmap.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true) { diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index a732f1447b..8c362c2b44 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -303,7 +303,7 @@ namespace osu.Game.Screens.SelectV2 .FadeTo(v.NewValue == Visibility.Visible ? 0f : 1f, 200, Easing.OutQuint); }); - Beatmap.BindValueChanged(_ => EnsureValidSelection()); + Beatmap.BindValueChanged(_ => ensureGlobalBeatmapValid()); } protected override void Update() @@ -406,18 +406,18 @@ namespace osu.Game.Screens.SelectV2 if (!this.IsCurrentScreen()) return; + // `ensureGlobalBeatmapValid` also performs this checks, but it will change the active selection on fail. + // By checking locally first, we can correctly perform a no-op rather than changing selection. + if (!checkBeatmapValidForSelection(beatmap, carousel.Criteria)) + return; + // Forced refetch is important here to guarantee correct invalidation across all difficulties (editor specific). Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap, true); if (Beatmap.IsDefault) return; - // EnsureValidSelection also performs these checks, but it will change the active selection on fail. - // We want no-op for such an edge case, so early return. - if (beatmap.BeatmapSet!.Protected || beatmap.BeatmapSet!.DeletePending) - return; - - if (!EnsureValidSelection()) + if (!ensureGlobalBeatmapValid()) return; startAction(); @@ -440,33 +440,57 @@ namespace osu.Game.Screens.SelectV2 selectionDebounce = Scheduler.AddDelayed(() => Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap), SELECTION_DEBOUNCE); } - protected bool EnsureValidSelection() + private bool ensureGlobalBeatmapValid() { if (!this.IsCurrentScreen()) return false; - bool validSelection = true; + // While filtering, let's not ever attempt to change selection. + // This will be resolved after the filter completes, see `newItemsPresented`. + bool carouselStateIsValid = filterDebounce?.State != ScheduledDelegate.RunState.Waiting && !carousel.IsFiltering; + if (!carouselStateIsValid) + return false; - if (Beatmap.Value.BeatmapSetInfo.Protected || Beatmap.Value.BeatmapSetInfo.DeletePending) + // Refetch to be confident that the current selection is still valid. It may have been deleted or hidden. + var currentBeatmap = beatmaps.GetWorkingBeatmap(Beatmap.Value.BeatmapInfo, true); + bool validSelection = checkBeatmapValidForSelection(currentBeatmap.BeatmapInfo, carousel.Criteria); + + if (Beatmap.IsDefault || !validSelection) { - if (!carousel.NextRandom()) - { - Beatmap.SetDefault(); - validSelection = false; - } + validSelection = carousel.NextRandom(); + if (selectionDebounce?.State == ScheduledDelegate.RunState.Waiting) + selectionDebounce?.RunTask(); } - carousel.CurrentSelection = Beatmap.Value.BeatmapInfo; + if (validSelection) + carousel.CurrentSelection = Beatmap.Value.BeatmapInfo; + else + Beatmap.SetDefault(); ensurePlayingSelected(); updateBackgroundDim(); - if (!validSelection) + return validSelection; + } + + private bool checkBeatmapValidForSelection(BeatmapInfo beatmap, FilterCriteria? criteria) + { + if (criteria == null) return false; - // TODO: Add things here like ruleset validation. Or maybe a forced carousel filter. + if (!beatmap.AllowGameplayWithRuleset(Ruleset.Value, criteria.AllowConvertedBeatmaps)) + return false; - return validSelection; + if (beatmap.Hidden) + return false; + + if (beatmap.BeatmapSet == null) + return false; + + if (beatmap.BeatmapSet.Protected || beatmap.BeatmapSet.DeletePending) + return false; + + return true; } #endregion @@ -523,7 +547,7 @@ namespace osu.Game.Screens.SelectV2 beginLooping(); attachTrackDuckingIfShould(); - EnsureValidSelection(); + ensureGlobalBeatmapValid(); } private void onLeavingScreen() @@ -600,11 +624,15 @@ namespace osu.Game.Screens.SelectV2 private void criteriaChanged(FilterCriteria criteria) { - // The first filter needs to be applied immediately as this triggers the initial carousel load. - double filterDelay = filterDebounce == null ? 0 : filter_delay; - filterDebounce?.Cancel(); - filterDebounce = Scheduler.AddDelayed(() => { carousel.Filter(criteria); }, filterDelay); + + // The first filter needs to be applied immediately as this triggers the initial carousel load. + bool isFirstFilter = filterDebounce == null; + + // Criteria change may have included a ruleset change which made the current selection invalid. + bool isSelectionValid = checkBeatmapValidForSelection(Beatmap.Value.BeatmapInfo, criteria); + + filterDebounce = Scheduler.AddDelayed(() => { carousel.Filter(criteria, !isSelectionValid); }, isFirstFilter || !isSelectionValid ? 0 : filter_delay); } private void newItemsPresented(IEnumerable carouselItems) @@ -620,21 +648,7 @@ namespace osu.Game.Screens.SelectV2 // but also in this case we want support for formatting a number within a string). filterControl.StatusText = count != 1 ? $"{count:#,0} matches" : $"{count:#,0} match"; - // Refetch to be confident that the current selection is still valid. It may have been deleted or hidden. - var currentBeatmap = beatmaps.GetWorkingBeatmap(Beatmap.Value.BeatmapInfo, true); - bool currentBeatmapNotValid = currentBeatmap.BeatmapInfo.Hidden || currentBeatmap.BeatmapSetInfo?.DeletePending == true; - - // If all results are filtered away don't deselect the current global beatmap selection... - if (!carouselItems.Any()) - { - // ...unless it has been deleted or hidden - if (currentBeatmapNotValid) - Beatmap.SetDefault(); - return; - } - - if (Beatmap.IsDefault || currentBeatmapNotValid) - carousel.NextRandom(); + ensureGlobalBeatmapValid(); } private void updateNoResultsPlaceholder() From 4fcdfa2cfc5cc92ba0f0f23e7e8700a9f56ee025 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Jun 2025 22:48:21 +0900 Subject: [PATCH 051/370] Move state updates to separate method and flow --- osu.Game/Screens/SelectV2/SongSelect.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 8c362c2b44..d77764d916 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -303,7 +303,17 @@ namespace osu.Game.Screens.SelectV2 .FadeTo(v.NewValue == Visibility.Visible ? 0f : 1f, 200, Easing.OutQuint); }); - Beatmap.BindValueChanged(_ => ensureGlobalBeatmapValid()); + Beatmap.BindValueChanged(_ => + { + ensureGlobalBeatmapValid(); + updateStateFromCurrentBeatmap(); + }); + } + + private void updateStateFromCurrentBeatmap() + { + ensurePlayingSelected(); + updateBackgroundDim(); } protected override void Update() @@ -467,9 +477,6 @@ namespace osu.Game.Screens.SelectV2 else Beatmap.SetDefault(); - ensurePlayingSelected(); - updateBackgroundDim(); - return validSelection; } @@ -548,6 +555,8 @@ namespace osu.Game.Screens.SelectV2 attachTrackDuckingIfShould(); ensureGlobalBeatmapValid(); + + updateStateFromCurrentBeatmap(); } private void onLeavingScreen() From c009d4d03ca2745f000b5e268776be46359458e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Jun 2025 14:06:19 +0900 Subject: [PATCH 052/370] Ensure we use valid criteria when attempting to select a beatmap `OnEntering` runs before `FilterControl.LoadComplete` meaning that `BeatmapCarousel` doesn't have filter populated yet... --- osu.Game/Screens/SelectV2/SongSelect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index d77764d916..28e11930da 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -463,7 +463,7 @@ namespace osu.Game.Screens.SelectV2 // Refetch to be confident that the current selection is still valid. It may have been deleted or hidden. var currentBeatmap = beatmaps.GetWorkingBeatmap(Beatmap.Value.BeatmapInfo, true); - bool validSelection = checkBeatmapValidForSelection(currentBeatmap.BeatmapInfo, carousel.Criteria); + bool validSelection = checkBeatmapValidForSelection(currentBeatmap.BeatmapInfo, filterControl.CreateCriteria()); if (Beatmap.IsDefault || !validSelection) { From f086be9c189cba2e562ee66c3f451a20777eb65d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Jun 2025 13:55:25 +0900 Subject: [PATCH 053/370] Fix audio not correctly continuing on resuming from gameplay --- .../Navigation/TestSceneScreenNavigation.cs | 8 +++ .../TestSceneSongSelectNavigation.cs | 61 +++++++++++++++++++ osu.Game/Screens/SelectV2/SongSelect.cs | 25 +++++--- 3 files changed, 84 insertions(+), 10 deletions(-) create mode 100644 osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 7aa2ecb06c..e2b2e41456 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -796,6 +796,14 @@ namespace osu.Game.Tests.Visual.Navigation AddWaitStep("wait two frames", 2); } + [Test] + public void TestPushSongSelectAndPressBackButtonImmediatelyV2() + { + AddStep("push song select", () => Game.ScreenStack.Push(new TestPlaySongSelect())); + AddStep("press back button", () => Game.ChildrenOfType().First().Action!.Invoke()); + AddWaitStep("wait two frames", 2); + } + [Test] public void TestExitSongSelectWithClick() { diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs new file mode 100644 index 0000000000..f369a52ae7 --- /dev/null +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs @@ -0,0 +1,61 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Game.Beatmaps; +using osu.Game.Overlays; +using osu.Game.Screens.Play; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Beatmaps.IO; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Navigation +{ + /// + /// Tests copied out of `TestSceneScreenNavigation` which are specific to song select. + /// These are for SongSelectV2. Eventually, the tests in the above class should be deleted along with old song select. + /// + public class TestSceneSongSelectNavigation : OsuGameTestScene + { + [TestCase(true)] + [TestCase(false)] + public void TestSongContinuesAfterExitPlayer(bool withUserPause) + { + Player? player = null; + + IWorkingBeatmap beatmap() => Game.Beatmap.Value; + + PushAndConfirm(() => new SoloSongSelect()); + + AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game, virtualTrack: true).WaitSafely()); + + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + if (withUserPause) + AddStep("pause", () => Game.Dependencies.Get().Stop(true)); + + AddStep("press enter", () => InputManager.Key(Key.Enter)); + + AddUntilStep("wait for player", () => + { + DismissAnyNotifications(); + return (player = Game.ScreenStack.CurrentScreen as Player) != null; + }); + + AddUntilStep("wait for fail", () => player?.GameplayState.HasFailed, () => Is.True); + + AddUntilStep("wait for track stop", () => !Game.MusicController.IsPlaying); + AddAssert("Ensure time before preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().BeatmapInfo.Metadata.PreviewTime); + + pushEscape(); + + AddUntilStep("wait for track playing", () => Game.MusicController.IsPlaying); + AddAssert("Ensure time wasn't reset to preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().BeatmapInfo.Metadata.PreviewTime); + } + + private void pushEscape() => + AddStep("Press escape", () => InputManager.Key(Key.Escape)); + } +} diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 28e11930da..2c382ecd7a 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -306,14 +306,10 @@ namespace osu.Game.Screens.SelectV2 Beatmap.BindValueChanged(_ => { ensureGlobalBeatmapValid(); - updateStateFromCurrentBeatmap(); - }); - } - private void updateStateFromCurrentBeatmap() - { - ensurePlayingSelected(); - updateBackgroundDim(); + ensurePlayingSelected(true); + updateBackgroundDim(); + }); } protected override void Update() @@ -334,7 +330,7 @@ namespace osu.Game.Screens.SelectV2 /// Ensures some music is playing for the current track. /// Will resume playback from a manual user pause if the track has changed. /// - private void ensurePlayingSelected() + private void ensurePlayingSelected(bool restart) { if (!ControlGlobalMusic) return; @@ -346,7 +342,7 @@ namespace osu.Game.Screens.SelectV2 if (!track.IsRunning && (music.UserPauseRequested != true || isNewTrack)) { Logger.Log($"Song select decided to {nameof(ensurePlayingSelected)}"); - music.Play(true); + music.Play(restart); } lastTrack.SetTarget(track); @@ -518,6 +514,14 @@ namespace osu.Game.Screens.SelectV2 this.FadeIn(fade_duration, Easing.OutQuint); onArrivingAtScreen(); + + if (ControlGlobalMusic) + { + // restart playback on returning to song select, regardless. + // not sure this should be a permanent thing (we may want to leave a user pause paused even on returning) + music.ResetTrackAdjustments(); + music.Play(requestedByUser: true); + } } public override void OnSuspending(ScreenTransitionEvent e) @@ -556,7 +560,8 @@ namespace osu.Game.Screens.SelectV2 ensureGlobalBeatmapValid(); - updateStateFromCurrentBeatmap(); + ensurePlayingSelected(false); + updateBackgroundDim(); } private void onLeavingScreen() From 8842f935f12f8fd4da4a8491892b6f13a4ab9b72 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Jun 2025 17:00:22 +0900 Subject: [PATCH 054/370] Avoid showing wedge until a valid beatmap is selected --- osu.Game/Screens/SelectV2/SongSelect.cs | 27 +++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 2c382ecd7a..cfa6bafff3 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -309,6 +309,7 @@ namespace osu.Game.Screens.SelectV2 ensurePlayingSelected(true); updateBackgroundDim(); + updateWedgeVisibility(); }); } @@ -515,6 +516,8 @@ namespace osu.Game.Screens.SelectV2 this.FadeIn(fade_duration, Easing.OutQuint); onArrivingAtScreen(); + ensureGlobalBeatmapValid(); + if (ControlGlobalMusic) { // restart playback on returning to song select, regardless. @@ -551,9 +554,7 @@ namespace osu.Game.Screens.SelectV2 carousel.VisuallyFocusSelected = false; - titleWedge.Show(); - detailsArea.Show(); - filterControl.Show(); + updateWedgeVisibility(); beginLooping(); attachTrackDuckingIfShould(); @@ -569,9 +570,7 @@ namespace osu.Game.Screens.SelectV2 modSelectOverlay.SelectedMods.UnbindFrom(Mods); modSelectOverlay.Beatmap.UnbindFrom(Beatmap); - titleWedge.Hide(); - detailsArea.Hide(); - filterControl.Hide(); + updateWedgeVisibility(); endLooping(); detachTrackDucking(); @@ -616,6 +615,22 @@ namespace osu.Game.Screens.SelectV2 logo.FadeOut(120, Easing.Out); } + private void updateWedgeVisibility() + { + if (!carousel.VisuallyFocusSelected && checkBeatmapValidForSelection(Beatmap.Value.BeatmapInfo, filterControl.CreateCriteria())) + { + titleWedge.Show(); + detailsArea.Show(); + filterControl.Show(); + } + else + { + titleWedge.Hide(); + detailsArea.Hide(); + filterControl.Hide(); + } + } + private void updateBackgroundDim() => ApplyToBackground(backgroundModeBeatmap => { backgroundModeBeatmap.BlurAmount.Value = 0; From 7d8df1d8d4dcb91aa1adbfe3c49b9e9c35eee0fb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Jun 2025 17:30:57 +0900 Subject: [PATCH 055/370] Add back test covering immediate exit song select --- .../Navigation/TestSceneScreenNavigation.cs | 9 +-------- .../Navigation/TestSceneSongSelectNavigation.cs | 16 ++++++++++++++++ osu.Game/OsuGame.cs | 3 +++ 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index e2b2e41456..8ba914c05f 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -793,15 +793,8 @@ namespace osu.Game.Tests.Visual.Navigation { AddStep("push song select", () => Game.ScreenStack.Push(new TestPlaySongSelect())); AddStep("press back button", () => Game.ChildrenOfType().First().Action!.Invoke()); - AddWaitStep("wait two frames", 2); - } - [Test] - public void TestPushSongSelectAndPressBackButtonImmediatelyV2() - { - AddStep("push song select", () => Game.ScreenStack.Push(new TestPlaySongSelect())); - AddStep("press back button", () => Game.ChildrenOfType().First().Action!.Invoke()); - AddWaitStep("wait two frames", 2); + ConfirmAtMainMenu(); } [Test] diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs index f369a52ae7..264f09f6b9 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs @@ -1,11 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Overlays; +using osu.Game.Screens.Footer; using osu.Game.Screens.Play; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Beatmaps.IO; @@ -19,6 +22,19 @@ namespace osu.Game.Tests.Visual.Navigation /// public class TestSceneSongSelectNavigation : OsuGameTestScene { + [Test] + public void TestPushSongSelectAndPressBackButtonImmediately() + { + AddStep("push song select", () => Game.ScreenStack.Push(new SoloSongSelect())); + + // TODO: without this step, a critical bug will be hit, see inline comment in `OsuGame.handleBackButton`. + AddUntilStep("Wait for song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect select && select.IsLoaded); + + AddStep("press back button", () => Game.ChildrenOfType().First().Action!.Invoke()); + + ConfirmAtMainMenu(); + } + [TestCase(true)] [TestCase(false)] public void TestSongContinuesAfterExitPlayer(bool withUserPause) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 32ffc52be1..628d9d990c 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1306,6 +1306,9 @@ namespace osu.Game private void handleBackButton() { + // TODO: this is SUPER SUPER bad. + // It can potentially exit the wrong screen if screens are not loaded yet. + // ScreenFooter / ScreenBackButton should be aware of which screen it is currently being handled by. if (!(ScreenStack.CurrentScreen is IOsuScreen currentScreen)) return; if (!((Drawable)currentScreen).IsLoaded || (currentScreen.AllowUserExit && !currentScreen.OnBackButton())) ScreenStack.Exit(); From 2cd923ba26a309b725b2f851cac12f6176574049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 4 Jun 2025 11:23:10 +0200 Subject: [PATCH 056/370] Mark test scene class partial --- .../Visual/Navigation/TestSceneSongSelectNavigation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs index 264f09f6b9..29511a6548 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Navigation /// Tests copied out of `TestSceneScreenNavigation` which are specific to song select. /// These are for SongSelectV2. Eventually, the tests in the above class should be deleted along with old song select. /// - public class TestSceneSongSelectNavigation : OsuGameTestScene + public partial class TestSceneSongSelectNavigation : OsuGameTestScene { [Test] public void TestPushSongSelectAndPressBackButtonImmediately() From d79de43a29935f4ab1f9f071d6eee74d54afc4d9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Jun 2025 19:51:46 +0900 Subject: [PATCH 057/370] Fix wedge pieces disappearing when they shouldn't --- osu.Game/Screens/SelectV2/SongSelect.cs | 27 ++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index cfa6bafff3..107fb44683 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -617,18 +617,25 @@ namespace osu.Game.Screens.SelectV2 private void updateWedgeVisibility() { - if (!carousel.VisuallyFocusSelected && checkBeatmapValidForSelection(Beatmap.Value.BeatmapInfo, filterControl.CreateCriteria())) - { - titleWedge.Show(); - detailsArea.Show(); - filterControl.Show(); - } - else + // Ensure we don't show an invalid selection before the carousel has finished initially filtering. + // This avoids a flicker of a placeholder or invalid beatmap before a proper selection. + // + // After the carousel finishes filtering, it will attempt a selection then call this method again. + if (!carouselItemsPresented && !checkBeatmapValidForSelection(Beatmap.Value.BeatmapInfo, filterControl.CreateCriteria())) + return; + + if (carousel.VisuallyFocusSelected) { titleWedge.Hide(); detailsArea.Hide(); filterControl.Hide(); } + else + { + titleWedge.Show(); + detailsArea.Show(); + filterControl.Show(); + } } private void updateBackgroundDim() => ApplyToBackground(backgroundModeBeatmap => @@ -647,6 +654,8 @@ namespace osu.Game.Screens.SelectV2 #region Filtering + private bool carouselItemsPresented; + private const double filter_delay = 250; private ScheduledDelegate? filterDebounce; @@ -669,6 +678,8 @@ namespace osu.Game.Screens.SelectV2 if (carousel.Criteria == null) return; + carouselItemsPresented = true; + int count = carousel.MatchedBeatmapsCount; updateNoResultsPlaceholder(); @@ -678,6 +689,8 @@ namespace osu.Game.Screens.SelectV2 filterControl.StatusText = count != 1 ? $"{count:#,0} matches" : $"{count:#,0} match"; ensureGlobalBeatmapValid(); + + updateWedgeVisibility(); } private void updateNoResultsPlaceholder() From 136c5c866bae55e0b7d22b2df69eecdd65639758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 4 Jun 2025 13:25:01 +0200 Subject: [PATCH 058/370] SongSelectV2: Fix triangles being sheared on leaderboard panels --- osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs index 6a810a83b4..5a4a0ad208 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs @@ -370,6 +370,7 @@ namespace osu.Game.Screens.SelectV2 }, new TrianglesV2 { + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, RelativeSizeAxes = Axes.Both, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, From d262b6e88f82f3713d1345a53f443cbbdd156eb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 4 Jun 2025 11:14:42 +0200 Subject: [PATCH 059/370] Update framework Contains a native libs / BASS rollback due to https://github.com/ppy/osu/discussions/33260. - Reopens https://github.com/ppy/osu/issues/31702. - Reopens https://github.com/ppy/osu/issues/26879. --- 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 92e3312fd8..52cafa5c75 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 205e85ba51..14863083f5 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 68795255eeb878a01ec756bd2c10fe107bb9044e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 4 Jun 2025 12:41:01 +0200 Subject: [PATCH 060/370] Fix cursor test --- osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs index de303fe074..f356873220 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs @@ -25,6 +25,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.Menu; using osu.Game.Skinning; +using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.Navigation @@ -83,6 +84,7 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestCursorHidesWhenIdle() { + AddStep("move mouse inside game bounds", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.TopLeft + new Vector2(20))); AddStep("click mouse", () => InputManager.Click(MouseButton.Left)); AddUntilStep("wait until idle", () => Game.IsIdle.Value); AddUntilStep("menu cursor hidden", () => Game.GlobalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 0); From 7e922763c1cd5197a2322430a2618c8422b15e05 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Jun 2025 21:19:47 +0900 Subject: [PATCH 061/370] Add failing test showing recommended selection occurring on already selected set --- .../TestSceneSongSelectFiltering.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs index 4665262097..19fccdf94d 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs @@ -16,6 +16,7 @@ using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.Select.Filter; +using osu.Game.Screens.SelectV2; using FilterControl = osu.Game.Screens.SelectV2.FilterControl; using NoResultsPlaceholder = osu.Game.Screens.SelectV2.NoResultsPlaceholder; @@ -65,6 +66,28 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("filter count is 0", () => filterOperationsCount, () => Is.EqualTo(0)); } + [Test] + public void TestFilterSingleResult_RetainsSelectedDifficulty() + { + LoadSongSelect(); + + ImportBeatmapForRuleset(0); + + AddUntilStep("wait for single set", () => Carousel.ChildrenOfType().Count(), () => Is.EqualTo(1)); + + AddStep("select last difficulty", () => + { + Beatmap.Value = Beatmaps.GetWorkingBeatmap(Beatmaps.GetAllUsableBeatmapSets().First().Beatmaps.Last()); + }); + + AddStep("set filter text", () => filterTextBox.Current.Value = " "); + + AddWaitStep("wait for debounce", 5); + AddUntilStep("wait for filter", () => !Carousel.IsFiltering); + + AddAssert("selection unchanged", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(Beatmaps.GetAllUsableBeatmapSets().First().Beatmaps.Last())); + } + [Test] public void TestFilterOnResumeAfterChange() { From f52fabbb0cdfbe4294e4830222c145232107e5ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Jun 2025 21:11:38 +0900 Subject: [PATCH 062/370] SongSelectV2: Fix incorrect selection change when filtered down to one set --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index cc40921562..700ee6a05e 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -318,7 +318,12 @@ namespace osu.Game.Screens.SelectV2 } } - RequestRecommendedSelection(items.Select(i => i.Model).OfType()); + var beatmaps = items.Select(i => i.Model).OfType(); + + if (beatmaps.Any(b => b.Equals(CurrentSelection as BeatmapInfo))) + return; + + RequestRecommendedSelection(beatmaps); } protected override bool CheckValidForGroupSelection(CarouselItem item) From 7ecf81d3a0c4e5a9b8ae791823b4ca4720fa19f3 Mon Sep 17 00:00:00 2001 From: marvin Date: Wed, 4 Jun 2025 19:58:04 +0200 Subject: [PATCH 063/370] Add ghost drawable --- .../UserInterface/TestSceneGhostIcon.cs | 22 +++ osu.Game/Graphics/GhostIcon.cs | 130 ++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneGhostIcon.cs create mode 100644 osu.Game/Graphics/GhostIcon.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneGhostIcon.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneGhostIcon.cs new file mode 100644 index 0000000000..5ae46d0224 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneGhostIcon.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Game.Graphics; +using osuTK; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public partial class TestSceneGhostIcon : OsuTestScene + { + public TestSceneGhostIcon() + { + Add(new GhostIcon + { + Size = new Vector2(64), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + } + } +} diff --git a/osu.Game/Graphics/GhostIcon.cs b/osu.Game/Graphics/GhostIcon.cs new file mode 100644 index 0000000000..e72359219c --- /dev/null +++ b/osu.Game/Graphics/GhostIcon.cs @@ -0,0 +1,130 @@ +// 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.Runtime.InteropServices; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Rendering.Vertices; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Graphics.Shaders.Types; +using osuTK; + +namespace osu.Game.Graphics +{ + public partial class GhostIcon : Drawable + { + private IShader ghostShader = null!; + + [BackgroundDependencyLoader] + private void load(ShaderManager shaders) + { + ghostShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "Ghost"); + } + + protected override void Update() + { + base.Update(); + + Invalidate(Invalidation.DrawNode); + } + + protected override DrawNode CreateDrawNode() => new GhostIconDrawNode(this); + + private class GhostIconDrawNode : DrawNode + { + protected new GhostIcon Source => (GhostIcon)base.Source; + + public GhostIconDrawNode(IDrawable source) + : base(source) + { + } + + private Quad screenSpaceDrawQuad; + private Vector4 drawRectangle; + private Vector2 blend; + private IShader shader = null!; + private float time; + + public override void ApplyState() + { + base.ApplyState(); + + screenSpaceDrawQuad = Source.ScreenSpaceDrawQuad; + drawRectangle = new Vector4(0, 0, Source.DrawWidth, Source.DrawHeight); + shader = Source.ghostShader; + blend = new Vector2(Math.Min(Source.DrawWidth, Source.DrawHeight) / Math.Min(screenSpaceDrawQuad.Width, screenSpaceDrawQuad.Height)); + time = (float)(Source.Time.Current % 1000f) * 0.0005f; + } + + private IUniformBuffer? ghostParametersBuffer; + + private IVertexBatch? quadBatch; + + protected override void Draw(IRenderer renderer) + { + base.Draw(renderer); + + if (!renderer.BindTexture(renderer.WhitePixel)) + return; + + quadBatch ??= renderer.CreateQuadBatch(1, 2); + ghostParametersBuffer ??= renderer.CreateUniformBuffer(); + + ghostParametersBuffer.Data = new GhostParameters + { + Time = time + }; + + shader.Bind(); + shader.BindUniformBlock("m_GhostParameters", ghostParametersBuffer); + + var vertexAction = quadBatch.AddAction; + + vertexAction(new TexturedVertex2D(renderer) + { + Position = screenSpaceDrawQuad.BottomLeft, + TexturePosition = new Vector2(0, 1), + TextureRect = drawRectangle, + BlendRange = blend, + Colour = DrawColourInfo.Colour.BottomLeft.SRGB, + }); + vertexAction(new TexturedVertex2D(renderer) + { + Position = screenSpaceDrawQuad.BottomRight, + TexturePosition = new Vector2(1, 1), + TextureRect = drawRectangle, + BlendRange = blend, + Colour = DrawColourInfo.Colour.BottomRight.SRGB, + }); + vertexAction(new TexturedVertex2D(renderer) + { + Position = screenSpaceDrawQuad.TopRight, + TexturePosition = new Vector2(1, 0), + TextureRect = drawRectangle, + BlendRange = blend, + Colour = DrawColourInfo.Colour.TopRight.SRGB, + }); + vertexAction(new TexturedVertex2D(renderer) + { + Position = screenSpaceDrawQuad.TopLeft, + TexturePosition = Vector2.Zero, + TextureRect = drawRectangle, + BlendRange = blend, + Colour = DrawColourInfo.Colour.TopLeft.SRGB, + }); + + shader.Unbind(); + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private record struct GhostParameters + { + public UniformFloat Time; + private UniformPadding12 pad; + } + } + } +} From 8d1702fbaca0f07b18e9f8b9706783fcde7d22fb Mon Sep 17 00:00:00 2001 From: marvin Date: Wed, 4 Jun 2025 19:58:55 +0200 Subject: [PATCH 064/370] Use ghost icon in NoResultsPlaceholder --- osu.Game/Screens/SelectV2/NoResultsPlaceholder.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/SelectV2/NoResultsPlaceholder.cs b/osu.Game/Screens/SelectV2/NoResultsPlaceholder.cs index 46f8859255..36f87127f8 100644 --- a/osu.Game/Screens/SelectV2/NoResultsPlaceholder.cs +++ b/osu.Game/Screens/SelectV2/NoResultsPlaceholder.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.SelectV2 private LinkFlowContainer textFlow = null!; - private SpriteIcon icon = null!; + private GhostIcon icon = null!; [Resolved] private BeatmapManager beatmaps { get; set; } = null!; @@ -71,13 +71,9 @@ namespace osu.Game.Screens.SelectV2 AutoSizeAxes = Axes.Y, Children = new Drawable[] { - icon = new SpriteIcon + icon = new GhostIcon { - Icon = FontAwesome.Solid.Ghost, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Margin = new MarginPadding(10), - Size = new Vector2(50), + RelativeSizeAxes = Axes.Both, }, new OsuSpriteText { From 7f618b1dc72f499caa1ebcdd8456da47d71f3801 Mon Sep 17 00:00:00 2001 From: marvin Date: Wed, 4 Jun 2025 19:59:52 +0200 Subject: [PATCH 065/370] Change ghost animation to up-down movement --- .../Screens/SelectV2/NoResultsPlaceholder.cs | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/SelectV2/NoResultsPlaceholder.cs b/osu.Game/Screens/SelectV2/NoResultsPlaceholder.cs index 36f87127f8..f3637f9949 100644 --- a/osu.Game/Screens/SelectV2/NoResultsPlaceholder.cs +++ b/osu.Game/Screens/SelectV2/NoResultsPlaceholder.cs @@ -71,9 +71,16 @@ namespace osu.Game.Screens.SelectV2 AutoSizeAxes = Axes.Y, Children = new Drawable[] { - icon = new GhostIcon + new Container { - RelativeSizeAxes = Axes.Both, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Margin = new MarginPadding(10), + Size = new Vector2(50), + Child = icon = new GhostIcon + { + RelativeSizeAxes = Axes.Both, + }, }, new OsuSpriteText { @@ -97,6 +104,17 @@ namespace osu.Game.Screens.SelectV2 }; } + protected override void LoadComplete() + { + base.LoadComplete(); + + icon.Loop(t => + t.MoveToY(-10, 2000, Easing.InOutSine) + .Then() + .MoveToY(0, 2000, Easing.InOutSine) + ); + } + protected override void PopIn() { this.FadeIn(600, Easing.OutQuint); @@ -117,9 +135,6 @@ namespace osu.Game.Screens.SelectV2 this.ScaleTo(0.9f) .ScaleTo(1f, 1000, Easing.OutQuint); - icon.ScaleTo(new Vector2(-1, 1)) - .ScaleTo(new Vector2(1, 1), 500, Easing.InOutSine); - textFlow.FadeInFromZero(800, Easing.OutQuint); textFlow.Clear(); From 96db7b3af642be3731c8b4fdb2e76a5b06494b1d Mon Sep 17 00:00:00 2001 From: marvin Date: Wed, 4 Jun 2025 20:03:40 +0200 Subject: [PATCH 066/370] Add xmldoc --- osu.Game/Graphics/GhostIcon.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Graphics/GhostIcon.cs b/osu.Game/Graphics/GhostIcon.cs index e72359219c..4322a612ca 100644 --- a/osu.Game/Graphics/GhostIcon.cs +++ b/osu.Game/Graphics/GhostIcon.cs @@ -10,10 +10,14 @@ using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Rendering.Vertices; using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Shaders.Types; +using osu.Framework.Graphics.Sprites; using osuTK; namespace osu.Game.Graphics { + /// + /// A (very cute) animated version of the icon. + /// public partial class GhostIcon : Drawable { private IShader ghostShader = null!; From 7b7e57543c990d8d18c5e5911c1dd77c00b6c22c Mon Sep 17 00:00:00 2001 From: marvin Date: Wed, 4 Jun 2025 20:03:50 +0200 Subject: [PATCH 067/370] Add disposal logic in ghost drawnode --- osu.Game/Graphics/GhostIcon.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Graphics/GhostIcon.cs b/osu.Game/Graphics/GhostIcon.cs index 4322a612ca..e681611424 100644 --- a/osu.Game/Graphics/GhostIcon.cs +++ b/osu.Game/Graphics/GhostIcon.cs @@ -129,6 +129,14 @@ namespace osu.Game.Graphics public UniformFloat Time; private UniformPadding12 pad; } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + ghostParametersBuffer?.Dispose(); + quadBatch?.Dispose(); + } } } } From 24e102066b913c5d28f3f3624fe8ebf2952606dd Mon Sep 17 00:00:00 2001 From: marvin Date: Wed, 4 Jun 2025 20:15:29 +0200 Subject: [PATCH 068/370] Extract animation duration property --- osu.Game/Graphics/GhostIcon.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/GhostIcon.cs b/osu.Game/Graphics/GhostIcon.cs index e681611424..ec61495622 100644 --- a/osu.Game/Graphics/GhostIcon.cs +++ b/osu.Game/Graphics/GhostIcon.cs @@ -22,6 +22,11 @@ namespace osu.Game.Graphics { private IShader ghostShader = null!; + /// + /// How long one complete loop of the ghost's animation takes, in milliseconds + /// + public float AnimationDuration = 2000; + [BackgroundDependencyLoader] private void load(ShaderManager shaders) { @@ -60,7 +65,7 @@ namespace osu.Game.Graphics drawRectangle = new Vector4(0, 0, Source.DrawWidth, Source.DrawHeight); shader = Source.ghostShader; blend = new Vector2(Math.Min(Source.DrawWidth, Source.DrawHeight) / Math.Min(screenSpaceDrawQuad.Width, screenSpaceDrawQuad.Height)); - time = (float)(Source.Time.Current % 1000f) * 0.0005f; + time = (float)(Source.Time.Current % 1000f) / Source.AnimationDuration; } private IUniformBuffer? ghostParametersBuffer; From 1c32b5364f0f0fa0d04d8cca66c38b0ae1074c6b Mon Sep 17 00:00:00 2001 From: marvin Date: Wed, 4 Jun 2025 20:43:01 +0200 Subject: [PATCH 069/370] Fix incorrect operation order for calculating shader time uniform --- osu.Game/Graphics/GhostIcon.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/GhostIcon.cs b/osu.Game/Graphics/GhostIcon.cs index ec61495622..9ff036adf0 100644 --- a/osu.Game/Graphics/GhostIcon.cs +++ b/osu.Game/Graphics/GhostIcon.cs @@ -65,7 +65,7 @@ namespace osu.Game.Graphics drawRectangle = new Vector4(0, 0, Source.DrawWidth, Source.DrawHeight); shader = Source.ghostShader; blend = new Vector2(Math.Min(Source.DrawWidth, Source.DrawHeight) / Math.Min(screenSpaceDrawQuad.Width, screenSpaceDrawQuad.Height)); - time = (float)(Source.Time.Current % 1000f) / Source.AnimationDuration; + time = (float)(Source.Time.Current / Source.AnimationDuration) % 1f; } private IUniformBuffer? ghostParametersBuffer; From 118583bf21e40cbfc1d3fd0c4ecc25df5bdebe47 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Jun 2025 21:20:09 +0900 Subject: [PATCH 070/370] Add back more song select navigation tests --- .../TestSceneSongSelectNavigation.cs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs index 29511a6548..85191a5c72 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs @@ -1,15 +1,21 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions; +using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Overlays; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens; using osu.Game.Screens.Footer; using osu.Game.Screens.Play; +using osu.Game.Screens.Ranking; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Beatmaps.IO; using osuTK.Input; @@ -22,6 +28,15 @@ namespace osu.Game.Tests.Visual.Navigation /// public partial class TestSceneSongSelectNavigation : OsuGameTestScene { + [Test] + public void TestRetryFromResults() + { + var getOriginalPlayer = playToResults(); + + AddStep("attempt to retry", () => ((ResultsScreen)Game.ScreenStack.CurrentScreen).ChildrenOfType().First().Action()); + AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != getOriginalPlayer() && Game.ScreenStack.CurrentScreen is Player); + } + [Test] public void TestPushSongSelectAndPressBackButtonImmediately() { @@ -71,6 +86,48 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("Ensure time wasn't reset to preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().BeatmapInfo.Metadata.PreviewTime); } + private Func playToResults() + { + var player = playToCompletion(); + AddUntilStep("wait for results", () => (Game.ScreenStack.CurrentScreen as ResultsScreen)?.IsLoaded == true); + return player; + } + + private Func playToCompletion() + { + Player? player = null; + + IWorkingBeatmap beatmap() => Game.Beatmap.Value; + + PushAndConfirm(() => new SoloSongSelect()); + + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("set mods", () => Game.SelectedMods.Value = new Mod[] { new OsuModNoFail(), new OsuModDoubleTime { SpeedChange = { Value = 2 } } }); + + pushEnter(); + + AddUntilStep("wait for player", () => + { + DismissAnyNotifications(); + return (player = Game.ScreenStack.CurrentScreen as Player) != null; + }); + + AddUntilStep("wait for track playing", () => beatmap().Track.IsRunning); + AddStep("seek to near end", () => player.ChildrenOfType().First().Seek(beatmap().Beatmap.HitObjects[^1].StartTime - 1000)); + AddUntilStep("wait for complete", () => player?.GameplayState.HasPassed, () => Is.True); + + return () => player!; + } + + private void waitForScreen() where T : OsuScreen => + AddUntilStep($"Wait for {typeof(T).ReadableName()}", () => Game.ScreenStack.CurrentScreen is T screen && screen.IsLoaded); + + private void pushEnter() => + AddStep("Press enter", () => InputManager.Key(Key.Enter)); + private void pushEscape() => AddStep("Press escape", () => InputManager.Key(Key.Escape)); } From bb49bb5e2398f69345ecddc67de6a73255bc4543 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Jun 2025 03:32:49 +0900 Subject: [PATCH 071/370] Add test coverage of editor flow from song select --- .../TestSceneSongSelectNavigation.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs index 85191a5c72..506c02dc17 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs @@ -13,6 +13,7 @@ using osu.Game.Overlays; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens; +using osu.Game.Screens.Edit; using osu.Game.Screens.Footer; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; @@ -50,6 +51,28 @@ namespace osu.Game.Tests.Visual.Navigation ConfirmAtMainMenu(); } + [Test] + public void TestEditBeatmap() + { + PushAndConfirm(() => new SoloSongSelect()); + + AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game, virtualTrack: true).WaitSafely()); + + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("open menu", () => InputManager.Key(Key.F3)); + AddStep("trigger edit", () => + { + // TODO: should be 5, not 4. + InputManager.Key(Key.Number4); + }); + + waitForScreen(); + + pushEscape(); + waitForScreen(); + } + [TestCase(true)] [TestCase(false)] public void TestSongContinuesAfterExitPlayer(bool withUserPause) From e35d0f8953d27cbb297376b64f2652aab895263f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Jun 2025 03:32:39 +0900 Subject: [PATCH 072/370] Fix beatmap changes handling when song select is not active --- osu.Game/Screens/SelectV2/SongSelect.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 107fb44683..19e48ef21c 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -305,6 +305,9 @@ namespace osu.Game.Screens.SelectV2 Beatmap.BindValueChanged(_ => { + if (!this.IsCurrentScreen()) + return; + ensureGlobalBeatmapValid(); ensurePlayingSelected(true); From 63d52de1a79efd1771129d48f7cf7d37d955c1f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 1 Jun 2025 02:26:22 +0900 Subject: [PATCH 073/370] Change method of accessing song select v2 to hold --- osu.Game/Screens/Menu/MainMenu.cs | 106 ++++++++++++++++++++++++++---- 1 file changed, 94 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 2d7981113b..9b3620d3b2 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -14,11 +14,13 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Screens; +using osu.Framework.Testing; using osu.Framework.Threading; using osu.Game.Audio; using osu.Game.Beatmaps; @@ -27,6 +29,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Input.Bindings; using osu.Game.IO; +using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; @@ -39,11 +42,11 @@ using osu.Game.Screens.OnlinePlay.DailyChallenge; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Select; +using osu.Game.Screens.SelectV2; using osu.Game.Seasonal; using osuTK; using osuTK.Graphics; -using osu.Game.Localisation; -using osu.Game.Screens.SelectV2; +using osuTK.Input; namespace osu.Game.Screens.Menu { @@ -90,6 +93,8 @@ namespace osu.Game.Screens.Menu IBindable ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled; private readonly Bindable samplePlaybackDisabled = new Bindable(); + private InputManager inputManager; + protected override BackgroundScreen CreateBackground() => new BackgroundScreenDefault(); protected override bool PlayExitSound => false; @@ -155,7 +160,7 @@ namespace osu.Game.Screens.Menu { skinEditor?.Show(); }, - OnSolo = loadSoloSongSelect, + OnSolo = loadPreferredSongSelect, OnMultiplayer = () => this.Push(new Multiplayer()), OnPlaylists = () => this.Push(new Playlists()), OnDailyChallenge = room => @@ -236,18 +241,23 @@ namespace osu.Game.Screens.Menu Buttons.OnBeatmapListing = () => beatmapListing?.ToggleVisibility(); reappearSampleSwoosh = audio.Samples.Get(@"Menu/reappear-swoosh"); + loadSongSelectV2Samples(audio); + } + + protected override void Update() + { + base.Update(); + updateSongSelectV2HoldState(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + inputManager = GetContainingInputManager(); } public void ReturnToOsuLogo() => Buttons.State = ButtonSystemState.Initial; - private void loadSoloSongSelect() - { - if (GetContainingInputManager()!.CurrentState.Keyboard.ControlPressed) - this.Push(new SoloSongSelect()); - else - this.Push(new PlaySongSelect()); - } - public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); @@ -453,7 +463,7 @@ namespace osu.Game.Screens.Menu Beatmap.Value = beatmap; Ruleset.Value = ruleset; - Schedule(loadSoloSongSelect); + Schedule(loadPreferredSongSelect); } public bool OnPressed(KeyBindingPressEvent e) @@ -477,6 +487,78 @@ namespace osu.Game.Screens.Menu { } + #region TEMPORARY: Song Select v2 easter egg + + private const double required_hold_time = 500; + + private double holdTime; + private bool ssv2Expanded; + private IDisposable ssv2Duck; + private Sample ssv2Sample; + + private void loadPreferredSongSelect() + { + if (holdTime >= required_hold_time) + { + ssv2Sample?.Play(); + this.Push(new SoloSongSelect()); + } + else + this.Push(new PlaySongSelect()); + } + + private void loadSongSelectV2Samples(AudioManager audio) + { + ssv2Sample = audio.Samples.Get(@"UI/bss-complete"); + } + + private void updateSongSelectV2HoldState() + { + if (Buttons.State == ButtonSystemState.Play && + inputManager.CurrentState.Mouse.IsPressed(MouseButton.Left) && + inputManager.HoveredDrawables.Any(h => h is OsuLogo || (h is MainMenuButton b && b.TriggerKeys.Contains(Key.P)))) + holdTime += Time.Elapsed; + else + { + var transformTarget = Game.ChildrenOfType().First(); + transformTarget.ScaleTo(1, 200, Easing.OutQuint) + .RotateTo(0, 200, Easing.OutQuint) + .FadeColour(OsuColour.Gray(1f), 200, Easing.OutQuint); + + ssv2Duck?.Dispose(); + ssv2Duck = null; + + ssv2Expanded = false; + holdTime = 0; + } + + if (holdTime >= required_hold_time && !ssv2Expanded) + { + var transformTarget = Game.ChildrenOfType().First(); + + transformTarget.Anchor = Anchor.Centre; + transformTarget.Origin = Anchor.Centre; + + transformTarget.ScaleTo(1.2f, 5000, Easing.OutPow10) + .RotateTo(2, 5000, Easing.OutPow10) + .FadeColour(Color4.BlueViolet, 10000, Easing.OutPow10); + + ssv2Duck = musicController.Duck(new DuckParameters + { + DuckDuration = 2000, + DuckVolumeTo = 0.8f, + DuckCutoffTo = 500, + DuckEasing = Easing.OutQuint, + RestoreDuration = 200, + RestoreEasing = Easing.OutQuint + }); + + ssv2Expanded = true; + } + } + + #endregion + private partial class MobileDisclaimerDialog : PopupDialog { public MobileDisclaimerDialog(Action confirmed) From 2082a31a6905928a23b543e4a9a59867f5e11c18 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Jun 2025 04:08:23 +0900 Subject: [PATCH 074/370] Fix tiny stats showing when larger ones still could --- .../SelectV2/BeatmapTitleWedge_DifficultyStatisticsDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyStatisticsDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyStatisticsDisplay.cs index 571fc82fc1..365ed9977b 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyStatisticsDisplay.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyStatisticsDisplay.cs @@ -137,7 +137,7 @@ namespace osu.Game.Screens.SelectV2 return; float flowWidth = statisticsFlow[0].Width * statisticsFlow.Count + statisticsFlow.Spacing.X * (statisticsFlow.Count - 1); - bool tiny = !autoSize && DrawWidth < flowWidth; + bool tiny = !autoSize && DrawWidth < flowWidth - 20; if (displayedTinyStatistics != tiny) { From db7afd0e21b08b2b17dde057be23e31183285dd5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Jun 2025 04:18:53 +0900 Subject: [PATCH 075/370] Fix heights of title wedge sections to avoid weirdness when showing placeholder --- osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs | 3 ++- .../Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs index 0fb4616db2..6b80fc69c9 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs @@ -147,7 +147,8 @@ namespace osu.Game.Screens.SelectV2 new ShearAligningWrapper(statisticsFlow = new FillFlowContainer { Shear = -OsuGame.SHEAR, - AutoSizeAxes = Axes.Both, + AutoSizeAxes = Axes.X, + Height = 30, Direction = FillDirection.Horizontal, Spacing = new Vector2(2f, 0f), Children = new Drawable[] diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs index 734d768241..a4be87953c 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs @@ -96,7 +96,7 @@ namespace osu.Game.Screens.SelectV2 Shear = -OsuGame.SHEAR, AlwaysPresent = true, RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + Height = 20, Margin = new MarginPadding { Vertical = 5f }, Padding = new MarginPadding { Left = SongSelect.WEDGE_CONTENT_MARGIN }, RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, @@ -159,7 +159,7 @@ namespace osu.Game.Screens.SelectV2 { Shear = -OsuGame.SHEAR, RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + Height = 53, Padding = new MarginPadding { Bottom = border_weight, Right = border_weight }, Child = new Container { From 08f4e6512e6dfc9c7ffaa94f758605904aca4525 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Jun 2025 14:49:35 +0900 Subject: [PATCH 076/370] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 799003cde1..5fdd937729 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 64d9868ddf4d5f990a205ec084ca06b08854ba37 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Jun 2025 14:55:18 +0900 Subject: [PATCH 077/370] Adjust background fade for placeholder to run faster --- osu.Game/Screens/SelectV2/SongSelect.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 107fb44683..b7a2ab161a 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -703,14 +703,14 @@ namespace osu.Game.Screens.SelectV2 noResultsPlaceholder.Filter = carousel.Criteria!; attachTrackDuckingIfShould(); - rightGradientBackground.ResizeWidthTo(3, 1000, Easing.OutQuint); + rightGradientBackground.ResizeWidthTo(3, 1000, Easing.OutPow10); } else { noResultsPlaceholder.Hide(); detachTrackDucking(); - rightGradientBackground.ResizeWidthTo(1, 500, Easing.OutQuint); + rightGradientBackground.ResizeWidthTo(1, 400, Easing.OutPow10); } } From 0c2fc5c74f360855fcc65a81e36b95eda14da476 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Jun 2025 15:31:51 +0900 Subject: [PATCH 078/370] Debounce leaderboard load operations This avoids needless web requests / server load, but also improves the placeholder display. It used to update during ruleset changes three (or more) times since it had no debounce and was reacting to multiple changes. This is no longer the case. --- .../SelectV2/BeatmapLeaderboardWedge.cs | 48 +++++++++++-------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs index ff70596b2f..3d2494ef45 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -192,33 +193,40 @@ namespace osu.Game.Screens.SelectV2 private bool initialFetchComplete; + private ScheduledDelegate? refetchOperation; + private void refetchScores() { - SetScores(Array.Empty()); - - if (beatmap.IsDefault) + refetchOperation?.Cancel(); + refetchOperation = Scheduler.AddDelayed(() => { - SetState(LeaderboardState.NoneSelected); - return; - } + SetScores(Array.Empty()); - SetState(LeaderboardState.Retrieving); + if (beatmap.IsDefault) + { + SetState(LeaderboardState.NoneSelected); + return; + } - var fetchBeatmapInfo = beatmap.Value.BeatmapInfo; - var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset; + SetState(LeaderboardState.Retrieving); - // For now, we forcefully refresh to keep things simple. - // In the future, removing this requirement may be deemed useful, but will need ample testing of edge case scenarios - // (like returning from gameplay after setting a new score, returning to song select after main menu). - leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope.Value, FilterBySelectedMods.Value ? mods.Value.ToArray() : null), forceRefresh: true); + var fetchBeatmapInfo = beatmap.Value.BeatmapInfo; + var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset; - if (!initialFetchComplete) - { - // only bind this after the first fetch to avoid reading stale scores. - fetchedScores.BindTo(leaderboardManager.Scores); - fetchedScores.BindValueChanged(_ => updateScores(), true); - initialFetchComplete = true; - } + // For now, we forcefully refresh to keep things simple. + // In the future, removing this requirement may be deemed useful, but will need ample testing of edge case scenarios + // (like returning from gameplay after setting a new score, returning to song select after main menu). + leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope.Value, FilterBySelectedMods.Value ? mods.Value.ToArray() : null), + forceRefresh: true); + + if (!initialFetchComplete) + { + // only bind this after the first fetch to avoid reading stale scores. + fetchedScores.BindTo(leaderboardManager.Scores); + fetchedScores.BindValueChanged(_ => updateScores(), true); + initialFetchComplete = true; + } + }, initialFetchComplete ? 200 : 0); } private void updateScores() From b6c4a713d3a1e35406c62f4411b79f6c530b9716 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 5 Jun 2025 15:27:36 +0900 Subject: [PATCH 079/370] Add GHA deploy workflow --- .github/workflows/deploy.yml | 88 ++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000000..6aa207b8c1 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,88 @@ +name: Pack and nuget + +on: + push: + tags: + - '*' + +jobs: +# notify_pending_production_deploy: +# runs-on: ubuntu-latest +# steps: +# - +# name: Submit pending deployment notification +# run: | +# export TITLE="Pending osu Production Deployment: $GITHUB_REF_NAME" +# export URL="https://github.com/ppy/osu/actions/runs/$GITHUB_RUN_ID" +# export DESCRIPTION="Awaiting approval for building NuGet packages for tag $GITHUB_REF_NAME: +# [View Workflow Run]($URL)" +# export ACTOR_ICON="https://avatars.githubusercontent.com/u/$GITHUB_ACTOR_ID" +# +# BODY="$(jq --null-input '{ +# "embeds": [ +# { +# "title": env.TITLE, +# "color": 15098112, +# "description": env.DESCRIPTION, +# "url": env.URL, +# "author": { +# "name": env.GITHUB_ACTOR, +# "icon_url": env.ACTOR_ICON +# } +# } +# ] +# }')" +# +# curl \ +# -H "Content-Type: application/json" \ +# -d "$BODY" \ +# "${{ secrets.DISCORD_INFRA_WEBHOOK_URL }}" + + pack: + name: Pack + runs-on: ubuntu-latest +# environment: production + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set artifacts directory + id: artifactsPath + run: echo "::set-output name=nuget_artifacts::${{github.workspace}}/artifacts" + + - name: Install .NET 8.0.x + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0.x" + + - name: Pack + run: | + # Replace project references in templates with package reference, because they're included as source files. + dotnet remove Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj reference osu.Game/osu.Game.csproj + dotnet remove Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj reference osu.Game/osu.Game.csproj + dotnet remove Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj reference osu.Game/osu.Game.csproj + dotnet remove Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj reference osu.Game/osu.Game.csproj + + dotnet add Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj package ppy.osu.Game -n -v ${{ github.ref_name }} + dotnet add Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj package ppy.osu.Game -n -v ${{ github.ref_name }} + dotnet add Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj package ppy.osu.Game -n -v ${{ github.ref_name }} + dotnet add Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj package ppy.osu.Game -n -v ${{ github.ref_name }} + + # Pack + dotnet pack -c Release osu.Game /p:Version=${{ github.ref_name }} /p:GenerateDocumentationFile=true /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg -o ${{steps.artifactsPath.outputs.nuget_artifacts}} + dotnet pack -c Release osu.Game.Rulesets.Osu /p:Version=${{ github.ref_name }} /p:GenerateDocumentationFile=true /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg -o ${{steps.artifactsPath.outputs.nuget_artifacts}} + dotnet pack -c Release osu.Game.Rulesets.Taiko /p:Version=${{ github.ref_name }} /p:GenerateDocumentationFile=true /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg -o ${{steps.artifactsPath.outputs.nuget_artifacts}} + dotnet pack -c Release osu.Game.Rulesets.Catch /p:Version=${{ github.ref_name }} /p:GenerateDocumentationFile=true /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg -o ${{steps.artifactsPath.outputs.nuget_artifacts}} + dotnet pack -c Release osu.Game.Rulesets.Mania /p:Version=${{ github.ref_name }} /p:GenerateDocumentationFile=true /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg -o ${{steps.artifactsPath.outputs.nuget_artifacts}} + dotnet pack -c Release Templates /p:Version=${{ github.ref_name }} /p:GenerateDocumentationFile=true /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg -o ${{steps.artifactsPath.outputs.nuget_artifacts}} + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: osu + path: | + ${{steps.artifactsPath.outputs.nuget_artifacts}}/*.nupkg + ${{steps.artifactsPath.outputs.nuget_artifacts}}/*.snupkg + +# - name: Publish packages to nuget.org +# run: dotnet nuget push ${{steps.artifactsPath.outputs.nuget_artifacts}}/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json From 4b92e338d5d729728804a642cedd90aced912174 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 5 Jun 2025 15:46:05 +0900 Subject: [PATCH 080/370] No symbols for templates package --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6aa207b8c1..6a3c15ef91 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -74,7 +74,7 @@ jobs: dotnet pack -c Release osu.Game.Rulesets.Taiko /p:Version=${{ github.ref_name }} /p:GenerateDocumentationFile=true /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg -o ${{steps.artifactsPath.outputs.nuget_artifacts}} dotnet pack -c Release osu.Game.Rulesets.Catch /p:Version=${{ github.ref_name }} /p:GenerateDocumentationFile=true /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg -o ${{steps.artifactsPath.outputs.nuget_artifacts}} dotnet pack -c Release osu.Game.Rulesets.Mania /p:Version=${{ github.ref_name }} /p:GenerateDocumentationFile=true /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg -o ${{steps.artifactsPath.outputs.nuget_artifacts}} - dotnet pack -c Release Templates /p:Version=${{ github.ref_name }} /p:GenerateDocumentationFile=true /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg -o ${{steps.artifactsPath.outputs.nuget_artifacts}} + dotnet pack -c Release Templates /p:Version=${{ github.ref_name }} -o ${{steps.artifactsPath.outputs.nuget_artifacts}} - name: Upload artifacts uses: actions/upload-artifact@v4 From 7145acc73f4fbdde64498729b3d5f73cfbc30486 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 5 Jun 2025 15:52:38 +0900 Subject: [PATCH 081/370] Enable nuget uploads and delete appveyor workflows --- .github/workflows/deploy.yml | 67 ++++++++++++++-------------- appveyor.yml | 32 -------------- appveyor_deploy.yml | 86 ------------------------------------ 3 files changed, 33 insertions(+), 152 deletions(-) delete mode 100644 appveyor.yml delete mode 100644 appveyor_deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6a3c15ef91..1a921b21ae 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -6,42 +6,41 @@ on: - '*' jobs: -# notify_pending_production_deploy: -# runs-on: ubuntu-latest -# steps: -# - -# name: Submit pending deployment notification -# run: | -# export TITLE="Pending osu Production Deployment: $GITHUB_REF_NAME" -# export URL="https://github.com/ppy/osu/actions/runs/$GITHUB_RUN_ID" -# export DESCRIPTION="Awaiting approval for building NuGet packages for tag $GITHUB_REF_NAME: -# [View Workflow Run]($URL)" -# export ACTOR_ICON="https://avatars.githubusercontent.com/u/$GITHUB_ACTOR_ID" -# -# BODY="$(jq --null-input '{ -# "embeds": [ -# { -# "title": env.TITLE, -# "color": 15098112, -# "description": env.DESCRIPTION, -# "url": env.URL, -# "author": { -# "name": env.GITHUB_ACTOR, -# "icon_url": env.ACTOR_ICON -# } -# } -# ] -# }')" -# -# curl \ -# -H "Content-Type: application/json" \ -# -d "$BODY" \ -# "${{ secrets.DISCORD_INFRA_WEBHOOK_URL }}" + notify_pending_production_deploy: + runs-on: ubuntu-latest + steps: + - name: Submit pending deployment notification + run: | + export TITLE="Pending osu Production Deployment: $GITHUB_REF_NAME" + export URL="https://github.com/ppy/osu/actions/runs/$GITHUB_RUN_ID" + export DESCRIPTION="Awaiting approval for building NuGet packages for tag $GITHUB_REF_NAME: + [View Workflow Run]($URL)" + export ACTOR_ICON="https://avatars.githubusercontent.com/u/$GITHUB_ACTOR_ID" + + BODY="$(jq --null-input '{ + "embeds": [ + { + "title": env.TITLE, + "color": 15098112, + "description": env.DESCRIPTION, + "url": env.URL, + "author": { + "name": env.GITHUB_ACTOR, + "icon_url": env.ACTOR_ICON + } + } + ] + }')" + + curl \ + -H "Content-Type: application/json" \ + -d "$BODY" \ + "${{ secrets.DISCORD_INFRA_WEBHOOK_URL }}" pack: name: Pack runs-on: ubuntu-latest -# environment: production + environment: production steps: - name: Checkout uses: actions/checkout@v4 @@ -84,5 +83,5 @@ jobs: ${{steps.artifactsPath.outputs.nuget_artifacts}}/*.nupkg ${{steps.artifactsPath.outputs.nuget_artifacts}}/*.snupkg -# - name: Publish packages to nuget.org -# run: dotnet nuget push ${{steps.artifactsPath.outputs.nuget_artifacts}}/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json + - name: Publish packages to nuget.org + run: dotnet nuget push ${{steps.artifactsPath.outputs.nuget_artifacts}}/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index ed48a997e8..0000000000 --- a/appveyor.yml +++ /dev/null @@ -1,32 +0,0 @@ -clone_depth: 1 -version: '{branch}-{build}' -image: Visual Studio 2022 -cache: - - '%LOCALAPPDATA%\NuGet\v3-cache -> appveyor.yml' - -dotnet_csproj: - patch: true - file: 'osu.Game\osu.Game.csproj' # Use wildcard when it's able to exclude Xamarin projects - version: '0.0.{build}' - -before_build: - - cmd: dotnet --info # Useful when version mismatch between CI and local - - cmd: dotnet workload install maui-android # Change to `dotnet workload restore` once there's no old projects - - cmd: dotnet workload install maui-ios # Change to `dotnet workload restore` once there's no old projects - - cmd: nuget restore -verbosity quiet # Only nuget.exe knows both new (.NET Core) and old (Xamarin) projects - -build: - project: osu.sln - parallel: true - verbosity: minimal - publish_nuget: true - -after_build: - - ps: .\InspectCode.ps1 - -test: - assemblies: - except: - - '**\*Android*' - - '**\*iOS*' - - 'build\**\*' diff --git a/appveyor_deploy.yml b/appveyor_deploy.yml deleted file mode 100644 index 175c8d0f1b..0000000000 --- a/appveyor_deploy.yml +++ /dev/null @@ -1,86 +0,0 @@ -clone_depth: 1 -version: '{build}' -image: Visual Studio 2022 -test: off -skip_non_tags: true -configuration: Release - -environment: - matrix: - - job_name: osu-game - - job_name: osu-ruleset - job_depends_on: osu-game - - job_name: taiko-ruleset - job_depends_on: osu-game - - job_name: catch-ruleset - job_depends_on: osu-game - - job_name: mania-ruleset - job_depends_on: osu-game - - job_name: templates - job_depends_on: osu-game - -nuget: - project_feed: true - -for: - - - matrix: - only: - - job_name: osu-game - build_script: - - cmd: dotnet pack osu.Game\osu.Game.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME% - - - matrix: - only: - - job_name: osu-ruleset - build_script: - - cmd: dotnet remove osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj reference osu.Game\osu.Game.csproj - - cmd: dotnet add osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME% - - cmd: dotnet pack osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME% - - - matrix: - only: - - job_name: taiko-ruleset - build_script: - - cmd: dotnet remove osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj reference osu.Game\osu.Game.csproj - - cmd: dotnet add osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME% - - cmd: dotnet pack osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME% - - - matrix: - only: - - job_name: catch-ruleset - build_script: - - cmd: dotnet remove osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj reference osu.Game\osu.Game.csproj - - cmd: dotnet add osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME% - - cmd: dotnet pack osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME% - - - matrix: - only: - - job_name: mania-ruleset - build_script: - - cmd: dotnet remove osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj reference osu.Game\osu.Game.csproj - - cmd: dotnet add osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME% - - cmd: dotnet pack osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME% - - - matrix: - only: - - job_name: templates - build_script: - - cmd: dotnet remove Templates\Rulesets\ruleset-empty\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj reference osu.Game\osu.Game.csproj - - cmd: dotnet remove Templates\Rulesets\ruleset-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj reference osu.Game\osu.Game.csproj - - cmd: dotnet remove Templates\Rulesets\ruleset-scrolling-empty\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj reference osu.Game\osu.Game.csproj - - cmd: dotnet remove Templates\Rulesets\ruleset-scrolling-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj reference osu.Game\osu.Game.csproj - - - cmd: dotnet add Templates\Rulesets\ruleset-empty\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME% - - cmd: dotnet add Templates\Rulesets\ruleset-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME% - - cmd: dotnet add Templates\Rulesets\ruleset-scrolling-empty\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME% - - cmd: dotnet add Templates\Rulesets\ruleset-scrolling-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME% - - - cmd: dotnet pack Templates\osu.Game.Templates.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME% - -artifacts: - - path: '**\*.nupkg' - -deploy: - - provider: Environment - name: nuget From 39e050ba4b96f41724fdf29df39cbe0f284176d8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Jun 2025 16:03:02 +0900 Subject: [PATCH 082/370] Add beat synced flashing to song select v2 panels --- osu.Game/Screens/SelectV2/Panel.cs | 69 +++++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Panel.cs b/osu.Game/Screens/SelectV2/Panel.cs index a4a8c8d104..40c09b5de7 100644 --- a/osu.Game/Screens/SelectV2/Panel.cs +++ b/osu.Game/Screens/SelectV2/Panel.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -13,8 +14,10 @@ using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Carousel; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; @@ -37,10 +40,13 @@ namespace osu.Game.Screens.SelectV2 private Container backgroundLayerHorizontalPadding = null!; private Container backgroundContainer = null!; private Container iconContainer = null!; - private Box activationFlash = null!; - private Box hoverLayer = null!; - private Box keyboardSelectionLayer = null!; - private Box selectionLayer = null!; + + private Drawable activationFlash = null!; + private Drawable hoverLayer = null!; + + private Drawable keyboardSelectionLayer = null!; + + private PulsatingBox selectionLayer = null!; public Container TopLevelContent { get; private set; } = null!; @@ -109,7 +115,7 @@ namespace osu.Game.Screens.SelectV2 Hollow = true, Radius = 2, }, - Children = new Drawable[] + Children = new[] { new BufferedContainer { @@ -162,11 +168,11 @@ namespace osu.Game.Screens.SelectV2 Blending = BlendingParameters.Additive, RelativeSizeAxes = Axes.Both, }, - selectionLayer = new Box + selectionLayer = new PulsatingBox { Alpha = 0, RelativeSizeAxes = Axes.Both, - Width = 0.6f, + Width = 0.8f, Blending = BlendingParameters.Additive, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, @@ -192,6 +198,51 @@ namespace osu.Game.Screens.SelectV2 backgroundGradient.Colour = ColourInfo.GradientHorizontal(colourProvider.Background3, colourProvider.Background4); } + public partial class PulsatingBox : BeatSyncedContainer + { + public double FlashOffset; + + private readonly Box box; + + public PulsatingBox() + { + EarlyActivationMilliseconds = 50; + + InternalChildren = new Drawable[] + { + box = new Box + { + RelativeSizeAxes = Axes.Both, + }, + }; + } + + private int separation = 1; + + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) + { + base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + + if (beatIndex % separation != 0) + return; + + double length = timingPoint.BeatLength; + separation = 1; + + while (length < 500) + { + length *= 2; + separation *= 2; + } + + box + .Delay(FlashOffset) + .FadeTo(0.8f, length / 6, Easing.Out) + .Then() + .FadeTo(0.4f, length, Easing.Out); + } + } + protected override void LoadComplete() { base.LoadComplete(); @@ -223,6 +274,10 @@ namespace osu.Game.Screens.SelectV2 { base.PrepareForUse(); + // Slightly offset the flash animation based on the panel depth. + // This assumes a minimum depth of -2 (groups). + selectionLayer.FlashOffset = (2 + Item!.DepthLayer) * 50; + updateAccentColour(); updateXOffset(animated: false); From b106daabdea2febe4a976248492ac697ed60adb2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Jun 2025 16:10:21 +0900 Subject: [PATCH 083/370] Adjust keyboard selection animation and opacity slightly --- osu.Game/Screens/SelectV2/Panel.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Panel.cs b/osu.Game/Screens/SelectV2/Panel.cs index 40c09b5de7..0ba293ca7b 100644 --- a/osu.Game/Screens/SelectV2/Panel.cs +++ b/osu.Game/Screens/SelectV2/Panel.cs @@ -180,7 +180,7 @@ namespace osu.Game.Screens.SelectV2 keyboardSelectionLayer = new Box { Alpha = 0, - Colour = colourProvider.Highlight1.Opacity(0.1f), + Colour = ColourInfo.GradientHorizontal(colourProvider.Highlight1.Opacity(0.1f), colourProvider.Highlight1.Opacity(0.4f)), Blending = BlendingParameters.Additive, RelativeSizeAxes = Axes.Both, }, @@ -262,7 +262,11 @@ namespace osu.Game.Screens.SelectV2 KeyboardSelected.BindValueChanged(selected => { if (selected.NewValue) - keyboardSelectionLayer.FadeIn(100, Easing.OutQuint); + { + keyboardSelectionLayer.FadeIn(80, Easing.Out) + .Then() + .FadeTo(0.5f, 2000, Easing.OutQuint); + } else keyboardSelectionLayer.FadeOut(1000, Easing.OutQuint); From e335704c83df9cd7a5e2515c2a1d8aba1b72ea81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 5 Jun 2025 14:41:57 +0200 Subject: [PATCH 084/370] Add failing test --- osu.Game.Tests/Mods/ModUtilsTest.cs | 33 +++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index 6b3bc5f10f..f389182a00 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -354,6 +354,39 @@ namespace osu.Game.Tests.Mods }); } + [Test] + public void TestModsValidForRequiredFreestyleAreConsistentlyCompatibleAcrossRulesets() + { + Dictionary<(string firstMod, string secondMod), bool> compatibilityMap = new Dictionary<(string, string), bool>(); + + Assert.Multiple(() => + { + for (int rulesetId = 0; rulesetId < 4; ++rulesetId) + { + var rulesetStore = new AssemblyRulesetStore(); + var ruleset = rulesetStore.GetRuleset(rulesetId)!.CreateInstance(); + + var modsValidForFreestyleAsRequired = ruleset.CreateAllMods().Where(m => m.ValidForFreestyleAsRequiredMod).OrderBy(m => m.Acronym).ToList(); + + for (int i = 0; i < modsValidForFreestyleAsRequired.Count; i++) + { + for (int j = i; j < modsValidForFreestyleAsRequired.Count; ++j) + { + var first = modsValidForFreestyleAsRequired[i]; + var second = modsValidForFreestyleAsRequired[j]; + + bool compatible = ModUtils.CheckCompatibleSet([first, second]); + + if (!compatibilityMap.TryGetValue((first.Acronym, second.Acronym), out bool previousCompatible)) + compatibilityMap[(first.Acronym, second.Acronym)] = compatible; + else if (previousCompatible != compatible) + Assert.Fail($"{first.Acronym} and {second.Acronym} declare {nameof(Mod.ValidForFreestyleAsRequiredMod)} while not being consistently compatible in all four rulesets!"); + } + } + } + }); + } + public abstract class CustomMod1 : Mod, IModCompatibilitySpecification { } From 05496111d6fee16f765d4a9caf53429e1878d497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 5 Jun 2025 14:54:00 +0200 Subject: [PATCH 085/370] Disallow selected mods from being valid for freestyle as required mods due to them not being consistently compatible with other mods across rulesets Closes https://github.com/ppy/osu/issues/33444. The issue here is that for a set of required mods to make sense in the context of a freestyle playlist item, it must be consistently either valid or invalid *as a combination* across all four default rulesets, because freestyle also permits changing ruleset. There are two pertinent cases here: - Flashlight and Hidden are compatible in osu!, taiko, and catch, but not compatible in mania. In this case I've disallowed both mods because of symmetry, basically - I don't see one "better mod" to disallow here. - Accuracy Challenge and Easy are incompatible in osu!, catch, and mania (because the mod gives extra lives) there, but *compatible* in taiko, where it does not. In this case I've disallowed Accuracy Challenge only, because I find its value in being forced on a freestyle room to be much smaller than Easy's. In the large scale of things I don't see this being very important because my view is that 99% of the use case of required mods in freestyle is going to be changing the track speed. So I don't think anyone is going to care about this going away - but we can reassess if I'm proven wrong. --- osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs | 2 -- osu.Game/Rulesets/Mods/ModFlashlight.cs | 1 - osu.Game/Rulesets/Mods/ModHidden.cs | 1 - 3 files changed, 4 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs index 83d5fb027e..db16e771d3 100644 --- a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs +++ b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs @@ -34,8 +34,6 @@ namespace osu.Game.Rulesets.Mods public override bool Ranked => true; - public override bool ValidForFreestyleAsRequiredMod => true; - public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription { get diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index da45b7cc92..64c193d25f 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs @@ -37,7 +37,6 @@ namespace osu.Game.Rulesets.Mods public override ModType Type => ModType.DifficultyIncrease; public override LocalisableString Description => "Restricted view area."; public override bool Ranked => UsesDefaultConfiguration; - public override bool ValidForFreestyleAsRequiredMod => true; [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")] public abstract BindableFloat SizeMultiplier { get; } diff --git a/osu.Game/Rulesets/Mods/ModHidden.cs b/osu.Game/Rulesets/Mods/ModHidden.cs index f7a1336fd2..2915cb9bea 100644 --- a/osu.Game/Rulesets/Mods/ModHidden.cs +++ b/osu.Game/Rulesets/Mods/ModHidden.cs @@ -15,7 +15,6 @@ namespace osu.Game.Rulesets.Mods public override IconUsage? Icon => OsuIcon.ModHidden; public override ModType Type => ModType.DifficultyIncrease; public override bool Ranked => UsesDefaultConfiguration; - public override bool ValidForFreestyleAsRequiredMod => true; public virtual void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) { From e3259f7a7f4dfae66fd999574b595dcf5786a659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 5 Jun 2025 14:54:15 +0200 Subject: [PATCH 086/370] Adjust test cases to pass --- osu.Game.Tests/Mods/ModUtilsTest.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index f389182a00..b780d60817 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -259,9 +259,6 @@ namespace osu.Game.Tests.Mods new MultiplayerTestScenario(true, true, [new OsuModPerfect()], []), new MultiplayerTestScenario(true, true, [new OsuModDoubleTime()], []), new MultiplayerTestScenario(true, true, [new OsuModNightcore()], []), - new MultiplayerTestScenario(true, true, [new OsuModHidden()], []), - new MultiplayerTestScenario(true, true, [new OsuModFlashlight()], []), - new MultiplayerTestScenario(true, true, [new OsuModAccuracyChallenge()], []), new MultiplayerTestScenario(true, true, [new OsuModDifficultyAdjust()], []), new MultiplayerTestScenario(true, true, [new ModWindUp()], []), new MultiplayerTestScenario(true, true, [new ModWindDown()], []), @@ -347,8 +344,11 @@ namespace osu.Game.Tests.Mods { if (mod.ValidForFreestyleAsRequiredMod && mod.UserPlayable && !commonAcronyms.Contains(mod.Acronym)) Assert.Fail($"{mod.GetType().ReadableName()} declares {nameof(Mod.ValidForFreestyleAsRequiredMod)} but does not exist in all four basic rulesets!"); + + // downgraded to warning, because there are valid reasons why they may still not be specified to be valid for freestyle as required + // (see `TestModsValidForRequiredFreestyleAreConsistentlyCompatibleAcrossRulesets()` test case below). if (!mod.ValidForFreestyleAsRequiredMod && mod.UserPlayable && commonAcronyms.Contains(mod.Acronym)) - Assert.Fail($"{mod.GetType().ReadableName()} does not declare {nameof(Mod.ValidForFreestyleAsRequiredMod)} but exists in all four basic rulesets!"); + Assert.Warn($"{mod.GetType().ReadableName()} does not declare {nameof(Mod.ValidForFreestyleAsRequiredMod)} but exists in all four basic rulesets."); } } }); From 218d8290cb727a2d9642c38707a456bb6ab6fe2e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 5 Jun 2025 23:01:31 +0900 Subject: [PATCH 087/370] Fix extreme Drawable thrashing on initial leaderboard population --- .../Leaderboards/SoloGameplayLeaderboardProvider.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs index d17d55e4dd..70743ec5ec 100644 --- a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.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.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -34,10 +35,12 @@ namespace osu.Game.Screens.Select.Leaderboards isPartial = leaderboardManager?.CurrentCriteria?.Scope != BeatmapLeaderboardScope.Local && globalScores?.TopScores.Count >= 50; + List newScores = new List(); + if (globalScores != null) { foreach (var topScore in globalScores.AllScores.OrderByTotalScore()) - scores.Add(new GameplayLeaderboardScore(topScore, false)); + newScores.Add(new GameplayLeaderboardScore(topScore, false)); } if (gameplayState != null) @@ -48,9 +51,11 @@ namespace osu.Game.Screens.Select.Leaderboards TotalScoreTiebreaker = long.MaxValue }; localScore.TotalScore.BindValueChanged(_ => sorting.Invalidate()); - scores.Add(localScore); + newScores.Add(localScore); } + scores.AddRange(newScores); + Scheduler.AddDelayed(sort, 1000, true); } From 3af348c6eb947e1dacd37859e1f5408169a7b92b Mon Sep 17 00:00:00 2001 From: Chris Ehmann Date: Thu, 5 Jun 2025 17:46:31 -0700 Subject: [PATCH 088/370] Add test for undoing after quick deleting an object while it is dragged. --- .../Editing/TestSceneComposerSelection.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs index fd3431c08b..72adba64d4 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs @@ -615,6 +615,33 @@ namespace osu.Game.Tests.Visual.Editing AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); } + [Test] + public void TestUndoAfterQuickDeletingObjectWhileDragged() + { + AddStep("add hitobject", () => EditorBeatmap.Add( + new HitCircle { StartTime = 0, Position = new Vector2(200, 200) } + )); + + moveMouseToObject(() => EditorBeatmap.HitObjects[0]); + + AddStep("hold left click", () => InputManager.PressButton(MouseButton.Left)); + + AddStep("drag hitobject to different coordinate", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.BottomRight)); + + AddStep("click middle mouse button", () => InputManager.Click(MouseButton.Middle)); + + AddStep("release left click", () => InputManager.ReleaseButton(MouseButton.Left)); + + AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0); + + AddStep("hold ctrl", () => InputManager.PressKey(Key.ControlLeft)); + AddStep("press z", () => InputManager.PressKey(Key.Z)); + AddStep("release ctrl", () => InputManager.ReleaseKey(Key.ControlLeft)); + AddStep("press z", () => InputManager.ReleaseKey(Key.Z)); + + AddAssert("one hitobject in beatmap", () => EditorBeatmap.HitObjects.Count == 1); + } + [Test] public void TestShiftModifierMaintainsAspectRatio() { From 36eea335a47532997916035d4c6ee58387f9a9c6 Mon Sep 17 00:00:00 2001 From: Chris Ehmann Date: Thu, 5 Jun 2025 17:50:01 -0700 Subject: [PATCH 089/370] Add comment to explain guard clause --- osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index ce0411a027..69de242583 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -166,6 +166,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override void OnMouseUp(MouseUpEvent e) { + // Ensure that only left MouseUpEvents are considered when an object is being dragged. if (e.Button != MouseButton.Left) return; From 5e3eb708294a92bab444287d3f717d70c5d2e046 Mon Sep 17 00:00:00 2001 From: Chris Ehmann Date: Thu, 5 Jun 2025 18:28:17 -0700 Subject: [PATCH 090/370] better comment, fix typo in test case --- osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs | 4 ++-- .../Screens/Edit/Compose/Components/BlueprintContainer.cs | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs index 72adba64d4..c2c2872825 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs @@ -626,7 +626,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("hold left click", () => InputManager.PressButton(MouseButton.Left)); - AddStep("drag hitobject to different coordinate", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.BottomRight)); + AddStep("drag hitobject to different position", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.BottomRight)); AddStep("click middle mouse button", () => InputManager.Click(MouseButton.Middle)); @@ -637,7 +637,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("hold ctrl", () => InputManager.PressKey(Key.ControlLeft)); AddStep("press z", () => InputManager.PressKey(Key.Z)); AddStep("release ctrl", () => InputManager.ReleaseKey(Key.ControlLeft)); - AddStep("press z", () => InputManager.ReleaseKey(Key.Z)); + AddStep("release z", () => InputManager.ReleaseKey(Key.Z)); AddAssert("one hitobject in beatmap", () => EditorBeatmap.HitObjects.Count == 1); } diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 69de242583..d4c70d53df 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -166,7 +166,8 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override void OnMouseUp(MouseUpEvent e) { - // Ensure that only left MouseUpEvents are considered when an object is being dragged. + // When an object is being dragged, ONLY a left MouseUpEvent should end the drag and finalize the changes caused by the drag. + // Otherwise, other mouse inputs while a drag is occurring will cause change transactions to lock up. if (e.Button != MouseButton.Left) return; From 51a758d3f680cb6709e295342828d65a6360c7e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 Jun 2025 07:42:10 +0200 Subject: [PATCH 091/370] Fix user country flags no longer showing on multiplayer participants list --- .../OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index e55f8de61b..19868082fa 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -62,6 +62,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants private UserCoverBackground userCover = null!; private UpdateableAvatar userAvatar = null!; + private UpdateableFlag userFlag = null!; private OsuSpriteText username = null!; private Container teamFlagContainer = null!; private OsuSpriteText userRankText = null!; @@ -140,7 +141,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fit, }, - new UpdateableFlag + userFlag = new UpdateableFlag { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -241,6 +242,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants userCover.User = user; userAvatar.User = user; + userFlag.CountryCode = user?.CountryCode ?? default; teamFlagContainer.Child = new UpdateableTeamFlag(user?.Team) { Size = new Vector2(40, 20) From 3931ae3499f42d3697a11ea3798dcfb1833131a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 5 Jun 2025 12:43:22 +0200 Subject: [PATCH 092/370] Hack around hold-for-right-click mobile thing not allowing to hold to access song select v2 in main menu This is terrible but I sincerely believe that anything else trying to do this "properly" would be as terrible if not more. - You can try to handle touch events in `MainMenu` but then you'd have to awkwardly still manually hand them off to mouse handlers of the logo / menu button in a weird way for them to do what they're supposed to be doing. So any fix here would likely be smeared across `OsuLogo` and `MainMenuButton` anyway. - The logic in https://github.com/ppy/osu/blob/278a372a907c22f04fe28289c305ef47d5bcef45/osu.Game/Screens/Menu/MainMenu.cs#L517-L520 fundamentally doesn't work with raw touch events because it doesn't check for active touches (easy part) and because drawables do not become "hovered" in the input manager from being touched (hard part) I'm not willing to spend any more time on this. --- osu.Game/Screens/Menu/MainMenuButton.cs | 10 ++++++++++ osu.Game/Screens/Menu/OsuLogo.cs | 18 ++++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenuButton.cs b/osu.Game/Screens/Menu/MainMenuButton.cs index f8824795d8..235babeed2 100644 --- a/osu.Game/Screens/Menu/MainMenuButton.cs +++ b/osu.Game/Screens/Menu/MainMenuButton.cs @@ -20,6 +20,7 @@ using osu.Game.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; +using osu.Framework.Input.StateChanges; using osu.Framework.Localisation; using osu.Game.Beatmaps.ControlPoints; @@ -257,6 +258,15 @@ namespace osu.Game.Screens.Menu protected override void OnMouseUp(MouseUpEvent e) { + // HORRIBLE HACK + // This is here so that on mobile, the main menu button that progresses to song select can correctly progress to song select v2 when held. + // Once the temporary solution of holding the button to access song select v2 is removed, this should be too. + // Without this, the long-press-to-right-click flow intercepts the hold and converts it to a right click which would not trigger the button + // and therefore not progress to song select. + if (e.Button == MouseButton.Right && e.CurrentState.Mouse.LastSource is ISourcedFromTouch) + trigger(e); + // END OF HORRIBLE HACK + boxHoverLayer.FadeTo(0, 1000, Easing.OutQuint); base.OnMouseUp(e); } diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 31f47c1349..c9884dfd10 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -16,6 +16,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Input.Events; +using osu.Framework.Input.StateChanges; using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.Backgrounds; @@ -391,12 +392,27 @@ namespace osu.Game.Screens.Menu protected override void OnMouseUp(MouseUpEvent e) { + // HORRIBLE HACK + // This is here so that on mobile, the logo can correctly progress from main menu to song select v2 when held. + // Once the temporary solution of holding the logo to access song select v2 is removed, this should be too. + // Without this, the long-press-to-right-click flow intercepts the hold and converts it to a right click which would not trigger the logo + // and therefore not progress to song select. + if (e.Button == MouseButton.Right && e.CurrentState.Mouse.LastSource is ISourcedFromTouch) + triggerClick(); + // END OF HORRIBLE HACK + if (e.Button != MouseButton.Left) return; logoBounceContainer.ScaleTo(1f, 500, Easing.OutElastic); } protected override bool OnClick(ClickEvent e) + { + triggerClick(); + return true; + } + + private void triggerClick() { flashLayer.ClearTransforms(); flashLayer.Alpha = 0.4f; @@ -408,8 +424,6 @@ namespace osu.Game.Screens.Menu sampleClickChannel = sampleClick.GetChannel(); sampleClickChannel.Play(); } - - return true; } protected override bool OnHover(HoverEvent e) From f577cea715f3a658a83624fcf09d5f87b284e2d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 Jun 2025 09:08:26 +0200 Subject: [PATCH 093/370] Add failing test --- .../TestSceneReplayRecording.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneReplayRecording.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneReplayRecording.cs index 6b867a7729..8608ea1dfc 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneReplayRecording.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneReplayRecording.cs @@ -78,6 +78,16 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("smoke button press recorded to replay", () => Player.Score.Replay.Frames.OfType().Any(f => f.Actions.SequenceEqual([OsuAction.Smoke]))); } + [Test] + public void TestPressAndReleaseOnSameFrame() + { + seekTo(0); + AddStep("move cursor to circle", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.Single())); + AddStep("press X", () => InputManager.PressKey(Key.X)); + AddStep("release X", () => InputManager.ReleaseKey(Key.X)); + AddAssert("right button press recorded to replay", () => Player.Score.Replay.Frames.OfType().Any(f => f.Actions.SequenceEqual([OsuAction.RightButton]))); + } + private void seekTo(double time) { AddStep($"seek to {time}ms", () => Player.GameplayClockContainer.Seek(time)); From 12cc8e38da98568aa730df5a9ef6c93fb0e1c8d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 Jun 2025 09:10:51 +0200 Subject: [PATCH 094/370] Fix replays being misrecorded if an action is pressed and released in one update frame Closes https://github.com/ppy/osu/issues/33465 probably. This reverts the replay frame de-duplication logic to what it was before https://github.com/ppy/osu/pull/33148#discussion_r2091549388. I don't have good reproduction steps. I tried to write a test case for this that isn't just "press and release a key in the same frame", thinking that maybe there was some loophole in the osu! touch input mapper that may produce this situation artificially, but I could not in many configurations. So I have to assume that this just *can happen* organically. --- .../Replays/EmptyFreeformReplayFrame.cs | 4 ++++ .../Replays/PippidonReplayFrame.cs | 3 +++ .../Replays/EmptyScrollingReplayFrame.cs | 4 ++++ .../Replays/PippidonReplayFrame.cs | 4 ++++ osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs | 8 ++++++++ osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs | 4 ++++ osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs | 4 ++++ osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs | 4 ++++ osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs | 3 +++ osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs | 3 +++ .../Visual/Gameplay/TestSceneSpectatorPlayback.cs | 3 +++ osu.Game/Online/Spectator/SpectatorClient.cs | 6 ++---- osu.Game/Replays/Legacy/LegacyReplayFrame.cs | 7 +++++++ osu.Game/Rulesets/Replays/ReplayFrame.cs | 5 +++++ osu.Game/Rulesets/UI/ReplayRecorder.cs | 3 +-- 15 files changed, 59 insertions(+), 6 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformReplayFrame.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformReplayFrame.cs index c84101ca70..c6be5d6861 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformReplayFrame.cs +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformReplayFrame.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Game.Rulesets.Replays; using osuTK; @@ -17,5 +18,8 @@ namespace osu.Game.Rulesets.EmptyFreeform.Replays if (button.HasValue) Actions.Add(button.Value); } + + public override bool IsEquivalentTo(ReplayFrame other) + => other is EmptyFreeformReplayFrame freeformFrame && Time == freeformFrame.Time && Position == freeformFrame.Position && Actions.SequenceEqual(freeformFrame.Actions); } } diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonReplayFrame.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonReplayFrame.cs index 949ca160be..c434b62257 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonReplayFrame.cs +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonReplayFrame.cs @@ -9,5 +9,8 @@ namespace osu.Game.Rulesets.Pippidon.Replays public class PippidonReplayFrame : ReplayFrame { public Vector2 Position; + + public override bool IsEquivalentTo(ReplayFrame other) + => other is PippidonReplayFrame pippidonFrame && Time == pippidonFrame.Time && Position == pippidonFrame.Position; } } diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingReplayFrame.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingReplayFrame.cs index 2f19cffd2a..722eff6f05 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingReplayFrame.cs +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingReplayFrame.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Game.Rulesets.Replays; namespace osu.Game.Rulesets.EmptyScrolling.Replays @@ -15,5 +16,8 @@ namespace osu.Game.Rulesets.EmptyScrolling.Replays if (button.HasValue) Actions.Add(button.Value); } + + public override bool IsEquivalentTo(ReplayFrame other) + => other is EmptyScrollingReplayFrame scrollingFrame && Time == scrollingFrame.Time && Actions.SequenceEqual(scrollingFrame.Actions); } } diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonReplayFrame.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonReplayFrame.cs index 468ac9c725..c8df06f6d7 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonReplayFrame.cs +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonReplayFrame.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Game.Rulesets.Replays; namespace osu.Game.Rulesets.Pippidon.Replays @@ -15,5 +16,8 @@ namespace osu.Game.Rulesets.Pippidon.Replays if (button.HasValue) Actions.Add(button.Value); } + + public override bool IsEquivalentTo(ReplayFrame other) + => other is PippidonReplayFrame pippidonFrame && Time == pippidonFrame.Time && Actions.SequenceEqual(pippidonFrame.Actions); } } diff --git a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs index e30e535e9b..dd1ada21bd 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Game.Beatmaps; using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Replays; @@ -64,5 +65,12 @@ namespace osu.Game.Rulesets.Catch.Replays return new LegacyReplayFrame(Time, Position, null, state); } + + public override bool IsEquivalentTo(ReplayFrame other) + => other is CatchReplayFrame catchFrame + && Time == catchFrame.Time + && Position == catchFrame.Position + && Dashing == catchFrame.Dashing + && Actions.SequenceEqual(catchFrame.Actions); } } diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs index f80c442025..abbaa374f0 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Game.Beatmaps; using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Replays; @@ -47,5 +48,8 @@ namespace osu.Game.Rulesets.Mania.Replays return new LegacyReplayFrame(Time, keys, null, ReplayButtonState.None); } + + public override bool IsEquivalentTo(ReplayFrame other) + => other is ManiaReplayFrame maniaFrame && Time == maniaFrame.Time && Actions.SequenceEqual(maniaFrame.Actions); } } diff --git a/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs b/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs index 8082c5aef4..db2d9eaeda 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Game.Beatmaps; using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Replays; @@ -47,5 +48,8 @@ namespace osu.Game.Rulesets.Osu.Replays return new LegacyReplayFrame(Time, Position.X, Position.Y, state); } + + public override bool IsEquivalentTo(ReplayFrame other) + => other is OsuReplayFrame osuFrame && Time == osuFrame.Time && Position == osuFrame.Position && Actions.SequenceEqual(osuFrame.Actions); } } diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs index a0a687dca6..6f10c03a96 100644 --- a/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs +++ b/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Game.Beatmaps; using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Replays; @@ -42,5 +43,8 @@ namespace osu.Game.Rulesets.Taiko.Replays return new LegacyReplayFrame(Time, null, null, state); } + + public override bool IsEquivalentTo(ReplayFrame other) + => other is TaikoReplayFrame taikoFrame && Time == taikoFrame.Time && Actions.SequenceEqual(taikoFrame.Actions); } } diff --git a/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs b/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs index 541ad1e8bb..ffb21f124c 100644 --- a/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs +++ b/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs @@ -383,6 +383,9 @@ namespace osu.Game.Tests.NonVisual IsImportant = isImportant; FrameIndex = frameIndex; } + + public override bool IsEquivalentTo(ReplayFrame other) + => other is TestReplayFrame testFrame && Time == testFrame.Time && IsImportant == testFrame.IsImportant && FrameIndex == testFrame.FrameIndex; } private class TestInputHandler : FramedReplayInputHandler diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index 4ad6bc66e3..8ada550174 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -317,6 +317,9 @@ namespace osu.Game.Tests.Visual.Gameplay Position = position; Actions.AddRange(actions); } + + public override bool IsEquivalentTo(ReplayFrame other) + => other is TestReplayFrame testFrame && Time == testFrame.Time && Position == testFrame.Position && Actions.SequenceEqual(testFrame.Actions); } public enum TestAction diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index dd5bbf70b4..062d73abbf 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -353,6 +353,9 @@ namespace osu.Game.Tests.Visual.Gameplay return new LegacyReplayFrame(Time, Position.X, Position.Y, state); } + + public override bool IsEquivalentTo(ReplayFrame other) + => other is TestReplayFrame testFrame && Time == testFrame.Time && Position == testFrame.Position && Actions.SequenceEqual(testFrame.Actions); } public enum TestAction diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index dd0e03463c..7f09fbdc9e 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -247,12 +247,10 @@ namespace osu.Game.Online.Spectator var convertedFrame = convertible.ToLegacy(currentBeatmap); - // only keep the last recorded frame for a given timestamp. // this reduces redundancy of frames in the resulting replay. - // - // this is also done at `ReplayRecorded`, but needs to be done here as well + // it is also done at `ReplayRecorder`, but needs to be done here as well // due to the flow being handled differently. - if (pendingFrames.LastOrDefault()?.Time == convertedFrame.Time) + if (pendingFrames.LastOrDefault()?.IsEquivalentTo(convertedFrame) == true) pendingFrames[^1] = convertedFrame; else pendingFrames.Add(convertedFrame); diff --git a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs b/osu.Game/Replays/Legacy/LegacyReplayFrame.cs index b48fc44963..bfc8ad5df8 100644 --- a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs +++ b/osu.Game/Replays/Legacy/LegacyReplayFrame.cs @@ -64,5 +64,12 @@ namespace osu.Game.Replays.Legacy { return $"{Time}\t({MouseX},{MouseY})\t{MouseLeft}\t{MouseRight}\t{MouseLeft1}\t{MouseRight1}\t{MouseLeft2}\t{MouseRight2}\t{ButtonState}"; } + + public override bool IsEquivalentTo(ReplayFrame other) + => other is LegacyReplayFrame legacyFrame + && Time == legacyFrame.Time + && MouseX == legacyFrame.MouseX + && MouseY == legacyFrame.MouseY + && ButtonState == legacyFrame.ButtonState; } } diff --git a/osu.Game/Rulesets/Replays/ReplayFrame.cs b/osu.Game/Rulesets/Replays/ReplayFrame.cs index 433be6e4b7..269de228b1 100644 --- a/osu.Game/Rulesets/Replays/ReplayFrame.cs +++ b/osu.Game/Rulesets/Replays/ReplayFrame.cs @@ -30,5 +30,10 @@ namespace osu.Game.Rulesets.Replays { Time = time; } + + /// + /// Whether this frame is equivalent to with respect to replay recording. + /// + public virtual bool IsEquivalentTo(ReplayFrame other) => Time == other.Time; } } diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index c2187b0634..8829c15a21 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -86,9 +86,8 @@ namespace osu.Game.Rulesets.UI if (frame != null) { - // only keep the last recorded frame for a given timestamp. // this reduces redundancy of frames in the resulting replay. - if (last?.Time == frame.Time) + if (last?.IsEquivalentTo(frame) == true) target.Replay.Frames[^1] = frame; else target.Replay.Frames.Add(frame); From a92a659662eddb41bfaf25bb419815257161f457 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 Jun 2025 09:54:39 +0200 Subject: [PATCH 095/370] Fix general confusion in which combo should be read on which gameplay leaderboard Closes https://github.com/ppy/osu/issues/33455. The fundamental misunderstanding and source of confusion in https://github.com/ppy/osu/pull/33062 is that solo wants to show *maximum combo*, and multiplayer wants to show *current combo*, for their own, valid reasons. Which is spelled out explicitly in this change. --- .../TestSceneHUDOverlayRulesetLayouts.cs | 5 +-- .../Spectator/SpectatorScoreProcessor.cs | 6 +++ .../Leaderboards/GameplayLeaderboardScore.cs | 40 ++++++++++++------- .../MultiplayerLeaderboardProvider.cs | 6 ++- .../SoloGameplayLeaderboardProvider.cs | 6 ++- 5 files changed, 42 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlayRulesetLayouts.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlayRulesetLayouts.cs index b64ba387b0..47791dd462 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlayRulesetLayouts.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlayRulesetLayouts.cs @@ -136,7 +136,6 @@ namespace osu.Game.Tests.Visual.Gameplay { IBindableList IGameplayLeaderboardProvider.Scores => Scores; public BindableList Scores { get; } = new BindableList(); - public bool IsPartial { get; } = false; public TestGameplayLeaderboardProvider() { @@ -147,8 +146,8 @@ namespace osu.Game.Tests.Visual.Gameplay User = new APIUser { Username = $"User {i}" }, TotalScore = (20 - i) * 50_000, Accuracy = i * 0.05, - Combo = i * 50 - }, i == 19)); + MaxCombo = i * 50, + }, i == 19, GameplayLeaderboardScore.ComboDisplayMode.Highest)); } } } diff --git a/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs b/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs index 3242e21994..7da0b4f279 100644 --- a/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs +++ b/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs @@ -39,6 +39,11 @@ namespace osu.Game.Online.Spectator /// public readonly BindableInt Combo = new BindableInt(); + /// + /// The highest combo achieved in the score thus far. + /// + public readonly BindableInt HighestCombo = new BindableInt(); + /// /// The used to calculate scores. /// @@ -157,6 +162,7 @@ namespace osu.Game.Online.Spectator Accuracy.Value = frame.Header.Accuracy; Combo.Value = frame.Header.Combo; + HighestCombo.Value = frame.Header.MaxCombo; TotalScore.Value = frame.Header.TotalScore; } diff --git a/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs b/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs index bf99472dd7..bb6c402379 100644 --- a/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs @@ -8,6 +8,7 @@ using osu.Game.Online.Spectator; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; +using osu.Game.Screens.Play; using osu.Game.Users; namespace osu.Game.Screens.Select.Leaderboards @@ -40,7 +41,8 @@ namespace osu.Game.Screens.Select.Leaderboards public BindableDouble Accuracy { get; } = new BindableDouble(); /// - /// The current combo of the score. + /// The combo of the score to display. + /// Can be either highest combo or current combo, depending on constructor parameters. /// public BindableInt Combo { get; } = new BindableInt(); @@ -87,33 +89,35 @@ namespace osu.Game.Screens.Select.Leaderboards /// public Bindable DisplayOrder { get; } = new BindableLong(); - public GameplayLeaderboardScore(IUser user, ScoreProcessor scoreProcessor, bool tracked) + public GameplayLeaderboardScore(GameplayState gameplayState, bool tracked, ComboDisplayMode comboMode) + { + User = gameplayState.Score.ScoreInfo.User; + Tracked = tracked; + + var scoreProcessor = gameplayState.ScoreProcessor; + TotalScore.BindTarget = scoreProcessor.TotalScore; + Accuracy.BindTarget = scoreProcessor.Accuracy; + Combo.BindTarget = comboMode == ComboDisplayMode.Current ? scoreProcessor.Combo : scoreProcessor.HighestCombo; + GetDisplayScore = scoreProcessor.GetDisplayScore; + } + + public GameplayLeaderboardScore(IUser user, SpectatorScoreProcessor scoreProcessor, bool tracked, ComboDisplayMode comboMode) { User = user; Tracked = tracked; TotalScore.BindTarget = scoreProcessor.TotalScore; Accuracy.BindTarget = scoreProcessor.Accuracy; - Combo.BindTarget = scoreProcessor.Combo; + Combo.BindTarget = comboMode == ComboDisplayMode.Current ? scoreProcessor.Combo : scoreProcessor.HighestCombo; GetDisplayScore = scoreProcessor.GetDisplayScore; } - public GameplayLeaderboardScore(IUser user, SpectatorScoreProcessor scoreProcessor, bool tracked) - { - User = user; - Tracked = tracked; - TotalScore.BindTarget = scoreProcessor.TotalScore; - Accuracy.BindTarget = scoreProcessor.Accuracy; - Combo.BindTarget = scoreProcessor.Combo; - GetDisplayScore = scoreProcessor.GetDisplayScore; - } - - public GameplayLeaderboardScore(ScoreInfo scoreInfo, bool tracked) + public GameplayLeaderboardScore(ScoreInfo scoreInfo, bool tracked, ComboDisplayMode comboMode) { User = scoreInfo.User; Tracked = tracked; TotalScore.Value = scoreInfo.TotalScore; Accuracy.Value = scoreInfo.Accuracy; - Combo.Value = scoreInfo.MaxCombo; + Combo.Value = comboMode == ComboDisplayMode.Current ? scoreInfo.Combo : scoreInfo.MaxCombo; TotalScoreTiebreaker = scoreInfo.OnlineID > 0 ? scoreInfo.OnlineID : scoreInfo.Date.ToUnixTimeSeconds(); GetDisplayScore = scoreInfo.GetDisplayScore; InitialPosition = scoreInfo.Position; @@ -129,5 +133,11 @@ namespace osu.Game.Screens.Select.Leaderboards TotalScore.BindTarget = displayScore; GetDisplayScore = _ => displayScore.Value; } + + public enum ComboDisplayMode + { + Current, + Highest, + } } } diff --git a/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs index 80a5692841..ac4bd06fb1 100644 --- a/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs @@ -99,7 +99,11 @@ namespace osu.Game.Screens.Select.Leaderboards var trackedUser = UserScores[user.Id]; - var leaderboardScore = new GameplayLeaderboardScore(user, trackedUser.ScoreProcessor, user.Id == api.LocalUser.Value.Id) + var leaderboardScore = new GameplayLeaderboardScore( + user, + trackedUser.ScoreProcessor, + user.Id == api.LocalUser.Value.Id, + GameplayLeaderboardScore.ComboDisplayMode.Current) { HasQuit = { BindTarget = trackedUser.UserQuit }, TeamColour = UserScores[user.OnlineID].Team is int team ? getTeamColour(team) : null, diff --git a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs index 70743ec5ec..ba59dba7b2 100644 --- a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs @@ -40,12 +40,14 @@ namespace osu.Game.Screens.Select.Leaderboards if (globalScores != null) { foreach (var topScore in globalScores.AllScores.OrderByTotalScore()) - newScores.Add(new GameplayLeaderboardScore(topScore, false)); + { + newScores.Add(new GameplayLeaderboardScore(topScore, false, GameplayLeaderboardScore.ComboDisplayMode.Highest)); + } } if (gameplayState != null) { - var localScore = new GameplayLeaderboardScore(gameplayState.Score.ScoreInfo.User, gameplayState.ScoreProcessor, true) + var localScore = new GameplayLeaderboardScore(gameplayState, tracked: true, GameplayLeaderboardScore.ComboDisplayMode.Highest) { // Local score should always show lower than any existing scores in cases of ties. TotalScoreTiebreaker = long.MaxValue From 9c682cc2edb3d6dd961625a2a2e81e01146f1dfc Mon Sep 17 00:00:00 2001 From: Stedoss <29103029+Stedoss@users.noreply.github.com> Date: Fri, 6 Jun 2025 09:43:39 +0100 Subject: [PATCH 096/370] Fix `TagsOverflow` song select dependency --- osu.Game/Screens/SelectV2/BeatmapMetadataWedge_TagsLine.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_TagsLine.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_TagsLine.cs index 8df1596720..683cd428e9 100644 --- a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_TagsLine.cs +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_TagsLine.cs @@ -133,7 +133,7 @@ namespace osu.Game.Screens.SelectV2 private OverlayColourProvider colourProvider { get; set; } = null!; [Resolved] - private SongSelect? songSelect { get; set; } + private ISongSelect? songSelect { get; set; } public float LineBaseHeight => text.LineBaseHeight; @@ -196,7 +196,7 @@ namespace osu.Game.Screens.SelectV2 private readonly string[] tags; private readonly ISongSelect? songSelect; - public TagsOverflowPopover(string[] tags, SongSelect? songSelect) + public TagsOverflowPopover(string[] tags, ISongSelect? songSelect) { this.tags = tags; this.songSelect = songSelect; From b61688596bbc5b345f0f573df6226fa07578aff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 Jun 2025 11:11:01 +0200 Subject: [PATCH 097/370] Fix several issues with leaderboard score display - Enforces minimum width on accuracy / max combo displays which could previously look broken in CJK languages, thus fixing https://github.com/ppy/osu/issues/33434. Minimum sizes were chosen to accomodate what could be considered reasonably possible with some leeway on top. - Fixes hilariously broken logic that was supposed to highlight perfect / FC / max combo scores in green but instead did nothing due to two disparate bugs in a single line of code. - Extends the highlighting logic to also apply to 100% accuracy because web does this and I think it's nice. --- .../TestSceneBeatmapLeaderboardScore.cs | 4 +-- .../SelectV2/BeatmapLeaderboardScore.cs | 36 +++++++++++-------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs index 1b6d56df16..e9c055bcdd 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs @@ -205,7 +205,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Position = 999, Rank = ScoreRank.X, Accuracy = 1, - MaxCombo = 244, + MaxCombo = 3000, TotalScore = RNG.Next(1_800_000, 2_000_000), MaximumStatistics = { { HitResult.Great, 3000 } }, Ruleset = new OsuRuleset().RulesetInfo, @@ -223,7 +223,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Position = 22333, Rank = ScoreRank.S, Accuracy = 0.1f, - MaxCombo = 32040, + MaxCombo = 2204, TotalScore = RNG.Next(1_200_000, 1_500_000), MaximumStatistics = { { HitResult.Great, 3000 } }, Ruleset = new OsuRuleset().RulesetInfo, diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs index 5a4a0ad208..be80b3724d 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs @@ -330,7 +330,11 @@ namespace osu.Game.Screens.SelectV2 Origin = Anchor.CentreRight, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Children = getStatistics(score).Select(s => new ScoreComponentLabel(s, score)).ToList(), + Children = new Drawable[] + { + new ScoreComponentLabel(BeatmapsetsStrings.ShowScoreboardHeadersCombo.ToUpper(), $"{score.MaxCombo.ToString()}x", score.MaxCombo == score.GetMaximumAchievableCombo(), 60), + new ScoreComponentLabel(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy.ToUpper(), score.DisplayAccuracy, score.Accuracy == 1, 55), + }, Alpha = 0, } } @@ -640,48 +644,50 @@ namespace osu.Game.Screens.SelectV2 private partial class ScoreComponentLabel : Container { - private readonly (LocalisableString Name, LocalisableString Value) statisticInfo; - private readonly ScoreInfo score; + private readonly LocalisableString name; + private readonly LocalisableString value; + private readonly bool perfect; + private readonly float minWidth; private FillFlowContainer content = null!; public override bool Contains(Vector2 screenSpacePos) => content.Contains(screenSpacePos); - public ScoreComponentLabel((LocalisableString Name, LocalisableString Value) statisticInfo, ScoreInfo score) + public ScoreComponentLabel(LocalisableString name, LocalisableString value, bool perfect, float minWidth) { - this.statisticInfo = statisticInfo; - this.score = score; + this.name = name; + this.value = value; + this.perfect = perfect; + this.minWidth = minWidth; } [BackgroundDependencyLoader] private void load(OsuColour colours, OverlayColourProvider colourProvider) { AutoSizeAxes = Axes.Both; - OsuSpriteText value; Child = content = new FillFlowContainer { AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, - Children = new Drawable[] + Children = new[] { new OsuSpriteText { Colour = colourProvider.Content2, - Text = statisticInfo.Name, + Text = name, Font = OsuFont.Style.Caption2.With(weight: FontWeight.SemiBold), }, - value = new OsuSpriteText + new OsuSpriteText { // We don't want the value setting the horizontal size, since it leads to wonky accuracy container length, // since the accuracy is sometimes longer than its name. BypassAutoSizeAxes = Axes.X, - Text = statisticInfo.Value, + Text = value, Font = OsuFont.Style.Body, - } + Colour = perfect ? colours.Lime1 : Color4.White, + }, + Empty().With(d => d.Width = minWidth), } }; - - if (score.Combo != score.MaxCombo && statisticInfo.Name == BeatmapsetsStrings.ShowScoreboardHeadersCombo) - value.Colour = colours.Lime1; } } From 7ea9db1b72762a908bcbcb34ea83e1ce82c826e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 Jun 2025 11:41:58 +0200 Subject: [PATCH 098/370] Fix leaderboard score display not respecting local timezone & user 12/24hr settings Closes https://github.com/ppy/osu/issues/33473. Cross-reference previous implementation: https://github.com/ppy/osu/blob/828e8da7726109888aa1a6a41921daad254b75c0/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs#L140-L141 --- .../BeatmapLeaderboardScore_Tooltip.cs | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs index c6fe1e5f25..80ff3513e5 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs @@ -3,6 +3,7 @@ using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; @@ -12,6 +13,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -88,17 +90,24 @@ namespace osu.Game.Screens.SelectV2 private DrawableDate relativeDate = null!; private FillFlowContainer statistics = null!; + private readonly Bindable prefer24HourTime = new Bindable(); + [Resolved] private OsuColour colours { get; set; } = null!; [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; + private ScoreInfo score = null!; + public ScoreInfo Score { + get => score; set { - absoluteDate.Text = value.Date.ToLocalisableString(@"dd MMMM yyyy h:mm tt"); + score = value; + + updateAbsoluteDate(); relativeDate.Date = value.Date; var judgementsStatistics = value.GetStatisticsForDisplay().Select(s => @@ -131,7 +140,7 @@ namespace osu.Game.Screens.SelectV2 } [BackgroundDependencyLoader] - private void load() + private void load(OsuConfigManager configManager) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -205,7 +214,19 @@ namespace osu.Game.Screens.SelectV2 }, }, }; + + configManager.BindWith(OsuSetting.Prefer24HourTime, prefer24HourTime); } + + protected override void LoadComplete() + { + base.LoadComplete(); + + prefer24HourTime.BindValueChanged(_ => updateAbsoluteDate(), true); + } + + private void updateAbsoluteDate() + => absoluteDate.Text = score.Date.ToLocalTime().ToLocalisableString(prefer24HourTime.Value ? @"d MMMM yyyy HH:mm" : @"d MMMM yyyy h:mm tt"); } private partial class StatisticRow : CompositeDrawable From 37315d589ea930d05dbb6d5af36b062edfad6397 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Jun 2025 19:33:57 +0900 Subject: [PATCH 099/370] Avoid transform churn due to every frame `AccentColour` updates --- osu.Game/Screens/SelectV2/Panel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/SelectV2/Panel.cs b/osu.Game/Screens/SelectV2/Panel.cs index 0ba293ca7b..878248dcae 100644 --- a/osu.Game/Screens/SelectV2/Panel.cs +++ b/osu.Game/Screens/SelectV2/Panel.cs @@ -69,6 +69,9 @@ namespace osu.Game.Screens.SelectV2 get => accentColour; set { + if (value == accentColour) + return; + accentColour = value; updateAccentColour(); } From bef07c4b7373eeb52adfcfdd2936086dc8ee8427 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 6 Jun 2025 19:06:18 +0900 Subject: [PATCH 100/370] Add failing tests --- .../Mods/TestSceneTaikoModSimplifiedRhythm.cs | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSimplifiedRhythm.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSimplifiedRhythm.cs index 1e2c2a21ce..565b9c3362 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSimplifiedRhythm.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSimplifiedRhythm.cs @@ -148,5 +148,96 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods }, PassCondition = () => Player.ScoreProcessor.Combo.Value == 8 && Player.ScoreProcessor.Accuracy.Value == 1 }); + + /// + /// Regression tests a case of 1/3rd conversion where there are exactly div-3 number of hitobjects. + /// + [Test] + public void TestOnlyOneThirdConversion() + { + CreateModTest(new ModTestData + { + Mod = new TaikoModSimplifiedRhythm + { + OneThirdConversion = { Value = true }, + }, + Autoplay = false, + CreateBeatmap = () => new Beatmap + { + HitObjects = new List + { + new Hit { StartTime = 1000, Type = HitType.Centre }, + new Hit { StartTime = 1333, Type = HitType.Centre }, // mod removes this + new Hit { StartTime = 1666, Type = HitType.Centre }, // mod moves this to 1500 + new Hit { StartTime = 2000, Type = HitType.Centre }, + new Hit { StartTime = 2333, Type = HitType.Centre }, // mod removes this + new Hit { StartTime = 2666, Type = HitType.Centre }, // mod moves this to 2500 + }, + }, + ReplayFrames = new List + { + new TaikoReplayFrame(1000, TaikoAction.LeftCentre), + new TaikoReplayFrame(1200), + new TaikoReplayFrame(1500, TaikoAction.LeftCentre), + new TaikoReplayFrame(1700), + new TaikoReplayFrame(2000, TaikoAction.LeftCentre), + new TaikoReplayFrame(2200), + new TaikoReplayFrame(2500, TaikoAction.LeftCentre), + new TaikoReplayFrame(2700), + }, + PassCondition = () => Player.ScoreProcessor.Combo.Value == 4 && Player.ScoreProcessor.Accuracy.Value == 1 + }); + } + + /// + /// Regression tests a case of 1/6th conversion where there are exactly div-6 number of hitobjects. + /// + [Test] + public void TestOnlyOneSixthConversion() => CreateModTest(new ModTestData + { + Mod = new TaikoModSimplifiedRhythm + { + OneSixthConversion = { Value = true } + }, + Autoplay = false, + CreateBeatmap = () => new Beatmap + { + HitObjects = new List + { + new Hit { StartTime = 1000, Type = HitType.Centre }, + new Hit { StartTime = 1166, Type = HitType.Centre }, // mod removes this + new Hit { StartTime = 1333, Type = HitType.Centre }, // mod moves this to 1250 + new Hit { StartTime = 1500, Type = HitType.Centre }, + new Hit { StartTime = 1666, Type = HitType.Centre }, // mod removes this + new Hit { StartTime = 1833, Type = HitType.Centre }, // mod moves this to 1750 + new Hit { StartTime = 2000, Type = HitType.Centre }, + new Hit { StartTime = 2166, Type = HitType.Centre }, // mod removes this + new Hit { StartTime = 2333, Type = HitType.Centre }, // mod moves this to 2250 + new Hit { StartTime = 2500, Type = HitType.Centre }, + new Hit { StartTime = 2666, Type = HitType.Centre }, // mod removes this + new Hit { StartTime = 2833, Type = HitType.Centre }, // mod moves this to 2750 + }, + }, + ReplayFrames = new List + { + new TaikoReplayFrame(1000, TaikoAction.LeftCentre), + new TaikoReplayFrame(1200), + new TaikoReplayFrame(1250, TaikoAction.LeftCentre), + new TaikoReplayFrame(1450), + new TaikoReplayFrame(1500, TaikoAction.LeftCentre), + new TaikoReplayFrame(1600), + new TaikoReplayFrame(1750, TaikoAction.LeftCentre), + new TaikoReplayFrame(1800), + new TaikoReplayFrame(2000, TaikoAction.LeftCentre), + new TaikoReplayFrame(2200), + new TaikoReplayFrame(2250, TaikoAction.LeftCentre), + new TaikoReplayFrame(2450), + new TaikoReplayFrame(2500, TaikoAction.LeftCentre), + new TaikoReplayFrame(2600), + new TaikoReplayFrame(2750, TaikoAction.LeftCentre), + new TaikoReplayFrame(2800), + }, + PassCondition = () => Player.ScoreProcessor.Combo.Value == 8 && Player.ScoreProcessor.Accuracy.Value == 1 + }); } } From 03c3cce7610c1311c5cd2abebc4e1203146300af Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 6 Jun 2025 18:43:59 +0900 Subject: [PATCH 101/370] Refactor slightly --- osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs index e690ff075b..1014ed1f00 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.Mods var taikoBeatmap = (TaikoBeatmap)beatmap; var controlPointInfo = taikoBeatmap.ControlPointInfo; - Hit[] hits = taikoBeatmap.HitObjects.Where(obj => obj is Hit).Cast().ToArray(); + Hit[] hits = taikoBeatmap.HitObjects.OfType().ToArray(); if (hits.Length == 0) return; @@ -61,10 +61,10 @@ namespace osu.Game.Rulesets.Taiko.Mods if (inPattern) { // pattern continues - if (snapValue == baseRhythm) continue; + if (snapValue == baseRhythm) + continue; inPattern = false; - processPattern(i); } else From 558aacfed5649bd59dc55dca9b763777a16a07c7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 6 Jun 2025 18:50:38 +0900 Subject: [PATCH 102/370] Fix simplified rhythm mod not working on some beatmaps --- osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs index 1014ed1f00..6e9b974fbf 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs @@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Taiko.Mods if (indexInPattern % 3 == 1) taikoBeatmap.HitObjects.Remove(hits[j]); else if (indexInPattern % 3 == 2) - hits[j].StartTime = hits[j + 1].StartTime - controlPointInfo.TimingPointAt(hits[j].StartTime).BeatLength / adjustedRhythm; + hits[j].StartTime = hits[j - 2].StartTime + controlPointInfo.TimingPointAt(hits[j].StartTime).BeatLength / adjustedRhythm; break; } From 6835d7659d4f15c0f73c495fd36e57b794b0d885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 Jun 2025 12:50:20 +0200 Subject: [PATCH 103/370] Add toggle for rank change sound playback to default rank display Because some people don't seem to like it. Defaults to on still. --- .../Localisation/DefaultRankDisplayStrings.cs | 19 +++++++++++++++++++ .../Screens/Play/HUD/DefaultRankDisplay.cs | 7 ++++++- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Localisation/DefaultRankDisplayStrings.cs diff --git a/osu.Game/Localisation/DefaultRankDisplayStrings.cs b/osu.Game/Localisation/DefaultRankDisplayStrings.cs new file mode 100644 index 0000000000..88e3b4309a --- /dev/null +++ b/osu.Game/Localisation/DefaultRankDisplayStrings.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class DefaultRankDisplayStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.DefaultRankDisplay"; + + /// + /// "Play samples on rank change" + /// + public static LocalisableString PlaySamplesOnRankChange => new TranslatableString(getKey(@"play_samples_on_rank_change"), @"Play samples on rank change"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} \ No newline at end of file diff --git a/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs index 0f8f74f7fa..83c329bb8f 100644 --- a/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs @@ -6,11 +6,13 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Audio; +using osu.Game.Configuration; using osu.Game.Online.Leaderboards; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Skinning; using osuTK; +using osu.Game.Localisation; namespace osu.Game.Screens.Play.HUD { @@ -19,6 +21,9 @@ namespace osu.Game.Screens.Play.HUD [Resolved] private ScoreProcessor scoreProcessor { get; set; } = null!; + [SettingSource(typeof(DefaultRankDisplayStrings), nameof(DefaultRankDisplayStrings.PlaySamplesOnRankChange))] + public BindableBool PlaySamples { get; set; } = new BindableBool(true); + public bool UsesFixedAnchor { get; set; } private UpdateableRank rankDisplay = null!; @@ -55,7 +60,7 @@ namespace osu.Game.Screens.Play.HUD rank.BindValueChanged(r => { // Don't play rank-down sfx on quit/retry - if (r.NewValue != r.OldValue && r.NewValue > ScoreRank.F) + if (r.NewValue != r.OldValue && r.NewValue > ScoreRank.F && PlaySamples.Value) { if (r.NewValue > rankDisplay.Rank) rankUpSample.Play(); From e02cd28df629decb58a9f19d89c25b441b8f008f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 Jun 2025 12:51:53 +0200 Subject: [PATCH 104/370] Prevent rank display shown in skin editor toolbox from playing samples Closes https://github.com/ppy/osu/issues/33456. --- .../Overlays/SkinEditor/SkinComponentToolbox.cs | 17 ++++++++++++++--- osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs | 6 +++++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs b/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs index 85becc1a23..4a3ae99116 100644 --- a/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs +++ b/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs @@ -219,7 +219,7 @@ namespace osu.Game.Overlays.SkinEditor } } - public partial class DependencyBorrowingContainer : Container + private partial class DependencyBorrowingContainer : Container { protected override bool ShouldBeConsideredForInput(Drawable child) => false; @@ -232,8 +232,19 @@ namespace osu.Game.Overlays.SkinEditor this.donor = donor; } - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => - new DependencyContainer(donor?.Dependencies ?? base.CreateChildDependencies(parent)); + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var baseDependencies = base.CreateChildDependencies(parent); + if (donor == null) + return baseDependencies; + + var dependencies = new DependencyContainer(donor.Dependencies); + // inject `SkinEditor` again *on top* of the borrowed dependencies. + // this is designed to let components know when they are being displayed in the context of the skin editor + // via attempting to resolve `SkinEditor`. + dependencies.CacheAs(baseDependencies.Get()); + return dependencies; + } } } } diff --git a/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs index 83c329bb8f..61f0abd79c 100644 --- a/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Audio; using osu.Game.Configuration; using osu.Game.Online.Leaderboards; +using osu.Game.Overlays.SkinEditor; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Skinning; @@ -39,7 +40,7 @@ namespace osu.Game.Screens.Play.HUD } [BackgroundDependencyLoader] - private void load() + private void load(SkinEditor? skinEditor) { InternalChildren = new Drawable[] { @@ -50,6 +51,9 @@ namespace osu.Game.Screens.Play.HUD RelativeSizeAxes = Axes.Both }, }; + + if (skinEditor != null) + PlaySamples.Value = false; } protected override void LoadComplete() From 90084e27e5f241342f4156393e739dd3a3ed89cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 Jun 2025 13:53:32 +0200 Subject: [PATCH 105/370] SongSelectV2: Add back highlighting friend scores on the leaderboard --- .../TestSceneBeatmapLeaderboardScore.cs | 30 ++++++++++++- .../DailyChallengeLeaderboard.cs | 24 +++++++---- .../SelectV2/BeatmapLeaderboardScore.cs | 42 ++++++++++++++----- .../SelectV2/BeatmapLeaderboardWedge.cs | 26 +++++++++--- 4 files changed, 96 insertions(+), 26 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs index e9c055bcdd..7ef8da7673 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs @@ -66,10 +66,23 @@ namespace osu.Game.Tests.Visual.SongSelectV2 foreach (var scoreInfo in getTestScores()) { + BeatmapLeaderboardScore.HighlightType? highlightType = null; + + switch (scoreInfo.User.Id) + { + case 2: + highlightType = BeatmapLeaderboardScore.HighlightType.Own; + break; + + case 1541390: + highlightType = BeatmapLeaderboardScore.HighlightType.Friend; + break; + } + fillFlow.Add(new BeatmapLeaderboardScore(scoreInfo) { Rank = scoreInfo.Position, - IsPersonalBest = scoreInfo.User.Id == 2, + Highlight = highlightType, Shear = Vector2.Zero, }); } @@ -104,10 +117,23 @@ namespace osu.Game.Tests.Visual.SongSelectV2 foreach (var scoreInfo in getTestScores()) { + BeatmapLeaderboardScore.HighlightType? highlightType = null; + + switch (scoreInfo.User.Id) + { + case 2: + highlightType = BeatmapLeaderboardScore.HighlightType.Own; + break; + + case 1541390: + highlightType = BeatmapLeaderboardScore.HighlightType.Friend; + break; + } + fillFlow.Add(new BeatmapLeaderboardScore(scoreInfo, sheared: false) { Rank = scoreInfo.Position, - IsPersonalBest = scoreInfo.User.Id == 2, + Highlight = highlightType, }); } diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs index 401053599e..8fcb09723e 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs @@ -158,13 +158,23 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge } else { - LoadComponentsAsync(best.Select((s, index) => new BeatmapLeaderboardScore(s, sheared: false) + LoadComponentsAsync(best.Select((s, index) => { - Rank = index + 1, - IsPersonalBest = s.UserID == api.LocalUser.Value.Id, - Action = () => PresentScore?.Invoke(s.OnlineID), - SelectedMods = { BindTarget = SelectedMods }, - IsValidMod = IsValidMod, + BeatmapLeaderboardScore.HighlightType? highlightType = null; + + if (s.UserID == api.LocalUser.Value.Id) + highlightType = BeatmapLeaderboardScore.HighlightType.Own; + else if (api.Friends.Any(r => r.TargetID == s.UserID)) + highlightType = BeatmapLeaderboardScore.HighlightType.Friend; + + return new BeatmapLeaderboardScore(s, sheared: false) + { + Rank = index + 1, + Highlight = highlightType, + Action = () => PresentScore?.Invoke(s.OnlineID), + SelectedMods = { BindTarget = SelectedMods }, + IsValidMod = IsValidMod, + }; }), loaded => { scoreFlow.Clear(); @@ -181,7 +191,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge userBestContainer.Add(new BeatmapLeaderboardScore(userBest, sheared: false) { Rank = userBest.Position, - IsPersonalBest = true, + Highlight = BeatmapLeaderboardScore.HighlightType.Own, Action = () => PresentScore?.Invoke(userBest.OnlineID), SelectedMods = { BindTarget = SelectedMods }, IsValidMod = IsValidMod, diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs index be80b3724d..c00ddcebca 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs @@ -56,11 +56,14 @@ namespace osu.Game.Screens.SelectV2 public Func IsValidMod { get; set; } = _ => true; public int? Rank { get; init; } - public bool IsPersonalBest { get; init; } + public HighlightType? Highlight { get; init; } [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; + [Resolved] + private OsuColour colours { get; set; } = null!; + [Resolved] private IDialogOverlay? dialogOverlay { get; set; } @@ -93,8 +96,6 @@ namespace osu.Game.Screens.SelectV2 private Colour4 backgroundColour; private ColourInfo totalScoreBackgroundGradient; - private ColourInfo personalBestGradient; - private IBindable scoringMode { get; set; } = null!; private Box background = null!; @@ -109,7 +110,7 @@ namespace osu.Game.Screens.SelectV2 private Box totalScoreBackground = null!; private FillFlowContainer statisticsContainer = null!; - private Container personalBestIndicator = null!; + private Container highlightGradient = null!; private Container rankLabelStandalone = null!; private Container rankLabelOverlay = null!; @@ -142,7 +143,6 @@ namespace osu.Game.Screens.SelectV2 foregroundColour = colourProvider.Background5; backgroundColour = colourProvider.Background3; totalScoreBackgroundGradient = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), backgroundColour); - personalBestGradient = ColourInfo.GradientHorizontal(personal_best_gradient_left, personal_best_gradient_right); Child = new Container { @@ -176,15 +176,15 @@ namespace osu.Game.Screens.SelectV2 RelativeSizeAxes = Axes.Y, Children = new Drawable[] { - personalBestIndicator = new Container + highlightGradient = new Container { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Right = -10f }, - Alpha = IsPersonalBest ? 1 : 0, - Colour = personalBestGradient, + Alpha = Highlight != null ? 1 : 0, + Colour = getHighlightColour(Highlight), Child = new Box { RelativeSizeAxes = Axes.Both }, }, - new RankLabel(Rank, sheared, darkText: IsPersonalBest) + new RankLabel(Rank, sheared, darkText: Highlight == HighlightType.Own) { RelativeSizeAxes = Axes.Both, } @@ -476,6 +476,21 @@ namespace osu.Game.Screens.SelectV2 innerAvatar.OnLoadComplete += d => d.FadeInFromZero(200); } + private ColourInfo getHighlightColour(HighlightType? highlightType, float lightenAmount = 0) + { + switch (highlightType) + { + case HighlightType.Own: + return ColourInfo.GradientHorizontal(personal_best_gradient_left.Lighten(lightenAmount), personal_best_gradient_right.Lighten(lightenAmount)); + + case HighlightType.Friend: + return ColourInfo.GradientHorizontal(colours.Pink1.Lighten(lightenAmount), colours.Pink3.Lighten(lightenAmount)); + + default: + return Colour4.White; + } + } + protected override void LoadComplete() { base.LoadComplete(); @@ -533,12 +548,11 @@ namespace osu.Game.Screens.SelectV2 private void updateState() { var lightenedGradient = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0).Lighten(0.2f), backgroundColour.Lighten(0.2f)); - var personalBestLightenedGradient = ColourInfo.GradientHorizontal(personal_best_gradient_left.Lighten(0.2f), personal_best_gradient_right.Lighten(0.2f)); foreground.FadeColour(IsHovered ? foregroundColour.Lighten(0.2f) : foregroundColour, transition_duration, Easing.OutQuint); background.FadeColour(IsHovered ? backgroundColour.Lighten(0.2f) : backgroundColour, transition_duration, Easing.OutQuint); totalScoreBackground.FadeColour(IsHovered ? lightenedGradient : totalScoreBackgroundGradient, transition_duration, Easing.OutQuint); - personalBestIndicator.FadeColour(IsHovered ? personalBestLightenedGradient : personalBestGradient, transition_duration, Easing.OutQuint); + highlightGradient.FadeColour(getHighlightColour(Highlight, IsHovered ? 0.2f : 0), transition_duration, Easing.OutQuint); if (IsHovered && currentMode != DisplayMode.Full) rankLabelOverlay.FadeIn(transition_duration, Easing.OutQuint); @@ -721,5 +735,11 @@ namespace osu.Game.Screens.SelectV2 public LocalisableString TooltipText { get; } } + + public enum HighlightType + { + Own, + Friend, + } } } diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs index 3d2494ef45..bbcf793a33 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs @@ -22,6 +22,7 @@ using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; +using osu.Game.Online.API; using osu.Game.Online.Leaderboards; using osu.Game.Online.Placeholders; using osu.Game.Overlays; @@ -60,6 +61,9 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private ISongSelect? songSelect { get; set; } + [Resolved] + private IAPIProvider api { get; set; } = null!; + private Container placeholderContainer = null!; private Placeholder? placeholder; @@ -255,12 +259,22 @@ namespace osu.Game.Screens.SelectV2 return; } - LoadComponentsAsync(scores.Select((s, i) => new BeatmapLeaderboardScore(s) + LoadComponentsAsync(scores.Select((s, i) => { - Rank = i + 1, - IsPersonalBest = s.OnlineID == userScore?.OnlineID, - SelectedMods = { BindTarget = mods }, - Action = () => onLeaderboardScoreClicked(s), + BeatmapLeaderboardScore.HighlightType? highlightType = null; + + if (s.OnlineID == userScore?.OnlineID) + highlightType = BeatmapLeaderboardScore.HighlightType.Own; + else if (api.Friends.Any(r => r.TargetID == s.UserID) && Scope.Value != BeatmapLeaderboardScope.Friend) + highlightType = BeatmapLeaderboardScore.HighlightType.Friend; + + return new BeatmapLeaderboardScore(s) + { + Rank = i + 1, + Highlight = highlightType, + SelectedMods = { BindTarget = mods }, + Action = () => onLeaderboardScoreClicked(s), + }; }), loadedScores => { int delay = 200; @@ -293,7 +307,7 @@ namespace osu.Game.Screens.SelectV2 personalBestDisplay.FadeIn(600, Easing.OutQuint); personalBestScoreContainer.Child = new BeatmapLeaderboardScore(userScore) { - IsPersonalBest = true, + Highlight = BeatmapLeaderboardScore.HighlightType.Own, Rank = userScore.Position, SelectedMods = { BindTarget = mods }, Action = () => onLeaderboardScoreClicked(userScore), From 6ab9ee76b7306dd5a1a28ab53c67bc24ade82b42 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Jun 2025 23:20:38 +0900 Subject: [PATCH 106/370] Use equality when updated current carousel selection --- osu.Game/Graphics/Carousel/Carousel.cs | 4 ++-- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index 22167350cf..552b7652f6 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -108,7 +108,7 @@ namespace osu.Game.Graphics.Carousel get => currentSelection.Model; set { - if (currentSelection.Model != value) + if (!CheckModelEquality(currentSelection.Model, value)) { HandleItemSelected(value); @@ -210,7 +210,7 @@ namespace osu.Game.Graphics.Carousel /// /// Check whether two models are the same for display purposes. /// - protected virtual bool CheckModelEquality(object x, object y) => ReferenceEquals(x, y); + protected virtual bool CheckModelEquality(object? x, object? y) => ReferenceEquals(x, y); /// /// Create a drawable for the given carousel item so it can be displayed. diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 700ee6a05e..1bd2ff4746 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -553,7 +553,7 @@ namespace osu.Game.Screens.SelectV2 AddInternal(setPanelPool); } - protected override bool CheckModelEquality(object x, object y) + protected override bool CheckModelEquality(object? x, object? y) { // In the confines of the carousel logic, we assume that CurrentSelection (and all items) are using non-stale // BeatmapInfo reference, and that we can match based on beatmap / beatmapset (GU)IDs. From ed2889c9839fa6599dd5268d0bc2dc0313074f54 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Jun 2025 23:30:36 +0900 Subject: [PATCH 107/370] Fix clicking beatmap header causing leaderboard to refresh --- osu.Game/Screens/SelectV2/SongSelect.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 43c554f8d8..033f9e9c78 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -447,7 +447,13 @@ namespace osu.Game.Screens.SelectV2 // Debounce consideration is to avoid beatmap churn on key repeat selection. selectionDebounce?.Cancel(); - selectionDebounce = Scheduler.AddDelayed(() => Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap), SELECTION_DEBOUNCE); + selectionDebounce = Scheduler.AddDelayed(() => + { + if (Beatmap.Value.BeatmapInfo.Equals(beatmap)) + return; + + Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap); + }, SELECTION_DEBOUNCE); } private bool ensureGlobalBeatmapValid() From 15be762f71664e97fa5dca206e739366d4a7f628 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Fri, 6 Jun 2025 08:47:32 -0700 Subject: [PATCH 108/370] Fix loading spinner without a box clipping --- osu.Game/Graphics/UserInterface/LoadingSpinner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/LoadingSpinner.cs b/osu.Game/Graphics/UserInterface/LoadingSpinner.cs index df921c5c81..5aa339c7c5 100644 --- a/osu.Game/Graphics/UserInterface/LoadingSpinner.cs +++ b/osu.Game/Graphics/UserInterface/LoadingSpinner.cs @@ -40,7 +40,7 @@ namespace osu.Game.Graphics.UserInterface Child = MainContents = new Container { RelativeSizeAxes = Axes.Both, - Masking = true, + Masking = withBox, CornerRadius = 20, Anchor = Anchor.Centre, Origin = Anchor.Centre, From b3bda6a560321b8c552ab6c52aebfbc92ee07759 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Jun 2025 01:25:25 +0900 Subject: [PATCH 109/370] Ensure `beatmapSetsChanged` code doesn't run during gameplay --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 700ee6a05e..dca8018c14 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -119,8 +119,15 @@ namespace osu.Game.Screens.SelectV2 #region Beatmap source hookup - private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed) + private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed) => Schedule(() => { + // This callback is scheduled to ensure there's no added overhead during gameplay. + // If this ever becomes an issue, it's important to note that the actual carousel filtering is already + // implemented in a way it will only run when at song select. + // + // The overhead we are avoiding here is that of this method directly – things like Items.IndexOf calls + // that can be slow for very large beatmap libraries. There are definitely ways to optimise this further. + // 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? newItems = changed.NewItems?.Cast(); @@ -191,7 +198,7 @@ namespace osu.Game.Screens.SelectV2 Items.Clear(); break; } - } + }); #endregion From c25396c7d6ef5be26b1304f09b54549a1b5ddc2e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Jun 2025 02:19:15 +0900 Subject: [PATCH 110/370] Adjust loading spinner to not use masking as all when no box --- .../Graphics/UserInterface/LoadingSpinner.cs | 67 ++++++++++++------- 1 file changed, 42 insertions(+), 25 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/LoadingSpinner.cs b/osu.Game/Graphics/UserInterface/LoadingSpinner.cs index 5aa339c7c5..92e64d5b78 100644 --- a/osu.Game/Graphics/UserInterface/LoadingSpinner.cs +++ b/osu.Game/Graphics/UserInterface/LoadingSpinner.cs @@ -15,13 +15,15 @@ namespace osu.Game.Graphics.UserInterface /// public partial class LoadingSpinner : VisibilityContainer { + public const float TRANSITION_DURATION = 500; + private readonly SpriteIcon spinner; protected override bool StartHidden => true; - protected Container MainContents; + protected Drawable MainContents; - public const float TRANSITION_DURATION = 500; + private readonly Container? roundedContent; private const float spin_duration = 900; @@ -37,32 +39,46 @@ namespace osu.Game.Graphics.UserInterface Anchor = Anchor.Centre; Origin = Anchor.Centre; - Child = MainContents = new Container + if (withBox) { - RelativeSizeAxes = Axes.Both, - Masking = withBox, - CornerRadius = 20, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] + Child = MainContents = roundedContent = new Container { - new Box + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 20, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] { - Colour = inverted ? Color4.White : Color4.Black, - RelativeSizeAxes = Axes.Both, - Alpha = withBox ? 0.7f : 0 - }, - spinner = new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = inverted ? Color4.Black : Color4.White, - Scale = new Vector2(withBox ? 0.6f : 1), - RelativeSizeAxes = Axes.Both, - Icon = FontAwesome.Solid.CircleNotch + new Box + { + Colour = inverted ? Color4.White : Color4.Black, + RelativeSizeAxes = Axes.Both, + Alpha = 0.7f, + }, + spinner = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = inverted ? Color4.Black : Color4.White, + Scale = new Vector2(0.6f), + RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.Solid.CircleNotch + } } - } - }; + }; + } + else + { + Child = MainContents = spinner = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = inverted ? Color4.Black : Color4.White, + RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.Solid.CircleNotch + }; + } } protected override void LoadComplete() @@ -76,7 +92,8 @@ namespace osu.Game.Graphics.UserInterface { base.Update(); - MainContents.CornerRadius = MainContents.DrawWidth / 4; + if (roundedContent != null) + roundedContent.CornerRadius = MainContents.DrawWidth / 4; } protected override void PopIn() From 45c88919759aa880c230f99ec388ca6918e8a7a8 Mon Sep 17 00:00:00 2001 From: nobbele Date: Sun, 8 Jun 2025 00:27:15 +0200 Subject: [PATCH 111/370] SongSelectV2: Calculate PP for leaderboard tooltip if missing --- .../BeatmapLeaderboardScore_Tooltip.cs | 15 ++-- ...rdScore_Tooltip_PerformanceStatisticRow.cs | 83 +++++++++++++++++++ 2 files changed, 88 insertions(+), 10 deletions(-) create mode 100644 osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip_PerformanceStatisticRow.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs index 80ff3513e5..d0a7299597 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs @@ -120,19 +120,12 @@ namespace osu.Game.Screens.SelectV2 var generalStatistics = new[] { + new PerformanceStatisticRow(BeatmapsetsStrings.ShowScoreboardHeaderspp.ToUpper(), colourProvider.Content2, score), new StatisticRow("Score Multiplier", colourProvider.Content2, ModUtils.FormatScoreMultiplier(multiplier)), new StatisticRow(BeatmapsetsStrings.ShowScoreboardHeadersCombo, colourProvider.Content2, value.MaxCombo.ToLocalisableString(@"0\x")), new StatisticRow(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy, colourProvider.Content2, value.Accuracy.FormatAccuracy()), }; - if (value.PP != null) - { - generalStatistics = new[] - { - new StatisticRow(BeatmapsetsStrings.ShowScoreboardHeaderspp.ToUpper(), colourProvider.Content2, value.PP.ToLocalisableString("N0")) - }.Concat(generalStatistics).ToArray(); - } - statistics.ChildrenEnumerable = judgementsStatistics .Append(Empty().With(d => d.Height = 20)) .Concat(generalStatistics); @@ -229,8 +222,10 @@ namespace osu.Game.Screens.SelectV2 => absoluteDate.Text = score.Date.ToLocalTime().ToLocalisableString(prefer24HourTime.Value ? @"d MMMM yyyy HH:mm" : @"d MMMM yyyy h:mm tt"); } - private partial class StatisticRow : CompositeDrawable + public partial class StatisticRow : CompositeDrawable { + protected OsuSpriteText ValueLabel; + public StatisticRow(LocalisableString label, Color4 labelColour, LocalisableString value) { RelativeSizeAxes = Axes.X; @@ -244,7 +239,7 @@ namespace osu.Game.Screens.SelectV2 Colour = labelColour, Font = OsuFont.Style.Caption2.With(weight: FontWeight.SemiBold), }, - new OsuSpriteText + ValueLabel = new OsuSpriteText { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip_PerformanceStatisticRow.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip_PerformanceStatisticRow.cs new file mode 100644 index 0000000000..cefab86edc --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip_PerformanceStatisticRow.cs @@ -0,0 +1,83 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; +using osu.Game.Scoring; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapLeaderboardScore + { + public partial class LeaderboardScoreTooltip + { + public partial class PerformanceStatisticRow : StatisticRow + { + private readonly ScoreInfo score; + + public PerformanceStatisticRow(LocalisableString label, Color4 labelColour, ScoreInfo score) + : base(label, labelColour, 0.ToLocalisableString("N0")) + { + this.score = score; + } + + [BackgroundDependencyLoader] + private void load(BeatmapDifficultyCache difficultyCache, CancellationToken? cancellationToken) + { + if (score.PP.HasValue) + { + setPerformanceValue(score, score.PP.Value); + return; + } + + Task.Run(async () => + { + var attributes = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods, cancellationToken ?? default).ConfigureAwait(false); + var performanceCalculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(); + + // Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value. + if (attributes?.DifficultyAttributes == null || performanceCalculator == null) + return; + + var result = await performanceCalculator.CalculateAsync(score, attributes.Value.DifficultyAttributes, cancellationToken ?? default).ConfigureAwait(false); + + Schedule(() => setPerformanceValue(score, result.Total)); + }, cancellationToken ?? default); + } + + private void setPerformanceValue(ScoreInfo scoreInfo, double? pp) + { + if (pp.HasValue) + { + int ppValue = (int)Math.Round(pp.Value, MidpointRounding.AwayFromZero); + ValueLabel.Text = ppValue.ToLocalisableString("N0"); + + if (!scoreInfo.BeatmapInfo!.Status.GrantsPerformancePoints() || hasUnrankedMods(scoreInfo)) + Alpha = 0.5f; + else + Alpha = 1f; + } + } + + private static bool hasUnrankedMods(ScoreInfo scoreInfo) + { + IEnumerable modsToCheck = scoreInfo.Mods; + + if (scoreInfo.IsLegacyScore) + modsToCheck = modsToCheck.Where(m => m is not ModClassic); + + return modsToCheck.Any(m => !m.Ranked); + } + } + } + } +} From a89b7d27adebc1edffad945e357a366a2a3fbf2f Mon Sep 17 00:00:00 2001 From: nobbele Date: Sun, 8 Jun 2025 00:32:10 +0200 Subject: [PATCH 112/370] Move PerformanceStatisticRow into tooltip class --- .../BeatmapLeaderboardScore_Tooltip.cs | 64 ++++++++++++++ ...rdScore_Tooltip_PerformanceStatisticRow.cs | 83 ------------------- 2 files changed, 64 insertions(+), 83 deletions(-) delete mode 100644 osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip_PerformanceStatisticRow.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs index d0a7299597..178fb1df00 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs @@ -1,7 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -13,6 +17,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; +using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -251,6 +256,65 @@ namespace osu.Game.Screens.SelectV2 } } + public partial class PerformanceStatisticRow : StatisticRow + { + private readonly ScoreInfo score; + + public PerformanceStatisticRow(LocalisableString label, Color4 labelColour, ScoreInfo score) + : base(label, labelColour, 0.ToLocalisableString("N0")) + { + this.score = score; + } + + [BackgroundDependencyLoader] + private void load(BeatmapDifficultyCache difficultyCache, CancellationToken? cancellationToken) + { + if (score.PP.HasValue) + { + setPerformanceValue(score, score.PP.Value); + return; + } + + Task.Run(async () => + { + var attributes = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods, cancellationToken ?? default).ConfigureAwait(false); + var performanceCalculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(); + + // Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value. + if (attributes?.DifficultyAttributes == null || performanceCalculator == null) + return; + + var result = await performanceCalculator.CalculateAsync(score, attributes.Value.DifficultyAttributes, cancellationToken ?? default).ConfigureAwait(false); + + Schedule(() => setPerformanceValue(score, result.Total)); + }, cancellationToken ?? default); + } + + private void setPerformanceValue(ScoreInfo scoreInfo, double? pp) + { + if (pp.HasValue) + { + int ppValue = (int)Math.Round(pp.Value, MidpointRounding.AwayFromZero); + ValueLabel.Text = ppValue.ToLocalisableString("N0"); + + if (!scoreInfo.BeatmapInfo!.Status.GrantsPerformancePoints() || hasUnrankedMods(scoreInfo)) + Alpha = 0.5f; + else + Alpha = 1f; + } + } + + private static bool hasUnrankedMods(ScoreInfo scoreInfo) + { + IEnumerable modsToCheck = scoreInfo.Mods; + + if (scoreInfo.IsLegacyScore) + modsToCheck = modsToCheck.Where(m => m is not ModClassic); + + return modsToCheck.Any(m => !m.Ranked); + } + } + private partial class ModsPanel : CompositeDrawable { private FillFlowContainer modsFlow = null!; diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip_PerformanceStatisticRow.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip_PerformanceStatisticRow.cs deleted file mode 100644 index cefab86edc..0000000000 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip_PerformanceStatisticRow.cs +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using osu.Framework.Allocation; -using osu.Framework.Extensions.LocalisationExtensions; -using osu.Framework.Localisation; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Mods; -using osu.Game.Scoring; -using osuTK.Graphics; - -namespace osu.Game.Screens.SelectV2 -{ - public partial class BeatmapLeaderboardScore - { - public partial class LeaderboardScoreTooltip - { - public partial class PerformanceStatisticRow : StatisticRow - { - private readonly ScoreInfo score; - - public PerformanceStatisticRow(LocalisableString label, Color4 labelColour, ScoreInfo score) - : base(label, labelColour, 0.ToLocalisableString("N0")) - { - this.score = score; - } - - [BackgroundDependencyLoader] - private void load(BeatmapDifficultyCache difficultyCache, CancellationToken? cancellationToken) - { - if (score.PP.HasValue) - { - setPerformanceValue(score, score.PP.Value); - return; - } - - Task.Run(async () => - { - var attributes = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods, cancellationToken ?? default).ConfigureAwait(false); - var performanceCalculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(); - - // Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value. - if (attributes?.DifficultyAttributes == null || performanceCalculator == null) - return; - - var result = await performanceCalculator.CalculateAsync(score, attributes.Value.DifficultyAttributes, cancellationToken ?? default).ConfigureAwait(false); - - Schedule(() => setPerformanceValue(score, result.Total)); - }, cancellationToken ?? default); - } - - private void setPerformanceValue(ScoreInfo scoreInfo, double? pp) - { - if (pp.HasValue) - { - int ppValue = (int)Math.Round(pp.Value, MidpointRounding.AwayFromZero); - ValueLabel.Text = ppValue.ToLocalisableString("N0"); - - if (!scoreInfo.BeatmapInfo!.Status.GrantsPerformancePoints() || hasUnrankedMods(scoreInfo)) - Alpha = 0.5f; - else - Alpha = 1f; - } - } - - private static bool hasUnrankedMods(ScoreInfo scoreInfo) - { - IEnumerable modsToCheck = scoreInfo.Mods; - - if (scoreInfo.IsLegacyScore) - modsToCheck = modsToCheck.Where(m => m is not ModClassic); - - return modsToCheck.Any(m => !m.Ranked); - } - } - } - } -} From 4e97731161091c051e0622184d028eb7a55d6459 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sat, 7 Jun 2025 19:10:17 -0700 Subject: [PATCH 113/370] Fix mods blocking drag and right-click input on leaderboards --- osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs index c00ddcebca..64c078ddd4 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs @@ -446,18 +446,14 @@ namespace osu.Game.Screens.SelectV2 Font = OsuFont.Style.Subtitle.With(weight: FontWeight.Light, fixedWidth: true), Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, }, - new InputBlockingContainer + modsContainer = new FillFlowContainer { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, AutoSizeAxes = Axes.Both, - Child = modsContainer = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(-10, 0), - Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, - }, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(-10, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, }, } } From be0c43b4ab2b552d2b3f7d0aebbd377964b2b8bc Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Sun, 8 Jun 2025 14:08:26 +0100 Subject: [PATCH 114/370] Use touch device detector in song select V2 --- osu.Game/Screens/SelectV2/SoloSongSelect.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/SelectV2/SoloSongSelect.cs b/osu.Game/Screens/SelectV2/SoloSongSelect.cs index 6d0a2b3b62..3771528a80 100644 --- a/osu.Game/Screens/SelectV2/SoloSongSelect.cs +++ b/osu.Game/Screens/SelectV2/SoloSongSelect.cs @@ -54,6 +54,8 @@ namespace osu.Game.Screens.SelectV2 private void load(AudioManager audio) { sampleConfirmSelection = audio.Samples.Get(@"SongSelect/confirm-selection"); + + AddInternal(new SongSelectTouchInputDetector()); } public override IEnumerable GetForwardActions(BeatmapInfo beatmap) From a03448af7fe43c7025d9f9c2cee9af90975dd2df Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 8 Jun 2025 23:08:09 +0300 Subject: [PATCH 115/370] Remove BufferedContainers from songselectv2 --- osu.Game/Screens/SelectV2/Panel.cs | 39 ++++++++----------- .../Screens/SelectV2/PanelSetBackground.cs | 6 +-- 2 files changed, 17 insertions(+), 28 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Panel.cs b/osu.Game/Screens/SelectV2/Panel.cs index 878248dcae..6a1b5cc3a6 100644 --- a/osu.Game/Screens/SelectV2/Panel.cs +++ b/osu.Game/Screens/SelectV2/Panel.cs @@ -120,35 +120,28 @@ namespace osu.Game.Screens.SelectV2 }, Children = new[] { - new BufferedContainer + backgroundBorder = new Box { RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + Colour = Color4.Black, + }, + backgroundLayerHorizontalPadding = new Container + { + RelativeSizeAxes = Axes.Both, + Child = new Container { - backgroundBorder = new Box + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = corner_radius, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - }, - backgroundLayerHorizontalPadding = new Container - { - RelativeSizeAxes = Axes.Both, - Child = new Container + backgroundGradient = new Box + { + RelativeSizeAxes = Axes.Both, + }, + backgroundContainer = new Container { RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = corner_radius, - Children = new Drawable[] - { - backgroundGradient = new Box - { - RelativeSizeAxes = Axes.Both, - }, - backgroundContainer = new Container - { - RelativeSizeAxes = Axes.Both, - }, - } }, } }, diff --git a/osu.Game/Screens/SelectV2/PanelSetBackground.cs b/osu.Game/Screens/SelectV2/PanelSetBackground.cs index d81a6007d8..ea82755810 100644 --- a/osu.Game/Screens/SelectV2/PanelSetBackground.cs +++ b/osu.Game/Screens/SelectV2/PanelSetBackground.cs @@ -18,7 +18,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public partial class PanelSetBackground : BufferedContainer + public partial class PanelSetBackground : Container { [Resolved] private BeatmapCarousel? beatmapCarousel { get; set; } @@ -52,10 +52,6 @@ namespace osu.Game.Screens.SelectV2 } public PanelSetBackground() - // TODO: for performance reasons we may want this to be true. - // Setting to true will require that the buffered portion is moved to a child such that `FadeIn`/`FadeOut` transforms - // still work. - : base(cachedFrameBuffer: false) { RelativeSizeAxes = Axes.Both; } From 77cf39ac0dbdfd5f88a3865b39d7516811219ede Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 9 Jun 2025 16:05:52 +0900 Subject: [PATCH 116/370] Move release stream handling to base class --- osu.Desktop/Updater/VelopackUpdateManager.cs | 32 +++++++------------- osu.Game/Updater/UpdateManager.cs | 19 ++++++++---- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/osu.Desktop/Updater/VelopackUpdateManager.cs b/osu.Desktop/Updater/VelopackUpdateManager.cs index 6f22fd5940..51744345a4 100644 --- a/osu.Desktop/Updater/VelopackUpdateManager.cs +++ b/osu.Desktop/Updater/VelopackUpdateManager.cs @@ -4,7 +4,6 @@ using System; using System.Threading.Tasks; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Logging; using osu.Game; using osu.Game.Configuration; @@ -27,36 +26,27 @@ namespace osu.Desktop.Updater [Resolved] private ILocalUserPlayInfo? localUserInfo { get; set; } - [Resolved] - private OsuConfigManager osuConfigManager { get; set; } = null!; - private bool isInGameplay => localUserInfo?.PlayingState.Value != LocalUserPlayingState.NotPlaying; - private readonly Bindable releaseStream = new Bindable(); private UpdateManager? updateManager; private UpdateInfo? pendingUpdate; + private ReleaseStream? lastReleaseStream; - protected override void LoadComplete() + protected override async Task PerformUpdateCheck() { - // Used by the base implementation. - osuConfigManager.BindWith(OsuSetting.ReleaseStream, releaseStream); - releaseStream.BindValueChanged(_ => onReleaseStreamChanged(), true); - - base.LoadComplete(); - } - - private void onReleaseStreamChanged() - { - updateManager = new UpdateManager(new GithubSource(@"https://github.com/ppy/osu", null, releaseStream.Value == ReleaseStream.Tachyon), new UpdateOptions + if (ReleaseStream.Value != lastReleaseStream) { - AllowVersionDowngrade = true, - }); + updateManager = new UpdateManager(new GithubSource(@"https://github.com/ppy/osu", null, ReleaseStream.Value == Game.Configuration.ReleaseStream.Tachyon), new UpdateOptions + { + AllowVersionDowngrade = true, + }); - Schedule(() => Task.Run(CheckForUpdateAsync)); + lastReleaseStream = ReleaseStream.Value; + } + + return await checkForUpdateAsync().ConfigureAwait(false); } - protected override async Task PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false); - private async Task checkForUpdateAsync() { // whether to check again in 30 minutes. generally only if there's an error or no update was found (yet). diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index c114e3a8d0..354a8da46b 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -5,6 +5,7 @@ using System.Reflection; using System.Threading.Tasks; using osu.Framework; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -39,14 +40,17 @@ namespace osu.Game.Updater [Resolved] protected INotificationOverlay Notifications { get; private set; } = null!; + protected IBindable ReleaseStream => releaseStream; + + private readonly Bindable releaseStream = new Bindable(); + private readonly object updateTaskLock = new object(); + private Task? updateCheckTask; + protected override void LoadComplete() { base.LoadComplete(); - Schedule(() => Task.Run(CheckForUpdateAsync)); - string version = game.Version; - string lastVersion = config.Get(OsuSetting.Version); if (game.IsDeployedBuild && version != lastVersion) @@ -62,11 +66,14 @@ namespace osu.Game.Updater // debug / local compilations will reset to a non-release string. // can be useful to check when an install has transitioned between release and otherwise (see OsuConfigManager's migrations). config.SetValue(OsuSetting.Version, version); + + config.BindWith(OsuSetting.ReleaseStream, releaseStream); + releaseStream.BindValueChanged(_ => scheduleUpdateCheck()); + + scheduleUpdateCheck(); } - private readonly object updateTaskLock = new object(); - - private Task? updateCheckTask; + private void scheduleUpdateCheck() => Schedule(() => Task.Run(CheckForUpdateAsync)); public async Task CheckForUpdateAsync() { From 35ff330e85ed0815a0eedc2f9530bbd5f82bd901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 9 Jun 2025 09:16:19 +0200 Subject: [PATCH 117/370] Fix up test - Use constraints for better assert messages - Use `Editor.Undo()` rather than manual input manager synthesizing ctrl-z (ctrl-z is not a platform agnostic binding, see macOS) --- .../Visual/Editing/TestSceneComposerSelection.cs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs index c2c2872825..6a9ca1292c 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs @@ -625,21 +625,13 @@ namespace osu.Game.Tests.Visual.Editing moveMouseToObject(() => EditorBeatmap.HitObjects[0]); AddStep("hold left click", () => InputManager.PressButton(MouseButton.Left)); - AddStep("drag hitobject to different position", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.BottomRight)); - AddStep("click middle mouse button", () => InputManager.Click(MouseButton.Middle)); - AddStep("release left click", () => InputManager.ReleaseButton(MouseButton.Left)); + AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count, () => Is.Zero); - AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0); - - AddStep("hold ctrl", () => InputManager.PressKey(Key.ControlLeft)); - AddStep("press z", () => InputManager.PressKey(Key.Z)); - AddStep("release ctrl", () => InputManager.ReleaseKey(Key.ControlLeft)); - AddStep("release z", () => InputManager.ReleaseKey(Key.Z)); - - AddAssert("one hitobject in beatmap", () => EditorBeatmap.HitObjects.Count == 1); + AddStep("undo", () => Editor.Undo()); + AddAssert("one hitobject in beatmap", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1)); } [Test] From d7fd7a3f81de2da633e2631b6048a014814f2580 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 9 Jun 2025 16:21:12 +0900 Subject: [PATCH 118/370] Adjust packaged update manager to check release stream --- osu.Game/Updater/GitHubRelease.cs | 8 ++++++++ osu.Game/Updater/NoActionUpdateManager.cs | 15 +++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/osu.Game/Updater/GitHubRelease.cs b/osu.Game/Updater/GitHubRelease.cs index effabdbc04..9b97dc0994 100644 --- a/osu.Game/Updater/GitHubRelease.cs +++ b/osu.Game/Updater/GitHubRelease.cs @@ -3,7 +3,9 @@ #nullable disable +using System; using System.Collections.Generic; +using System.Text.Json.Serialization; using Newtonsoft.Json; namespace osu.Game.Updater @@ -18,5 +20,11 @@ namespace osu.Game.Updater [JsonProperty("assets")] public List Assets { get; set; } + + [JsonProperty("prerelease")] + public bool Prerelease { get; set; } + + [JsonPropertyName("published_at")] + public DateTime? PublishedAt { get; set; } } } diff --git a/osu.Game/Updater/NoActionUpdateManager.cs b/osu.Game/Updater/NoActionUpdateManager.cs index f776cd67be..2318396d08 100644 --- a/osu.Game/Updater/NoActionUpdateManager.cs +++ b/osu.Game/Updater/NoActionUpdateManager.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 System.Threading.Tasks; using osu.Framework.Allocation; @@ -18,7 +16,7 @@ namespace osu.Game.Updater /// public partial class NoActionUpdateManager : UpdateManager { - private string version; + private string version = string.Empty; [BackgroundDependencyLoader] private void load(OsuGameBase game) @@ -30,11 +28,16 @@ namespace osu.Game.Updater { try { - var releases = new OsuJsonWebRequest("https://api.github.com/repos/ppy/osu/releases/latest"); + bool includePrerelease = ReleaseStream.Value == Configuration.ReleaseStream.Tachyon; - await releases.PerformAsync().ConfigureAwait(false); + OsuJsonWebRequest releasesRequest = new OsuJsonWebRequest("https://api.github.com/repos/ppy/osu/releases?per_page=10&page=1"); + await releasesRequest.PerformAsync().ConfigureAwait(false); - var latest = releases.ResponseObject; + GitHubRelease[] releases = releasesRequest.ResponseObject; + GitHubRelease? latest = releases.OrderByDescending(r => r.PublishedAt).FirstOrDefault(r => includePrerelease || !r.Prerelease); + + if (latest == null) + return false; // avoid any discrepancies due to build suffixes for now. // eventually we will want to support release streams and consider these. From fb0800131726ba1eb3dadf9a188fc5b8d24cd7b5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 9 Jun 2025 16:26:39 +0900 Subject: [PATCH 119/370] Add `OSU_EXTERNAL_UPDATE_STREAM` Valid values are `Tachyon`/`Lazer`. --- osu.Game/Updater/NoActionUpdateManager.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Updater/NoActionUpdateManager.cs b/osu.Game/Updater/NoActionUpdateManager.cs index 2318396d08..4b8a3f000f 100644 --- a/osu.Game/Updater/NoActionUpdateManager.cs +++ b/osu.Game/Updater/NoActionUpdateManager.cs @@ -1,10 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; +using osu.Game.Configuration; using osu.Game.Online.API; using osu.Game.Overlays.Notifications; @@ -16,6 +18,8 @@ namespace osu.Game.Updater /// public partial class NoActionUpdateManager : UpdateManager { + private static ReleaseStream? externalReleaseStream => Enum.TryParse(Environment.GetEnvironmentVariable("OSU_EXTERNAL_UPDATE_STREAM"), out ReleaseStream stream) ? stream : null; + private string version = string.Empty; [BackgroundDependencyLoader] @@ -28,7 +32,8 @@ namespace osu.Game.Updater { try { - bool includePrerelease = ReleaseStream.Value == Configuration.ReleaseStream.Tachyon; + ReleaseStream stream = externalReleaseStream ?? ReleaseStream.Value; + bool includePrerelease = stream == Configuration.ReleaseStream.Tachyon; OsuJsonWebRequest releasesRequest = new OsuJsonWebRequest("https://api.github.com/repos/ppy/osu/releases?per_page=10&page=1"); await releasesRequest.PerformAsync().ConfigureAwait(false); From 4895f678a9bfa5a773691b4764d0c54bcf958ee0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Jun 2025 17:31:58 +0900 Subject: [PATCH 120/370] Adjust max sizing at song select slightly --- osu.Game/Screens/SelectV2/SongSelect.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 033f9e9c78..a5e9ca1f89 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -153,9 +153,9 @@ namespace osu.Game.Screens.SelectV2 RelativeSizeAxes = Axes.Both, ColumnDimensions = new[] { - new Dimension(GridSizeMode.Relative, 0.5f, maxSize: 660), + new Dimension(GridSizeMode.Relative, 0.5f, maxSize: 700), new Dimension(), - new Dimension(GridSizeMode.Relative, 0.5f, maxSize: 620), + new Dimension(GridSizeMode.Relative, 0.5f, maxSize: 660), }, Content = new[] { From 9707fd43f43ba37228308709329ef63137054eb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 9 Jun 2025 10:52:10 +0200 Subject: [PATCH 121/370] Add failing test coverage --- .../Ranking/TestSceneSoloResultsScreen.cs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneSoloResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneSoloResultsScreen.cs index 1ea5e13c49..9f77956f4d 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneSoloResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneSoloResultsScreen.cs @@ -218,6 +218,63 @@ namespace osu.Game.Tests.Visual.Ranking AddAssert("previous user best not shown", () => this.ChildrenOfType().All(p => p.Score.OnlineID != 123456)); } + [Test] + public void TestOnlineLeaderboardWithLessThan50Scores_ShowingAnotherUserScore() + { + var scores = new List(); + var soloScores = new List(); + + AddStep("set leaderboard to global", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(importedBeatmap, importedBeatmap.Ruleset, BeatmapLeaderboardScope.Global, null))); + AddStep("set up request handling", () => + { + for (int i = 0; i < 30; ++i) + { + var score = TestResources.CreateTestScoreInfo(importedBeatmap); + score.TotalScore = 10_000 * (30 - i); + score.Position = i + 1; + score.User = new APIUser { Id = i }; + score.BeatmapInfo = new BeatmapInfo + { + OnlineID = 123123, + Status = BeatmapOnlineStatus.Ranked, + }; + score.OnlineID = i; + scores.Add(score); + + var soloScore = SoloScoreInfo.ForSubmission(score); + soloScore.ID = (ulong)i; + soloScores.Add(soloScore); + } + + scores[^1].User = API.LocalUser.Value; + soloScores[^1].UserID = API.LocalUser.Value.OnlineID; + + dummyAPI.HandleRequest = req => + { + switch (req) + { + case GetScoresRequest getScoresRequest: + getScoresRequest.TriggerSuccess(new APIScoresCollection + { + Scores = soloScores, + UserScore = new APIScoreWithPosition + { + Score = soloScores[^1], + Position = 30 + } + }); + return true; + } + + return false; + }; + }); + + AddStep("show results", () => LoadScreen(new SoloResultsScreen(scores[0]))); + AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded); + AddAssert("local user best shown", () => this.ChildrenOfType().Any(p => p.Score.UserID == API.LocalUser.Value.Id)); + } + [Test] public void TestOnlineLeaderboardWithLessThan50Scores_UserIsLast() { From 348c727264cf7bc5e3083ed6f37773ca5ea031f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 9 Jun 2025 11:01:45 +0200 Subject: [PATCH 122/370] Fix oversight in other test --- .../Visual/Ranking/TestSceneSoloResultsScreen.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneSoloResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneSoloResultsScreen.cs index 9f77956f4d..cd8f234f04 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneSoloResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneSoloResultsScreen.cs @@ -90,6 +90,7 @@ namespace osu.Game.Tests.Visual.Ranking localScore = TestResources.CreateTestScoreInfo(importedBeatmap); localScore.TotalScore = 151_000; localScore.Position = null; + localScore.User = API.LocalUser.Value; scoreManager.Import(localScore); localScore = localScore.Detach(); }); @@ -119,6 +120,7 @@ namespace osu.Game.Tests.Visual.Ranking localScore.TotalScore = 151_000; localScore.OnlineID = 30; localScore.Position = null; + localScore.User = API.LocalUser.Value; scoreManager.Import(localScore); localScore = localScore.Detach(); }); @@ -161,6 +163,7 @@ namespace osu.Game.Tests.Visual.Ranking localScore = TestResources.CreateTestScoreInfo(importedBeatmap); localScore.TotalScore = 151_000; localScore.Position = null; + localScore.User = API.LocalUser.Value; LoadScreen(new SoloResultsScreen(localScore)); }); AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded); @@ -211,6 +214,7 @@ namespace osu.Game.Tests.Visual.Ranking localScore = TestResources.CreateTestScoreInfo(importedBeatmap); localScore.TotalScore = 151_000; localScore.Position = null; + localScore.User = API.LocalUser.Value; LoadScreen(new SoloResultsScreen(localScore)); }); AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded); @@ -308,6 +312,7 @@ namespace osu.Game.Tests.Visual.Ranking localScore = TestResources.CreateTestScoreInfo(importedBeatmap); localScore.TotalScore = 151_000; localScore.Position = null; + localScore.User = API.LocalUser.Value; LoadScreen(new SoloResultsScreen(localScore)); }); AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded); @@ -359,6 +364,7 @@ namespace osu.Game.Tests.Visual.Ranking localScore = TestResources.CreateTestScoreInfo(importedBeatmap); localScore.TotalScore = 31_000; localScore.Position = null; + localScore.User = API.LocalUser.Value; LoadScreen(new SoloResultsScreen(localScore)); }); AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded); @@ -412,6 +418,7 @@ namespace osu.Game.Tests.Visual.Ranking localScore = TestResources.CreateTestScoreInfo(importedBeatmap); localScore.TotalScore = 151_000; localScore.Position = null; + localScore.User = API.LocalUser.Value; LoadScreen(new SoloResultsScreen(localScore)); }); AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded); @@ -465,6 +472,7 @@ namespace osu.Game.Tests.Visual.Ranking localScore = TestResources.CreateTestScoreInfo(importedBeatmap); localScore.TotalScore = 651_000; localScore.Position = null; + localScore.User = API.LocalUser.Value; LoadScreen(new SoloResultsScreen(localScore)); }); AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded); @@ -516,6 +524,7 @@ namespace osu.Game.Tests.Visual.Ranking localScore.TotalScore = 151_000; localScore.OnlineID = 12345; localScore.Position = null; + localScore.User = API.LocalUser.Value; LoadScreen(new SoloResultsScreen(localScore)); }); AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded); From 19114c74159a05e09fec6b7851bca8cb54a0a546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 9 Jun 2025 11:11:16 +0200 Subject: [PATCH 123/370] Fix presenting another user's score hiding local user's score on results screen Closes https://github.com/ppy/osu/issues/33567. --- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index d11e7db178..b967c9de93 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -81,8 +81,17 @@ namespace osu.Game.Screens.Ranking Score.Position = clonedScore.Position; sortedScores.Add(Score); } - else if (criteria.Scope == BeatmapLeaderboardScope.Local || clonedScore.UserID != api.LocalUser.Value.OnlineID || clonedScore.TotalScore > Score.TotalScore) + else + { + bool isOnlineLeaderboard = criteria.Scope != BeatmapLeaderboardScope.Local; + bool presentingLocalUserScore = Score.UserID == api.LocalUser.Value.OnlineID; + bool presentedLocalUserScoreIsBetter = presentingLocalUserScore && clonedScore.UserID == api.LocalUser.Value.OnlineID && clonedScore.TotalScore < Score.TotalScore; + + if (isOnlineLeaderboard && presentedLocalUserScoreIsBetter) + continue; + sortedScores.Add(clonedScore); + } } // if we haven't encountered a match for the presented score, we still need to attach it. From 81aaddca349417057fabc34a827df8a657260d51 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Jun 2025 19:25:07 +0900 Subject: [PATCH 124/370] Change lazer's valid filename method to match stable On revisiting the issue at hand, this honestly seems like the best way forward. It also addresses my concerns that with the method we were using, filenames could end up being half underscores. The main reason for choosing to change the lazer end is that stable's difficulty update process is based on sending the beatmap's filename to the server. This means that if we use a proposed fix of checking online ID, it will still mean beatmaps cannot be updated on stable (for all users which have downloaded the beatmap) if a mapper updates once on lazer. Implementation inspired by: - https://referencesource.microsoft.com/#mscorlib/system/io/path.cs,1144ad3c4eff3f24 - https://github.com/peppy/osu-stable-reference/blob/996648fba06baf4e7d2e0b248959399444017895/osu!/GameplayElements/Beatmaps/Beatmap.cs#L1575-L1590 Closes #33060. --- osu.Game/Extensions/ModelExtensions.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game/Extensions/ModelExtensions.cs b/osu.Game/Extensions/ModelExtensions.cs index ec6b5ac6de..2514c6029a 100644 --- a/osu.Game/Extensions/ModelExtensions.cs +++ b/osu.Game/Extensions/ModelExtensions.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.IO; -using System.Text.RegularExpressions; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.IO; @@ -16,8 +15,6 @@ namespace osu.Game.Extensions { public static class ModelExtensions { - private static readonly Regex invalid_filename_chars = new Regex(@"(?!$)[^A-Za-z0-9_()[\]. \-]", RegexOptions.Compiled); - /// /// Get the relative path in osu! storage for this file. /// @@ -156,6 +153,14 @@ namespace osu.Game.Extensions return instance.OnlineID.Equals(other.OnlineID); } + // intentionally chosen to match stable. + // see https://referencesource.microsoft.com/#mscorlib/system/io/path.cs,88 + private static readonly char[] invalid_filename_chars = + { + '\"', '<', '>', '|', '\0', (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10, (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, + (char)18, (char)19, (char)20, (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30, (char)31, ':', '*', '?', '\\', '/' + }; + /// /// Create a valid filename which should work across all platforms. /// @@ -164,7 +169,12 @@ namespace osu.Game.Extensions /// across all operating systems. We are using this in place of as /// that function does not have per-platform considerations (and is only made to work on windows). /// - public static string GetValidFilename(this string filename) => invalid_filename_chars.Replace(filename, "_"); + public static string GetValidFilename(this string filename) + { + foreach (char c in invalid_filename_chars) + filename = filename.Replace(c.ToString(), string.Empty); + return filename; + } public static bool RequiresSupporter(this BeatmapLeaderboardScope scope, bool filterMods) { From 6416f59c7bdab2fabfb6e89edb6723e1df818885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 9 Jun 2025 12:51:58 +0200 Subject: [PATCH 125/370] Disallow placing gameplay leaderboard in skins outside player Closes https://github.com/ppy/osu/issues/33542. For a diff this simple this took much more hemming and hawing because things are a bit annoying here from a few angles: - The only way that is considered idiomatic right now for a skin component to not be applicable to a screen is to require a dependency from DI that is only provided by applicable screens. `DrawableGameplayLeaderboard` has a few of those dependencies, but the scope of all the usages makes it so that the only really viable one to use here is `IGameplayLeaderboardProvider` itself (see: visual tests, and also the usage of multiplayer spectator, where the leaderboard is *not* under a player instance). - The smelly part of this is that the `Player` inheritance hierarchy must ensure that *every* non-abstract class has an `IGameplayLeaderboardProvider` cached. It is not trivial - if not straight up impossible - to force this via some `Player` level abstract method, because such a method would need to somehow accommodate all possible leaderboard providers. That however also means that every possible future `Player` implementor *must inherently know* to also cache a leaderboard provider lest it die at runtime. I don't love that, but I also don't see better alternatives. - Speaking of which, I also noticed that solo spectator and playlists don't have gameplay leaderboards. At all. Which I don't believe to be something that I broke with the leaderboard work - I'm pretty sure that was the pre-existing state - however I don't see any reason why they *couldn't* receive gameplay leaderboards. I'm not doing that here, though, just leaving TODOs for later. --- .../TestSceneSkinEditorMultipleSkins.cs | 4 ++++ .../Gameplay/TestSceneSkinnableHUDOverlay.cs | 4 ++++ .../Screens/Edit/GameplayTest/EditorPlayer.cs | 4 ++++ .../OnlinePlay/Playlists/PlaylistsPlayer.cs | 5 +++++ .../Play/HUD/DrawableGameplayLeaderboard.cs | 17 +++++++---------- osu.Game/Screens/Play/SpectatorPlayer.cs | 6 ++++++ .../IGameplayLeaderboardProvider.cs | 5 +++++ 7 files changed, 35 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs index 656873e9ed..00369ade18 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs @@ -15,6 +15,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Edit; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Select.Leaderboards; using osu.Game.Tests.Gameplay; using osuTK.Input; @@ -37,6 +38,9 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached] public readonly EditorClipboard Clipboard = new EditorClipboard(); + [Cached(typeof(IGameplayLeaderboardProvider))] + private EmptyGameplayLeaderboardProvider leaderboardProvider = new EmptyGameplayLeaderboardProvider(); + public TestSceneSkinEditorMultipleSkins() { scoreProcessor = gameplayState.ScoreProcessor; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs index fcaa2996e1..754ec841d8 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs @@ -21,6 +21,7 @@ using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Select.Leaderboards; using osu.Game.Skinning; using osu.Game.Tests.Gameplay; @@ -42,6 +43,9 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached(typeof(IGameplayClock))] private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new TrackVirtual(60000), false, false); + [Cached(typeof(IGameplayLeaderboardProvider))] + private EmptyGameplayLeaderboardProvider leaderboardProvider = new EmptyGameplayLeaderboardProvider(); + private IEnumerable hudOverlays => CreatedDrawables.OfType(); // best way to check without exposing. diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 820b31c032..02eb38ffa6 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -18,6 +18,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; +using osu.Game.Screens.Select.Leaderboards; using osu.Game.Users; namespace osu.Game.Screens.Edit.GameplayTest @@ -32,6 +33,9 @@ namespace osu.Game.Screens.Edit.GameplayTest [Resolved] private MusicController musicController { get; set; } = null!; + [Cached(typeof(IGameplayLeaderboardProvider))] + private EmptyGameplayLeaderboardProvider leaderboardProvider = new EmptyGameplayLeaderboardProvider(); + public EditorPlayer(Editor editor) : base(new PlayerConfiguration { ShowResults = false }) { diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index dc4078cb1f..9dc51f9cd3 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; +using osu.Game.Screens.Select.Leaderboards; using osu.Game.Users; namespace osu.Game.Screens.OnlinePlay.Playlists @@ -23,6 +24,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override UserActivity InitialActivity => new UserActivity.InPlaylistGame(Beatmap.Value.BeatmapInfo, Ruleset.Value); + // TODO: should be replaced with a provider providing scores from the `PlaylistItem` + [Cached(typeof(IGameplayLeaderboardProvider))] + private EmptyGameplayLeaderboardProvider leaderboardProvider = new EmptyGameplayLeaderboardProvider(); + public PlaylistsPlayer(Room room, PlaylistItem playlistItem, PlayerConfiguration? configuration = null) : base(room, playlistItem, configuration) { diff --git a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs index e04d91b5b7..a7c4bc99b2 100644 --- a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Play.HUD private Player? player { get; set; } [Resolved] - private IGameplayLeaderboardProvider? leaderboardProvider { get; set; } + private IGameplayLeaderboardProvider leaderboardProvider { get; set; } = null!; private readonly IBindableList scores = new BindableList(); private readonly Bindable configVisibility = new Bindable(); @@ -86,16 +86,13 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); - if (leaderboardProvider != null) + scores.BindTo(leaderboardProvider.Scores); + scores.BindCollectionChanged((_, _) => { - scores.BindTo(leaderboardProvider.Scores); - scores.BindCollectionChanged((_, _) => - { - Clear(); - foreach (var score in scores) - Add(score); - }, true); - } + Clear(); + foreach (var score in scores) + Add(score); + }, true); configVisibility.BindValueChanged(_ => Scheduler.AddOnce(updateState)); userPlayingState.BindValueChanged(_ => Scheduler.AddOnce(updateState)); diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index b2ac946642..6bfb6e033a 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -13,11 +13,17 @@ using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; using osu.Game.Scoring; using osu.Game.Screens.Ranking; +using osu.Game.Screens.Select.Leaderboards; namespace osu.Game.Screens.Play { public abstract partial class SpectatorPlayer : Player { + // TODO: maybe consider giving this proper scores. + // `SoloGameplayLeaderboardProvider` doesn't immediately work because there's no guarantee that `LeaderboardManager` global state matches the currently spectated beatmap. + [Cached(typeof(IGameplayLeaderboardProvider))] + private readonly EmptyGameplayLeaderboardProvider leaderboardProvider = new EmptyGameplayLeaderboardProvider(); + [Resolved] protected SpectatorClient SpectatorClient { get; private set; } = null!; diff --git a/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs index b41329a489..6118529780 100644 --- a/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs @@ -15,4 +15,9 @@ namespace osu.Game.Screens.Select.Leaderboards /// IBindableList Scores { get; } } + + public class EmptyGameplayLeaderboardProvider : IGameplayLeaderboardProvider + { + public IBindableList Scores { get; } = new BindableList(); + } } From 257fec87958635f36f0370bd99e2498ab0d2736e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 9 Jun 2025 14:34:41 +0200 Subject: [PATCH 126/370] Correct xmldoc of `GetValidFilename()` and make it intentionally scary --- osu.Game/Extensions/ModelExtensions.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Extensions/ModelExtensions.cs b/osu.Game/Extensions/ModelExtensions.cs index 2514c6029a..18c991297a 100644 --- a/osu.Game/Extensions/ModelExtensions.cs +++ b/osu.Game/Extensions/ModelExtensions.cs @@ -165,9 +165,15 @@ namespace osu.Game.Extensions /// Create a valid filename which should work across all platforms. /// /// - /// This function replaces all characters not included in a very pessimistic list which should be compatible - /// across all operating systems. We are using this in place of as - /// that function does not have per-platform considerations (and is only made to work on windows). + /// + /// We are using this in place of + /// as that function works per-platform, and therefore returns a different set of characters on different OSes. + /// + /// + /// Note that the behaviour of this method is LOAD-BEARING for things such as interoperability of beatmap exports with stable, + /// especially with respect to beatmap submission. + /// DO NOT CHANGE THE SEMANTICS OF THIS METHOD unless you know well what you are doing. + /// /// public static string GetValidFilename(this string filename) { From 83ce468c57cb4f9b19e4ccf245c3da5f29c08f71 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 9 Jun 2025 21:53:19 +0900 Subject: [PATCH 127/370] Fix flaky collections test --- .../Visual/SongSelect/TestSceneManageCollectionsDialog.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneManageCollectionsDialog.cs index 4c895faf27..475d8ec461 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneManageCollectionsDialog.cs @@ -139,6 +139,8 @@ namespace osu.Game.Tests.Visual.SongSelect }); }); + assertCollectionCount(2); + AddStep("remove first collection", () => Realm.Write(r => r.Remove(first))); assertCollectionCount(1); assertCollectionName(0, "2"); From 55a3ec502600e40fef814a1a678587bd0aa5bac6 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 9 Jun 2025 23:15:48 +0900 Subject: [PATCH 128/370] Fix intermittent online play mod select tests --- .../Multiplayer/TestSceneMultiplayerMatchSubScreen.cs | 7 +++++-- .../Visual/Multiplayer/TestScenePlaylistsSongSelect.cs | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index a94f440a01..920a920b9b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -59,9 +59,12 @@ namespace osu.Game.Tests.Visual.Multiplayer importedSet = beatmaps.GetAllUsableBeatmapSets().First(); } - [SetUpSteps] - public void SetupSteps() + public override void SetUpSteps() { + base.SetUpSteps(); + + AddUntilStep("wait for mod select removed", () => this.ChildrenOfType().Count(), () => Is.Zero); + AddStep("load match", () => { room = new Room { Name = "Test Room" }; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs index 77fe96310f..066c981cd2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs @@ -56,6 +56,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); + AddUntilStep("wait for mod select removed", () => this.ChildrenOfType().Count(), () => Is.Zero); + AddStep("reset", () => { room = new Room(); From 3fbfa4b3f4e8f2cf13f974212d9126aae2351e5a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Jun 2025 12:38:24 +0900 Subject: [PATCH 129/370] Remove logo scale when mod select appears This was causing the logo to not be clickable immediately after closing the overlay, which was reported as frustrating by some user. I hoped to fix this by unfuckulating the logo logic but it's a multi-day excursion that I'd rather avoid for now. --- osu.Game/Screens/SelectV2/SongSelect.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index a5e9ca1f89..59b196f700 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -299,8 +299,7 @@ namespace osu.Game.Screens.SelectV2 if (!this.IsCurrentScreen()) return; - logo?.ScaleTo(v.NewValue == Visibility.Visible ? 0f : logo_scale, 400, Easing.OutQuint) - .FadeTo(v.NewValue == Visibility.Visible ? 0f : 1f, 200, Easing.OutQuint); + logo?.FadeTo(v.NewValue == Visibility.Visible ? 0f : 1f, 200, Easing.OutQuint); }); Beatmap.BindValueChanged(_ => From cc150faf81a71d48a1277dc432b3541af208c320 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Jun 2025 12:55:22 +0900 Subject: [PATCH 130/370] Allow changing difficulties using up and down arrows when sets are grouped --- .../TestSceneBeatmapCarouselArtistGrouping.cs | 8 ++++++-- .../TestSceneBeatmapCarouselNoGrouping.cs | 6 ------ osu.Game/Graphics/Carousel/Carousel.cs | 16 ++++++++++++++-- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 3 +++ 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs index 15ae35ad28..e230dee918 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs @@ -163,13 +163,17 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextGroup(); WaitForGroupSelection(0, 1); + // Difficulties should get immediate selection even when using up and down traversal. SelectNextPanel(); + WaitForGroupSelection(0, 2); SelectNextPanel(); + WaitForGroupSelection(0, 3); + SelectNextPanel(); - SelectNextPanel(); + WaitForGroupSelection(0, 3); SelectNextGroup(); - WaitForGroupSelection(0, 1); + WaitForGroupSelection(0, 5); SelectNextPanel(); SelectNextGroup(); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs index 8a173d3e71..3c839f46d1 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs @@ -171,15 +171,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 WaitForSelection(3, 0); SelectNextPanel(); - WaitForSelection(3, 0); - - Select(); WaitForSelection(3, 1); SelectNextPanel(); - WaitForSelection(3, 1); - - Select(); WaitForSelection(3, 2); SelectNextPanel(); diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index 552b7652f6..37806b733f 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -240,6 +240,12 @@ namespace osu.Game.Graphics.Carousel /// Whether the provided item is a valid group target. If false, more panels will be checked in the user's requested direction until a valid target is found. protected virtual bool CheckValidForGroupSelection(CarouselItem item) => true; + /// + /// Keyboard selection usually does not automatically activate an item. There may be exceptions to this rule. + /// Returning true here will make keyboard traversal act like group traversal for the target item. + /// + protected virtual bool ShouldActivateOnKeyboardSelection(CarouselItem item) => false; + /// /// Called after an item becomes the . /// Should be used to handle any group expansion, item visibility changes, etc. @@ -500,8 +506,14 @@ namespace osu.Game.Graphics.Carousel if (newItem.IsVisible) { - playTraversalSound(); - setKeyboardSelection(newItem.Model); + if (currentSelection.Model != newItem.Model && ShouldActivateOnKeyboardSelection(newItem)) + Activate(newItem); + else + { + playTraversalSound(); + setKeyboardSelection(newItem.Model); + } + return; } } while (newIndex != originalIndex); diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index c8f5796d76..d11184f138 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -208,6 +208,9 @@ namespace osu.Game.Screens.SelectV2 protected BeatmapSetInfo? ExpandedBeatmapSet { get; private set; } + protected override bool ShouldActivateOnKeyboardSelection(CarouselItem item) => + grouping.BeatmapSetsGroupedTogether && item.Model is BeatmapInfo; + protected override void HandleItemActivated(CarouselItem item) { try From 0a694862e7e02fb401604b509ced73e6c9b5231f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Jun 2025 12:55:41 +0900 Subject: [PATCH 131/370] Slightly increase delay before leaderboard scores are loaded --- .../SelectV2/BeatmapLeaderboardWedge.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs index bbcf793a33..10917f08ac 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs @@ -201,19 +201,19 @@ namespace osu.Game.Screens.SelectV2 private void refetchScores() { + SetScores(Array.Empty()); + + if (beatmap.IsDefault) + { + SetState(LeaderboardState.NoneSelected); + return; + } + + SetState(LeaderboardState.Retrieving); + refetchOperation?.Cancel(); refetchOperation = Scheduler.AddDelayed(() => { - SetScores(Array.Empty()); - - if (beatmap.IsDefault) - { - SetState(LeaderboardState.NoneSelected); - return; - } - - SetState(LeaderboardState.Retrieving); - var fetchBeatmapInfo = beatmap.Value.BeatmapInfo; var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset; @@ -230,7 +230,7 @@ namespace osu.Game.Screens.SelectV2 fetchedScores.BindValueChanged(_ => updateScores(), true); initialFetchComplete = true; } - }, initialFetchComplete ? 200 : 0); + }, initialFetchComplete ? 300 : 0); } private void updateScores() From 8afac6a00d2ee9b6845eb032001355f381317594 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Jun 2025 14:01:49 +0900 Subject: [PATCH 132/370] Remove shear on update button to match non-sheared panel design --- .../TestScenePanelUpdateBeatmapButton.cs | 34 +++++++++---------- .../SelectV2/PanelUpdateBeatmapButton.cs | 2 -- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelUpdateBeatmapButton.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelUpdateBeatmapButton.cs index 781691d3db..8156842eb9 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelUpdateBeatmapButton.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelUpdateBeatmapButton.cs @@ -23,23 +23,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }; }); - [Test] - public void TestNullBeatmap() - { - AddStep("null beatmap", () => button.BeatmapSet = null); - AddAssert("button invisible", () => button.Alpha == 0f); - } - - [Test] - public void TestUpdatedBeatmap() - { - AddStep("updated beatmap", () => button.BeatmapSet = new BeatmapSetInfo - { - Beatmaps = { new BeatmapInfo() } - }); - AddAssert("button invisible", () => button.Alpha == 0f); - } - [Test] public void TestNonUpdatedBeatmap() { @@ -58,5 +41,22 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("button visible", () => button.Alpha == 1f); } + + [Test] + public void TestNullBeatmap() + { + AddStep("null beatmap", () => button.BeatmapSet = null); + AddAssert("button invisible", () => button.Alpha == 0f); + } + + [Test] + public void TestUpdatedBeatmap() + { + AddStep("updated beatmap", () => button.BeatmapSet = new BeatmapSetInfo + { + Beatmaps = { new BeatmapInfo() } + }); + AddAssert("button invisible", () => button.Alpha == 0f); + } } } diff --git a/osu.Game/Screens/SelectV2/PanelUpdateBeatmapButton.cs b/osu.Game/Screens/SelectV2/PanelUpdateBeatmapButton.cs index 4c767df9d8..b133da71f7 100644 --- a/osu.Game/Screens/SelectV2/PanelUpdateBeatmapButton.cs +++ b/osu.Game/Screens/SelectV2/PanelUpdateBeatmapButton.cs @@ -69,7 +69,6 @@ namespace osu.Game.Screens.SelectV2 Content.Anchor = Anchor.Centre; Content.Origin = Anchor.Centre; - Content.Shear = OsuGame.SHEAR; Content.AddRange(new Drawable[] { @@ -87,7 +86,6 @@ namespace osu.Game.Screens.SelectV2 AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(4), - Shear = -OsuGame.SHEAR, Children = new Drawable[] { new Container From 6383d8cc23fd78c3604e43a8e7d8b5531fd0cbb7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Jun 2025 15:06:29 +0900 Subject: [PATCH 133/370] Add confirmation step before blocking a user Closes https://github.com/ppy/osu/issues/33585. --- osu.Game/Localisation/ContextMenuStrings.cs | 10 +++++++++ osu.Game/Users/UserPanel.cs | 25 ++++++++++++++++++--- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/osu.Game/Localisation/ContextMenuStrings.cs b/osu.Game/Localisation/ContextMenuStrings.cs index cb18a2159c..b2ca941287 100644 --- a/osu.Game/Localisation/ContextMenuStrings.cs +++ b/osu.Game/Localisation/ContextMenuStrings.cs @@ -29,6 +29,16 @@ namespace osu.Game.Localisation /// public static LocalisableString SpectatePlayer => new TranslatableString(getKey(@"spectate_player"), @"Spectate"); + /// + /// "Are you sure you want to block {0}?" + /// + public static LocalisableString ConfirmBlockUser(string username) => new TranslatableString(getKey(@"confirm_block_user"), @"Are you sure you want to block {0}?", username); + + /// + /// "Are you sure you want to unblock {0}?" + /// + public static LocalisableString ConfirmUnblockUser(string username) => new TranslatableString(getKey(@"confirm_unblock_user"), @"Are you sure you want to unblock {0}?", username); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index fc261163da..51550e9f64 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -26,6 +26,7 @@ using osu.Game.Localisation; using osu.Game.Online.API.Requests; using osu.Game.Online.Metadata; using osu.Game.Online.Multiplayer; +using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Notifications; using osu.Game.Screens; using osu.Game.Screens.Play; @@ -68,6 +69,9 @@ namespace osu.Game.Users [Resolved] private ChatOverlay? chatOverlay { get; set; } + [Resolved] + private IDialogOverlay? dialogOverlay { get; set; } + [Resolved] protected OverlayColourProvider? ColourProvider { get; private set; } @@ -163,9 +167,15 @@ namespace osu.Game.Users chatOverlay?.Show(); })); - items.Add(isUserBlocked() - ? new OsuMenuItem(UsersStrings.BlocksButtonUnblock, MenuItemType.Standard, () => toggleBlock(false)) - : new OsuMenuItem(UsersStrings.BlocksButtonBlock, MenuItemType.Destructive, () => toggleBlock(true))); + items.Add(!isUserBlocked() + ? new OsuMenuItem(UsersStrings.BlocksButtonBlock, MenuItemType.Destructive, () => + { + dialogOverlay?.Push(new ConfirmBlockActionDialog(ContextMenuStrings.ConfirmBlockUser(User.Username), () => toggleBlock(true))); + }) + : new OsuMenuItem(UsersStrings.BlocksButtonUnblock, MenuItemType.Standard, () => + { + dialogOverlay?.Push(new ConfirmBlockActionDialog(ContextMenuStrings.ConfirmUnblockUser(User.Username), () => toggleBlock(false))); + })); if (isUserOnline()) { @@ -228,5 +238,14 @@ namespace osu.Game.Users } public bool FilteringActive { get; set; } + + private partial class ConfirmBlockActionDialog : DangerousActionDialog + { + public ConfirmBlockActionDialog(LocalisableString text, Action? action = null) + { + BodyText = text; + DangerousAction = action; + } + } } } From f16a7309f8975df2a98b81be4927dbca74d0f824 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Jun 2025 15:33:52 +0900 Subject: [PATCH 134/370] SongSelectV2: Show full mod details in footer User feedback is that it's no longer possible to see the applied rate adjust change when it's non-default without hovering. This fixes that issue. I've adjusted the visuals a bit so you can still get a hint at which mods are displayed, even when they are overflowing. --- osu.Game/Screens/SelectV2/FooterButtonMods.cs | 60 +++++++++++++------ 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/SelectV2/FooterButtonMods.cs b/osu.Game/Screens/SelectV2/FooterButtonMods.cs index 9e2b53012a..f38d6d9376 100644 --- a/osu.Game/Screens/SelectV2/FooterButtonMods.cs +++ b/osu.Game/Screens/SelectV2/FooterButtonMods.cs @@ -52,10 +52,13 @@ namespace osu.Game.Screens.SelectV2 private Drawable unrankedBadge = null!; private ModDisplay modDisplay = null!; - private OsuSpriteText modCountText = null!; private OsuSpriteText multiplierText { get; set; } = null!; + private Container modContainer = null!; + + private Container overflowModCountDisplay = null!; + [Resolved] private OsuColour colours { get; set; } = null!; @@ -117,7 +120,7 @@ namespace osu.Game.Screens.SelectV2 Font = OsuFont.Torus.With(size: 14f, weight: FontWeight.Bold) } }, - new Container + modContainer = new Container { CornerRadius = Y_OFFSET, RelativeSizeAxes = Axes.Both, @@ -130,7 +133,7 @@ namespace osu.Game.Screens.SelectV2 Colour = colourProvider.Background3, RelativeSizeAxes = Axes.Both, }, - modDisplay = new ModDisplay(showExtendedInformation: false) + modDisplay = new ModDisplay(showExtendedInformation: true) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -139,14 +142,27 @@ namespace osu.Game.Screens.SelectV2 Current = { BindTarget = Current }, ExpansionMode = ExpansionMode.AlwaysContracted, }, - modCountText = new ModCountText + overflowModCountDisplay = new Container { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Shear = -OsuGame.SHEAR, - Font = OsuFont.Torus.With(size: 14f, weight: FontWeight.Bold), - Mods = { BindTarget = Current }, - } + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = colourProvider.Background3, + Alpha = 0.8f, + RelativeSizeAxes = Axes.Both, + }, + new ModCountText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Shear = -OsuGame.SHEAR, + Font = OsuFont.Torus.With(size: 14f, weight: FontWeight.Bold), + Mods = { BindTarget = Current }, + } + } + }, } }, } @@ -198,7 +214,7 @@ namespace osu.Game.Screens.SelectV2 modDisplayBar.MoveToY(20, duration, easing); modDisplayBar.FadeOut(duration, easing); modDisplay.FadeOut(duration, easing); - modCountText.FadeOut(duration, easing); + overflowModCountDisplay.FadeOut(duration, easing); unrankedBadge.MoveToY(20, duration, easing); unrankedBadge.FadeOut(duration, easing); @@ -208,14 +224,6 @@ namespace osu.Game.Screens.SelectV2 } else { - modDisplay.Hide(); - modCountText.Hide(); - - if (Current.Value.Count >= 5) - modCountText.Show(); - else - modDisplay.Show(); - if (Current.Value.Any(m => !m.Ranked)) { unrankedBadge.MoveToX(0, duration, easing); @@ -234,6 +242,7 @@ namespace osu.Game.Screens.SelectV2 modDisplayBar.MoveToY(-5, duration, Easing.OutQuint); unrankedBadge.MoveToY(-5, duration, easing); modDisplayBar.FadeIn(duration, easing); + modDisplay.FadeIn(duration, easing); } double multiplier = Current.Value?.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier) ?? 1; @@ -247,6 +256,19 @@ namespace osu.Game.Screens.SelectV2 multiplierText.FadeColour(Color4.White, duration, easing); } + protected override void Update() + { + base.Update(); + + if (Current.Value.Count == 0) + return; + + if (modDisplay.DrawWidth * modDisplay.Scale.X > modContainer.DrawWidth) + overflowModCountDisplay.Show(); + else + overflowModCountDisplay.Hide(); + } + private partial class ModCountText : OsuSpriteText, IHasCustomTooltip> { public readonly Bindable> Mods = new Bindable>(); From bf2a77ac226dba923933bbdb4f481b7b68287d43 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Jun 2025 15:38:53 +0900 Subject: [PATCH 135/370] Update velopack to fix macOS update overheads Closes https://github.com/ppy/osu/issues/33091. I figure we can push this out on tachyon for testing. --- osu.Desktop/Program.cs | 8 ++++---- osu.Desktop/osu.Desktop.csproj | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index a311e42d6d..50d0f06150 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -184,7 +184,7 @@ namespace osu.Desktop var app = VelopackApp.Build(); - app.WithFirstRun(_ => isFirstRun = true); + app.OnFirstRun(_ => isFirstRun = true); if (OperatingSystem.IsWindows()) configureWindows(app); @@ -195,9 +195,9 @@ namespace osu.Desktop [SupportedOSPlatform("windows")] private static void configureWindows(VelopackApp app) { - app.WithFirstRun(_ => WindowsAssociationManager.InstallAssociations()); - app.WithAfterUpdateFastCallback(_ => WindowsAssociationManager.UpdateAssociations()); - app.WithBeforeUninstallFastCallback(_ => WindowsAssociationManager.UninstallAssociations()); + app.OnFirstRun(_ => WindowsAssociationManager.InstallAssociations()); + app.OnAfterUpdateFastCallback(_ => WindowsAssociationManager.UpdateAssociations()); + app.OnBeforeUninstallFastCallback(_ => WindowsAssociationManager.UninstallAssociations()); } } } diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 05d5bb19fb..b0c5c953d4 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -26,7 +26,7 @@ - + From 599b4c82365452ff397591c63e6a4f8d24f95da5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Jun 2025 17:17:47 +0900 Subject: [PATCH 136/370] Fix tooltip not being displayed around edges of text content --- osu.Game/Screens/SelectV2/FooterButtonMods.cs | 50 ++++++++++--------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/SelectV2/FooterButtonMods.cs b/osu.Game/Screens/SelectV2/FooterButtonMods.cs index f38d6d9376..8ea08a0085 100644 --- a/osu.Game/Screens/SelectV2/FooterButtonMods.cs +++ b/osu.Game/Screens/SelectV2/FooterButtonMods.cs @@ -57,7 +57,7 @@ namespace osu.Game.Screens.SelectV2 private Container modContainer = null!; - private Container overflowModCountDisplay = null!; + private ModCountText overflowModCountDisplay = null!; [Resolved] private OsuColour colours { get; set; } = null!; @@ -142,27 +142,7 @@ namespace osu.Game.Screens.SelectV2 Current = { BindTarget = Current }, ExpansionMode = ExpansionMode.AlwaysContracted, }, - overflowModCountDisplay = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - Colour = colourProvider.Background3, - Alpha = 0.8f, - RelativeSizeAxes = Axes.Both, - }, - new ModCountText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Shear = -OsuGame.SHEAR, - Font = OsuFont.Torus.With(size: 14f, weight: FontWeight.Bold), - Mods = { BindTarget = Current }, - } - } - }, + overflowModCountDisplay = new ModCountText { Mods = { BindTarget = Current }, }, } }, } @@ -269,17 +249,39 @@ namespace osu.Game.Screens.SelectV2 overflowModCountDisplay.Hide(); } - private partial class ModCountText : OsuSpriteText, IHasCustomTooltip> + private partial class ModCountText : CompositeDrawable, IHasCustomTooltip> { public readonly Bindable> Mods = new Bindable>(); + private OsuSpriteText text = null!; + [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; protected override void LoadComplete() { base.LoadComplete(); - Mods.BindValueChanged(v => Text = ModSelectOverlayStrings.Mods(v.NewValue.Count).ToUpper(), true); + + RelativeSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = colourProvider.Background3, + Alpha = 0.8f, + RelativeSizeAxes = Axes.Both, + }, + text = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Torus.With(size: 14f, weight: FontWeight.Bold), + Shear = -OsuGame.SHEAR, + } + }; + + Mods.BindValueChanged(v => text.Text = ModSelectOverlayStrings.Mods(v.NewValue.Count).ToUpper(), true); } public ITooltip> GetCustomTooltip() => new ModOverflowTooltip(colourProvider); From de57daeb3c65bd0c8be1de69e7a4cd4b3f78594b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 10 Jun 2025 10:48:26 +0200 Subject: [PATCH 137/370] Fix results screen not showing local scores on results screen for some beatmap statuses Closes https://github.com/ppy/osu/issues/33609. Case of leftover code that should have been removed. This condition is still active in the online leaderboards path via https://github.com/ppy/osu/blob/4dcc928c7e46e020c62f4d60e7b859ba18c9142f/osu.Game/Online/Leaderboards/LeaderboardManager.cs#L91-L95 Not sure trying to add test coverage is a productive use of time? Will do on request though. --- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index d11e7db178..7d57bc80aa 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -8,7 +8,6 @@ using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Logging; -using osu.Game.Beatmaps; using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.Leaderboards; @@ -42,9 +41,6 @@ namespace osu.Game.Screens.Ranking { Debug.Assert(Score != null); - if (Score.BeatmapInfo!.OnlineID <= 0 || Score.BeatmapInfo.Status <= BeatmapOnlineStatus.Pending) - return []; - var criteria = new LeaderboardCriteria( Score.BeatmapInfo!, Score.Ruleset, From d4fc6693b4db3b11ffb5ba0ab5ee4a1fc250bc88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 10 Jun 2025 11:46:34 +0200 Subject: [PATCH 138/370] Fix stable scores importing with a `LegacyOnlineID` of 0 Closes https://github.com/ppy/osu/issues/33435. The root cause of the issue is that the user's database contained a whole lot of scores with `LegacyOnlineID` of 0, which would trip up https://github.com/ppy/osu/blob/97e6187f0d7c3dbee896596a623e34627135bf0e/osu.Game/Extensions/ModelExtensions.cs#L128-L129 as that method would thus consider scores that are not the same as the same because of the zero, which later trips up https://github.com/ppy/osu/blob/97e6187f0d7c3dbee896596a623e34627135bf0e/osu.Game/Screens/Ranking/SoloResultsScreen.cs#L79 which ends up inserting `Score` into the list several times, which causes the crash. You might remember that I tried to fix this once before in https://github.com/ppy/osu/pull/24794. What I did not realise, however, is that stable *can still produce replays* that have an online ID of zero in them, because zero *is just `default(long)`*: https://github.com/peppy/osu-stable-reference/blob/7205341bb70000a87fa1bd54e7642772e2af85d7/osu!/GameplayElements/Scoring/Score.cs#L123 https://github.com/peppy/osu-stable-reference/blob/7205341bb70000a87fa1bd54e7642772e2af85d7/osu!/GameplayElements/Scoring/Score.cs#L350 The alternative way of fixing this would be just to change `MatchesOnlineID` to reject zeroes, but I think this is a saner overall direction. --- osu.Game/Database/RealmAccess.cs | 9 ++++++++- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 3 +++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 7142f2b300..49bde7c505 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -98,8 +98,9 @@ namespace osu.Game.Database /// 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. /// 48 2025-03-19 Clear online status for all qualified beatmaps (some were stuck in a qualified state due to local caching issues). + /// 49 2025-06-10 Reset the LegacyOnlineID to -1 for all scores that have it set to 0 (which is semantically the same) for consistency of handling with OnlineID. /// - private const int schema_version = 48; + private const int schema_version = 49; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. @@ -1255,6 +1256,12 @@ namespace osu.Game.Database foreach (var beatmap in beatmaps) beatmap.ResetOnlineInfo(resetOnlineId: false); break; + + case 49: + foreach (var score in migration.NewRealm.All().Where(s => s.LegacyOnlineID == 0)) + score.LegacyOnlineID = -1; + + break; } Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms"); diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index a32c05c4eb..cf6819b086 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -110,6 +110,9 @@ namespace osu.Game.Scoring.Legacy else if (version >= 20121008) scoreInfo.LegacyOnlineID = sr.ReadInt32(); + if (scoreInfo.LegacyOnlineID == 0) + scoreInfo.LegacyOnlineID = -1; + byte[] compressedScoreInfo = null; if (version >= 30000001) From c84988a2ecef08d1c5aa309a28d7f2f2757372f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 10 Jun 2025 12:32:03 +0200 Subject: [PATCH 139/370] Do not attempt to add displayed score to list of sorted scores more than once This is a very dodgy fix, but it fixes an edge case that has so far - to my knowledge - not been reported by users in the wild, only by me trying to break things, so my hope is that we can do this and move on for now. --- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 7d57bc80aa..1d09654063 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -74,6 +74,12 @@ namespace osu.Game.Screens.Ranking // this simplifies handling later. if (clonedScore.Equals(Score) || clonedScore.MatchesOnlineID(Score)) { + // this is a precautionary guard that prevents `Score` from appearing multiple times in the list. + // that can occur in rare cases wherein two local scores have the same online ID but different replay contents + // (this is possible e.g. in cases of client-side vs server-side recorded replays, see https://github.com/ppy/osu-server-spectator/issues/193) + if (sortedScores.Contains(Score)) + continue; + Score.Position = clonedScore.Position; sortedScores.Add(Score); } From 26bbc3202aa18580b2324b3e7455578c8f75b620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 10 Jun 2025 13:36:16 +0200 Subject: [PATCH 140/370] Add failing test case --- .../Formats/LegacyBeatmapEncoderTest.cs | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index caebf52026..e27146a86f 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -211,6 +211,31 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(decodedAfterEncode.skin.Configuration.CustomComboColours, Has.Count.EqualTo(8)); } + [Test] + public void TestEncodeStabilityOfSliderWithFractionalCoordinates() + { + Slider originalSlider = new Slider + { + Position = new Vector2(0.6f), + Path = new SliderPath(new[] + { + new PathControlPoint(Vector2.Zero, PathType.PERFECT_CURVE), + new PathControlPoint(new Vector2(25.6f, 78.4f)), + new PathControlPoint(new Vector2(55.8f, 34.2f)), + }) + }; + var beatmap = new Beatmap + { + HitObjects = { originalSlider } + }; + + var encoded = encodeToLegacy((beatmap, new TestLegacySkin(beatmaps_resource_store, string.Empty))); + var decodedAfterEncode = decodeFromLegacy(encoded, string.Empty, version: LegacyBeatmapEncoder.FIRST_LAZER_VERSION); + var decodedSlider = (Slider)decodedAfterEncode.beatmap.HitObjects[0]; + Assert.That(decodedSlider.Path.ControlPoints.Select(p => p.Position), + Is.EquivalentTo(originalSlider.Path.ControlPoints.Select(p => p.Position))); + } + private bool areComboColoursEqual(IHasComboColours a, IHasComboColours b) { // equal to null, no need to SequenceEqual @@ -233,11 +258,11 @@ namespace osu.Game.Tests.Beatmaps.Formats } } - private (IBeatmap beatmap, TestLegacySkin skin) decodeFromLegacy(Stream stream, string name) + private (IBeatmap beatmap, TestLegacySkin skin) decodeFromLegacy(Stream stream, string name, int version = LegacyDecoder.LATEST_VERSION) { using (var reader = new LineBufferedReader(stream)) { - var beatmap = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(reader); + var beatmap = new LegacyBeatmapDecoder(version) { ApplyOffsets = false }.Decode(reader); var beatmapSkin = new TestLegacySkin(beatmaps_resource_store, name); stream.Seek(0, SeekOrigin.Begin); beatmapSkin.Configuration = new LegacySkinDecoder().Decode(reader); From eab02a2aa54bedb6305d0c4b4d90ded243fe29e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 10 Jun 2025 14:04:56 +0200 Subject: [PATCH 141/370] Fix lack of slider encode-decode stability due to truncating control point coordinates Mostly closes https://github.com/ppy/osu/issues/33505. Compare https://github.com/ppy/osu/blob/97e6187f0d7c3dbee896596a623e34627135bf0e/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs#L56-L59 I say "mostly" here because I'm rather skeptical that this is 100% rock solid still, for one reason - namely that the game stores path control point coordinates relative to the head, then turns them into absolute coordinates when encoding, and then on decoding turns them back into coordinates relative to the head, which in floating-point world is a Bad Idea because of round-off error. But I'm not fixing that without introducing a completely new beatmap format or rewriting half the editor to address that, so I'll just pretend that I don't know any of this until someone notices. --- osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 0162c8017b..c5a6c9e83d 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -335,11 +335,14 @@ namespace osu.Game.Rulesets.Objects.Legacy ArrayPool<(PathType, int)>.Shared.Return(segmentsBuffer); } - static Vector2 readPoint(string value, Vector2 startPos) + Vector2 readPoint(string value, Vector2 startPos) { string[] vertexSplit = value.Split(':'); - Vector2 pos = new Vector2((int)Parsing.ParseDouble(vertexSplit[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseDouble(vertexSplit[1], Parsing.MAX_COORDINATE_VALUE)) - startPos; + Vector2 pos = formatVersion >= LegacyBeatmapEncoder.FIRST_LAZER_VERSION + ? new Vector2(Parsing.ParseFloat(vertexSplit[0], Parsing.MAX_COORDINATE_VALUE), Parsing.ParseFloat(vertexSplit[1], Parsing.MAX_COORDINATE_VALUE)) + : new Vector2((int)Parsing.ParseFloat(vertexSplit[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseFloat(vertexSplit[1], Parsing.MAX_COORDINATE_VALUE)); + pos -= startPos; return pos; } } From f5aeedc7e193ef872a99433676bb44373965f899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 10 Jun 2025 14:38:25 +0200 Subject: [PATCH 142/370] Fix timeline not updating ticks correctly after arbitrary timing control point changes (again) Closes https://github.com/ppy/osu/issues/33393. This is admittedly a half-assed diff. This was apparently "fixed" once before, eons ago, in https://github.com/ppy/osu/pull/11032, but I'm not sure whether it regressed, or where, because I don't want to bisect four years back. (At that time `ControlPointInfo.ControlPointsChanged` did not exist yet.) Also there's the part where changes to control points do not undo or redo (see https://github.com/ppy/osu/issues/31942), but I'm not touching that *either*, because if I start touching that, then I will get yelled at for not reviewing the 2.5k line PR that rewrites the entirety of change handling in editor instead (https://github.com/ppy/osu/pull/30314). I will attempt to get through that mental block sometime within the year. Please do not rush me. The cheap cop-out argument is that hooking this up to `ControlPointInfo` specifically is probably "more efficient" anyway. --- .../Components/Timeline/TimelineTickDisplay.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index 66d0df9e18..faefdee096 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -31,9 +32,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private BindableBeatDivisor beatDivisor { get; set; } = null!; - [Resolved] - private IEditorChangeHandler? changeHandler { get; set; } - [Resolved] private OsuColour colours { get; set; } = null!; @@ -51,9 +49,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { beatDivisor.BindValueChanged(_ => invalidateTicks()); - if (changeHandler != null) - // currently this is the best way to handle any kind of timing changes. - changeHandler.OnStateChange += invalidateTicks; + beatmap.ControlPointInfo.ControlPointsChanged += invalidateTicks; configManager.BindWith(OsuSetting.EditorTimelineShowTimingChanges, showTimingChanges); showTimingChanges.BindValueChanged(_ => invalidateTicks()); @@ -194,8 +190,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { base.Dispose(isDisposing); - if (changeHandler != null) - changeHandler.OnStateChange -= invalidateTicks; + if (beatmap.IsNotNull()) + beatmap.ControlPointInfo.ControlPointsChanged -= invalidateTicks; } } } From b16cb1ae587d75b881c5e99f3116dc0e31633e9e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Jun 2025 21:58:43 +0900 Subject: [PATCH 143/370] Fix incorrect equality check --- osu.Game/Graphics/Carousel/Carousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index 37806b733f..0eac894dd4 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -506,7 +506,7 @@ namespace osu.Game.Graphics.Carousel if (newItem.IsVisible) { - if (currentSelection.Model != newItem.Model && ShouldActivateOnKeyboardSelection(newItem)) + if (!CheckModelEquality(currentSelection.Model, newItem.Model) && ShouldActivateOnKeyboardSelection(newItem)) Activate(newItem); else { From cad389722ebef46ce10ad288ca7327ece4b894c2 Mon Sep 17 00:00:00 2001 From: marvin Date: Tue, 10 Jun 2025 21:22:57 +0200 Subject: [PATCH 144/370] Maintain scroll position relative to hovered drawable in ExpandingToolboxContainer --- .../Graphics/Containers/ExpandingContainer.cs | 25 +++---- .../Edit/ExpandingToolboxContainer.cs | 70 +++++++++++++++++++ 2 files changed, 83 insertions(+), 12 deletions(-) diff --git a/osu.Game/Graphics/Containers/ExpandingContainer.cs b/osu.Game/Graphics/Containers/ExpandingContainer.cs index 477de616ac..93595f186f 100644 --- a/osu.Game/Graphics/Containers/ExpandingContainer.cs +++ b/osu.Game/Graphics/Containers/ExpandingContainer.cs @@ -40,21 +40,22 @@ namespace osu.Game.Graphics.Containers RelativeSizeAxes = Axes.Y; Width = contractedWidth; - InternalChild = new OsuScrollContainer + InternalChild = CreateScrollContainer().With(s => { - RelativeSizeAxes = Axes.Both, - ScrollbarVisible = false, - Child = FillFlow = new FillFlowContainer - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - }, - }; + s.RelativeSizeAxes = Axes.Both; + s.ScrollbarVisible = false; + }).WithChild(FillFlow = new FillFlowContainer + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + }); } + protected virtual OsuScrollContainer CreateScrollContainer() => new OsuScrollContainer(); + private ScheduledDelegate? hoverExpandEvent; protected override void LoadComplete() diff --git a/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs b/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs index 2a94ae6017..4119943f76 100644 --- a/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs +++ b/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs @@ -1,10 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Events; +using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Screens.Edit; @@ -21,6 +24,7 @@ namespace osu.Game.Rulesets.Edit private readonly Bindable contractSidebars = new Bindable(); private bool expandOnHover; + private OffsetMaintainingScrollContainer scrollContainer = null!; [Resolved] private Editor? editor { get; set; } @@ -42,6 +46,25 @@ namespace osu.Game.Rulesets.Edit config.BindWith(OsuSetting.EditorContractSidebars, contractSidebars); } + protected override OsuScrollContainer CreateScrollContainer() => scrollContainer = new OffsetMaintainingScrollContainer(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + var inputManager = GetContainingInputManager(); + + if (inputManager != null) + { + Expanded.BindValueChanged(_ => + { + var position = new Vector2(ScreenSpaceDrawQuad.Centre.X, inputManager.CurrentState.Mouse.Position.Y); + + scrollContainer.TargetDrawable = Children.FirstOrDefault(it => it.Contains(position)); + }); + } + } + protected override void Update() { base.Update(); @@ -53,10 +76,57 @@ namespace osu.Game.Rulesets.Edit expandOnHover = requireContracting; Expanded.Value = !expandOnHover; } + + if (scrollContainer.TargetDrawable != null && !TransformsForTargetMember(nameof(Width)).Any()) + scrollContainer.TargetDrawable = null; } protected override bool OnMouseDown(MouseDownEvent e) => true; protected override bool OnClick(ClickEvent e) => true; + + private partial class OffsetMaintainingScrollContainer : OsuScrollContainer + { + private Drawable? targetDrawable; + private float targetPosition; + + public Drawable? TargetDrawable + { + get => targetDrawable; + set + { + targetDrawable = value; + + if (value != null) + targetPosition = ToLocalSpace(value.ScreenSpaceDrawQuad.TopLeft).Y; + } + } + + protected override void UpdateAfterChildren() + { + if (targetDrawable != null) + { + float currentPosition = ToLocalSpace(targetDrawable.ScreenSpaceDrawQuad.TopLeft).Y; + + if (!Precision.AlmostEquals(targetPosition, currentPosition)) + { + double offset = currentPosition - targetPosition; + + double scrollTarget = Math.Clamp(Current + offset, 0, ScrollableExtent); + + ScrollTo(scrollTarget, false, double.PositiveInfinity); + } + } + + base.UpdateAfterChildren(); + } + + protected override void OnUserScroll(double value, bool animated = true, double? distanceDecay = null) + { + targetDrawable = null; + + base.OnUserScroll(value, animated, distanceDecay); + } + } } } From 16f7140254c545810b092f33001abed8040a769d Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 10 Jun 2025 20:35:43 -0700 Subject: [PATCH 145/370] Add "version" keyword to release stream setting --- osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index ac6215f3ad..18d6a9cb18 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -55,6 +55,7 @@ namespace osu.Game.Overlays.Settings.Sections.General { LabelText = GeneralSettingsStrings.ReleaseStream, Current = { Value = configReleaseStream.Value }, + Keywords = new[] { @"version" }, }); Add(checkForUpdatesButton = new SettingsButton From cdb2f216f27d97141675e66e9966bbdd677663eb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Jun 2025 15:26:02 +0900 Subject: [PATCH 146/370] Allow using previous valid score for offset calibration when subsequent retries are too short As proposed in https://github.com/ppy/osu/discussions/33572. Note that this won't work if you leave to song select. We could make that work but it would require further global faffing and I don't think it's worth the effort. --- .../Gameplay/TestSceneBeatmapOffsetControl.cs | 38 +++++++++++++++++++ .../PlayerSettings/BeatmapOffsetControl.cs | 20 ++++++---- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs index 92a10628ff..68fd824d7c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs @@ -45,6 +45,44 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); } + /// + /// When a beatmap offset was already set, the calibration should take it into account. + /// + [Test] + public void TestTooShortToDisplay_HasPreviousValidScore() + { + const double average_error = -4.5; + const double initial_offset = -2; + + AddStep("Set offset non-neutral", () => offsetControl.Current.Value = initial_offset); + AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); + + AddStep("Set reference score", () => + { + offsetControl.ReferenceScore.Value = new ScoreInfo + { + HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error), + BeatmapInfo = Beatmap.Value.BeatmapInfo, + }; + }); + + AddUntilStep("Has calibration button", () => offsetControl.ChildrenOfType().Any()); + + AddStep("Set short reference score", () => + { + offsetControl.ReferenceScore.Value = new ScoreInfo + { + HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(0, 2), + BeatmapInfo = Beatmap.Value.BeatmapInfo, + }; + }); + + AddUntilStep("Still calibration button", () => offsetControl.ChildrenOfType().Any()); + + AddStep("Press button", () => offsetControl.ChildrenOfType().Single().TriggerClick()); + AddAssert("Offset is adjusted", () => offsetControl.Current.Value == initial_offset - average_error); + } + [Test] public void TestNotEnoughTimedHitEvents() { diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index b0b4f6cc5d..64ffe6d191 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -69,6 +69,7 @@ namespace osu.Game.Screens.Play.PlayerSettings private IDisposable? beatmapOffsetSubscription; private Task? realmWriteTask; + private ScoreInfo? lastValidScore; public BeatmapOffsetControl() { @@ -177,8 +178,6 @@ namespace osu.Game.Screens.Play.PlayerSettings private void scoreChanged(ValueChangedEvent score) { - referenceScoreContainer.Clear(); - if (score.NewValue == null) return; @@ -196,6 +195,15 @@ namespace osu.Game.Screens.Play.PlayerSettings if (!(hitEvents.CalculateMedianHitError() is double median)) return; + // affecting unstable rate here is used as a substitute of determining if a hit event represents a *timed* hit event, + // i.e. an user input that the user had to *time to the track*, + // i.e. one that it *makes sense to use* when doing anything with timing and offsets. + bool hasEnoughUsableEvents = hitEvents.Count(HitEventExtensions.AffectsUnstableRate) >= 50; + + // If we are already displaying a score, continue displaying it rather than showing the user "play too short" message. + if (lastValidScore != null && !hasEnoughUsableEvents) + return; + referenceScoreContainer.Children = new Drawable[] { new OsuSpriteText @@ -204,10 +212,7 @@ namespace osu.Game.Screens.Play.PlayerSettings }, }; - // affecting unstable rate here is used as a substitute of determining if a hit event represents a *timed* hit event, - // i.e. an user input that the user had to *time to the track*, - // i.e. one that it *makes sense to use* when doing anything with timing and offsets. - if (hitEvents.Count(HitEventExtensions.AffectsUnstableRate) < 50) + if (!hasEnoughUsableEvents) { referenceScoreContainer.AddRange(new Drawable[] { @@ -223,6 +228,7 @@ namespace osu.Game.Screens.Play.PlayerSettings return; } + lastValidScore = score.NewValue!; lastPlayMedian = median; lastPlayBeatmapOffset = Current.Value; @@ -245,7 +251,7 @@ namespace osu.Game.Screens.Play.PlayerSettings return; Current.Value = lastPlayBeatmapOffset - lastPlayMedian; - lastAppliedScore.Value = ReferenceScore.Value; + lastAppliedScore.Value = lastValidScore; }, }, globalOffsetText = new LinkFlowContainer From 38ad48bb357d2977d1cc8d5a3056422731e85d5f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Jun 2025 15:55:08 +0900 Subject: [PATCH 147/370] Adjust commentary to not suck MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs | 2 +- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs index 68fd824d7c..ba31dc928e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs @@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.Gameplay } /// - /// When a beatmap offset was already set, the calibration should take it into account. + /// If we already have an old score with enough hit events and the new score doesn't have enough, continue displaying the old one rather than showing the user "play too short" message. /// [Test] public void TestTooShortToDisplay_HasPreviousValidScore() diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 64ffe6d191..5efef16d08 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -200,7 +200,7 @@ namespace osu.Game.Screens.Play.PlayerSettings // i.e. one that it *makes sense to use* when doing anything with timing and offsets. bool hasEnoughUsableEvents = hitEvents.Count(HitEventExtensions.AffectsUnstableRate) >= 50; - // If we are already displaying a score, continue displaying it rather than showing the user "play too short" message. + // If we already have an old score with enough hit events and the new score doesn't have enough, continue displaying the old one rather than showing the user "play too short" message. if (lastValidScore != null && !hasEnoughUsableEvents) return; From cfd73cc900d5190ea2a9db291eb1ac22072f9cb3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Jun 2025 16:33:59 +0900 Subject: [PATCH 148/370] Add back scrollbar padding in new beatmap carousel Closes https://github.com/ppy/osu/issues/33447. Implementation copied from previous carousel. --- osu.Game/Graphics/Carousel/Carousel.cs | 23 ++++++++++++++++++++ osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 3 +++ 2 files changed, 26 insertions(+) diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index 0eac894dd4..94e864d71d 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -1009,6 +1009,29 @@ namespace osu.Game.Graphics.Carousel d.Y = (float)(((ICarouselPanel)d).DrawYPosition + scrollableExtent); } + #region Scrollbar padding + + public float ScrollbarPaddingTop { get; set; } = 5; + public float ScrollbarPaddingBottom { get; set; } = 5; + + protected override float ToScrollbarPosition(double scrollPosition) + { + if (Precision.AlmostEquals(0, ScrollableExtent)) + return 0; + + return (float)(ScrollbarPaddingTop + (ScrollbarMovementExtent - (ScrollbarPaddingTop + ScrollbarPaddingBottom)) * (scrollPosition / ScrollableExtent)); + } + + protected override float FromScrollbarPosition(float scrollbarPosition) + { + if (Precision.AlmostEquals(0, ScrollbarMovementExtent)) + return 0; + + return (float)(ScrollableExtent * ((scrollbarPosition - ScrollbarPaddingTop) / (ScrollbarMovementExtent - (ScrollbarPaddingTop + ScrollbarPaddingBottom)))); + } + + #endregion + #region Absolute scrolling private bool absoluteScrolling; diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index d11184f138..f580a3bc88 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -91,6 +91,9 @@ namespace osu.Game.Screens.SelectV2 DebounceDelay = 100; DistanceOffscreenToPreload = 100; + // Account for the osu! logo being in the way. + Scroll.ScrollbarPaddingBottom = 70; + Filters = new ICarouselFilter[] { matching = new BeatmapCarouselFilterMatching(() => Criteria!), From 41d4d55f22c1fca34596dce0ec9089b34b71eb78 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 11 Jun 2025 16:37:28 +0900 Subject: [PATCH 149/370] Add tests --- .../NonVisual/TestSceneUpdateManager.cs | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 osu.Game.Tests/NonVisual/TestSceneUpdateManager.cs diff --git a/osu.Game.Tests/NonVisual/TestSceneUpdateManager.cs b/osu.Game.Tests/NonVisual/TestSceneUpdateManager.cs new file mode 100644 index 0000000000..d118678fde --- /dev/null +++ b/osu.Game.Tests/NonVisual/TestSceneUpdateManager.cs @@ -0,0 +1,179 @@ +// 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.Testing; +using osu.Game.Configuration; +using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; +using osu.Game.Tests.Visual; +using osu.Game.Updater; + +namespace osu.Game.Tests.NonVisual +{ + [HeadlessTest] + public class TestSceneUpdateManager : OsuTestScene + { + [Cached(typeof(INotificationOverlay))] + private readonly INotificationOverlay notifications = new TestNotificationOverlay(); + + private TestUpdateManager manager = null!; + private OsuConfigManager config = null!; + + [SetUpSteps] + public void SetupSteps() + { + AddStep("add manager", () => + { + config = new OsuConfigManager(LocalStorage); + config.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Lazer); + + Child = new DependencyProvidingContainer + { + CachedDependencies = [(typeof(OsuConfigManager), config)], + Child = manager = new TestUpdateManager() + }; + }); + + // Updates should be checked when the object is loaded for the first time. + AddUntilStep("check pending", () => manager.IsPending); + AddStep("complete check", () => manager.Complete()); + AddUntilStep("1 check completed", () => manager.Completions, () => Is.EqualTo(1)); + AddUntilStep("no check pending", () => !manager.IsPending); + } + + /// + /// Updates should be checked when the release stream is changed. + /// + [Test] + public void TestReleaseStreamChanged() + { + AddStep("change release stream", () => config.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Tachyon)); + + AddUntilStep("check pending", () => manager.IsPending); + AddStep("complete check", () => manager.Complete()); + AddUntilStep("2 checks completed", () => manager.Completions, () => Is.EqualTo(2)); + AddUntilStep("no check pending", () => !manager.IsPending); + } + + /// + /// Updates should be checked once more if the release stream is changed during an going check + /// + [Test] + public void TestReleaseStreamChangedDuringCheck() + { + AddStep("change release stream", () => config.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Tachyon)); + AddUntilStep("check pending", () => manager.IsPending); + AddStep("change release stream", () => config.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Lazer)); + + AddStep("complete check", () => manager.Complete()); + AddUntilStep("2 checks completed", () => manager.Completions, () => Is.EqualTo(2)); + + AddUntilStep("check pending", () => manager.IsPending); + AddStep("complete one check", () => manager.Complete()); + AddUntilStep("3 checks completed", () => manager.Completions, () => Is.EqualTo(3)); + AddUntilStep("no check pending", () => !manager.IsPending); + } + + /// + /// Updates should be checked when the user requests them to. + /// + [Test] + public void TestUserRequest() + { + AddStep("request check", () => manager.CheckForUpdateAsync()); + + AddUntilStep("check pending", () => manager.IsPending); + AddStep("complete check", () => manager.Complete()); + AddUntilStep("2 checks completed", () => manager.Completions, () => Is.EqualTo(2)); + AddUntilStep("no check pending", () => !manager.IsPending); + } + + [Test] + public void TestUserRequestReturnsExistingCheck() + { + Task task1 = null!; + Task task2 = null!; + + // This part covering double user input is not really possible because the settings button is disabled during the check, + // but it's kept here for sanity in-case the update manager is used as a standalone object elsewhere. + + AddStep("request check", () => task1 = manager.CheckForUpdateAsync()); + AddUntilStep("check pending", () => manager.IsPending); + AddStep("request check", () => task2 = manager.CheckForUpdateAsync()); + AddAssert("second request returned original task", () => task2 == task1); + + AddStep("complete check", () => manager.Complete()); + AddUntilStep("2 checks completed", () => manager.Completions, () => Is.EqualTo(2)); + AddUntilStep("no check pending", () => !manager.IsPending); + + // This next part tests for the user requesting an update during a background check, and is possible to occur in practice. + + AddStep("change release stream", () => config.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Tachyon)); + AddUntilStep("check pending", () => manager.IsPending); + AddStep("request check", () => + { + task1 = manager.CurrentTask; + task2 = manager.CheckForUpdateAsync(); + }); + AddAssert("second request returned original task", () => task2 == task1); + + AddStep("complete check", () => manager.Complete()); + AddUntilStep("3 checks completed", () => manager.Completions, () => Is.EqualTo(3)); + AddUntilStep("no check pending", () => !manager.IsPending); + } + + private class TestUpdateManager : UpdateManager + { + public bool IsPending { get; private set; } + public int Completions { get; private set; } + + public Task CurrentTask { get; private set; } = Task.CompletedTask; + + private TaskCompletionSource? pendingCheck; + + protected override Task PerformUpdateCheck() + { + var task = Task.Run(async () => + { + var check = pendingCheck = new TaskCompletionSource(); + IsPending = true; + + bool result = await check.Task.ConfigureAwait(false); + IsPending = false; + Completions++; + + return result; + }); + + CurrentTask = task; + return task; + } + + public void Complete() + { + pendingCheck?.SetResult(true); + } + } + + private class TestNotificationOverlay : INotificationOverlay + { + public void Post(Notification notification) + { + } + + public void Hide() + { + } + + public IBindable UnreadCount { get; } = new Bindable(); + + public IEnumerable AllNotifications { get; } = Enumerable.Empty(); + } + } +} From e2d8c393886d00324f99c4c68472589b6ab71b05 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 11 Jun 2025 15:10:50 +0900 Subject: [PATCH 150/370] Adjust `UpdateManager` to keep a check stream There are two primary paths here: 1. A user requests an update. The existing request is used if it's in progress, or a new request is made and processed immediately. 2. Something is changed (e.g. the release stream) that triggers a background request. A new request is made to run after the existing one. --- osu.Game/Updater/UpdateManager.cs | 52 +++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index 354a8da46b..a297313de5 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -43,7 +43,7 @@ namespace osu.Game.Updater protected IBindable ReleaseStream => releaseStream; private readonly Bindable releaseStream = new Bindable(); - private readonly object updateTaskLock = new object(); + private bool updateCheckRequested; private Task? updateCheckTask; protected override void LoadComplete() @@ -73,24 +73,50 @@ namespace osu.Game.Updater scheduleUpdateCheck(); } - private void scheduleUpdateCheck() => Schedule(() => Task.Run(CheckForUpdateAsync)); - - public async Task CheckForUpdateAsync() + protected override void Update() { - if (!CanCheckForUpdate) - return false; + base.Update(); + processScheduledUpdateCheck(); + } - Task waitTask; + /// + /// Schedules a request to check for new updates to begin as soon as any existing check completes. + /// + private void scheduleUpdateCheck() + { + updateCheckRequested = true; + } - lock (updateTaskLock) - waitTask = (updateCheckTask ??= PerformUpdateCheck()); + /// + /// Processes an ongoing request to check for new updates. + /// + private void processScheduledUpdateCheck() + { + if (!updateCheckRequested) + return; - bool hasUpdates = await waitTask.ConfigureAwait(false); + if (updateCheckTask?.IsCompleted == false) + return; - lock (updateTaskLock) - updateCheckTask = null; + if (CanCheckForUpdate) + updateCheckTask = PerformUpdateCheck(); - return hasUpdates; + updateCheckRequested = false; + } + + /// + /// Immediately checks for any available updates, or returns the existing update task. + /// + /// true if any updates are available, false otherwise. + public Task CheckForUpdateAsync() + { + if (updateCheckTask?.IsCompleted == false) + return updateCheckTask; + + scheduleUpdateCheck(); + processScheduledUpdateCheck(); + + return updateCheckTask ?? Task.FromResult(false); } /// From 981cf62c23cb0bf5d20d290cde42444310f52f10 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 11 Jun 2025 16:43:32 +0900 Subject: [PATCH 151/370] Partial classes --- osu.Game.Tests/NonVisual/TestSceneUpdateManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/NonVisual/TestSceneUpdateManager.cs b/osu.Game.Tests/NonVisual/TestSceneUpdateManager.cs index d118678fde..ce5a1fd154 100644 --- a/osu.Game.Tests/NonVisual/TestSceneUpdateManager.cs +++ b/osu.Game.Tests/NonVisual/TestSceneUpdateManager.cs @@ -17,7 +17,7 @@ using osu.Game.Updater; namespace osu.Game.Tests.NonVisual { [HeadlessTest] - public class TestSceneUpdateManager : OsuTestScene + public partial class TestSceneUpdateManager : OsuTestScene { [Cached(typeof(INotificationOverlay))] private readonly INotificationOverlay notifications = new TestNotificationOverlay(); @@ -128,7 +128,7 @@ namespace osu.Game.Tests.NonVisual AddUntilStep("no check pending", () => !manager.IsPending); } - private class TestUpdateManager : UpdateManager + private partial class TestUpdateManager : UpdateManager { public bool IsPending { get; private set; } public int Completions { get; private set; } @@ -161,7 +161,7 @@ namespace osu.Game.Tests.NonVisual } } - private class TestNotificationOverlay : INotificationOverlay + private partial class TestNotificationOverlay : INotificationOverlay { public void Post(Notification notification) { From 359e3ac8a51aa57bf8c88591c885f0408a752336 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 11 Jun 2025 16:46:37 +0900 Subject: [PATCH 152/370] Expand tests to perform a second check To ensure it doesn't get stuck in a state where new checks aren't performed. --- .../NonVisual/TestSceneUpdateManager.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/NonVisual/TestSceneUpdateManager.cs b/osu.Game.Tests/NonVisual/TestSceneUpdateManager.cs index ce5a1fd154..fd2a3acb2f 100644 --- a/osu.Game.Tests/NonVisual/TestSceneUpdateManager.cs +++ b/osu.Game.Tests/NonVisual/TestSceneUpdateManager.cs @@ -59,10 +59,17 @@ namespace osu.Game.Tests.NonVisual AddStep("complete check", () => manager.Complete()); AddUntilStep("2 checks completed", () => manager.Completions, () => Is.EqualTo(2)); AddUntilStep("no check pending", () => !manager.IsPending); + + AddStep("change release stream", () => config.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Lazer)); + + AddUntilStep("check pending", () => manager.IsPending); + AddStep("complete check", () => manager.Complete()); + AddUntilStep("3 checks completed", () => manager.Completions, () => Is.EqualTo(3)); + AddUntilStep("no check pending", () => !manager.IsPending); } /// - /// Updates should be checked once more if the release stream is changed during an going check + /// Updates should be checked once more if the release stream is changed during an going check. /// [Test] public void TestReleaseStreamChangedDuringCheck() @@ -92,8 +99,18 @@ namespace osu.Game.Tests.NonVisual AddStep("complete check", () => manager.Complete()); AddUntilStep("2 checks completed", () => manager.Completions, () => Is.EqualTo(2)); AddUntilStep("no check pending", () => !manager.IsPending); + + AddStep("request check", () => manager.CheckForUpdateAsync()); + + AddUntilStep("check pending", () => manager.IsPending); + AddStep("complete check", () => manager.Complete()); + AddUntilStep("3 checks completed", () => manager.Completions, () => Is.EqualTo(3)); + AddUntilStep("no check pending", () => !manager.IsPending); } + /// + /// Any ongoing request should be returned when the user requests a new one. + /// [Test] public void TestUserRequestReturnsExistingCheck() { From 18979d0ed4bab65d94511d1deb14c2d5ae21f303 Mon Sep 17 00:00:00 2001 From: marvin Date: Wed, 11 Jun 2025 09:47:39 +0200 Subject: [PATCH 153/370] Add comment explaining mouse position handling --- osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs b/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs index 4119943f76..d6f8112514 100644 --- a/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs +++ b/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs @@ -58,6 +58,8 @@ namespace osu.Game.Rulesets.Edit { Expanded.BindValueChanged(_ => { + // When state changes from expanded -> collapsed the mouse is no longer within the toolbox so there would be no + // hovered children if we used the mouse position directly var position = new Vector2(ScreenSpaceDrawQuad.Centre.X, inputManager.CurrentState.Mouse.Position.Y); scrollContainer.TargetDrawable = Children.FirstOrDefault(it => it.Contains(position)); From e495843267c8354ea30b2a1a229ee6a445185dc5 Mon Sep 17 00:00:00 2001 From: marvin Date: Wed, 11 Jun 2025 09:48:51 +0200 Subject: [PATCH 154/370] Remove per-frame check for transform --- osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs b/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs index d6f8112514..55449ff4d9 100644 --- a/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs +++ b/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs @@ -78,9 +78,6 @@ namespace osu.Game.Rulesets.Edit expandOnHover = requireContracting; Expanded.Value = !expandOnHover; } - - if (scrollContainer.TargetDrawable != null && !TransformsForTargetMember(nameof(Width)).Any()) - scrollContainer.TargetDrawable = null; } protected override bool OnMouseDown(MouseDownEvent e) => true; From 9f133ac997422c8a1f11cef05578fcaa59f67483 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 11 Jun 2025 16:51:03 +0900 Subject: [PATCH 155/370] Make check task non-nullable Keeping unnecessary nullables around is a pet-peeve of mine. This simplifies things. --- osu.Game/Updater/UpdateManager.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index a297313de5..279517db6c 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -44,7 +44,7 @@ namespace osu.Game.Updater private readonly Bindable releaseStream = new Bindable(); private bool updateCheckRequested; - private Task? updateCheckTask; + private Task updateCheckTask = Task.FromResult(false); protected override void LoadComplete() { @@ -95,7 +95,7 @@ namespace osu.Game.Updater if (!updateCheckRequested) return; - if (updateCheckTask?.IsCompleted == false) + if (!updateCheckTask.IsCompleted) return; if (CanCheckForUpdate) @@ -110,13 +110,13 @@ namespace osu.Game.Updater /// true if any updates are available, false otherwise. public Task CheckForUpdateAsync() { - if (updateCheckTask?.IsCompleted == false) + if (!updateCheckTask.IsCompleted) return updateCheckTask; scheduleUpdateCheck(); processScheduledUpdateCheck(); - return updateCheckTask ?? Task.FromResult(false); + return updateCheckTask; } /// From 0cbc5e3593b37fa807cbfa24997c647707bc6286 Mon Sep 17 00:00:00 2001 From: marvin Date: Wed, 11 Jun 2025 10:15:54 +0200 Subject: [PATCH 156/370] Move `InternalChild` assignment into BDL --- .../Graphics/Containers/ExpandingContainer.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/osu.Game/Graphics/Containers/ExpandingContainer.cs b/osu.Game/Graphics/Containers/ExpandingContainer.cs index 93595f186f..ad5c65c10e 100644 --- a/osu.Game/Graphics/Containers/ExpandingContainer.cs +++ b/osu.Game/Graphics/Containers/ExpandingContainer.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -40,18 +41,24 @@ namespace osu.Game.Graphics.Containers RelativeSizeAxes = Axes.Y; Width = contractedWidth; - InternalChild = CreateScrollContainer().With(s => - { - s.RelativeSizeAxes = Axes.Both; - s.ScrollbarVisible = false; - }).WithChild(FillFlow = new FillFlowContainer + FillFlow = new FillFlowContainer { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, - }); + }; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = CreateScrollContainer().With(s => + { + s.RelativeSizeAxes = Axes.Both; + s.ScrollbarVisible = false; + }).WithChild(FillFlow); } protected virtual OsuScrollContainer CreateScrollContainer() => new OsuScrollContainer(); From edf62baae85d46cf5fa14fe982274860d774d2d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Jun 2025 17:19:10 +0900 Subject: [PATCH 157/370] Add ability to reveal background when long pressing in empty space As touched on in https://github.com/ppy/osu/discussions/33624. Maybe fine as a bit of an easter egg? --- osu.Game/Screens/SelectV2/SongSelect.cs | 69 ++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 59b196f700..6164f5b088 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -93,6 +93,7 @@ namespace osu.Game.Screens.SelectV2 private BeatmapDetailsArea detailsArea = null!; private FillFlowContainer wedgesContainer = null!; private Box rightGradientBackground = null!; + private Container mainContent = null!; private NoResultsPlaceholder noResultsPlaceholder = null!; @@ -130,14 +131,10 @@ namespace osu.Game.Screens.SelectV2 AddRangeInternal(new Drawable[] { new GlobalScrollAdjustsVolume(), - new Box - { - RelativeSizeAxes = Axes.Both, - Width = 0.6f, - Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.3f), Color4.Black.Opacity(0f)), - }, - new Container + mainContent = new Container { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Bottom = ScreenFooter.HEIGHT }, Child = new OsuContextMenuContainer @@ -148,6 +145,12 @@ namespace osu.Game.Screens.SelectV2 RelativeSizeAxes = Axes.Both, Children = new Drawable[] { + new Box + { + RelativeSizeAxes = Axes.Both, + Width = 0.6f, + Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.3f), Color4.Black.Opacity(0f)), + }, new GridContainer // used for max width implementation { RelativeSizeAxes = Axes.Both, @@ -575,6 +578,8 @@ namespace osu.Game.Screens.SelectV2 private void onLeavingScreen() { + restoreBackground(); + modSelectOverlay.SelectedMods.UnbindFrom(Mods); modSelectOverlay.Beatmap.UnbindFrom(Beatmap); @@ -724,7 +729,55 @@ namespace osu.Game.Screens.SelectV2 #endregion - #region Hotkeys + #region Input + + private ScheduledDelegate? revealingBackground; + + protected override bool OnMouseDown(MouseDownEvent e) + { + // I don't know why this works but it does. + // If the carousel panels are hovered, hovered no longer contains the screen. + // Maybe there's a better way of doing this, but I couldn't immeidately find a good setup. + bool mouseDownPriority = GetContainingInputManager()!.HoveredDrawables.Contains(this); + + if (e.Button == MouseButton.Left && mouseDownPriority) + { + revealingBackground = Scheduler.AddDelayed(() => + { + mainContent.ResizeWidthTo(1.2f, 600, Easing.OutQuint); + mainContent.ScaleTo(1.2f, 600, Easing.OutQuint); + mainContent.FadeOut(200, Easing.OutQuint); + + Footer?.Hide(); + }, 200); + } + + return base.OnMouseDown(e); + } + + protected override void OnMouseUp(MouseUpEvent e) + { + restoreBackground(); + base.OnMouseUp(e); + } + + private void restoreBackground() + { + if (revealingBackground == null) + return; + + if (revealingBackground.State == ScheduledDelegate.RunState.Complete) + { + mainContent.ResizeWidthTo(1f, 500, Easing.OutQuint); + mainContent.ScaleTo(1, 500, Easing.OutQuint); + mainContent.FadeIn(500, Easing.OutQuint); + + Footer?.Show(); + } + + revealingBackground.Cancel(); + revealingBackground = null; + } public virtual bool OnPressed(KeyBindingPressEvent e) { From 04871fe55a3f4855ff71627c8e09236229dd9cff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Jun 2025 17:35:53 +0900 Subject: [PATCH 158/370] Add setting to leaderboard to allow disabling automatic collapsing Was a 2 minute implementation, so why not? Addresses https://github.com/ppy/osu/discussions/33523. --- osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs index a7c4bc99b2..49b46298c9 100644 --- a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs @@ -29,6 +29,9 @@ namespace osu.Game.Screens.Play.HUD public DrawableGameplayLeaderboardScore? TrackedScore { get; private set; } + [SettingSource("Collapse during gameplay", "If enabled, the leaderboard will become more compact during active gameplay.")] + public Bindable CollapseDuringGameplay { get; } = new BindableBool(true); + [Resolved] private Player? player { get; set; } @@ -98,6 +101,7 @@ namespace osu.Game.Screens.Play.HUD userPlayingState.BindValueChanged(_ => Scheduler.AddOnce(updateState)); holdingForHUD.BindValueChanged(_ => Scheduler.AddOnce(updateState)); ForceExpand.BindValueChanged(_ => Scheduler.AddOnce(updateState)); + CollapseDuringGameplay.BindValueChanged(_ => Scheduler.AddOnce(updateState)); updateState(); } @@ -108,7 +112,7 @@ namespace osu.Game.Screens.Play.HUD scroll.ScrollToStart(false); Flow.FadeTo(player?.Configuration.ShowLeaderboard != false && configVisibility.Value ? 1 : 0, 100, Easing.OutQuint); - expanded.Value = ForceExpand.Value || userPlayingState.Value != LocalUserPlayingState.Playing || holdingForHUD.Value; + expanded.Value = !CollapseDuringGameplay.Value || ForceExpand.Value || userPlayingState.Value != LocalUserPlayingState.Playing || holdingForHUD.Value; } /// From 70a5474489d482acd08d572f076ba08e8d104b06 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Jun 2025 17:49:49 +0900 Subject: [PATCH 159/370] Increase hitbox for footer back button Based on countless feedback of users wanting to be able to throw their mouse into the corner of the screen to go back. It makes sense. --- .../Navigation/TestSceneSongSelectNavigation.cs | 17 +++++++++++++++++ osu.Game/Screens/Footer/ScreenBackButton.cs | 13 +++++++++++++ 2 files changed, 30 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs index 506c02dc17..8dc73af108 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs @@ -38,6 +38,23 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != getOriginalPlayer() && Game.ScreenStack.CurrentScreen is Player); } + [Test] + public void TestPushSongSelectAndClickBottomLeftCorner() + { + AddStep("push song select", () => Game.ScreenStack.Push(new SoloSongSelect())); + + // TODO: without this step, a critical bug will be hit, see inline comment in `OsuGame.handleBackButton`. + AddUntilStep("Wait for song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect select && select.IsLoaded); + + AddStep("click in corner", () => + { + InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.BottomLeft); + InputManager.Click(MouseButton.Left); + }); + + ConfirmAtMainMenu(); + } + [Test] public void TestPushSongSelectAndPressBackButtonImmediately() { diff --git a/osu.Game/Screens/Footer/ScreenBackButton.cs b/osu.Game/Screens/Footer/ScreenBackButton.cs index bf29186bb1..481192088c 100644 --- a/osu.Game/Screens/Footer/ScreenBackButton.cs +++ b/osu.Game/Screens/Footer/ScreenBackButton.cs @@ -19,6 +19,19 @@ namespace osu.Game.Screens.Footer { public const float BUTTON_WIDTH = 240; + public sealed override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + // Ensure clicks in the corner of the screen still trigger the back button. + // Need to apply more than 1x inflation due to shear. + var inputRectangle = DrawRectangle.Inflate(new MarginPadding + { + Left = OsuGame.SCREEN_EDGE_MARGIN * 2, + Bottom = OsuGame.SCREEN_EDGE_MARGIN * 2, + }); + + return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); + } + public ScreenBackButton() : base(BUTTON_WIDTH) { From 4810c7c84719e91fb2514128ff22b9304050669f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Jun 2025 11:21:26 +0200 Subject: [PATCH 160/370] Add support for showing leaderboard in playlists and daily challenge As touched on in https://github.com/ppy/osu/pull/33581. --- .../Scoring/Legacy/ScoreInfoExtensions.cs | 4 + .../DailyChallenge/DailyChallengePlayer.cs | 5 +- .../OnlinePlay/Playlists/PlaylistsPlayer.cs | 17 ++- .../Leaderboards/GameplayLeaderboardScore.cs | 13 ++ .../MultiplayerLeaderboardProvider.cs | 2 - .../PlaylistsGameplayLeaderboardProvider.cs | 126 ++++++++++++++++++ .../SoloGameplayLeaderboardProvider.cs | 1 + 7 files changed, 157 insertions(+), 11 deletions(-) create mode 100644 osu.Game/Screens/Select/Leaderboards/PlaylistsGameplayLeaderboardProvider.cs diff --git a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs index 23624401e2..664f1fd4ab 100644 --- a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Rooms; using osu.Game.Rulesets.Scoring; namespace osu.Game.Scoring.Legacy @@ -20,6 +21,9 @@ namespace osu.Game.Scoring.Legacy public static long GetDisplayScore(this SoloScoreInfo soloScoreInfo, ScoringMode mode) => getDisplayScore(soloScoreInfo.RulesetID, soloScoreInfo.TotalScore, mode, soloScoreInfo.MaximumStatistics); + public static long GetDisplayScore(this MultiplayerScore multiplayerScore, ScoringMode mode) + => getDisplayScore(multiplayerScore.RulesetId, multiplayerScore.TotalScore, mode, multiplayerScore.MaximumStatistics); + private static long getDisplayScore(int rulesetId, long score, ScoringMode mode, IReadOnlyDictionary maximumStatistics) { if (mode == ScoringMode.Standardised) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengePlayer.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengePlayer.cs index a5c61b8386..8097ce8b65 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengePlayer.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengePlayer.cs @@ -3,7 +3,6 @@ using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Playlists; -using osu.Game.Screens.Play; using osu.Game.Users; namespace osu.Game.Screens.OnlinePlay.DailyChallenge @@ -12,8 +11,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { protected override UserActivity InitialActivity => new UserActivity.PlayingDailyChallenge(Beatmap.Value.BeatmapInfo, Ruleset.Value); - public DailyChallengePlayer(Room room, PlaylistItem playlistItem, PlayerConfiguration? configuration = null) - : base(room, playlistItem, configuration) + public DailyChallengePlayer(Room room, PlaylistItem playlistItem) + : base(room, playlistItem) { } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index 9dc51f9cd3..69a1e3b763 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -22,15 +22,18 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { public Action? Exited; + [Cached(typeof(IGameplayLeaderboardProvider))] + private readonly PlaylistsGameplayLeaderboardProvider leaderboardProvider; + protected override UserActivity InitialActivity => new UserActivity.InPlaylistGame(Beatmap.Value.BeatmapInfo, Ruleset.Value); - // TODO: should be replaced with a provider providing scores from the `PlaylistItem` - [Cached(typeof(IGameplayLeaderboardProvider))] - private EmptyGameplayLeaderboardProvider leaderboardProvider = new EmptyGameplayLeaderboardProvider(); - - public PlaylistsPlayer(Room room, PlaylistItem playlistItem, PlayerConfiguration? configuration = null) - : base(room, playlistItem, configuration) + public PlaylistsPlayer(Room room, PlaylistItem playlistItem) + : base(room, playlistItem, new PlayerConfiguration + { + ShowLeaderboard = true, + }) { + leaderboardProvider = new PlaylistsGameplayLeaderboardProvider(room, playlistItem); } [BackgroundDependencyLoader] @@ -46,6 +49,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists var requiredLocalMods = PlaylistItem.RequiredMods.Select(m => m.ToMod(GameplayState.Ruleset)); if (!requiredLocalMods.All(m => Mods.Value.Any(m.Equals))) throw new InvalidOperationException("Current Mods do not match PlaylistItem's RequiredMods"); + + LoadComponentAsync(leaderboardProvider, AddInternal); } public override bool OnExiting(ScreenExitEvent e) diff --git a/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs b/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs index bb6c402379..dfe95b8ccd 100644 --- a/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Online.Rooms; using osu.Game.Online.Spectator; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -123,6 +124,18 @@ namespace osu.Game.Screens.Select.Leaderboards InitialPosition = scoreInfo.Position; } + public GameplayLeaderboardScore(MultiplayerScore score, bool tracked, ComboDisplayMode comboMode) + { + User = score.User; + Tracked = tracked; + TotalScore.Value = score.TotalScore; + Accuracy.Value = score.Accuracy; + Combo.Value = comboMode == ComboDisplayMode.Highest ? score.MaxCombo : throw new NotSupportedException($"{comboMode} {nameof(comboMode)} is not supported."); + TotalScoreTiebreaker = score.ID; + GetDisplayScore = score.GetDisplayScore; + InitialPosition = score.Position; + } + /// /// Used for testing. /// diff --git a/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs index ac4bd06fb1..08af8926df 100644 --- a/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs @@ -37,8 +37,6 @@ namespace osu.Game.Screens.Select.Leaderboards public bool HasTeams => TeamScores.Count > 0; - public bool IsPartial => false; - private readonly MultiplayerRoomUser[] users; private readonly Bindable scoringMode = new Bindable(); diff --git a/osu.Game/Screens/Select/Leaderboards/PlaylistsGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/PlaylistsGameplayLeaderboardProvider.cs new file mode 100644 index 0000000000..206b1375de --- /dev/null +++ b/osu.Game/Screens/Select/Leaderboards/PlaylistsGameplayLeaderboardProvider.cs @@ -0,0 +1,126 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Caching; +using osu.Framework.Graphics; +using osu.Game.Online.API; +using osu.Game.Online.Rooms; +using osu.Game.Screens.Play; + +namespace osu.Game.Screens.Select.Leaderboards +{ + [LongRunningLoad] + public partial class PlaylistsGameplayLeaderboardProvider : Component, IGameplayLeaderboardProvider + { + public IBindableList Scores => scores; + private readonly BindableList scores = new BindableList(); + + private readonly Room room; + private readonly PlaylistItem playlistItem; + + private readonly Cached sorting = new Cached(); + private bool isPartial; + + public PlaylistsGameplayLeaderboardProvider(Room room, PlaylistItem playlistItem) + { + this.room = room; + this.playlistItem = playlistItem; + } + + [BackgroundDependencyLoader] + private void load(IAPIProvider api, GameplayState? gameplayState) + { + var scoresRequest = new IndexPlaylistScoresRequest(room.RoomID!.Value, playlistItem.ID); + scoresRequest.Success += response => + { + var newScores = new List(); + + isPartial = response.Scores.Count < response.TotalScores; + + for (int i = 0; i < response.Scores.Count; i++) + { + var score = response.Scores[i]; + score.Position = i + 1; + newScores.Add(new GameplayLeaderboardScore(score, tracked: false, GameplayLeaderboardScore.ComboDisplayMode.Highest)); + } + + if (response.UserScore != null && response.Scores.All(s => s.ID != response.UserScore.ID)) + newScores.Add(new GameplayLeaderboardScore(response.UserScore, tracked: false, GameplayLeaderboardScore.ComboDisplayMode.Highest)); + + scores.AddRange(newScores); + }; + api.Perform(scoresRequest); + + if (gameplayState != null) + { + var localScore = new GameplayLeaderboardScore(gameplayState, tracked: true, GameplayLeaderboardScore.ComboDisplayMode.Highest); + localScore.TotalScore.BindValueChanged(_ => sorting.Invalidate()); + scores.Add(localScore); + } + + Scheduler.AddDelayed(sort, 1000, true); + } + + // logic shared with SoloGameplayLeaderboardProvider + private void sort() + { + if (sorting.IsValid) + return; + + var orderedByScore = scores + .OrderByDescending(i => i.TotalScore.Value) + .ThenBy(i => i.TotalScoreTiebreaker) + .ToList(); + + int delta = 0; + + for (int i = 0; i < orderedByScore.Count; i++) + { + var score = orderedByScore[i]; + + // see `SoloResultsScreen.FetchScores()` for another place that does the same thing with slight deviations + // if this code is changed, that code should probably be changed as well + + score.DisplayOrder.Value = i + 1; + + // if we know we have all scores there can ever be, we can do the simple and obvious thing. + if (!isPartial) + score.Position.Value = i + 1; + else + { + // we have a partial leaderboard, with potential gaps. + // we have initial score positions which were valid at the point of starting play. + // the assumption here is that non-tracked scores here cannot move around, only tracked ones can. + if (score.Tracked) + { + int? previousScorePosition = i > 0 ? orderedByScore[i - 1].InitialPosition : 0; + int? nextScorePosition = i < orderedByScore.Count - 1 ? orderedByScore[i + 1].InitialPosition : null; + + // if the tracked score is perfectly between two scores which have known neighbouring initial positions, + // we can assign it the position of the previous score plus one... + if (previousScorePosition != null && nextScorePosition != null && previousScorePosition + 1 == nextScorePosition) + { + score.Position.Value = previousScorePosition + 1; + // but we also need to ensure all subsequent scores get shifted down one position, too. + delta++; + } + // conversely, if the tracked score is not between neighbouring two scores and the leaderboard is partial, + // we can't really assign a valid position at all. it could be any number between the two neighbours. + else + score.Position.Value = null; + } + // for non-tracked scores, we just need to apply any delta that might have come from the tracked scores + // which might have been encountered and assigned a position earlier. + else + score.Position.Value = score.InitialPosition + delta; + } + } + + sorting.Validate(); + } + } +} diff --git a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs index ba59dba7b2..2ebef78a38 100644 --- a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs @@ -61,6 +61,7 @@ namespace osu.Game.Screens.Select.Leaderboards Scheduler.AddDelayed(sort, 1000, true); } + // logic shared with PlaylistsGameplayLeaderboardProvider private void sort() { if (sorting.IsValid) From 9a9cbcdd26cdbc775e0405e54a2a8c662b026a18 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 11 Jun 2025 14:51:17 +0300 Subject: [PATCH 161/370] Remove outer grid container to avoid masked-away issues --- .../SelectV2/BeatmapLeaderboardScore.cs | 591 +++++++++--------- 1 file changed, 296 insertions(+), 295 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs index 64c078ddd4..e8dc58ff1b 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs @@ -103,6 +103,7 @@ namespace osu.Game.Screens.SelectV2 private ClickableAvatar innerAvatar = null!; + private Container centreContent = null!; private Container rightContent = null!; private FillFlowContainer modsContainer = null!; @@ -157,318 +158,309 @@ namespace osu.Game.Screens.SelectV2 RelativeSizeAxes = Axes.Both, Colour = backgroundColour }, - new GridContainer + rankLabelStandalone = new Container { - RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] + Width = rank_label_width, + RelativeSizeAxes = Axes.Y, + Children = new Drawable[] { - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - }, - Content = new[] - { - new Drawable[] + highlightGradient = new Container { - rankLabelStandalone = new Container + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Right = -10f }, + Alpha = Highlight != null ? 1 : 0, + Colour = getHighlightColour(Highlight), + Child = new Box { RelativeSizeAxes = Axes.Both }, + }, + new RankLabel(Rank, sheared, darkText: Highlight == HighlightType.Own) + { + RelativeSizeAxes = Axes.Both, + } + }, + }, + centreContent = new Container + { + Name = @"Centre container", + RelativeSizeAxes = Axes.Both, + Child = new Container + { + Masking = true, + CornerRadius = corner_radius, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + foreground = new Box { - Width = rank_label_width, - RelativeSizeAxes = Axes.Y, - Children = new Drawable[] + Alpha = 0.4f, + RelativeSizeAxes = Axes.Both, + Colour = foregroundColour + }, + new UserCoverBackground + { + RelativeSizeAxes = Axes.Both, + User = score.User, + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Colour = ColourInfo.GradientHorizontal(Colour4.White.Opacity(0.5f), Colour4.FromHex(@"222A27").Opacity(1)), + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] { - highlightGradient = new Container + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Right = -10f }, - Alpha = Highlight != null ? 1 : 0, - Colour = getHighlightColour(Highlight), - Child = new Box { RelativeSizeAxes = Axes.Both }, - }, - new RankLabel(Rank, sheared, darkText: Highlight == HighlightType.Own) - { - RelativeSizeAxes = Axes.Both, + new Container + { + AutoSizeAxes = Axes.Both, + CornerRadius = corner_radius, + Masking = true, + Children = new Drawable[] + { + new DelayedLoadWrapper(innerAvatar = new ClickableAvatar(score.User) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1.1f), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, + RelativeSizeAxes = Axes.Both, + }) + { + RelativeSizeAxes = Axes.None, + Size = new Vector2(HEIGHT) + }, + rankLabelOverlay = new Container + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.Black.Opacity(0.5f), + }, + new RankLabel(Rank, sheared, false) + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + } + } + }, + }, + new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Horizontal = corner_radius }, + Children = new Drawable[] + { + new FillFlowContainer + { + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + AutoSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + new UpdateableFlag(score.User.CountryCode) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(20, 14), + }, + new UpdateableTeamFlag(score.User.Team) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(30, 15), + }, + new DateLabel(score.Date) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Colour = colourProvider.Content2, + UseFullGlyphHeight = false, + } + } + }, + new TruncatingSpriteText + { + RelativeSizeAxes = Axes.X, + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, + Text = score.User.Username, + Font = OsuFont.Style.Heading2, + } + } + }, + new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Child = statisticsContainer = new FillFlowContainer + { + Name = @"Statistics container", + Padding = new MarginPadding { Right = 10 }, + Spacing = new Vector2(20, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new ScoreComponentLabel(BeatmapsetsStrings.ShowScoreboardHeadersCombo.ToUpper(), $"{score.MaxCombo.ToString()}x", + score.MaxCombo == score.GetMaximumAchievableCombo(), 60), + new ScoreComponentLabel(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy.ToUpper(), score.DisplayAccuracy, score.Accuracy == 1, + 55), + }, + Alpha = 0, + } + } } + } + }, + }, + }, + }, + rightContent = new Container + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Name = @"Right content", + RelativeSizeAxes = Axes.Y, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Right = grade_width }, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank)), + }, + }, + new Box + { + RelativeSizeAxes = Axes.Y, + Width = grade_width, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Colour = OsuColour.ForRank(score.Rank), + }, + new TrianglesV2 + { + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + SpawnRatio = 2, + Velocity = 0.7f, + Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank).Darken(0.2f)), + }, + new Container + { + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Y, + Width = grade_width, + Child = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(-2), + Colour = DrawableRank.GetRankNameColour(score.Rank), + Font = OsuFont.Numeric.With(size: 14), + Text = DrawableRank.GetRankName(score.Rank), + ShadowColour = Color4.Black.Opacity(0.3f), + ShadowOffset = new Vector2(0, 0.08f), + Shadow = true, + UseFullGlyphHeight = false, }, }, new Container { - Name = @"Centre container", - Masking = true, - CornerRadius = corner_radius, RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - foreground = new Box - { - Alpha = 0.4f, - RelativeSizeAxes = Axes.Both, - Colour = foregroundColour - }, - new UserCoverBackground - { - RelativeSizeAxes = Axes.Both, - User = score.User, - Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Colour = ColourInfo.GradientHorizontal(Colour4.White.Opacity(0.5f), Colour4.FromHex(@"222A27").Opacity(1)), - }, - new GridContainer - { - RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - }, - Content = new[] - { - new Drawable[] - { - new Container - { - AutoSizeAxes = Axes.Both, - CornerRadius = corner_radius, - Masking = true, - Children = new Drawable[] - { - new DelayedLoadWrapper(innerAvatar = new ClickableAvatar(score.User) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(1.1f), - Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, - RelativeSizeAxes = Axes.Both, - }) - { - RelativeSizeAxes = Axes.None, - Size = new Vector2(HEIGHT) - }, - rankLabelOverlay = new Container - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Colour4.Black.Opacity(0.5f), - }, - new RankLabel(Rank, sheared, false) - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - } - } - }, - }, - new FillFlowContainer - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Padding = new MarginPadding { Horizontal = corner_radius }, - Children = new Drawable[] - { - new FillFlowContainer - { - Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5), - AutoSizeAxes = Axes.Both, - Masking = true, - Children = new Drawable[] - { - new UpdateableFlag(score.User.CountryCode) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Size = new Vector2(20, 14), - }, - new UpdateableTeamFlag(score.User.Team) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Size = new Vector2(30, 15), - }, - new DateLabel(score.Date) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Colour = colourProvider.Content2, - UseFullGlyphHeight = false, - } - } - }, - new TruncatingSpriteText - { - RelativeSizeAxes = Axes.X, - Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, - Text = score.User.Username, - Font = OsuFont.Style.Heading2, - } - } - }, - new Container - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Child = statisticsContainer = new FillFlowContainer - { - Name = @"Statistics container", - Padding = new MarginPadding { Right = 10 }, - Spacing = new Vector2(20, 0), - Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Children = new Drawable[] - { - new ScoreComponentLabel(BeatmapsetsStrings.ShowScoreboardHeadersCombo.ToUpper(), $"{score.MaxCombo.ToString()}x", score.MaxCombo == score.GetMaximumAchievableCombo(), 60), - new ScoreComponentLabel(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy.ToUpper(), score.DisplayAccuracy, score.Accuracy == 1, 55), - }, - Alpha = 0, - } - } - } - }, - }, - }, - }, - rightContent = new Container - { - Name = @"Right content", - RelativeSizeAxes = Axes.Y, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Padding = new MarginPadding { Right = grade_width }, Child = new Container { RelativeSizeAxes = Axes.Both, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, + Masking = true, + CornerRadius = corner_radius, Children = new Drawable[] { - new Container + totalScoreBackground = new Box { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Right = grade_width }, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank)), - }, + Colour = totalScoreBackgroundGradient, }, new Box { - RelativeSizeAxes = Axes.Y, - Width = grade_width, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Colour = OsuColour.ForRank(score.Rank), - }, - new TrianglesV2 - { - Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, RelativeSizeAxes = Axes.Both, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - SpawnRatio = 2, - Velocity = 0.7f, - Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank).Darken(0.2f)), + Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank).Opacity(0.5f)), }, - new Container + new FillFlowContainer { - Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, + AutoSizeAxes = Axes.Both, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - RelativeSizeAxes = Axes.Y, - Width = grade_width, - Child = new OsuSpriteText + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Horizontal = corner_radius }, + Spacing = new Vector2(0f, -2f), + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Spacing = new Vector2(-2), - Colour = DrawableRank.GetRankNameColour(score.Rank), - Font = OsuFont.Numeric.With(size: 14), - Text = DrawableRank.GetRankName(score.Rank), - ShadowColour = Color4.Black.Opacity(0.3f), - ShadowOffset = new Vector2(0, 0.08f), - Shadow = true, - UseFullGlyphHeight = false, - }, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Padding = new MarginPadding { Right = grade_width }, - Child = new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = corner_radius, - Children = new Drawable[] + new OsuSpriteText { - totalScoreBackground = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = totalScoreBackgroundGradient, - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank).Opacity(0.5f)), - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Direction = FillDirection.Vertical, - Padding = new MarginPadding { Horizontal = corner_radius }, - Spacing = new Vector2(0f, -2f), - Children = new Drawable[] - { - new OsuSpriteText - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - UseFullGlyphHeight = false, - Current = scoreManager.GetBindableTotalScoreString(score), - Spacing = new Vector2(-1.5f), - Font = OsuFont.Style.Subtitle.With(weight: FontWeight.Light, fixedWidth: true), - Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, - }, - modsContainer = new FillFlowContainer - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(-10, 0), - Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, - }, - } - } - } + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + UseFullGlyphHeight = false, + Current = scoreManager.GetBindableTotalScoreString(score), + Spacing = new Vector2(-1.5f), + Font = OsuFont.Style.Subtitle.With(weight: FontWeight.Light, fixedWidth: true), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, + }, + modsContainer = new FillFlowContainer + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(-10, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, + }, } } } - }, + } } } - } + }, } } }; - innerAvatar.OnLoadComplete += d => d.FadeInFromZero(200); } @@ -565,30 +557,39 @@ namespace osu.Game.Screens.SelectV2 DisplayMode mode = getCurrentDisplayMode(); if (currentMode != mode) + updateDisplayMode(mode); + + centreContent.Padding = new MarginPadding { - double duration = currentMode == null ? 0 : transition_duration; - if (mode >= DisplayMode.Full) - rankLabelStandalone.FadeIn(duration, Easing.OutQuint).ResizeWidthTo(rank_label_width, duration, Easing.OutQuint); - else - rankLabelStandalone.FadeOut(duration, Easing.OutQuint).ResizeWidthTo(0, duration, Easing.OutQuint); + Left = rankLabelStandalone.DrawWidth, + Right = rightContent.DrawWidth, + }; + } - if (mode >= DisplayMode.Regular) - { - statisticsContainer.FadeIn(duration, Easing.OutQuint).MoveToX(0, duration, Easing.OutQuint); - statisticsContainer.Direction = FillDirection.Horizontal; - statisticsContainer.ScaleTo(1, duration, Easing.OutQuint); - } - else if (mode >= DisplayMode.Compact) - { - statisticsContainer.FadeIn(duration, Easing.OutQuint).MoveToX(0, duration, Easing.OutQuint); - statisticsContainer.Direction = FillDirection.Vertical; - statisticsContainer.ScaleTo(0.8f, duration, Easing.OutQuint); - } - else - statisticsContainer.FadeOut(duration, Easing.OutQuint).MoveToX(statisticsContainer.DrawWidth, duration, Easing.OutQuint); + private void updateDisplayMode(DisplayMode mode) + { + double duration = currentMode == null ? 0 : transition_duration; + if (mode >= DisplayMode.Full) + rankLabelStandalone.FadeIn(duration, Easing.OutQuint).ResizeWidthTo(rank_label_width, duration, Easing.OutQuint); + else + rankLabelStandalone.FadeOut(duration, Easing.OutQuint).ResizeWidthTo(0, duration, Easing.OutQuint); - currentMode = mode; + if (mode >= DisplayMode.Regular) + { + statisticsContainer.FadeIn(duration, Easing.OutQuint).MoveToX(0, duration, Easing.OutQuint); + statisticsContainer.Direction = FillDirection.Horizontal; + statisticsContainer.ScaleTo(1, duration, Easing.OutQuint); } + else if (mode >= DisplayMode.Compact) + { + statisticsContainer.FadeIn(duration, Easing.OutQuint).MoveToX(0, duration, Easing.OutQuint); + statisticsContainer.Direction = FillDirection.Vertical; + statisticsContainer.ScaleTo(0.8f, duration, Easing.OutQuint); + } + else + statisticsContainer.FadeOut(duration, Easing.OutQuint).MoveToX(statisticsContainer.DrawWidth, duration, Easing.OutQuint); + + currentMode = mode; } private DisplayMode getCurrentDisplayMode() From b9e1b6969e78dfa798bb4afed8afae55e9e4adb1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Jun 2025 19:16:37 +0900 Subject: [PATCH 162/370] Move and rename next/previous "group" selection keybindings to make way for group-specific bindings --- .../SongSelectV2/BeatmapCarouselTestScene.cs | 36 +++++- .../TestSceneBeatmapCarouselArtistGrouping.cs | 66 +++++------ ...tSceneBeatmapCarouselDifficultyGrouping.cs | 56 ++++----- .../TestSceneBeatmapCarouselFiltering.cs | 92 +++++++-------- .../TestSceneBeatmapCarouselNoGrouping.cs | 110 +++++++++--------- .../TestSceneBeatmapCarouselRandom.cs | 4 +- .../TestSceneBeatmapCarouselUpdateHandling.cs | 12 +- osu.Game/Graphics/Carousel/Carousel.cs | 32 ++--- .../Input/Bindings/GlobalActionContainer.cs | 14 +-- .../GlobalActionKeyBindingStrings.cs | 8 +- osu.Game/Screens/Select/BeatmapCarousel.cs | 8 +- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 2 +- 12 files changed, 234 insertions(+), 206 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index f58d879141..4976a5312c 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -183,10 +183,24 @@ namespace osu.Game.Tests.Visual.SongSelectV2 protected void WaitForFiltering() => AddUntilStep("filtering finished", () => Carousel.IsFiltering, () => Is.False); protected void WaitForScrolling() => AddUntilStep("scroll finished", () => Scroll.Current, () => Is.EqualTo(Scroll.Target)); + protected void SelectNextGroup() => AddStep("select next group", () => + { + InputManager.PressKey(Key.ShiftLeft); + InputManager.Key(Key.Right); + InputManager.ReleaseKey(Key.ShiftLeft); + }); + + protected void SelectPrevGroup() => AddStep("select prev group", () => + { + InputManager.PressKey(Key.ShiftLeft); + InputManager.Key(Key.Left); + InputManager.ReleaseKey(Key.ShiftLeft); + }); + protected void SelectNextPanel() => AddStep("select next panel", () => InputManager.Key(Key.Down)); protected void SelectPrevPanel() => AddStep("select prev panel", () => InputManager.Key(Key.Up)); - protected void SelectNextGroup() => AddStep("select next group", () => InputManager.Key(Key.Right)); - protected void SelectPrevGroup() => AddStep("select prev group", () => InputManager.Key(Key.Left)); + protected void SelectNextSet() => AddStep("select next set", () => InputManager.Key(Key.Right)); + protected void SelectPrevSet() => AddStep("select prev set", () => InputManager.Key(Key.Left)); protected void Select() => AddStep("select", () => InputManager.Key(Key.Enter)); @@ -228,7 +242,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2 protected ICarouselPanel? GetSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); protected ICarouselPanel? GetKeyboardSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.KeyboardSelected.Value); - protected void WaitForGroupSelection(int group, int panel) + protected void WaitForExpandedGroup(int group) + { + AddUntilStep($"group {group} is expanded", () => + { + var groupingFilter = Carousel.Filters.OfType().Single(); + + GroupDefinition g = groupingFilter.GroupItems.Keys.ElementAt(group); + // offset by one because the group itself is included in the items list. + CarouselItem item = groupingFilter.GroupItems[g].ElementAt(0); + + return item.Model is GroupDefinition def && def == Carousel.ExpandedGroup; + }); + } + + protected void WaitForBeatmapSelection(int group, int panel) { AddUntilStep($"selected is group{group} panel{panel}", () => { @@ -243,7 +271,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }); } - protected void WaitForSelection(int set, int? diff = null) + protected void WaitForSetSelection(int set, int? diff = null) { AddUntilStep($"selected is set{set}{(diff.HasValue ? $" diff{diff.Value}" : "")}", () => { diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs index e230dee918..0603540c5e 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs @@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] public void TestCarouselRemembersSelection() { - SelectNextGroup(); + SelectNextSet(); object? selection = null; @@ -108,22 +108,22 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] public void TestGroupSelectionOnHeader() { - SelectNextGroup(); - WaitForGroupSelection(0, 1); + SelectNextSet(); + WaitForBeatmapSelection(0, 1); SelectPrevPanel(); SelectPrevPanel(); AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); - SelectPrevGroup(); + SelectPrevSet(); - WaitForGroupSelection(0, 1); + WaitForBeatmapSelection(0, 1); AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); - SelectPrevGroup(); + SelectPrevSet(); - WaitForGroupSelection(0, 1); + WaitForBeatmapSelection(0, 1); AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); } @@ -143,41 +143,41 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextPanel(); Select(); - WaitForGroupSelection(3, 1); + WaitForBeatmapSelection(3, 1); - SelectNextGroup(); - WaitForGroupSelection(3, 5); + SelectNextSet(); + WaitForBeatmapSelection(3, 5); - SelectNextGroup(); - WaitForGroupSelection(4, 1); + SelectNextSet(); + WaitForBeatmapSelection(4, 1); - SelectPrevGroup(); - WaitForGroupSelection(3, 5); + SelectPrevSet(); + WaitForBeatmapSelection(3, 5); - SelectNextGroup(); - WaitForGroupSelection(4, 1); + SelectNextSet(); + WaitForBeatmapSelection(4, 1); - SelectNextGroup(); - WaitForGroupSelection(4, 5); + SelectNextSet(); + WaitForBeatmapSelection(4, 5); - SelectNextGroup(); - WaitForGroupSelection(0, 1); + SelectNextSet(); + WaitForBeatmapSelection(0, 1); // Difficulties should get immediate selection even when using up and down traversal. SelectNextPanel(); - WaitForGroupSelection(0, 2); + WaitForBeatmapSelection(0, 2); SelectNextPanel(); - WaitForGroupSelection(0, 3); + WaitForBeatmapSelection(0, 3); SelectNextPanel(); - WaitForGroupSelection(0, 3); + WaitForBeatmapSelection(0, 3); - SelectNextGroup(); - WaitForGroupSelection(0, 5); + SelectNextSet(); + WaitForBeatmapSelection(0, 5); SelectNextPanel(); - SelectNextGroup(); - WaitForGroupSelection(1, 1); + SelectNextSet(); + WaitForBeatmapSelection(1, 1); } [Test] @@ -199,19 +199,19 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); ClickVisiblePanelWithOffset(0, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1))); - WaitForGroupSelection(0, 1); + WaitForBeatmapSelection(0, 1); AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels().Any()); // Beatmap panels expand their selection area to cover holes from spacing. ClickVisiblePanelWithOffset(0, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1))); - WaitForGroupSelection(0, 1); + WaitForBeatmapSelection(0, 1); ClickVisiblePanelWithOffset(1, new Vector2(0, CarouselItem.DEFAULT_HEIGHT / 2 + 1)); - WaitForGroupSelection(0, 2); + WaitForBeatmapSelection(0, 2); ClickVisiblePanelWithOffset(1, new Vector2(0, CarouselItem.DEFAULT_HEIGHT / 2 + 1)); - WaitForGroupSelection(0, 5); + WaitForBeatmapSelection(0, 5); } [Test] @@ -228,14 +228,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextPanel(); Select(); - WaitForGroupSelection(0, 2); + WaitForBeatmapSelection(0, 2); for (int i = 0; i < 6; i++) SelectNextPanel(); Select(); - WaitForGroupSelection(0, 3); + WaitForBeatmapSelection(0, 3); ApplyToFilter("remove filter", c => c.SearchText = string.Empty); WaitForFiltering(); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs index 8f7c901c37..3264f7f2ff 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] public void TestCarouselRemembersSelection() { - SelectNextGroup(); + SelectNextSet(); object? selection = null; @@ -98,28 +98,28 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] public void TestGroupSelectionOnHeaderKeyboard() { - SelectNextGroup(); - WaitForGroupSelection(0, 0); + SelectNextSet(); + WaitForBeatmapSelection(0, 0); SelectPrevPanel(); AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); - SelectPrevGroup(); + SelectPrevSet(); - WaitForGroupSelection(0, 0); + WaitForBeatmapSelection(0, 0); AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); - SelectPrevGroup(); + SelectPrevSet(); - WaitForGroupSelection(0, 0); + WaitForBeatmapSelection(0, 0); AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); } [Test] public void TestGroupSelectionOnHeaderMouse() { - SelectNextGroup(); - WaitForGroupSelection(0, 0); + SelectNextSet(); + WaitForBeatmapSelection(0, 0); AddAssert("keyboard selected panel is beatmap", GetKeyboardSelectedPanel, Is.TypeOf); AddAssert("selected panel is beatmap", GetSelectedPanel, Is.TypeOf); @@ -151,22 +151,22 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextPanel(); Select(); - WaitForGroupSelection(0, 0); + WaitForBeatmapSelection(0, 0); - SelectNextGroup(); - WaitForGroupSelection(0, 1); + SelectNextSet(); + WaitForBeatmapSelection(0, 1); - SelectNextGroup(); - WaitForGroupSelection(0, 2); + SelectNextSet(); + WaitForBeatmapSelection(0, 2); - SelectPrevGroup(); - WaitForGroupSelection(0, 1); + SelectPrevSet(); + WaitForBeatmapSelection(0, 1); - SelectPrevGroup(); - WaitForGroupSelection(0, 0); + SelectPrevSet(); + WaitForBeatmapSelection(0, 0); - SelectPrevGroup(); - WaitForGroupSelection(2, 9); + SelectPrevSet(); + WaitForBeatmapSelection(2, 9); } [Test] @@ -187,10 +187,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2 // Beatmap panels expand their selection area to cover holes from spacing. ClickVisiblePanelWithOffset(0, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1))); - WaitForGroupSelection(0, 0); + WaitForBeatmapSelection(0, 0); ClickVisiblePanelWithOffset(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); - WaitForGroupSelection(0, 1); + WaitForBeatmapSelection(0, 1); } [Test] @@ -203,11 +203,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2 CheckDisplayedBeatmapsCount(3); // Single result gets selected automatically - WaitForGroupSelection(0, 0); + WaitForBeatmapSelection(0, 0); SelectNextPanel(); Select(); - WaitForGroupSelection(0, 0); + WaitForBeatmapSelection(0, 0); for (int i = 0; i < 5; i++) SelectNextPanel(); @@ -216,7 +216,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextPanel(); Select(); - WaitForGroupSelection(1, 0); + WaitForBeatmapSelection(1, 0); ApplyToFilter("remove filter", c => c.SearchText = string.Empty); WaitForFiltering(); @@ -228,15 +228,15 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] public void TestExpandedGroupStillExpandedAfterFilter() { - SelectPrevGroup(); + SelectPrevSet(); - WaitForGroupSelection(2, 9); + WaitForBeatmapSelection(2, 9); AddAssert("expanded group is last", () => (Carousel.ExpandedGroup as StarDifficultyGroupDefinition)?.Difficulty.Stars, () => Is.EqualTo(6)); SelectNextPanel(); Select(); - WaitForGroupSelection(2, 9); + WaitForBeatmapSelection(2, 9); AddAssert("expanded group is first", () => (Carousel.ExpandedGroup as StarDifficultyGroupDefinition)?.Difficulty.Stars, () => Is.EqualTo(0)); // doesn't actually filter anything away, but triggers a filter. diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs index 00a00f7f24..267810ecfa 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs @@ -44,13 +44,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2 CheckDisplayedBeatmapSetsCount(1); CheckDisplayedBeatmapsCount(3); - WaitForSelection(2, 0); + WaitForSetSelection(2, 0); for (int i = 0; i < 5; i++) SelectNextPanel(); Select(); - WaitForSelection(2, 1); + WaitForSetSelection(2, 1); ApplyToFilter("remove filter", c => c.SearchText = string.Empty); WaitForFiltering(); @@ -135,7 +135,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddBeatmaps(50, 3); WaitForDrawablePanels(); - SelectNextGroup(); + SelectNextSet(); SelectNextPanel(); Select(); @@ -156,11 +156,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddBeatmaps(50, 3); WaitForDrawablePanels(); - SelectPrevGroup(); - WaitForSelection(49, 0); + SelectPrevSet(); + WaitForSetSelection(49, 0); ApplyToFilter("filter all but one", c => c.SearchText = BeatmapSets.First().Metadata.Title); - WaitForSelection(0, 0); + WaitForSetSelection(0, 0); } [Test] @@ -171,7 +171,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 CheckNoSelection(); ApplyToFilter("filter all but one", c => c.SearchText = BeatmapSets.First().Metadata.Title); - WaitForSelection(0, 0); + WaitForSetSelection(0, 0); } [Test] @@ -184,7 +184,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SortBy(SortMode.Difficulty); - SelectNextGroup(); + SelectNextSet(); AddStep("record selection", () => selectedID = ((BeatmapInfo)Carousel.CurrentSelection!).ID); @@ -318,8 +318,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddBeatmaps(2, 3); WaitForDrawablePanels(); - SelectNextGroup(); - WaitForSelection(0, 0); + SelectNextSet(); + WaitForSetSelection(0, 0); CheckDisplayedBeatmapsCount(6); @@ -328,10 +328,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2 CheckDisplayedBeatmapsCount(4); - SelectNextGroup(); - WaitForSelection(0, 1); - SelectPrevGroup(); - WaitForSelection(1, 1); + SelectNextSet(); + WaitForSetSelection(0, 1); + SelectPrevSet(); + WaitForSetSelection(1, 1); } [Test] @@ -340,16 +340,16 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddBeatmaps(5, 3); WaitForDrawablePanels(); - SelectNextGroup(); - SelectNextGroup(); - SelectNextGroup(); - WaitForSelection(2, 0); + SelectNextSet(); + SelectNextSet(); + SelectNextSet(); + WaitForSetSelection(2, 0); ApplyToFilter("filter first away", c => c.UserStarDifficulty.Min = 3); WaitForFiltering(); - SelectNextGroup(); - WaitForSelection(0, 1); + SelectNextSet(); + WaitForSetSelection(0, 1); } [Test] @@ -358,16 +358,16 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddBeatmaps(5, 3); WaitForDrawablePanels(); - SelectNextGroup(); - SelectNextGroup(); - SelectNextGroup(); - WaitForSelection(2, 0); + SelectNextSet(); + SelectNextSet(); + SelectNextSet(); + WaitForSetSelection(2, 0); ApplyToFilter("filter first away", c => c.UserStarDifficulty.Min = 3); WaitForFiltering(); - SelectPrevGroup(); - WaitForSelection(4, 1); + SelectPrevSet(); + WaitForSetSelection(4, 1); } [Test] @@ -376,14 +376,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddBeatmaps(2, 3); WaitForDrawablePanels(); - SelectPrevGroup(); - WaitForSelection(1, 0); + SelectPrevSet(); + WaitForSetSelection(1, 0); ApplyToFilter("filter last set away", c => c.SearchText = BeatmapSets.First().Metadata.Title); WaitForFiltering(); - SelectPrevGroup(); - WaitForSelection(0, 0); + SelectPrevSet(); + WaitForSetSelection(0, 0); } [Test] @@ -392,14 +392,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddBeatmaps(2, 3); WaitForDrawablePanels(); - SelectNextGroup(); - WaitForSelection(0, 0); + SelectNextSet(); + WaitForSetSelection(0, 0); ApplyToFilter("filter first set away", c => c.SearchText = BeatmapSets.Last().Metadata.Title); WaitForFiltering(); - SelectNextGroup(); - WaitForSelection(1, 0); + SelectNextSet(); + WaitForSetSelection(1, 0); } [Test] @@ -408,10 +408,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddBeatmaps(5, 3); WaitForDrawablePanels(); - SelectNextGroup(); - SelectNextGroup(); - SelectNextGroup(); - WaitForSelection(2, 0); + SelectNextSet(); + SelectNextSet(); + SelectNextSet(); + WaitForSetSelection(2, 0); ApplyToFilter("filter first away", c => c.UserStarDifficulty.Min = 3); WaitForFiltering(); @@ -426,10 +426,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddBeatmaps(5, 3); WaitForDrawablePanels(); - SelectNextGroup(); - SelectNextGroup(); - SelectNextGroup(); - WaitForSelection(2, 0); + SelectNextSet(); + SelectNextSet(); + SelectNextSet(); + WaitForSetSelection(2, 0); ApplyToFilter("filter first away", c => c.UserStarDifficulty.Min = 3); WaitForFiltering(); @@ -444,8 +444,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddBeatmaps(2, 3); WaitForDrawablePanels(); - SelectPrevGroup(); - WaitForSelection(1, 0); + SelectPrevSet(); + WaitForSetSelection(1, 0); ApplyToFilter("filter last set away", c => c.SearchText = BeatmapSets.First().Metadata.Title); WaitForFiltering(); @@ -460,8 +460,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddBeatmaps(2, 3); WaitForDrawablePanels(); - SelectNextGroup(); - WaitForSelection(0, 0); + SelectNextSet(); + WaitForSetSelection(0, 0); ApplyToFilter("filter first set away", c => c.SearchText = BeatmapSets.Last().Metadata.Title); WaitForFiltering(); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs index 3c839f46d1..6ca02e57a5 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs @@ -82,7 +82,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddBeatmaps(10); WaitForDrawablePanels(); - SelectNextGroup(); + SelectNextSet(); object? selection = null; @@ -116,10 +116,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddBeatmaps(total_set_count); WaitForDrawablePanels(); - SelectNextGroup(); - WaitForSelection(0, 0); - SelectPrevGroup(); - WaitForSelection(total_set_count - 1, 0); + SelectNextSet(); + WaitForSetSelection(0, 0); + SelectPrevSet(); + WaitForSetSelection(total_set_count - 1, 0); } [Test] @@ -130,10 +130,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddBeatmaps(total_set_count); WaitForDrawablePanels(); - SelectPrevGroup(); - WaitForSelection(total_set_count - 1, 0); - SelectNextGroup(); - WaitForSelection(0, 0); + SelectPrevSet(); + WaitForSetSelection(total_set_count - 1, 0); + SelectNextSet(); + WaitForSetSelection(0, 0); } [Test] @@ -142,17 +142,17 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddBeatmaps(10, 3); WaitForDrawablePanels(); - SelectNextGroup(); - SelectNextGroup(); - WaitForSelection(1, 0); + SelectNextSet(); + SelectNextSet(); + WaitForSetSelection(1, 0); SelectPrevPanel(); - SelectPrevGroup(); - WaitForSelection(1, 0); + SelectPrevSet(); + WaitForSetSelection(1, 0); SelectPrevPanel(); - SelectNextGroup(); - WaitForSelection(1, 0); + SelectNextSet(); + WaitForSetSelection(1, 0); } [Test] @@ -168,19 +168,19 @@ namespace osu.Game.Tests.Visual.SongSelectV2 CheckNoSelection(); Select(); - WaitForSelection(3, 0); + WaitForSetSelection(3, 0); SelectNextPanel(); - WaitForSelection(3, 1); + WaitForSetSelection(3, 1); SelectNextPanel(); - WaitForSelection(3, 2); + WaitForSetSelection(3, 2); SelectNextPanel(); - WaitForSelection(3, 2); + WaitForSetSelection(3, 2); Select(); - WaitForSelection(4, 0); + WaitForSetSelection(4, 0); } [Test] @@ -189,11 +189,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2 CheckNoSelection(); AddBeatmaps(1, 3); - WaitForSelection(0, 0); + WaitForSetSelection(0, 0); CheckActivationCount(0); - SelectNextGroup(); - WaitForSelection(0, 0); + SelectNextSet(); + WaitForSetSelection(0, 0); // In the case of a grouped beatmap set, the header gets activated and re-selects the recommended difficulty. // This is probably fine. @@ -201,8 +201,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 // We don't want it to request present though, which would start gameplay. CheckRequestPresentCount(0); - SelectPrevGroup(); - WaitForSelection(0, 0); + SelectPrevSet(); + WaitForSetSelection(0, 0); CheckActivationCount(1); CheckRequestPresentCount(0); @@ -216,11 +216,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2 CheckNoSelection(); AddBeatmaps(1, 1); - WaitForSelection(0, 0); + WaitForSetSelection(0, 0); CheckActivationCount(0); - SelectNextGroup(); - WaitForSelection(0, 0); + SelectNextSet(); + WaitForSetSelection(0, 0); // In the case of a grouped beatmap set, the header gets activated and re-selects the recommended difficulty. // This is probably fine. @@ -228,8 +228,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 // We don't want it to request present though, which would start gameplay. CheckRequestPresentCount(0); - SelectPrevGroup(); - WaitForSelection(0, 0); + SelectPrevSet(); + WaitForSetSelection(0, 0); CheckActivationCount(0); CheckRequestPresentCount(0); @@ -241,13 +241,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextPanel(); CheckNoSelection(); - SelectNextGroup(); + SelectNextSet(); CheckNoSelection(); SelectPrevPanel(); CheckNoSelection(); - SelectPrevGroup(); + SelectPrevSet(); CheckNoSelection(); } @@ -267,20 +267,20 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ClickVisiblePanelWithOffset(0, new Vector2(0, -(PanelBeatmapSet.HEIGHT / 2 - 1))); AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels().Any()); - WaitForSelection(0, 0); + WaitForSetSelection(0, 0); // Beatmap panels expand their selection area to cover holes from spacing. ClickVisiblePanelWithOffset(0, new Vector2(0, -(PanelBeatmap.HEIGHT / 2 + 1))); - WaitForSelection(0, 0); + WaitForSetSelection(0, 0); ClickVisiblePanelWithOffset(2, new Vector2(0, (PanelBeatmap.HEIGHT / 2 + 1))); - WaitForSelection(0, 2); + WaitForSetSelection(0, 2); ClickVisiblePanelWithOffset(2, new Vector2(0, -(PanelBeatmap.HEIGHT / 2 + 1))); - WaitForSelection(0, 2); + WaitForSetSelection(0, 2); ClickVisiblePanelWithOffset(3, new Vector2(0, (PanelBeatmap.HEIGHT / 2 + 1))); - WaitForSelection(0, 3); + WaitForSetSelection(0, 3); } [Test] @@ -294,17 +294,17 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddUntilStep("standalone panels displayed", () => GetVisiblePanels().Any()); - SelectNextGroup(); + SelectNextSet(); // both sets have a difficulty with 0.00* star rating. // in the case of a tie when sorting, the first tie-breaker is `DateAdded` descending, which will pick the last set added (see `TestResources.CreateTestBeatmapSetInfo()`). - WaitForSelection(1, 0); + WaitForSetSelection(1, 0); - SelectNextGroup(); - WaitForSelection(0, 0); + SelectNextSet(); + WaitForSetSelection(0, 0); SelectNextPanel(); Select(); - WaitForSelection(1, 1); + WaitForSetSelection(1, 1); } [Test] @@ -318,7 +318,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddUntilStep("standalone panels displayed", () => GetVisiblePanels().Count(), () => Is.EqualTo(3)); - WaitForSelection(0, 0); + WaitForSetSelection(0, 0); SortBy(SortMode.Title); @@ -335,30 +335,30 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("set recommendation algorithm", () => BeatmapRecommendationFunction = beatmaps => beatmaps.Last()); - SelectPrevGroup(); + SelectPrevSet(); // check recommended was selected - SelectNextGroup(); - WaitForSelection(0, 2); + SelectNextSet(); + WaitForSetSelection(0, 2); // change away from recommended SelectPrevPanel(); Select(); - WaitForSelection(0, 1); + WaitForSetSelection(0, 1); // next set, check recommended - SelectNextGroup(); - WaitForSelection(1, 2); + SelectNextSet(); + WaitForSetSelection(1, 2); // next set, check recommended - SelectNextGroup(); - WaitForSelection(2, 2); + SelectNextSet(); + WaitForSetSelection(2, 2); // go back to first set and ensure user selection was retained // todo: we don't do that yet. not sure if we will continue to have this. - // SelectPrevGroup(); - // SelectPrevGroup(); - // WaitForSelection(0, 1); + // SelectPrevSet(); + // SelectPrevSet(); + // WaitForSetSelection(0, 1); } private void checkSelectionIterating(bool isIterating) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs index 4d864e4dec..e1d25c51ac 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 for (int i = 0; i < 10; i++) { nextRandom(); - WaitForSelection(0, 9); + WaitForSetSelection(0, 9); } } @@ -110,7 +110,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddBeatmaps(local_set_count, 3, true); WaitForDrawablePanels(); - SelectNextGroup(); + SelectNextSet(); for (int i = 0; i < random_select_count; i++) nextRandom(); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs index eb8877738f..1ec5b37f0e 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs @@ -94,9 +94,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] public void TestSelectionHeld() { - SelectNextGroup(); + SelectNextSet(); - WaitForSelection(1, 0); + WaitForSetSelection(1, 0); AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); @@ -110,9 +110,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] // Checks that we keep selection based on online ID where possible. public void TestSelectionHeldDifficultyNameChanged() { - SelectNextGroup(); + SelectNextSet(); - WaitForSelection(1, 0); + WaitForSetSelection(1, 0); AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); @@ -126,9 +126,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] // Checks that we fallback to keeping selection based on difficulty name. public void TestSelectionHeldDifficultyOnlineIDChanged() { - SelectNextGroup(); + SelectNextSet(); - WaitForSelection(1, 0); + WaitForSetSelection(1, 0); AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index 94e864d71d..ab3e860f8b 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -234,27 +234,27 @@ namespace osu.Game.Graphics.Carousel Scroll.Panels.SingleOrDefault(p => ((ICarouselPanel)p).Item == item); /// - /// When a user is traversing the carousel via group selection keys, assert whether the item provided is a valid target. + /// When a user is traversing the carousel via set selection keys, assert whether the item provided is a valid target. /// /// The candidate item. - /// Whether the provided item is a valid group target. If false, more panels will be checked in the user's requested direction until a valid target is found. - protected virtual bool CheckValidForGroupSelection(CarouselItem item) => true; + /// Whether the provided item is a valid set target. If false, more panels will be checked in the user's requested direction until a valid target is found. + protected virtual bool CheckValidForSetSelection(CarouselItem item) => true; /// /// Keyboard selection usually does not automatically activate an item. There may be exceptions to this rule. - /// Returning true here will make keyboard traversal act like group traversal for the target item. + /// Returning true here will make keyboard traversal act like set traversal for the target item. /// protected virtual bool ShouldActivateOnKeyboardSelection(CarouselItem item) => false; /// /// Called after an item becomes the . - /// Should be used to handle any group expansion, item visibility changes, etc. + /// Should be used to handle any set expansion, item visibility changes, etc. /// protected virtual void HandleItemSelected(object? model) { } /// /// Called when the changes to a new selection. - /// Should be used to handle any group expansion, item visibility changes, etc. + /// Should be used to handle any set expansion, item visibility changes, etc. /// protected virtual void HandleItemDeselected(object? model) { } @@ -460,12 +460,12 @@ namespace osu.Game.Graphics.Carousel Scheduler.AddOnce(traverseKeyboardSelection, -1); return true; - case GlobalAction.SelectNextGroup: - Scheduler.AddOnce(traverseGroupSelection, 1); + case GlobalAction.ActivateNextSet: + Scheduler.AddOnce(traverseSetSelection, 1); return true; - case GlobalAction.SelectPreviousGroup: - Scheduler.AddOnce(traverseGroupSelection, -1); + case GlobalAction.ActivatePreviousSet: + Scheduler.AddOnce(traverseSetSelection, -1); return true; } @@ -525,12 +525,12 @@ namespace osu.Game.Graphics.Carousel /// /// Positive for downwards, negative for upwards. /// Whether selection was possible. - private void traverseGroupSelection(int direction) + private void traverseSetSelection(int direction) { if (carouselItems == null || carouselItems.Count == 0) return; // If the user has a different keyboard selection and requests - // group selection, first transfer the keyboard selection to actual selection. + // set selection, first transfer the keyboard selection to actual selection. if (currentKeyboardSelection.CarouselItem != null && currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) { Activate(currentKeyboardSelection.CarouselItem); @@ -549,11 +549,11 @@ namespace osu.Game.Graphics.Carousel { newIndex = originalIndex = currentKeyboardSelection.Index.Value; - // As a second special case, if we're group selecting backwards and the current selection isn't a group, - // make sure to go back to the group header this item belongs to, so that the block below doesn't find it and stop too early. + // As a second special case, if we're set selecting backwards and the current selection isn't a set, + // make sure to go back to the set header this item belongs to, so that the block below doesn't find it and stop too early. if (direction < 0) { - while (newIndex > 0 && !CheckValidForGroupSelection(carouselItems[newIndex])) + while (newIndex > 0 && !CheckValidForSetSelection(carouselItems[newIndex])) newIndex--; } } @@ -569,7 +569,7 @@ namespace osu.Game.Graphics.Carousel var newItem = carouselItems[newIndex]; - if (CheckValidForGroupSelection(newItem)) + if (CheckValidForSetSelection(newItem)) { HandleItemActivated(newItem); return; diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 6de2dabe2b..83c2af5d73 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -89,9 +89,6 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.Up, GlobalAction.SelectPrevious), new KeyBinding(InputKey.Down, GlobalAction.SelectNext), - new KeyBinding(InputKey.Left, GlobalAction.SelectPreviousGroup), - new KeyBinding(InputKey.Right, GlobalAction.SelectNextGroup), - new KeyBinding(InputKey.Space, GlobalAction.Select), new KeyBinding(InputKey.Enter, GlobalAction.Select), new KeyBinding(InputKey.KeypadEnter, GlobalAction.Select), @@ -199,6 +196,9 @@ namespace osu.Game.Input.Bindings private static IEnumerable songSelectKeyBindings => new[] { + new KeyBinding(InputKey.Left, GlobalAction.ActivatePreviousSet), + new KeyBinding(InputKey.Right, GlobalAction.ActivateNextSet), + new KeyBinding(InputKey.F1, GlobalAction.ToggleModSelection), new KeyBinding(InputKey.F2, GlobalAction.SelectNextRandom), new KeyBinding(new[] { InputKey.Shift, InputKey.F2 }, GlobalAction.SelectPreviousRandom), @@ -396,11 +396,11 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorDecreaseDistanceSpacing))] EditorDecreaseDistanceSpacing, - [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.SelectPreviousGroup))] - SelectPreviousGroup, + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ActivatePreviousSet))] + ActivatePreviousSet, - [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.SelectNextGroup))] - SelectNextGroup, + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ActivateNextSet))] + ActivateNextSet, [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.DeselectAllMods))] DeselectAllMods, diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 34b9e1fecc..4401efaced 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -130,14 +130,14 @@ namespace osu.Game.Localisation public static LocalisableString SelectNext => new TranslatableString(getKey(@"select_next"), @"Next selection"); /// - /// "Select previous group" + /// "Activate previous set" /// - public static LocalisableString SelectPreviousGroup => new TranslatableString(getKey(@"select_previous_group"), @"Select previous group"); + public static LocalisableString ActivatePreviousSet => new TranslatableString(getKey(@"activate_previous_set"), @"Activate previous set"); /// - /// "Select next group" + /// "Activate next set" /// - public static LocalisableString SelectNextGroup => new TranslatableString(getKey(@"select_next_group"), @"Select next group"); + public static LocalisableString ActivateNextSet => new TranslatableString(getKey(@"activate_next_set"), @"Activate next set"); /// /// "Home" diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index c474b36a89..9ccb8170f3 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -701,13 +701,13 @@ namespace osu.Game.Screens.Select switch (e.Action) { case GlobalAction.SelectNext: - case GlobalAction.SelectNextGroup: - SelectNext(1, e.Action == GlobalAction.SelectNextGroup); + case GlobalAction.ActivateNextSet: + SelectNext(1, e.Action == GlobalAction.ActivateNextSet); return true; case GlobalAction.SelectPrevious: - case GlobalAction.SelectPreviousGroup: - SelectNext(-1, e.Action == GlobalAction.SelectPreviousGroup); + case GlobalAction.ActivatePreviousSet: + SelectNext(-1, e.Action == GlobalAction.ActivatePreviousSet); return true; } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index f580a3bc88..4d066e0323 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -339,7 +339,7 @@ namespace osu.Game.Screens.SelectV2 RequestRecommendedSelection(beatmaps); } - protected override bool CheckValidForGroupSelection(CarouselItem item) + protected override bool CheckValidForSetSelection(CarouselItem item) { switch (item.Model) { From fc5ea7f3f2232c4edef5a30ed2727d062585f1f6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Jun 2025 22:28:50 +0900 Subject: [PATCH 163/370] Disable when using touch input --- osu.Game/Screens/SelectV2/SongSelect.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 6164f5b088..c166facab7 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -16,6 +16,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Input.StateChanges; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Framework.Threading; @@ -735,12 +736,16 @@ namespace osu.Game.Screens.SelectV2 protected override bool OnMouseDown(MouseDownEvent e) { - // I don't know why this works but it does. + // I don't know why this works, but it does. // If the carousel panels are hovered, hovered no longer contains the screen. - // Maybe there's a better way of doing this, but I couldn't immeidately find a good setup. + // Maybe there's a better way of doing this, but I couldn't immediately find a good setup. bool mouseDownPriority = GetContainingInputManager()!.HoveredDrawables.Contains(this); - if (e.Button == MouseButton.Left && mouseDownPriority) + // Touch input synthesises right clicks, which allow absolute scroll of the carousel. + // For simplicity, disable this functionality on mobile. + bool isTouchInput = e.CurrentState.Mouse.LastSource is ISourcedFromTouch; + + if (e.Button == MouseButton.Left && !isTouchInput && mouseDownPriority) { revealingBackground = Scheduler.AddDelayed(() => { From 90877a079c0406f17c1b6db933045b3e1d0028ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Jun 2025 22:47:47 +0900 Subject: [PATCH 164/370] Adjust tests based on changed behaviour Calibration no longer goes away on missing / invalid scores. --- .../Gameplay/TestSceneBeatmapOffsetControl.cs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs index ba31dc928e..25b36a0f33 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs @@ -163,10 +163,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("Has calibration button", () => offsetControl.ChildrenOfType().Any()); AddStep("Press button", () => offsetControl.ChildrenOfType().Single().TriggerClick()); AddAssert("Offset is adjusted", () => offsetControl.Current.Value == -average_error); - AddUntilStep("Button is disabled", () => !offsetControl.ChildrenOfType().Single().Enabled.Value); - AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null); - AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); recreateControl(); AddStep("Set same reference score", () => offsetControl.ReferenceScore.Value = referenceScore); @@ -179,6 +176,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestCalibrationFromNonZero() { + ScoreInfo referenceScore = null!; const double average_error = -4.5; const double initial_offset = -2; @@ -186,7 +184,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); AddStep("Set reference score", () => { - offsetControl.ReferenceScore.Value = new ScoreInfo + offsetControl.ReferenceScore.Value = referenceScore = new ScoreInfo { HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error), BeatmapInfo = Beatmap.Value.BeatmapInfo, @@ -196,9 +194,10 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("Has calibration button", () => offsetControl.ChildrenOfType().Any()); AddStep("Press button", () => offsetControl.ChildrenOfType().Single().TriggerClick()); AddAssert("Offset is adjusted", () => offsetControl.Current.Value == initial_offset - average_error); - AddUntilStep("Button is disabled", () => !offsetControl.ChildrenOfType().Single().Enabled.Value); - AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null); + + recreateControl(); + AddStep("Set same reference score", () => offsetControl.ReferenceScore.Value = referenceScore); AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); } @@ -247,10 +246,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("Has calibration button", () => offsetControl.ChildrenOfType().Any()); AddStep("Press button", () => offsetControl.ChildrenOfType().Single().TriggerClick()); AddAssert("Offset is adjusted", () => offsetControl.Current.Value, () => Is.EqualTo(initial_offset - average_error)); - AddUntilStep("Button is disabled", () => !offsetControl.ChildrenOfType().Single().Enabled.Value); - AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null); - AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); AddStep("Clean up beatmap", () => Realm.Write(r => r.RemoveAll())); } @@ -274,10 +270,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("Has calibration button", () => offsetControl.ChildrenOfType().Any()); AddStep("Press button", () => offsetControl.ChildrenOfType().Single().TriggerClick()); AddAssert("Offset is adjusted", () => offsetControl.Current.Value == -average_error); - AddUntilStep("Button is disabled", () => !offsetControl.ChildrenOfType().Single().Enabled.Value); - AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null); - AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); } private void recreateControl() From b729091244d1b8cebd709e3b77b7fa428061a89c Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Thu, 12 Jun 2025 14:42:11 +0900 Subject: [PATCH 165/370] Add support for rank change SFX to `LegacyRankDisplay` and debouncing --- osu.Game/Configuration/SessionStatics.cs | 9 ++++ .../Screens/Play/HUD/DefaultRankDisplay.cs | 10 ++++- osu.Game/Skinning/LegacyRankDisplay.cs | 43 +++++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index b816d1a88b..59e107a23e 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -8,6 +8,8 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Scoring; +using osu.Game.Screens.Play.HUD; +using osu.Game.Skinning; using osu.Game.Users; namespace osu.Game.Configuration @@ -25,6 +27,7 @@ namespace osu.Game.Configuration SetDefault(Static.FeaturedArtistDisclaimerShownOnce, false); SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null); SetDefault(Static.LastModSelectPanelSamplePlaybackTime, (double?)null); + SetDefault(Static.LastRankChangeSamplePlaybackTime, (double?)null); SetDefault(Static.SeasonalBackgrounds, null); SetDefault(Static.TouchInputActive, RuntimeInfo.IsMobile); SetDefault(Static.LastLocalUserScore, null); @@ -72,6 +75,12 @@ namespace osu.Game.Configuration /// LastModSelectPanelSamplePlaybackTime, + /// + /// The last playback time in milliseconds of a rank up/down sample (in and ). + /// Used to debounce rank change sounds game-wide to avoid potential volume saturation from multiple simultaneous playback. + /// + LastRankChangeSamplePlaybackTime, + /// /// Whether the last positional input received was a touch input. /// Used in touchscreen detection scenarios (). diff --git a/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs index 61f0abd79c..33912495b1 100644 --- a/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs @@ -32,6 +32,8 @@ namespace osu.Game.Screens.Play.HUD private SkinnableSound rankDownSample = null!; private SkinnableSound rankUpSample = null!; + private Bindable lastSamplePlaybackTime = null!; + private IBindable rank = null!; public DefaultRankDisplay() @@ -40,7 +42,7 @@ namespace osu.Game.Screens.Play.HUD } [BackgroundDependencyLoader] - private void load(SkinEditor? skinEditor) + private void load(SkinEditor? skinEditor, SessionStatics statics) { InternalChildren = new Drawable[] { @@ -54,6 +56,8 @@ namespace osu.Game.Screens.Play.HUD if (skinEditor != null) PlaySamples.Value = false; + + lastSamplePlaybackTime = statics.GetBindable(Static.LastRankChangeSamplePlaybackTime); } protected override void LoadComplete() @@ -63,8 +67,10 @@ namespace osu.Game.Screens.Play.HUD rank = scoreProcessor.Rank.GetBoundCopy(); rank.BindValueChanged(r => { + bool enoughTimeElapsed = !lastSamplePlaybackTime.Value.HasValue || Time.Current - lastSamplePlaybackTime.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME; + // Don't play rank-down sfx on quit/retry - if (r.NewValue != r.OldValue && r.NewValue > ScoreRank.F && PlaySamples.Value) + if (r.NewValue != r.OldValue && r.NewValue > ScoreRank.F && PlaySamples.Value && enoughTimeElapsed) { if (r.NewValue > rankDisplay.Rank) rankUpSample.Play(); diff --git a/osu.Game/Skinning/LegacyRankDisplay.cs b/osu.Game/Skinning/LegacyRankDisplay.cs index b11b01b08d..7c2f8ffdef 100644 --- a/osu.Game/Skinning/LegacyRankDisplay.cs +++ b/osu.Game/Skinning/LegacyRankDisplay.cs @@ -6,6 +6,10 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Game.Audio; +using osu.Game.Configuration; +using osu.Game.Localisation; +using osu.Game.Overlays.SkinEditor; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osuTK; @@ -22,9 +26,18 @@ namespace osu.Game.Skinning [Resolved] private ISkinSource source { get; set; } = null!; + [SettingSource(typeof(DefaultRankDisplayStrings), nameof(DefaultRankDisplayStrings.PlaySamplesOnRankChange))] + public BindableBool PlaySamples { get; set; } = new BindableBool(true); + private readonly Sprite rankDisplay; + private SkinnableSound rankDownSample = null!; + private SkinnableSound rankUpSample = null!; + + private Bindable lastSamplePlaybackTime = null!; + private IBindable rank = null!; + private ScoreRank lastRank; public LegacyRankDisplay() { @@ -37,6 +50,21 @@ namespace osu.Game.Skinning }); } + [BackgroundDependencyLoader] + private void load(SkinEditor? skinEditor, SessionStatics statics) + { + AddRangeInternal(new Drawable[] + { + rankDownSample = new SkinnableSound(new SampleInfo("Gameplay/rank-down")), + rankUpSample = new SkinnableSound(new SampleInfo("Gameplay/rank-up")), + }); + + if (skinEditor != null) + PlaySamples.Value = false; + + lastSamplePlaybackTime = statics.GetBindable(Static.LastRankChangeSamplePlaybackTime); + } + protected override void LoadComplete() { rank = scoreProcessor.Rank.GetBoundCopy(); @@ -61,6 +89,21 @@ namespace osu.Game.Skinning .ScaleTo(new Vector2(1.625f), 500, Easing.Out) .Expire(); } + + bool enoughTimeElapsed = !lastSamplePlaybackTime.Value.HasValue || Time.Current - lastSamplePlaybackTime.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME; + + // Don't play rank-down sfx on quit/retry + if (r.NewValue != r.OldValue && r.NewValue > ScoreRank.F && PlaySamples.Value && enoughTimeElapsed) + { + if (r.NewValue > lastRank) + rankUpSample.Play(); + else + rankDownSample.Play(); + + lastSamplePlaybackTime.Value = Time.Current; + } + + lastRank = r.NewValue; }, true); FinishTransforms(true); From 435128ebaa534401f927fd2d7364b01c4b9a950a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Jun 2025 15:16:29 +0900 Subject: [PATCH 166/370] Update logo tracking operations to use `IDisposable` flow --- .../TestSceneLogoTrackingContainer.cs | 9 ++++++-- .../Containers/LogoTrackingContainer.cs | 22 +++++++++++-------- osu.Game/Screens/Footer/ScreenFooter.cs | 7 ++++-- osu.Game/Screens/Menu/ButtonSystem.cs | 11 ++++++---- osu.Game/Screens/Play/PlayerLoader.cs | 15 +++++++++---- 5 files changed, 43 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs index 8d5c961265..931b5afa12 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs @@ -5,6 +5,7 @@ using System; using System.Linq; +using JetBrains.Annotations; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -28,6 +29,9 @@ namespace osu.Game.Tests.Visual.UserInterface private Drawable logoFacade; private bool randomPositions; + [CanBeNull] + private IDisposable logoTracking; + private const float visual_box_size = 72; [SetUpSteps] @@ -150,14 +154,15 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("Perform logo movements", () => { - trackingContainer.StopTracking(); + logoTracking?.Dispose(); + logo.MoveTo(new Vector2(0.5f), 500, Easing.InOutExpo); visualBox.Colour = Color4.White; Scheduler.AddDelayed(() => { - trackingContainer.StartTracking(logo, 1000, Easing.InOutExpo); + logoTracking = trackingContainer.StartTracking(logo, 1000, Easing.InOutExpo); visualBox.Colour = Color4.Tomato; }, 700); }); diff --git a/osu.Game/Graphics/Containers/LogoTrackingContainer.cs b/osu.Game/Graphics/Containers/LogoTrackingContainer.cs index 6819d97bc5..25ad526af6 100644 --- a/osu.Game/Graphics/Containers/LogoTrackingContainer.cs +++ b/osu.Game/Graphics/Containers/LogoTrackingContainer.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; @@ -32,7 +34,7 @@ namespace osu.Game.Graphics.Containers /// The instance of the logo to be used for tracking. /// The duration of the initial transform. Default is instant. /// The easing type of the initial transform. - public void StartTracking(OsuLogo logo, double duration = 0, Easing easing = Easing.None) + public IDisposable StartTracking(OsuLogo logo, double duration = 0, Easing easing = Easing.None) { if (Logo != null && Logo != logo) throw new InvalidOperationException("A different logo is already being tracked."); @@ -50,19 +52,21 @@ namespace osu.Game.Graphics.Containers startTime = null; startPosition = null; + + return new InvokeOnDisposal(stopTracking); + + void stopTracking() + { + Debug.Assert(Logo != null); + + Logo.IsTracking = false; + Logo = null; + } } /// /// Stops the logo assigned in from tracking the facade's position. /// - public void StopTracking() - { - if (Logo == null) return; - - Logo.IsTracking = false; - Logo = null; - } - /// /// Gets the position that the logo should move to with respect to the . /// Manually performs a conversion of the Facade's position to the Logo's parent's relative space. diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index 1baa4ae0ef..7fd5e8537a 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -48,7 +48,9 @@ namespace osu.Game.Screens.Footer private FillFlowContainer buttonsFlow = null!; private Container footerContentContainer = null!; private Container hiddenButtonsContainer = null!; + private LogoTrackingContainer logoTrackingContainer = null!; + private IDisposable? logoTracking; // TODO: This has some weird update logic local in this class, but it only works for overlay containers. // This is not what we want. The footer is to be displayed on *screens* with different colour schemes. @@ -145,13 +147,14 @@ namespace osu.Game.Screens.Footer changeLogoDepthDelegate?.Cancel(); changeLogoDepthDelegate = null; - logoTrackingContainer.StartTracking(logo, duration, easing); + logoTracking = logoTrackingContainer.StartTracking(logo, duration, easing); RequestLogoInFront?.Invoke(true); } public void StopTrackingLogo() { - logoTrackingContainer.StopTracking(); + logoTracking?.Dispose(); + logoTracking = null; changeLogoDepthDelegate = Scheduler.AddDelayed(() => RequestLogoInFront?.Invoke(false), transition_duration); } diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 25fa689d4c..4e41f4f35f 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -73,7 +73,8 @@ namespace osu.Game.Screens.Menu else { // We should stop tracking as the facade is now out of scope. - logoTrackingContainer.StopTracking(); + logoTracking?.Dispose(); + logoTracking = null; } } @@ -390,6 +391,7 @@ namespace osu.Game.Screens.Menu } private ScheduledDelegate? logoDelayedAction; + private IDisposable? logoTracking; private void updateLogoState(ButtonSystemState lastState = ButtonSystemState.Initial) { @@ -402,7 +404,8 @@ namespace osu.Game.Screens.Menu logoDelayedAction?.Cancel(); logoDelayedAction = Scheduler.AddDelayed(() => { - logoTrackingContainer.StopTracking(); + logoTracking?.Dispose(); + logoTracking = null; game?.Toolbar.Hide(); @@ -429,7 +432,7 @@ namespace osu.Game.Screens.Menu logo.ScaleTo(0.5f, 200, Easing.In); - logoTrackingContainer.StartTracking(logo, 200, Easing.In); + logoTracking = logoTrackingContainer.StartTracking(logo, 200, Easing.In); logoDelayedAction?.Cancel(); logoDelayedAction = Scheduler.AddDelayed(() => @@ -451,7 +454,7 @@ namespace osu.Game.Screens.Menu break; case ButtonSystemState.EnteringMode: - logoTrackingContainer.StartTracking(logo, lastState == ButtonSystemState.Initial ? MainMenu.FADE_OUT_DURATION : 0, Easing.InSine); + logoTracking = logoTrackingContainer.StartTracking(logo, lastState == ButtonSystemState.Initial ? MainMenu.FADE_OUT_DURATION : 0, Easing.InSine); break; } } diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index d22717abd4..94148c13d0 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -144,6 +144,7 @@ namespace osu.Game.Screens.Play private bool playerConsumed; private LogoTrackingContainer content = null!; + private IDisposable? logoTracking; private bool hideOverlays; @@ -379,21 +380,26 @@ namespace osu.Game.Screens.Play Scheduler.AddDelayed(() => { if (this.IsCurrentScreen()) - content.StartTracking(logo, resuming ? 0 : 500, Easing.InOutExpo); + logoTracking = content.StartTracking(logo, resuming ? 0 : 500, Easing.InOutExpo); }, resuming ? 0 : 250); } protected override void LogoExiting(OsuLogo logo) { base.LogoExiting(logo); - content.StopTracking(); + + logoTracking?.Dispose(); + logoTracking = null; + osuLogo = null; } protected override void LogoSuspending(OsuLogo logo) { base.LogoSuspending(logo); - content.StopTracking(); + + logoTracking?.Dispose(); + logoTracking = null; logo .FadeOut(CONTENT_OUT_DURATION / 2, Easing.OutQuint) @@ -538,7 +544,8 @@ namespace osu.Game.Screens.Play protected virtual void ContentOut() { // Ensure the logo is no longer tracking before we scale the content - content.StopTracking(); + logoTracking?.Dispose(); + logoTracking = null; content.ScaleTo(0.7f, CONTENT_OUT_DURATION * 2, Easing.OutQuint); content.FadeOut(CONTENT_OUT_DURATION, Easing.OutQuint) From a8be9d7b64b4c63c8486c8c3aac7a19b15f79308 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Jun 2025 15:19:26 +0900 Subject: [PATCH 167/370] Update logo proxy operations to use `IDisposable` flow --- osu.Game/Screens/Menu/MainMenu.cs | 11 ++++++++--- osu.Game/Screens/Menu/OsuLogo.cs | 23 +++++++++++------------ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 9b3620d3b2..06f62542f8 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -291,7 +291,7 @@ namespace osu.Game.Screens.Menu logo.FadeColour(Color4.White, 100, Easing.OutQuint); logo.FadeIn(100, Easing.OutQuint); - logo.ProxyToContainer(logoTarget); + logoProxy = logo.ProxyToContainer(logoTarget); if (resuming) { @@ -350,7 +350,8 @@ namespace osu.Game.Screens.Menu var seq = logo.FadeOut(300, Easing.InSine) .ScaleTo(0.2f, 300, Easing.InSine); - logo.ReturnProxy(); + logoProxy?.Dispose(); + logoProxy = null; seq.OnComplete(_ => Buttons.SetOsuLogo(null)); seq.OnAbort(_ => Buttons.SetOsuLogo(null)); @@ -360,7 +361,8 @@ namespace osu.Game.Screens.Menu { base.LogoExiting(logo); - logo.ReturnProxy(); + logoProxy?.Dispose(); + logoProxy = null; } public override void OnSuspending(ScreenTransitionEvent e) @@ -496,6 +498,9 @@ namespace osu.Game.Screens.Menu private IDisposable ssv2Duck; private Sample ssv2Sample; + [CanBeNull] + private IDisposable logoProxy; + private void loadPreferredSongSelect() { if (holdTime >= required_hold_time) diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index c9884dfd10..1b3317b12d 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -472,7 +473,7 @@ namespace osu.Game.Screens.Menu public void StopSamplePlayback() => sampleClickChannel?.Stop(); - public Drawable ProxyToContainer(Container c) + public IDisposable ProxyToContainer(Container c) { if (currentProxyTarget != null) throw new InvalidOperationException("Previous proxy usage was not returned"); @@ -484,21 +485,19 @@ namespace osu.Game.Screens.Menu defaultProxyTarget.Remove(proxy, false); currentProxyTarget.Add(proxy); - return proxy; - } - public void ReturnProxy() - { - if (currentProxyTarget == null) - throw new InvalidOperationException("No usage to return"); + return new InvokeOnDisposal(returnProxy); - if (defaultProxyTarget == null) - throw new InvalidOperationException($"{nameof(SetupDefaultContainer)} must be called first"); + void returnProxy() + { + Debug.Assert(currentProxyTarget != null); + Debug.Assert(defaultProxyTarget != null); - currentProxyTarget.Remove(proxy, false); - currentProxyTarget = null; + currentProxyTarget.Remove(proxy, false); + currentProxyTarget = null; - defaultProxyTarget.Add(proxy); + defaultProxyTarget.Add(proxy); + } } public void SetupDefaultContainer(Container container) From 2b0f1bfc4b609f5311f86f030a2903e657458411 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Thu, 12 Jun 2025 15:45:28 +0900 Subject: [PATCH 168/370] Add missing last sample playback update --- osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs index 33912495b1..f184ad6a03 100644 --- a/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs @@ -76,6 +76,8 @@ namespace osu.Game.Screens.Play.HUD rankUpSample.Play(); else rankDownSample.Play(); + + lastSamplePlaybackTime.Value = Time.Current; } rankDisplay.Rank = r.NewValue; From 8c0e535d41e290b4761ce4a516d7751228aa602d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 12 Jun 2025 09:32:53 +0200 Subject: [PATCH 169/370] Remove leftover xmldoc --- osu.Game/Graphics/Containers/LogoTrackingContainer.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Graphics/Containers/LogoTrackingContainer.cs b/osu.Game/Graphics/Containers/LogoTrackingContainer.cs index 25ad526af6..0c8e44ab5a 100644 --- a/osu.Game/Graphics/Containers/LogoTrackingContainer.cs +++ b/osu.Game/Graphics/Containers/LogoTrackingContainer.cs @@ -64,9 +64,6 @@ namespace osu.Game.Graphics.Containers } } - /// - /// Stops the logo assigned in from tracking the facade's position. - /// /// /// Gets the position that the logo should move to with respect to the . /// Manually performs a conversion of the Facade's position to the Logo's parent's relative space. From 968b5b00825e928f4dd85d4bb9d3e22506c03c28 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Jun 2025 16:48:23 +0900 Subject: [PATCH 170/370] Fix carousel drags being incorrectly handled as background reveal --- osu.Game/Screens/SelectV2/SongSelect.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index c166facab7..8682576573 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -736,10 +736,12 @@ namespace osu.Game.Screens.SelectV2 protected override bool OnMouseDown(MouseDownEvent e) { + var containingInputManager = GetContainingInputManager(); + // I don't know why this works, but it does. // If the carousel panels are hovered, hovered no longer contains the screen. // Maybe there's a better way of doing this, but I couldn't immediately find a good setup. - bool mouseDownPriority = GetContainingInputManager()!.HoveredDrawables.Contains(this); + bool mouseDownPriority = containingInputManager!.HoveredDrawables.Contains(this); // Touch input synthesises right clicks, which allow absolute scroll of the carousel. // For simplicity, disable this functionality on mobile. @@ -749,6 +751,12 @@ namespace osu.Game.Screens.SelectV2 { revealingBackground = Scheduler.AddDelayed(() => { + if (containingInputManager.DraggedDrawable != null) + { + revealingBackground = null; + return; + } + mainContent.ResizeWidthTo(1.2f, 600, Easing.OutQuint); mainContent.ScaleTo(1.2f, 600, Easing.OutQuint); mainContent.FadeOut(200, Easing.OutQuint); From 7cdc296c9ca9185d7407a1601b269165154a6e0c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Jun 2025 17:11:49 +0900 Subject: [PATCH 171/370] Always return previous tracking state before taking out a new tracking operation --- osu.Game/Graphics/Containers/LogoTrackingContainer.cs | 3 +++ osu.Game/Screens/Menu/ButtonSystem.cs | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/LogoTrackingContainer.cs b/osu.Game/Graphics/Containers/LogoTrackingContainer.cs index 0c8e44ab5a..432bd20540 100644 --- a/osu.Game/Graphics/Containers/LogoTrackingContainer.cs +++ b/osu.Game/Graphics/Containers/LogoTrackingContainer.cs @@ -44,6 +44,9 @@ namespace osu.Game.Graphics.Containers if (logo.IsTracking && Logo == null) throw new InvalidOperationException($"Cannot track an instance of {typeof(OsuLogo)} to multiple {typeof(LogoTrackingContainer)}s"); + if (logo.IsTracking) + throw new InvalidOperationException($"A previous tracking operation is still active. Dispose of its return value before starting a new tracking operation."); + Logo = logo; Logo.IsTracking = true; diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 4e41f4f35f..073a0d4021 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -432,6 +432,7 @@ namespace osu.Game.Screens.Menu logo.ScaleTo(0.5f, 200, Easing.In); + logoTracking?.Dispose(); logoTracking = logoTrackingContainer.StartTracking(logo, 200, Easing.In); logoDelayedAction?.Cancel(); @@ -446,7 +447,10 @@ namespace osu.Game.Screens.Menu default: logo.ClearTransforms(targetMember: nameof(Position)); - logoTrackingContainer.StartTracking(logo, 0, Easing.In); + + logoTracking?.Dispose(); + logoTracking = logoTrackingContainer.StartTracking(logo, 0, Easing.In); + logo.ScaleTo(0.5f, 200, Easing.OutQuint); break; } @@ -454,6 +458,7 @@ namespace osu.Game.Screens.Menu break; case ButtonSystemState.EnteringMode: + logoTracking?.Dispose(); logoTracking = logoTrackingContainer.StartTracking(logo, lastState == ButtonSystemState.Initial ? MainMenu.FADE_OUT_DURATION : 0, Easing.InSine); break; } From ebfc3c9ccf8a77f062e04236dc9f8392967007ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Jun 2025 17:27:23 +0900 Subject: [PATCH 172/370] Combine two similar flags into one --- .../Visual/Gameplay/TestSceneGameplayLeaderboard.cs | 2 +- .../Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs | 2 +- .../Multiplayer/TestSceneMultiSpectatorLeaderboard.cs | 2 +- .../TestSceneMultiplayerGameplayLeaderboardTeams.cs | 6 ++++-- .../OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs | 2 +- osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs | 5 +---- 6 files changed, 9 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index f45e6326d1..d026afcd6d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("toggle expanded", () => { if (leaderboard.IsNotNull()) - leaderboard.ForceExpand.Value = !leaderboard.ForceExpand.Value; + leaderboard.CollapseDuringGameplay.Value = !leaderboard.CollapseDuringGameplay.Value; }); AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v); diff --git a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs index 3008edf41f..955737578a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs @@ -167,7 +167,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void TestScoreUpdates() { AddRepeatStep("update state", UpdateUserStatesRandomly, 100); - AddToggleStep("switch compact mode", expanded => Leaderboard!.ForceExpand.Value = expanded); + AddToggleStep("switch compact mode", collapsed => Leaderboard!.CollapseDuringGameplay.Value = collapsed); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs index 131b644dcb..c39708352e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - ForceExpand = { Value = true } + CollapseDuringGameplay = { Value = false } } }); }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs index 40d8650c69..6141820cb7 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs @@ -44,14 +44,16 @@ namespace osu.Game.Tests.Visual.Multiplayer Team2Score = { BindTarget = LeaderboardProvider.TeamScores[1] } }, Add); - LoadComponentAsync(new GameplayMatchScoreDisplay + GameplayMatchScoreDisplay matchScoreDisplay; + LoadComponentAsync(matchScoreDisplay = new GameplayMatchScoreDisplay { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, Team1Score = { BindTarget = LeaderboardProvider.TeamScores[0] }, Team2Score = { BindTarget = LeaderboardProvider.TeamScores[1] }, - Expanded = { BindTarget = Leaderboard!.ForceExpand }, }, Add); + + Leaderboard!.CollapseDuringGameplay.BindValueChanged(_ => matchScoreDisplay.Expanded.Value = !Leaderboard.CollapseDuringGameplay.Value); }); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 06efffbf6e..7ad8bdf454 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -154,7 +154,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate }); leaderboardFlow.Insert(0, Leaderboard = new DrawableGameplayLeaderboard { - ForceExpand = { Value = true } + CollapseDuringGameplay = { Value = false } }); LoadComponentAsync(new GameplayChatDisplay(room) diff --git a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs index 49b46298c9..f8e54efbf2 100644 --- a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs @@ -20,8 +20,6 @@ namespace osu.Game.Screens.Play.HUD { public partial class DrawableGameplayLeaderboard : CompositeDrawable, ISerialisableDrawable { - public readonly Bindable ForceExpand = new Bindable(); - protected readonly FillFlowContainer Flow; private bool requiresScroll; @@ -100,7 +98,6 @@ namespace osu.Game.Screens.Play.HUD configVisibility.BindValueChanged(_ => Scheduler.AddOnce(updateState)); userPlayingState.BindValueChanged(_ => Scheduler.AddOnce(updateState)); holdingForHUD.BindValueChanged(_ => Scheduler.AddOnce(updateState)); - ForceExpand.BindValueChanged(_ => Scheduler.AddOnce(updateState)); CollapseDuringGameplay.BindValueChanged(_ => Scheduler.AddOnce(updateState)); updateState(); } @@ -112,7 +109,7 @@ namespace osu.Game.Screens.Play.HUD scroll.ScrollToStart(false); Flow.FadeTo(player?.Configuration.ShowLeaderboard != false && configVisibility.Value ? 1 : 0, 100, Easing.OutQuint); - expanded.Value = !CollapseDuringGameplay.Value || ForceExpand.Value || userPlayingState.Value != LocalUserPlayingState.Playing || holdingForHUD.Value; + expanded.Value = !CollapseDuringGameplay.Value || userPlayingState.Value != LocalUserPlayingState.Playing || holdingForHUD.Value; } /// From 612f853baabdc2e209de1b2047aac315fe7272c8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Jun 2025 17:30:11 +0900 Subject: [PATCH 173/370] Localise new setting --- .../SkinComponents/SkinnableComponentStrings.cs | 11 +++++++++++ .../Screens/Play/HUD/DrawableGameplayLeaderboard.cs | 3 ++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs index b21446e18a..66abf2bfd5 100644 --- a/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs +++ b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs @@ -84,6 +84,17 @@ namespace osu.Game.Localisation.SkinComponents /// public static LocalisableString UseRelativeSize => new TranslatableString(getKey(@"use_relative_size"), @"Use relative size"); + /// + /// "Collapse during gameplay" + /// + public static LocalisableString CollapseDuringGameplay => new TranslatableString(getKey(@"collapse_during_gameplay"), @"Collapse during gameplay"); + + /// + /// "If enabled, the leaderboard will become more compact during active gameplay." + /// + public static LocalisableString CollapseDuringGameplayDescription => + new TranslatableString(getKey(@"if_enabled_the_leaderboard_will"), @"If enabled, the leaderboard will become more compact during active gameplay."); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs index f8e54efbf2..dd55e5f926 100644 --- a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Game.Configuration; using osu.Game.Graphics.Containers; +using osu.Game.Localisation.SkinComponents; using osu.Game.Screens.Select.Leaderboards; using osu.Game.Skinning; using osuTK; @@ -27,7 +28,7 @@ namespace osu.Game.Screens.Play.HUD public DrawableGameplayLeaderboardScore? TrackedScore { get; private set; } - [SettingSource("Collapse during gameplay", "If enabled, the leaderboard will become more compact during active gameplay.")] + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.CollapseDuringGameplay), nameof(SkinnableComponentStrings.CollapseDuringGameplayDescription))] public Bindable CollapseDuringGameplay { get; } = new BindableBool(true); [Resolved] From ba31cb47861bf9d5166569b4eddaaa403fb5cd38 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Jun 2025 17:33:23 +0900 Subject: [PATCH 174/370] Fix incorrect text spacing in skin editor toolbar Probably regressed with framework flow changes. --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index c1c64cac1f..5ad969e5df 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -436,8 +436,8 @@ namespace osu.Game.Overlays.SkinEditor headerText.Clear(); - headerText.AddParagraph(SkinEditorStrings.SkinEditor, cp => cp.Font = OsuFont.Default.With(size: 16)); - headerText.NewParagraph(); + headerText.AddText(SkinEditorStrings.SkinEditor, cp => cp.Font = OsuFont.Default.With(size: 16)); + headerText.NewLine(); headerText.AddText(SkinEditorStrings.CurrentlyEditing, cp => { cp.Font = OsuFont.Default.With(size: 12); From a38e25115bb03dfdd7311354254bf8dfede9b178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 12 Jun 2025 10:33:42 +0200 Subject: [PATCH 175/370] Pick better initial beatmap status when submitting Addresses https://github.com/ppy/osu/discussions/33291 This is a half-baked RFC because things are awkward. For this to work correctly the submission flow has to do an API request, because one, the local beatmap status has been overwritten with "locally modified", and secondly, even if it *was* there, there's no guarantee that it was actually *up to date*. And if we have to do an API request then there are two choices: - Hard block on the API request and don't show anything until it completes which possibly means waiting at a spinner for several seconds if someone's on bad internet. - Don't block on the API request --- but then there's no guarantee what timing the API request completes at, which means that possibly the user could change the dropdown before the API request completes, and the API request will overwrite their choice, so to prevent that block the dropdown until the request completes. This is what this commit does. --- .../Submission/BeatmapSubmissionScreen.cs | 7 +++++ .../Submission/BeatmapSubmissionSettings.cs | 2 ++ .../Submission/ScreenSubmissionSettings.cs | 27 ++++++++++++++++++- 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 3a9eb2c1b0..03ab23d8e4 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -191,6 +191,13 @@ namespace osu.Game.Screens.Edit.Submission }); completedSample = audio.Samples.Get(@"UI/bss-complete"); + + if (Beatmap.Value.BeatmapSetInfo.OnlineID > 0) + { + var req = new GetBeatmapSetRequest(Beatmap.Value.BeatmapSetInfo.OnlineID); + api.Queue(req); + settings.LatestOnlineStateRequest = req; + } } private void createBeatmapSet() diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs index 8cccc339a6..a1f3861d29 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs @@ -8,6 +8,8 @@ namespace osu.Game.Screens.Edit.Submission { public class BeatmapSubmissionSettings { + public GetBeatmapSetRequest? LatestOnlineStateRequest { get; set; } + public Bindable Target { get; } = new Bindable(); public Bindable NotifyOnDiscussionReplies { get; } = new Bindable(); diff --git a/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs b/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs index 969105b5c6..7b80fdee7d 100644 --- a/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs +++ b/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs @@ -1,16 +1,19 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Localisation; +using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; +using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Overlays; using osuTK; @@ -25,8 +28,11 @@ namespace osu.Game.Screens.Edit.Submission public override LocalisableString? NextStepText => BeatmapSubmissionStrings.ConfirmSubmission; + [Resolved] + private BeatmapSubmissionSettings settings { get; set; } = null!; + [BackgroundDependencyLoader] - private void load(OsuConfigManager configManager, OsuColour colours, BeatmapSubmissionSettings settings) + private void load(OsuConfigManager configManager, OsuColour colours) { configManager.BindWith(OsuSetting.EditorSubmissionNotifyOnDiscussionReplies, settings.NotifyOnDiscussionReplies); configManager.BindWith(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission, loadInBrowserAfterSubmission); @@ -63,6 +69,25 @@ namespace osu.Game.Screens.Edit.Submission }, } }); + + switch (settings.LatestOnlineStateRequest?.CompletionState) + { + case APIRequestCompletionState.Completed: + setSubmissionTargetFromLatestOnlineState(); + break; + + case APIRequestCompletionState.Waiting: + settings.Target.Disabled = true; + settings.LatestOnlineStateRequest.Success += _ => setSubmissionTargetFromLatestOnlineState(); + break; + } + } + + private void setSubmissionTargetFromLatestOnlineState() + { + Debug.Assert(settings.LatestOnlineStateRequest != null); + settings.Target.Disabled = false; + settings.Target.Value = settings.LatestOnlineStateRequest.Response?.Status >= BeatmapOnlineStatus.Pending ? BeatmapSubmissionTarget.Pending : BeatmapSubmissionTarget.WIP; } } } From ee696e32f063000127ba70434e80294e2d9284b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 12 Jun 2025 11:10:43 +0200 Subject: [PATCH 176/370] Delete redundant string interpolation --- osu.Game/Graphics/Containers/LogoTrackingContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/LogoTrackingContainer.cs b/osu.Game/Graphics/Containers/LogoTrackingContainer.cs index 432bd20540..694388b92c 100644 --- a/osu.Game/Graphics/Containers/LogoTrackingContainer.cs +++ b/osu.Game/Graphics/Containers/LogoTrackingContainer.cs @@ -45,7 +45,7 @@ namespace osu.Game.Graphics.Containers throw new InvalidOperationException($"Cannot track an instance of {typeof(OsuLogo)} to multiple {typeof(LogoTrackingContainer)}s"); if (logo.IsTracking) - throw new InvalidOperationException($"A previous tracking operation is still active. Dispose of its return value before starting a new tracking operation."); + throw new InvalidOperationException("A previous tracking operation is still active. Dispose of its return value before starting a new tracking operation."); Logo = logo; Logo.IsTracking = true; From 7e632193f8bf3df7099ee06634d92a661678dc0c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Jun 2025 19:23:27 +0900 Subject: [PATCH 177/370] Fix carousel tests failing randomly depending on order run --- osu.Game.Tests/Resources/TestResources.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index 54204d412a..469bc8ee73 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -104,7 +104,7 @@ namespace osu.Game.Tests.Resources { // Create random metadata, then we can check if sorting works based on these Artist = "Some Artist " + RNG.Next(0, 9), - Title = $"Some Song (set id {setId:000}) {Guid.NewGuid()}", + Title = $"Some Song (set id {setId:000000}) {Guid.NewGuid()}", Author = { Username = "Some Guy " + RNG.Next(0, 9) }, }; From 7dba17f6b878c361c4333f1fd6fa635a6f0266f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Jun 2025 17:43:17 +0900 Subject: [PATCH 178/370] Give better feedback from test assertion There's a flaky test and currently the fail output it not really helpful. This makes it slightly more relevant. --- .../SongSelectV2/BeatmapCarouselTestScene.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index 4976a5312c..8779d66b9f 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -273,16 +273,16 @@ namespace osu.Game.Tests.Visual.SongSelectV2 protected void WaitForSetSelection(int set, int? diff = null) { - AddUntilStep($"selected is set{set}{(diff.HasValue ? $" diff{diff.Value}" : "")}", () => + if (diff != null) { - if (diff != null) - { - return (Carousel.CurrentSelection as BeatmapInfo)? - .Equals(BeatmapSets[set].Beatmaps[diff.Value]) == true; - } - - return BeatmapSets[set].Beatmaps.Contains(Carousel.CurrentSelection); - }); + AddUntilStep($"selected is set{set} diff{diff.Value}", + () => (Carousel.CurrentSelection as BeatmapInfo), + () => Is.EqualTo(BeatmapSets[set].Beatmaps[diff.Value])); + } + else + { + AddUntilStep($"selected is set{set}", () => BeatmapSets[set].Beatmaps.Contains(Carousel.CurrentSelection)); + } } protected IEnumerable GetVisiblePanels() From 054544818c5275f255748cfccca6af3eedbf8a50 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Jun 2025 19:13:15 +0900 Subject: [PATCH 179/370] Add all beatmaps in one go in tests to avoid hundreds of callbacks Just makes debugging easier. --- .../Visual/SongSelectV2/BeatmapCarouselTestScene.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index 8779d66b9f..36b755a071 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -334,8 +334,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2 /// Whether to randomise the metadata to make groupings more uniform. protected void AddBeatmaps(int count, int? fixedDifficultiesPerSet = null, bool randomMetadata = false) => AddStep($"add {count} beatmaps{(randomMetadata ? " with random data" : "")}", () => { + var beatmaps = new List(); + for (int i = 0; i < count; i++) - BeatmapSets.Add(CreateTestBeatmapSetInfo(fixedDifficultiesPerSet, randomMetadata)); + beatmaps.Add(CreateTestBeatmapSetInfo(fixedDifficultiesPerSet, randomMetadata)); + + BeatmapSets.AddRange(beatmaps); }); protected static BeatmapSetInfo CreateTestBeatmapSetInfo(int? fixedDifficultiesPerSet, bool randomMetadata) From d592b984e30aaa68ad96e37737734c50a690cc6d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Jun 2025 18:17:51 +0900 Subject: [PATCH 180/370] Ensure filtering is always waited on after a sort/filter change in tests --- .../SongSelectV2/BeatmapCarouselTestScene.cs | 17 ++-- .../TestSceneBeatmapCarouselArtistGrouping.cs | 6 +- ...tSceneBeatmapCarouselDifficultyGrouping.cs | 8 +- .../TestSceneBeatmapCarouselFiltering.cs | 86 +++++++------------ .../TestSceneBeatmapCarouselNoGrouping.cs | 2 - .../TestSceneBeatmapCarouselRandom.cs | 3 +- .../TestSceneBeatmapCarouselScrolling.cs | 6 +- .../TestSceneBeatmapCarouselUpdateHandling.cs | 4 - 8 files changed, 48 insertions(+), 84 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index 36b755a071..3943b13286 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -150,26 +150,25 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }, }; - Carousel.Filter(new FilterCriteria()); + // Prefer title sorting so that order of carousel panels match order of BeatmapSets bindable. + Carousel.Filter(new FilterCriteria { Sort = SortMode.Title }); }); - - // Prefer title sorting so that order of carousel panels match order of BeatmapSets bindable. - SortBy(SortMode.Title); } - protected void SortBy(SortMode mode) => ApplyToFilter($"sort by {mode.GetDescription().ToLowerInvariant()}", c => c.Sort = mode); - protected void GroupBy(GroupMode mode) => ApplyToFilter($"group by {mode.GetDescription().ToLowerInvariant()}", c => c.Group = mode); + protected void SortBy(SortMode mode) => ApplyToFilterAndWaitForFilter($"sort by {mode.GetDescription().ToLowerInvariant()}", c => c.Sort = mode); + + protected void GroupBy(GroupMode mode) => ApplyToFilterAndWaitForFilter($"group by {mode.GetDescription().ToLowerInvariant()}", c => c.Group = mode); protected void SortAndGroupBy(SortMode sort, GroupMode group) { - ApplyToFilter($"sort by {sort.GetDescription().ToLowerInvariant()} & group by {group.GetDescription().ToLowerInvariant()}", c => + ApplyToFilterAndWaitForFilter($"sort by {sort.GetDescription().ToLowerInvariant()} & group by {group.GetDescription().ToLowerInvariant()}", c => { c.Sort = sort; c.Group = group; }); } - protected void ApplyToFilter(string description, Action? apply) + protected void ApplyToFilterAndWaitForFilter(string description, Action? apply) { AddStep(description, () => { @@ -177,6 +176,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 apply?.Invoke(criteria); Carousel.Filter(criteria); }); + + WaitForFiltering(); } protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs index 0603540c5e..af3bda8928 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs @@ -217,8 +217,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] public void TestBasicFiltering() { - ApplyToFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title); - WaitForFiltering(); + ApplyToFilterAndWaitForFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title); CheckDisplayedGroupsCount(1); CheckDisplayedBeatmapSetsCount(1); @@ -237,8 +236,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 WaitForBeatmapSelection(0, 3); - ApplyToFilter("remove filter", c => c.SearchText = string.Empty); - WaitForFiltering(); + ApplyToFilterAndWaitForFilter("remove filter", c => c.SearchText = string.Empty); CheckDisplayedGroupsCount(5); CheckDisplayedBeatmapSetsCount(10); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs index 3264f7f2ff..52c89d7c4e 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs @@ -196,8 +196,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] public void TestBasicFiltering() { - ApplyToFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title); - WaitForFiltering(); + ApplyToFilterAndWaitForFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title); CheckDisplayedGroupsCount(3); CheckDisplayedBeatmapsCount(3); @@ -218,8 +217,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 WaitForBeatmapSelection(1, 0); - ApplyToFilter("remove filter", c => c.SearchText = string.Empty); - WaitForFiltering(); + ApplyToFilterAndWaitForFilter("remove filter", c => c.SearchText = string.Empty); CheckDisplayedGroupsCount(3); CheckDisplayedBeatmapsCount(30); @@ -240,7 +238,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("expanded group is first", () => (Carousel.ExpandedGroup as StarDifficultyGroupDefinition)?.Difficulty.Stars, () => Is.EqualTo(0)); // doesn't actually filter anything away, but triggers a filter. - ApplyToFilter("filter", c => c.SearchText = "Some"); + ApplyToFilterAndWaitForFilter("filter", c => c.SearchText = "Some"); AddAssert("expanded group is still first", () => (Carousel.ExpandedGroup as StarDifficultyGroupDefinition)?.Difficulty.Stars, () => Is.EqualTo(0)); } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs index 267810ecfa..8ed1b1745e 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs @@ -36,8 +36,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(1)); - ApplyToFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title); - WaitForFiltering(); + ApplyToFilterAndWaitForFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title); AddAssert("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(2)); @@ -52,8 +51,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Select(); WaitForSetSelection(2, 1); - ApplyToFilter("remove filter", c => c.SearchText = string.Empty); - WaitForFiltering(); + ApplyToFilterAndWaitForFilter("remove filter", c => c.SearchText = string.Empty); AddAssert("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(3)); @@ -84,46 +82,39 @@ namespace osu.Game.Tests.Visual.SongSelectV2 WaitForDrawablePanels(); - ApplyToFilter("filter [5..]", c => + ApplyToFilterAndWaitForFilter("filter [5..]", c => { c.UserStarDifficulty.Min = 5; c.UserStarDifficulty.Max = null; }); - WaitForFiltering(); CheckDisplayedBeatmapsCount(11); - ApplyToFilter("filter to [0..7]", c => + ApplyToFilterAndWaitForFilter("filter to [0..7]", c => { c.UserStarDifficulty.Min = null; c.UserStarDifficulty.Max = 7; }); - WaitForFiltering(); CheckDisplayedBeatmapsCount(7); - ApplyToFilter("filter to [5..7]", c => + ApplyToFilterAndWaitForFilter("filter to [5..7]", c => { c.UserStarDifficulty.Min = 5; c.UserStarDifficulty.Max = 7; }); - - WaitForFiltering(); CheckDisplayedBeatmapsCount(3); - ApplyToFilter("filter to [2..2]", c => + ApplyToFilterAndWaitForFilter("filter to [2..2]", c => { c.UserStarDifficulty.Min = 2; c.UserStarDifficulty.Max = 2; }); - - WaitForFiltering(); CheckDisplayedBeatmapsCount(1); - ApplyToFilter("filter to [0..]", c => + ApplyToFilterAndWaitForFilter("filter to [0..]", c => { c.UserStarDifficulty.Min = 0; c.UserStarDifficulty.Max = null; }); - WaitForFiltering(); CheckDisplayedBeatmapsCount(15); } @@ -143,9 +134,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 for (int i = 0; i < 5; i++) { - ApplyToFilter("filter all", c => c.SearchText = Guid.NewGuid().ToString()); + ApplyToFilterAndWaitForFilter("filter all", c => c.SearchText = Guid.NewGuid().ToString()); AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID); - ApplyToFilter("remove filter", c => c.SearchText = string.Empty); + ApplyToFilterAndWaitForFilter("remove filter", c => c.SearchText = string.Empty); AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID); } } @@ -159,7 +150,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectPrevSet(); WaitForSetSelection(49, 0); - ApplyToFilter("filter all but one", c => c.SearchText = BeatmapSets.First().Metadata.Title); + ApplyToFilterAndWaitForFilter("filter all but one", c => c.SearchText = BeatmapSets.First().Metadata.Title); WaitForSetSelection(0, 0); } @@ -170,7 +161,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 WaitForDrawablePanels(); CheckNoSelection(); - ApplyToFilter("filter all but one", c => c.SearchText = BeatmapSets.First().Metadata.Title); + ApplyToFilterAndWaitForFilter("filter all but one", c => c.SearchText = BeatmapSets.First().Metadata.Title); WaitForSetSelection(0, 0); } @@ -190,9 +181,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 for (int i = 0; i < 5; i++) { - ApplyToFilter("filter all", c => c.SearchText = Guid.NewGuid().ToString()); + ApplyToFilterAndWaitForFilter("filter all", c => c.SearchText = Guid.NewGuid().ToString()); AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID); - ApplyToFilter("remove filter", c => c.SearchText = string.Empty); + ApplyToFilterAndWaitForFilter("remove filter", c => c.SearchText = string.Empty); AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID); } } @@ -223,10 +214,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] public void TestExternalRulesetChange() { - ApplyToFilter("allow converted beatmaps", c => c.AllowConvertedBeatmaps = true); - ApplyToFilter("filter to osu", c => c.Ruleset = rulesets.AvailableRulesets.ElementAt(0)); - - WaitForFiltering(); + ApplyToFilterAndWaitForFilter("allow converted beatmaps, filter to osu", c => + { + c.AllowConvertedBeatmaps = true; + c.Ruleset = rulesets.AvailableRulesets.ElementAt(0); + }); AddStep("add mixed ruleset beatmapset", () => { @@ -250,9 +242,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 && visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 0) == 1; }); - ApplyToFilter("filter to taiko", c => c.Ruleset = rulesets.AvailableRulesets.ElementAt(1)); - - WaitForFiltering(); + ApplyToFilterAndWaitForFilter("filter to taiko", c => c.Ruleset = rulesets.AvailableRulesets.ElementAt(1)); AddUntilStep("wait for filtered difficulties", () => { @@ -263,9 +253,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 && visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 1) == 1; }); - ApplyToFilter("filter to catch", c => c.Ruleset = rulesets.AvailableRulesets.ElementAt(2)); - - WaitForFiltering(); + ApplyToFilterAndWaitForFilter("filter to catch", c => c.Ruleset = rulesets.AvailableRulesets.ElementAt(2)); AddUntilStep("wait for filtered difficulties", () => { @@ -297,17 +285,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }); SortBy(SortMode.Difficulty); - WaitForFiltering(); CheckDisplayedBeatmapsCount(local_set_count * diffs_per_set); - ApplyToFilter("filter to normal", c => c.SearchText = "Normal"); - WaitForFiltering(); + ApplyToFilterAndWaitForFilter("filter to normal", c => c.SearchText = "Normal"); CheckDisplayedBeatmapsCount(local_set_count); - ApplyToFilter("filter to insane", c => c.SearchText = "Insane"); - WaitForFiltering(); + ApplyToFilterAndWaitForFilter("filter to insane", c => c.SearchText = "Insane"); CheckDisplayedBeatmapsCount(local_set_count); } @@ -323,8 +308,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 CheckDisplayedBeatmapsCount(6); - ApplyToFilter("filter first away", c => c.UserStarDifficulty.Min = 3); - WaitForFiltering(); + ApplyToFilterAndWaitForFilter("filter first away", c => c.UserStarDifficulty.Min = 3); CheckDisplayedBeatmapsCount(4); @@ -345,8 +329,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextSet(); WaitForSetSelection(2, 0); - ApplyToFilter("filter first away", c => c.UserStarDifficulty.Min = 3); - WaitForFiltering(); + ApplyToFilterAndWaitForFilter("filter first away", c => c.UserStarDifficulty.Min = 3); SelectNextSet(); WaitForSetSelection(0, 1); @@ -363,8 +346,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextSet(); WaitForSetSelection(2, 0); - ApplyToFilter("filter first away", c => c.UserStarDifficulty.Min = 3); - WaitForFiltering(); + ApplyToFilterAndWaitForFilter("filter first away", c => c.UserStarDifficulty.Min = 3); SelectPrevSet(); WaitForSetSelection(4, 1); @@ -379,8 +361,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectPrevSet(); WaitForSetSelection(1, 0); - ApplyToFilter("filter last set away", c => c.SearchText = BeatmapSets.First().Metadata.Title); - WaitForFiltering(); + ApplyToFilterAndWaitForFilter("filter last set away", c => c.SearchText = BeatmapSets.First().Metadata.Title); SelectPrevSet(); WaitForSetSelection(0, 0); @@ -395,8 +376,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextSet(); WaitForSetSelection(0, 0); - ApplyToFilter("filter first set away", c => c.SearchText = BeatmapSets.Last().Metadata.Title); - WaitForFiltering(); + ApplyToFilterAndWaitForFilter("filter first set away", c => c.SearchText = BeatmapSets.Last().Metadata.Title); SelectNextSet(); WaitForSetSelection(1, 0); @@ -413,8 +393,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextSet(); WaitForSetSelection(2, 0); - ApplyToFilter("filter first away", c => c.UserStarDifficulty.Min = 3); - WaitForFiltering(); + ApplyToFilterAndWaitForFilter("filter first away", c => c.UserStarDifficulty.Min = 3); SelectNextPanel(); AddAssert("keyboard selected is first set", () => GetKeyboardSelectedPanel()?.Item?.Model, () => Is.EqualTo(BeatmapSets.First())); @@ -431,8 +410,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextSet(); WaitForSetSelection(2, 0); - ApplyToFilter("filter first away", c => c.UserStarDifficulty.Min = 3); - WaitForFiltering(); + ApplyToFilterAndWaitForFilter("filter first away", c => c.UserStarDifficulty.Min = 3); SelectPrevPanel(); AddAssert("keyboard selected is last set", () => GetKeyboardSelectedPanel()?.Item?.Model, () => Is.EqualTo(BeatmapSets.Last())); @@ -447,8 +425,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectPrevSet(); WaitForSetSelection(1, 0); - ApplyToFilter("filter last set away", c => c.SearchText = BeatmapSets.First().Metadata.Title); - WaitForFiltering(); + ApplyToFilterAndWaitForFilter("filter last set away", c => c.SearchText = BeatmapSets.First().Metadata.Title); SelectPrevPanel(); AddAssert("keyboard selected is first set", () => GetKeyboardSelectedPanel()?.Item?.Model, () => Is.EqualTo(BeatmapSets.First())); @@ -463,8 +440,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextSet(); WaitForSetSelection(0, 0); - ApplyToFilter("filter first set away", c => c.SearchText = BeatmapSets.Last().Metadata.Title); - WaitForFiltering(); + ApplyToFilterAndWaitForFilter("filter first set away", c => c.SearchText = BeatmapSets.Last().Metadata.Title); // Single result is automatically selected for us, so we iterate once backwards to the set header. SelectPrevPanel(); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs index 6ca02e57a5..a6ba6d76a3 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs @@ -290,7 +290,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 WaitForDrawablePanels(); SortAndGroupBy(SortMode.Difficulty, GroupMode.None); - WaitForFiltering(); AddUntilStep("standalone panels displayed", () => GetVisiblePanels().Any()); @@ -314,7 +313,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 WaitForDrawablePanels(); SortBy(SortMode.Difficulty); - WaitForFiltering(); AddUntilStep("standalone panels displayed", () => GetVisiblePanels().Count(), () => Is.EqualTo(3)); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs index e1d25c51ac..60cec0c2ec 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs @@ -24,8 +24,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { AddBeatmaps(2, 10, true); - ApplyToFilter("filter", c => c.SearchText = BeatmapSets[0].Beatmaps.Last().DifficultyName); - WaitForFiltering(); + ApplyToFilterAndWaitForFilter("filter", c => c.SearchText = BeatmapSets[0].Beatmaps.Last().DifficultyName); CheckDisplayedBeatmapSetsCount(1); CheckDisplayedBeatmapsCount(1); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs index 383ec47a69..29aa976fe3 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs @@ -97,8 +97,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("scroll to end", () => Scroll.ScrollToEnd()); WaitForScrolling(); - ApplyToFilter("search", f => f.SearchText = "Some"); - WaitForFiltering(); + ApplyToFilterAndWaitForFilter("search", f => f.SearchText = "Some"); AddUntilStep("select screen position returned to selection", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, () => Is.EqualTo(positionBefore)); @@ -121,8 +120,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); - ApplyToFilter("search", f => f.SearchText = "Some"); - WaitForFiltering(); + ApplyToFilterAndWaitForFilter("search", f => f.SearchText = "Some"); AddUntilStep("select screen position returned to selection", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, () => Is.EqualTo(positionBefore)); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs index 1ec5b37f0e..fdc9cc93a5 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs @@ -172,7 +172,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Guid[] originalOrder = null!; SortBy(SortMode.Artist); - WaitForFiltering(); AddAssert("Items in descending added order", () => Carousel.PostFilterBeatmaps.Select(b => b.BeatmapSet!.DateAdded), () => Is.Ordered.Descending); AddStep("Save order", () => originalOrder = Carousel.PostFilterBeatmaps.Select(b => b.ID).ToArray()); @@ -188,7 +187,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("Order didn't change", () => Carousel.PostFilterBeatmaps.Select(b => b.ID), () => Is.EqualTo(originalOrder)); SortBy(SortMode.Title); - WaitForFiltering(); AddAssert("Order didn't change", () => Carousel.PostFilterBeatmaps.Select(b => b.ID), () => Is.EqualTo(originalOrder)); } @@ -225,7 +223,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Guid[] originalOrder = null!; SortBy(SortMode.Artist); - WaitForFiltering(); AddAssert("Items in descending added order", () => Carousel.PostFilterBeatmaps.Select(b => b.BeatmapSet!.DateAdded), () => Is.Ordered.Descending); AddStep("Save order", () => originalOrder = Carousel.PostFilterBeatmaps.Select(b => b.ID).ToArray()); @@ -252,7 +249,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("Order didn't change", () => Carousel.PostFilterBeatmaps.Select(b => b.ID), () => Is.EqualTo(originalOrder)); SortBy(SortMode.Title); - WaitForFiltering(); AddAssert("Order didn't change", () => Carousel.PostFilterBeatmaps.Select(b => b.ID), () => Is.EqualTo(originalOrder)); } From 3816c5d95f3ec88e0ab4a0ebf433d0740de86931 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Jun 2025 19:15:51 +0900 Subject: [PATCH 181/370] Add support for traversing between groups using shift-left/right Closes https://github.com/ppy/osu/issues/33599. --- .../TestSceneBeatmapCarouselArtistGrouping.cs | 10 +++- ...tSceneBeatmapCarouselDifficultyGrouping.cs | 58 +++++++++++++++++-- osu.Game/Graphics/Carousel/Carousel.cs | 42 ++++++++++++-- .../Input/Bindings/GlobalActionContainer.cs | 9 +++ .../GlobalActionKeyBindingStrings.cs | 10 ++++ osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 2 + 6 files changed, 120 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs index af3bda8928..521221f0c7 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs @@ -114,17 +114,25 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectPrevPanel(); SelectPrevPanel(); + ICarouselPanel? groupPanel = null; + + AddStep("get group panel", () => groupPanel = GetKeyboardSelectedPanel()); + AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); + AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, () => Is.EqualTo(groupPanel)); SelectPrevSet(); WaitForBeatmapSelection(0, 1); AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); + AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, () => Is.EqualTo(groupPanel)); SelectPrevSet(); WaitForBeatmapSelection(0, 1); - AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); + // Expanding a group will move keyboard selection to the selected beatmap if contained. + AddAssert("keyboard selected panel is expanded", () => groupPanel?.Expanded.Value, () => Is.True); + AddAssert("keyboard selected panel is beatmap", () => GetKeyboardSelectedPanel()?.Item?.Model, Is.TypeOf); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs index 52c89d7c4e..2c127f80a9 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs @@ -102,19 +102,70 @@ namespace osu.Game.Tests.Visual.SongSelectV2 WaitForBeatmapSelection(0, 0); SelectPrevPanel(); + + ICarouselPanel? groupPanel = null; + + AddStep("get group panel", () => groupPanel = GetKeyboardSelectedPanel()); + AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); + AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, () => Is.EqualTo(groupPanel)); SelectPrevSet(); WaitForBeatmapSelection(0, 0); AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); + AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, () => Is.EqualTo(groupPanel)); SelectPrevSet(); WaitForBeatmapSelection(0, 0); - AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); + // Expanding a group will move keyboard selection to the selected beatmap if contained. + AddAssert("keyboard selected panel is expanded", () => groupPanel?.Expanded.Value, () => Is.True); + AddAssert("keyboard selected panel is beatmap", () => GetKeyboardSelectedPanel()?.Item?.Model, Is.TypeOf); } + [Test] + public void TestKeyboardGroupTraversal() + { + SelectNextSet(); + WaitForBeatmapSelection(0, 0); + checkBeatmapIsKeyboardSelected(); + + SelectNextGroup(); + WaitForBeatmapSelection(0, 0); + WaitForExpandedGroup(1); + checkGroupKeyboardSelected(1); + + SelectNextGroup(); + WaitForBeatmapSelection(0, 0); + WaitForExpandedGroup(2); + checkGroupKeyboardSelected(2); + + SelectNextGroup(); + WaitForBeatmapSelection(0, 0); + WaitForExpandedGroup(0); + checkBeatmapIsKeyboardSelected(); + + SelectPrevGroup(); + WaitForBeatmapSelection(0, 0); + WaitForExpandedGroup(2); + checkGroupKeyboardSelected(2); + } + + private void checkBeatmapIsKeyboardSelected() => + AddUntilStep("check keyboard selected group is beatmap", () => GetKeyboardSelectedPanel()?.Item?.Model, () => Is.EqualTo(Carousel.CurrentSelection)); + + private void checkGroupKeyboardSelected(int index) => AddUntilStep($"check keyboard selected group is {index}", () => GetKeyboardSelectedPanel()?.Item?.Model, () => + { + var groupingFilter = Carousel.Filters.OfType().Single(); + + GroupDefinition g = groupingFilter.GroupItems.Keys.ElementAt(index); + // offset by one because the group itself is included in the items list. + CarouselItem item = groupingFilter.GroupItems[g].ElementAt(0); + + return Is.EqualTo(item.Model); + }); + [Test] public void TestGroupSelectionOnHeaderMouse() { @@ -129,9 +180,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); ClickVisiblePanel(0); - AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf); - AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); - + // Expanding a group will move keyboard selection to the selected beatmap if contained. + AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf); AddAssert("selected panel is still beatmap", GetSelectedPanel, Is.TypeOf); } diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index ab3e860f8b..deadb4f288 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -233,6 +233,13 @@ namespace osu.Game.Graphics.Carousel protected Drawable? GetMaterialisedDrawableForItem(CarouselItem item) => Scroll.Panels.SingleOrDefault(p => ((ICarouselPanel)p).Item == item); + /// + /// When a user is traversing the carousel via group selection keys, assert whether the item provided is a valid target. + /// + /// The candidate item. + /// Whether the provided item is a valid group target. If false, more panels will be checked in the user's requested direction until a valid target is found. + protected virtual bool CheckValidForGroupSelection(CarouselItem item) => false; + /// /// When a user is traversing the carousel via set selection keys, assert whether the item provided is a valid target. /// @@ -467,6 +474,14 @@ namespace osu.Game.Graphics.Carousel case GlobalAction.ActivatePreviousSet: Scheduler.AddOnce(traverseSetSelection, -1); return true; + + case GlobalAction.ExpandPreviousGroup: + Scheduler.AddOnce(traverseGroupSelection, -1); + return true; + + case GlobalAction.ExpandNextGroup: + Scheduler.AddOnce(traverseGroupSelection, 1); + return true; } return false; @@ -520,23 +535,38 @@ namespace osu.Game.Graphics.Carousel } /// - /// Select the next valid selection relative to a current selection. + /// Select the next valid group selection relative to a current selection. + /// This is generally for keyboard based traversal. + /// + /// Positive for downwards, negative for upwards. + /// Whether selection was possible. + private void traverseGroupSelection(int direction) => traverseSelection(direction, CheckValidForGroupSelection); + + /// + /// Select the next valid set selection relative to a current selection. /// This is generally for keyboard based traversal. /// /// Positive for downwards, negative for upwards. /// Whether selection was possible. private void traverseSetSelection(int direction) { - if (carouselItems == null || carouselItems.Count == 0) return; - // If the user has a different keyboard selection and requests // set selection, first transfer the keyboard selection to actual selection. + // + // It is assumed that selecting a set will immediately change selection to one of its children. if (currentKeyboardSelection.CarouselItem != null && currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) { Activate(currentKeyboardSelection.CarouselItem); return; } + traverseSelection(direction, CheckValidForSetSelection); + } + + private void traverseSelection(int direction, Func predicate) + { + if (carouselItems == null || carouselItems.Count == 0) return; + int originalIndex; int newIndex; @@ -553,7 +583,7 @@ namespace osu.Game.Graphics.Carousel // make sure to go back to the set header this item belongs to, so that the block below doesn't find it and stop too early. if (direction < 0) { - while (newIndex > 0 && !CheckValidForSetSelection(carouselItems[newIndex])) + while (newIndex > 0 && !predicate(carouselItems[newIndex])) newIndex--; } } @@ -569,9 +599,9 @@ namespace osu.Game.Graphics.Carousel var newItem = carouselItems[newIndex]; - if (CheckValidForSetSelection(newItem)) + if (predicate(newItem)) { - HandleItemActivated(newItem); + Activate(newItem); return; } } while (true); diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 83c2af5d73..db7d1158e4 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -199,6 +199,9 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.Left, GlobalAction.ActivatePreviousSet), new KeyBinding(InputKey.Right, GlobalAction.ActivateNextSet), + new KeyBinding(new[] { InputKey.Shift, InputKey.Left }, GlobalAction.ExpandPreviousGroup), + new KeyBinding(new[] { InputKey.Shift, InputKey.Right }, GlobalAction.ExpandNextGroup), + new KeyBinding(InputKey.F1, GlobalAction.ToggleModSelection), new KeyBinding(InputKey.F2, GlobalAction.SelectNextRandom), new KeyBinding(new[] { InputKey.Shift, InputKey.F2 }, GlobalAction.SelectPreviousRandom), @@ -506,6 +509,12 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorDiscardUnsavedChanges))] EditorDiscardUnsavedChanges, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ExpandPreviousGroup))] + ExpandPreviousGroup, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ExpandNextGroup))] + ExpandNextGroup, } public enum GlobalActionCategory diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 4401efaced..994f924d6b 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -139,6 +139,16 @@ namespace osu.Game.Localisation /// public static LocalisableString ActivateNextSet => new TranslatableString(getKey(@"activate_next_set"), @"Activate next set"); + /// + /// "Expand previous group" + /// + public static LocalisableString ExpandPreviousGroup => new TranslatableString(getKey(@"expand_previous_group"), @"Expand previous group"); + + /// + /// "Expand next group" + /// + public static LocalisableString ExpandNextGroup => new TranslatableString(getKey(@"expand_next_group"), @"Expand next group"); + /// /// "Home" /// diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 4d066e0323..5031e02443 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -339,6 +339,8 @@ namespace osu.Game.Screens.SelectV2 RequestRecommendedSelection(beatmaps); } + protected override bool CheckValidForGroupSelection(CarouselItem item) => item.Model is GroupDefinition; + protected override bool CheckValidForSetSelection(CarouselItem item) { switch (item.Model) From f82cb6d76798fb0491ddca0ef9ff194919c652d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Jun 2025 19:49:33 +0900 Subject: [PATCH 182/370] Fix textbox shift-left/right conflicting with new group changing bindings --- osu.Game/Screens/SelectV2/FilterControl.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/SelectV2/FilterControl.cs b/osu.Game/Screens/SelectV2/FilterControl.cs index 4700842a96..c0ccf0ab93 100644 --- a/osu.Game/Screens/SelectV2/FilterControl.cs +++ b/osu.Game/Screens/SelectV2/FilterControl.cs @@ -279,6 +279,10 @@ namespace osu.Game.Screens.SelectV2 public override bool OnPressed(KeyBindingPressEvent e) { + // Conflicts with default group navigation keys (shift-left shift-right). + if (e.Action == PlatformAction.SelectBackwardChar || e.Action == PlatformAction.SelectForwardChar) + return false; + // the "cut" platform key binding (shift-delete) conflicts with the beatmap deletion action. if (e.Action == PlatformAction.Cut && e.ShiftPressed && e.CurrentState.Keyboard.Keys.IsPressed(Key.Delete)) return false; From 52ba22ca1f0f298332b46fc731947212799111c6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Jun 2025 19:57:20 +0900 Subject: [PATCH 183/370] Add support for toggling current group expansion using shift-enter Closes https://github.com/ppy/osu/issues/33617. --- .../SongSelectV2/BeatmapCarouselTestScene.cs | 7 +++++ ...tSceneBeatmapCarouselDifficultyGrouping.cs | 31 +++++++++++++++++++ osu.Game/Graphics/Carousel/Carousel.cs | 29 +++++++++++++---- .../Input/Bindings/GlobalActionContainer.cs | 5 +++ .../GlobalActionKeyBindingStrings.cs | 5 +++ osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 5 +++ 6 files changed, 76 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index 3943b13286..fc5c09ecef 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -184,6 +184,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2 protected void WaitForFiltering() => AddUntilStep("filtering finished", () => Carousel.IsFiltering, () => Is.False); protected void WaitForScrolling() => AddUntilStep("scroll finished", () => Scroll.Current, () => Is.EqualTo(Scroll.Target)); + protected void ToggleGroupCollapse() => AddStep("toggle group collapse", () => + { + InputManager.PressKey(Key.ShiftLeft); + InputManager.Key(Key.Enter); + InputManager.ReleaseKey(Key.ShiftLeft); + }); + protected void SelectNextGroup() => AddStep("select next group", () => { InputManager.PressKey(Key.ShiftLeft); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs index 2c127f80a9..5a03e05344 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs @@ -124,6 +124,37 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("keyboard selected panel is beatmap", () => GetKeyboardSelectedPanel()?.Item?.Model, Is.TypeOf); } + [Test] + public void TestKeyboardGroupToggleCollapse_SelectionContained() + { + SelectNextSet(); + WaitForBeatmapSelection(0, 0); + checkBeatmapIsKeyboardSelected(); + + ToggleGroupCollapse(); + checkGroupKeyboardSelected(0); + + ToggleGroupCollapse(); + checkBeatmapIsKeyboardSelected(); + } + + [Test] + public void TestKeyboardGroupToggleCollapse_SelectionNotContained() + { + SelectNextSet(); + WaitForBeatmapSelection(0, 0); + checkBeatmapIsKeyboardSelected(); + + SelectNextGroup(); + checkGroupKeyboardSelected(1); + + ToggleGroupCollapse(); + checkGroupKeyboardSelected(1); + + ToggleGroupCollapse(); + checkGroupKeyboardSelected(1); + } + [Test] public void TestKeyboardGroupTraversal() { diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index deadb4f288..231b2958c6 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -482,6 +482,20 @@ namespace osu.Game.Graphics.Carousel case GlobalAction.ExpandNextGroup: Scheduler.AddOnce(traverseGroupSelection, 1); return true; + + case GlobalAction.ToggleCurrentGroup: + if (currentKeyboardSelection.CarouselItem != null && CheckValidForGroupSelection(currentKeyboardSelection.CarouselItem)) + { + // If keyboard selection is a group, toggle group and then change keyboard selection to actual selection. + Activate(currentKeyboardSelection.CarouselItem); + } + else + { + // If current keyboard selection is not a group, toggle closest group and move keyboard selection to that group. + traverseSelection(-1, CheckValidForGroupSelection, false); + } + + return true; } return false; @@ -563,7 +577,7 @@ namespace osu.Game.Graphics.Carousel traverseSelection(direction, CheckValidForSetSelection); } - private void traverseSelection(int direction, Func predicate) + private void traverseSelection(int direction, Func predicate, bool skipFirst = true) { if (carouselItems == null || carouselItems.Count == 0) return; @@ -579,12 +593,15 @@ namespace osu.Game.Graphics.Carousel { newIndex = originalIndex = currentKeyboardSelection.Index.Value; - // As a second special case, if we're set selecting backwards and the current selection isn't a set, - // make sure to go back to the set header this item belongs to, so that the block below doesn't find it and stop too early. - if (direction < 0) + if (skipFirst) { - while (newIndex > 0 && !predicate(carouselItems[newIndex])) - newIndex--; + // As a second special case, if we're set selecting backwards and the current selection isn't a set, + // make sure to go back to the set header this item belongs to, so that the block below doesn't find it and stop too early. + if (direction < 0) + { + while (newIndex > 0 && !predicate(carouselItems[newIndex])) + newIndex--; + } } } diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index db7d1158e4..2aeb73d6c5 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -202,6 +202,8 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Shift, InputKey.Left }, GlobalAction.ExpandPreviousGroup), new KeyBinding(new[] { InputKey.Shift, InputKey.Right }, GlobalAction.ExpandNextGroup), + new KeyBinding(new[] { InputKey.Shift, InputKey.Enter }, GlobalAction.ToggleCurrentGroup), + new KeyBinding(InputKey.F1, GlobalAction.ToggleModSelection), new KeyBinding(InputKey.F2, GlobalAction.SelectNextRandom), new KeyBinding(new[] { InputKey.Shift, InputKey.F2 }, GlobalAction.SelectPreviousRandom), @@ -515,6 +517,9 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ExpandNextGroup))] ExpandNextGroup, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleCurrentGroup))] + ToggleCurrentGroup, } public enum GlobalActionCategory diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 994f924d6b..9c484a5cb0 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -149,6 +149,11 @@ namespace osu.Game.Localisation /// public static LocalisableString ExpandNextGroup => new TranslatableString(getKey(@"expand_next_group"), @"Expand next group"); + /// + /// "Toggle expansion of current group" + /// + public static LocalisableString ToggleCurrentGroup => new TranslatableString(getKey(@"toggle_current_group"), @"Toggle expansion of current group"); + /// /// "Home" /// diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 5031e02443..9f749ca2d2 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -230,6 +230,11 @@ namespace osu.Game.Screens.SelectV2 } setExpandedGroup(group); + + // If the active selection is within this group, it should get keyboard focus immediately. + if (CurrentSelectionItem?.IsVisible == true && CurrentSelection is BeatmapInfo info) + RequestSelection(info); + return; case BeatmapSetInfo setInfo: From e59f9b1aa7bada9ada95c7d89cdcf81359a3a005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 12 Jun 2025 13:37:41 +0200 Subject: [PATCH 184/370] Add failing test scene --- .../Visual/Gameplay/TestSceneReplayPlayer.cs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs index 81dd23661c..5db7a78983 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs @@ -6,11 +6,15 @@ using NUnit.Framework; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Replays; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Resources; +using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay @@ -157,6 +161,37 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("Jumped forwards", () => Player.GameplayClockContainer.CurrentTime - lastTime > 500); } + [Test] + public void TestReplayDoesNotFailUntilRunningOutOfFrames() + { + var score = new Score + { + ScoreInfo = TestResources.CreateTestScoreInfo(Beatmap.Value.BeatmapInfo), + Replay = new Replay + { + Frames = + { + new OsuReplayFrame(0, Vector2.Zero), + new OsuReplayFrame(10000, Vector2.Zero), + } + } + }; + score.ScoreInfo.Mods = []; + score.ScoreInfo.Rank = ScoreRank.F; + AddStep("set global state", () => + { + Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); + Ruleset.Value = Beatmap.Value.BeatmapInfo.Ruleset; + SelectedMods.Value = score.ScoreInfo.Mods; + }); + AddStep("create player", () => Player = new TestReplayPlayer(score, showResults: false)); + AddStep("load player", () => LoadScreen(Player)); + AddUntilStep("wait for loaded", () => Player.IsCurrentScreen()); + AddStep("seek to 8000", () => Player.Seek(8000)); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); + AddAssert("player failed after 10000", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(10000)); + } + private void loadPlayerWithBeatmap(IBeatmap? beatmap = null) { AddStep("create player", () => From 4fd2a488b7a8b4158e7a610cc3cf92efbe4a6ab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Jun 2025 08:39:49 +0200 Subject: [PATCH 185/370] Floor star rating to 2 decimal places rather than rounding Rounding semi-regularly confuses users who aim for star rating pass / FC medals and then feel they have been cheated out of a medal because they passed an "X-star beatmap", but the actual star rating of the beatmap is slightly under X. The latest instance of this can be found at https://osu.ppy.sh/community/forums/topics/2091333?n=2. The relevant beatmap there is https://osu.ppy.sh/beatmapsets/2162554#osu/4746232, whose raw star rating is 6.9976070253117344. The other direction would be to fix the star rating medals instead, but I think this is more reasonable given we already do similar things to accuracy displays. --- osu.Game.Tests/NonVisual/FormatUtilsTest.cs | 13 +++++++++++++ osu.Game/Utils/FormatUtils.cs | 11 ++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/NonVisual/FormatUtilsTest.cs b/osu.Game.Tests/NonVisual/FormatUtilsTest.cs index a12658bd8b..0fcf754cf6 100644 --- a/osu.Game.Tests/NonVisual/FormatUtilsTest.cs +++ b/osu.Game.Tests/NonVisual/FormatUtilsTest.cs @@ -21,5 +21,18 @@ namespace osu.Game.Tests.NonVisual { Assert.AreEqual(expectedOutput, input.FormatAccuracy().ToString()); } + + [TestCase(3, "3.00")] + [TestCase(3.3, "3.30")] + [TestCase(3.55, "3.55")] + [TestCase(3.553, "3.55")] + [TestCase(3.557, "3.55")] + [TestCase(3.9999, "3.99")] + [TestCase(3.999999, "3.99")] + [TestCase(4, "4.00")] + public void TestStarRatingFormatting(double input, string expectedOutput) + { + Assert.AreEqual(expectedOutput, input.FormatStarRating().ToString()); + } } } diff --git a/osu.Game/Utils/FormatUtils.cs b/osu.Game/Utils/FormatUtils.cs index f7250c6833..fa7d6595e9 100644 --- a/osu.Game/Utils/FormatUtils.cs +++ b/osu.Game/Utils/FormatUtils.cs @@ -36,7 +36,16 @@ namespace osu.Game.Utils /// Formats the supplied star rating in a consistent, simplified way. /// /// The star rating to be formatted. - public static LocalisableString FormatStarRating(this double starRating) => starRating.ToLocalisableString("0.00"); + public static LocalisableString FormatStarRating(this double starRating) + { + // for the sake of display purposes, we don't want to show a user a "rounded up" star rating to the next whole number. + // i.e. a beatmap which has a star rating of 6.9999* should never show as 7.00*. + // this matters for star rating medals which use hard cutoffs at whole numbers, + // which then confuses users when they beat a 6.9999* beatmap but don't get the 7-star medal. + starRating = Math.Floor(starRating * 100) / 100; + + return starRating.ToLocalisableString("0.00"); + } /// /// Finds the number of digits after the decimal. From eda489b911a1f8dbcf718f26eb21a396283f6870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Jun 2025 08:58:01 +0200 Subject: [PATCH 186/370] Adjust implementation of star rating filtering in song select to new star rating flooring behaviour Required for things to not be broken after the previous change. --- .../Filtering/FilterQueryParserTest.cs | 34 ++++++++++++------- .../Select/Carousel/CarouselBeatmap.cs | 3 +- osu.Game/Screens/Select/FilterQueryParser.cs | 8 ++++- .../SelectV2/BeatmapCarouselFilterMatching.cs | 3 +- osu.Game/Utils/FormatUtils.cs | 14 ++++---- 5 files changed, 41 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index f4e324d7ba..37b7d71d2b 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -85,16 +85,6 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.That(filterCriteria.SearchTerms[0].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.IsolatedPhrase)); } - /* - * The following tests have been written a bit strangely (they don't check exact - * bound equality with what the filter says). - * This is to account for floating-point arithmetic issues. - * For example, specifying a bpm<140 filter would previously match beatmaps with BPM - * of 139.99999, which would be displayed in the UI as 140. - * Due to this the tests check the last tick inside the range and the first tick - * outside of the range. - */ - [TestCase("star")] [TestCase("stars")] public void TestApplyStarQueries(string variant) @@ -105,11 +95,31 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.AreEqual("easy", filterCriteria.SearchText.Trim()); Assert.AreEqual(1, filterCriteria.SearchTerms.Length); Assert.IsNotNull(filterCriteria.StarDifficulty.Max); - Assert.Greater(filterCriteria.StarDifficulty.Max, 3.99d); - Assert.Less(filterCriteria.StarDifficulty.Max, 4.00d); + Assert.AreEqual(filterCriteria.StarDifficulty.Max, 4.00d); Assert.IsNull(filterCriteria.StarDifficulty.Min); } + [Test] + public void TestStarQueriesInclusive() + { + const string query = $"stars>=6"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual(filterCriteria.StarDifficulty.Min, 6.00d); + Assert.True(filterCriteria.StarDifficulty.IsLowerInclusive); + Assert.IsNull(filterCriteria.StarDifficulty.Max); + } + + /* + * The following tests have been written a bit strangely (they don't check exact + * bound equality with what the filter says). + * This is to account for floating-point arithmetic issues. + * For example, specifying a bpm<140 filter would previously match beatmaps with BPM + * of 139.99999, which would be displayed in the UI as 140. + * Due to this the tests check the last tick inside the range and the first tick + * outside of the range. + */ + [Test] public void TestApplyApproachRateQueries() { diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index dc77b0101e..02b5eb5b7a 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Screens.Select.Filter; +using osu.Game.Utils; namespace osu.Game.Screens.Select.Carousel { @@ -59,7 +60,7 @@ namespace osu.Game.Screens.Select.Carousel if (!match) return false; - match &= !criteria.StarDifficulty.HasFilter || criteria.StarDifficulty.IsInRange(BeatmapInfo.StarRating); + match &= !criteria.StarDifficulty.HasFilter || criteria.StarDifficulty.IsInRange(BeatmapInfo.StarRating.FloorToDecimalDigits(2)); match &= !criteria.ApproachRate.HasFilter || criteria.ApproachRate.IsInRange(BeatmapInfo.Difficulty.ApproachRate); match &= !criteria.DrainRate.HasFilter || criteria.DrainRate.IsInRange(BeatmapInfo.Difficulty.DrainRate); match &= !criteria.CircleSize.HasFilter || criteria.CircleSize.IsInRange(BeatmapInfo.Difficulty.CircleSize); diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 1094d88730..02a6da146e 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Select case "star": case "stars": case "sr": - return TryUpdateCriteriaRange(ref criteria.StarDifficulty, op, value, 0.01d / 2); + return TryUpdateCriteriaRange(ref criteria.StarDifficulty, op, value, 0); case "ar": return TryUpdateCriteriaRange(ref criteria.ApproachRate, op, value); @@ -309,6 +309,8 @@ namespace osu.Game.Screens.Select case Operator.Equal: range.Min = value - tolerance; range.Max = value + tolerance; + if (tolerance == 0) + range.IsLowerInclusive = range.IsUpperInclusive = true; break; case Operator.Greater: @@ -317,6 +319,8 @@ namespace osu.Game.Screens.Select case Operator.GreaterOrEqual: range.Min = value - tolerance; + if (tolerance == 0) + range.IsLowerInclusive = true; break; case Operator.Less: @@ -325,6 +329,8 @@ namespace osu.Game.Screens.Select case Operator.LessOrEqual: range.Max = value + tolerance; + if (tolerance == 0) + range.IsUpperInclusive = true; break; } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs index a776b2f796..f2f246093d 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using osu.Game.Beatmaps; using osu.Game.Graphics.Carousel; using osu.Game.Screens.Select; +using osu.Game.Utils; namespace osu.Game.Screens.SelectV2 { @@ -81,7 +82,7 @@ namespace osu.Game.Screens.SelectV2 if (!match) return false; - match &= !criteria.StarDifficulty.HasFilter || criteria.StarDifficulty.IsInRange(beatmap.StarRating); + match &= !criteria.StarDifficulty.HasFilter || criteria.StarDifficulty.IsInRange(beatmap.StarRating.FloorToDecimalDigits(2)); match &= !criteria.ApproachRate.HasFilter || criteria.ApproachRate.IsInRange(beatmap.Difficulty.ApproachRate); match &= !criteria.DrainRate.HasFilter || criteria.DrainRate.IsInRange(beatmap.Difficulty.DrainRate); match &= !criteria.CircleSize.HasFilter || criteria.CircleSize.IsInRange(beatmap.Difficulty.CircleSize); diff --git a/osu.Game/Utils/FormatUtils.cs b/osu.Game/Utils/FormatUtils.cs index fa7d6595e9..28776ea0bf 100644 --- a/osu.Game/Utils/FormatUtils.cs +++ b/osu.Game/Utils/FormatUtils.cs @@ -10,6 +10,12 @@ namespace osu.Game.Utils { public static class FormatUtils { + public static double FloorToDecimalDigits(this double value, uint digits) + { + double base10 = Math.Pow(10, digits); + return Math.Floor(value * base10) / base10; + } + /// /// Turns the provided accuracy into a percentage with 2 decimal places. /// @@ -21,9 +27,7 @@ namespace osu.Game.Utils // ie. a score which gets 89.99999% shouldn't ever show as 90%. // the reasoning for this is that cutoffs for grade increases are at whole numbers and displaying the required // percentile with a non-matching grade is confusing. - accuracy = Math.Floor(accuracy * 10000) / 10000; - - return accuracy.ToLocalisableString("0.00%"); + return accuracy.FloorToDecimalDigits(4).ToLocalisableString("0.00%"); } /// @@ -42,9 +46,7 @@ namespace osu.Game.Utils // i.e. a beatmap which has a star rating of 6.9999* should never show as 7.00*. // this matters for star rating medals which use hard cutoffs at whole numbers, // which then confuses users when they beat a 6.9999* beatmap but don't get the 7-star medal. - starRating = Math.Floor(starRating * 100) / 100; - - return starRating.ToLocalisableString("0.00"); + return starRating.FloorToDecimalDigits(2).ToLocalisableString("0.00"); } /// From 73a1f10dafc37968641ce4ee4413e82cfd0fb970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 12 Jun 2025 14:03:55 +0200 Subject: [PATCH 187/370] Ensure partial failed replays are played to their end Closes https://github.com/ppy/osu/issues/24285. This is not a perfect solution, as it is still possible for a replay to play *beyond* its end if the HP system doesn't fail it after it runs out of frames, but it's probably the best that can be done at this time. Notably this removes existing F rank checks because they were really not reliable. - Scores coming from stable will never present F rank, because rank is not stored to the replay, and the lowest rank that can be produced by `StandardisedScoreMigrationTools` is D. - lazer scores set prior to https://github.com/ppy/osu/pull/28058 will present F rank as long as the user has kept them in their local database and never exported and reimported them, for the same reason as above (rank not stored to replay). Also there have been many mechanics changes since, so it's not impossible for the replay to fail *before* the user actually did even in this case. - lazer scores set after https://github.com/ppy/osu/pull/28058 could technically rely on F rank but making them rely on it is annoying for several reasons: - The PR in question didn't bump `LegacyScoreEncoder.LATEST_VERSION`, so any checks based on the replay version field would be half-reliable anyway. - *Even after* the above, the replay version is only stored to realm as `TotalScoreVersion`, which *then gets bumped* on score version upgrades. So it can't even be used for any checks from that angle, you'd have to decode it from the score. - You *could* use `ClientVersion` because that's somewhat reliable, but that's stored *as string*, so you'd have to do some snipping to split off the `-lazer` suffix, then parse the version, then compare it. I started going through the motions of that before deciding that this is an edge case of an edge case and probably not worth spending time over the simple and obvious solution of just doing away with the rank check. Until I'm proven wrong, I guess. --- osu.Game/Screens/Play/ReplayPlayer.cs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 882e556965..c058238a0a 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -29,8 +29,6 @@ namespace osu.Game.Screens.Play private readonly Func, Score> createScore; - private readonly bool replayIsFailedScore; - private PlaybackSettings playbackSettings; [Cached(typeof(IGameplayLeaderboardProvider))] @@ -40,19 +38,28 @@ namespace osu.Game.Screens.Play private bool isAutoplayPlayback => GameplayState.Mods.OfType().Any(); - // Disallow replays from failing. (see https://github.com/ppy/osu/issues/6108) + private double? lastFrameTime; + protected override bool CheckModsAllowFailure() { - if (!replayIsFailedScore && !isAutoplayPlayback) - return false; + // autoplay should be able to fail if the beatmap is not humanly beatable + if (isAutoplayPlayback) + return base.CheckModsAllowFailure(); - return base.CheckModsAllowFailure(); + // non-autoplay replays should be able to fail, but only after they've exhausted their frames. + // note that the rank isn't checked here - that's because it is generally unreliable. + // stable replays, as well as lazer replays recorded prior to https://github.com/ppy/osu/pull/28058, + // do not even *contain* the user's rank. + // not to mention possible gameplay mechanics changes that could make a replay fail sooner than it really should. + if (GameplayClockContainer.CurrentTime >= lastFrameTime) + return base.CheckModsAllowFailure(); + + return false; } public ReplayPlayer(Score score, PlayerConfiguration configuration = null) : this((_, _) => score, configuration) { - replayIsFailedScore = score.ScoreInfo.Rank == ScoreRank.F; } public ReplayPlayer(Func, Score> createScore, PlayerConfiguration configuration = null) @@ -95,6 +102,7 @@ namespace osu.Game.Screens.Play protected override void PrepareReplay() { DrawableRuleset?.SetReplayScore(Score); + lastFrameTime = Score.Replay.Frames.LastOrDefault()?.Time; } protected override Score CreateScore(IBeatmap beatmap) => createScore(beatmap, Mods.Value); From dc38c190df7e28225b731f9243de84f77f6f9f55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Jun 2025 10:06:44 +0200 Subject: [PATCH 188/370] Fix code quality --- osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index 37b7d71d2b..578698b724 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -102,7 +102,7 @@ namespace osu.Game.Tests.NonVisual.Filtering [Test] public void TestStarQueriesInclusive() { - const string query = $"stars>=6"; + const string query = "stars>=6"; var filterCriteria = new FilterCriteria(); FilterQueryParser.ApplyQueries(filterCriteria, query); Assert.AreEqual(filterCriteria.StarDifficulty.Min, 6.00d); From f4bf2ae7a5bbe1815e53c11c269add5fafbfcbc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Jun 2025 10:33:54 +0200 Subject: [PATCH 189/370] Fix SHOCKING test What the actual heck is that magic number stupidity. I'm not looking into why the test falls over so badly that it apparently dies on some main menu logic just because assertions fail, because the main menu logic is for hold-to-song-select-v2 and is thus temporary. --- .../Visual/SongSelect/TestSceneBeatmapRecommendations.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index 5c89e8a02c..d459eac3c2 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -64,16 +64,16 @@ namespace osu.Game.Tests.Visual.SongSelect switch (rulesetID) { case 0: - return 336; // recommended star rating of 2 + return 337; // recommended star rating of 2 case 1: return 973; // SR 3 case 2: - return 1905; // SR 4 + return 1906; // SR 4 case 3: - return 3329; // SR 5 + return 3330; // SR 5 default: return 0; From 3291a4cb7be4daa858f7b2e24984662dc7d8c680 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Jun 2025 18:15:52 +0900 Subject: [PATCH 190/370] Add support for reading release stream from assembly version --- .../Visual/Online/TestSceneChangelogOverlay.cs | 2 +- .../API/Requests/Responses/APIUpdateStream.cs | 3 ++- osu.Game/OsuGame.cs | 7 +++---- osu.Game/OsuGameBase.cs | 16 ++++++++++------ osu.Game/Overlays/ChangelogOverlay.cs | 8 +++++--- osu.Game/Overlays/Settings/SettingsFooter.cs | 2 +- osu.Game/Updater/UpdateManager.cs | 2 +- osu.Game/Utils/SentryLogger.cs | 2 +- 8 files changed, 24 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs index 040b903636..ee88bf917c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs @@ -107,7 +107,7 @@ namespace osu.Game.Tests.Visual.Online { Version = "2018.712.0", DisplayVersion = "2018.712.0", - UpdateStream = streams[OsuGameBase.CLIENT_STREAM_NAME], + UpdateStream = streams["lazer"], CreatedAt = new DateTime(2018, 7, 12), ChangelogEntries = new List { diff --git a/osu.Game/Online/API/Requests/Responses/APIUpdateStream.cs b/osu.Game/Online/API/Requests/Responses/APIUpdateStream.cs index dac72f2488..7586f56a0e 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUpdateStream.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUpdateStream.cs @@ -39,7 +39,8 @@ namespace osu.Game.Online.API.Requests.Responses ["stable"] = new Color4(34, 153, 187, 255), ["beta40"] = new Color4(255, 221, 85, 255), ["cuttingedge"] = new Color4(238, 170, 0, 255), - [OsuGameBase.CLIENT_STREAM_NAME] = new Color4(237, 18, 33, 255), + ["lazer"] = new Color4(237, 18, 33, 255), + ["tachyon"] = new Color4(206, 0, 255, 255), ["web"] = new Color4(136, 102, 238, 255) }; diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 628d9d990c..46d9c004c9 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -519,7 +519,7 @@ namespace osu.Game else { string[] changelogArgs = argString.Split("/"); - ShowChangelogBuild(changelogArgs[0], changelogArgs[1]); + ShowChangelogBuild($"{changelogArgs[1]}-{changelogArgs[0]}"); } break; @@ -600,9 +600,8 @@ namespace osu.Game /// /// Show changelog's build as an overlay /// - /// The update stream name - /// The build version of the update stream - public void ShowChangelogBuild(string updateStream, string version) => waitForReady(() => changelogOverlay, _ => changelogOverlay.ShowBuild(updateStream, version)); + /// The build version, including stream suffix. + public void ShowChangelogBuild(string version) => waitForReady(() => changelogOverlay, _ => changelogOverlay.ShowBuild(version)); /// /// Joins a multiplayer or playlists room with the given . diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 3bbebb9244..3c23ccc5cf 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -83,8 +83,6 @@ namespace osu.Game public const string OSU_PROTOCOL = "osu://"; - public const string CLIENT_STREAM_NAME = @"lazer"; - /// /// The filename of the main client database. /// @@ -120,8 +118,6 @@ namespace osu.Game public bool IsDeployedBuild => AssemblyVersion.Major > 0; - internal const string BUILD_SUFFIX = "lazer"; - public virtual string Version { get @@ -129,8 +125,16 @@ namespace osu.Game if (!IsDeployedBuild) return @"local " + (DebugUtils.IsDebugBuild ? @"debug" : @"release"); - var version = AssemblyVersion; - return $@"{version.Major}.{version.Minor}.{version.Build}-{BUILD_SUFFIX}"; + string informationalVersion = Assembly.GetEntryAssembly()? + .GetCustomAttribute()? + .InformationalVersion; + + // Example: [assembly: AssemblyInformationalVersion("2025.613.0-tachyon+d934e574b2539e8787956c3c9ecce9dadebb10ee")] + if (!string.IsNullOrEmpty(informationalVersion)) + return informationalVersion.Split('+').First(); + + Version version = AssemblyVersion; + return $@"{version.Major}.{version.Minor}.{version.Build}-lazer"; } } diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs index 4cc38c41e4..dafa14f7e7 100644 --- a/osu.Game/Overlays/ChangelogOverlay.cs +++ b/osu.Game/Overlays/ChangelogOverlay.cs @@ -76,16 +76,18 @@ namespace osu.Game.Overlays Show(); } - public void ShowBuild([NotNull] string updateStream, [NotNull] string version) + public void ShowBuild([NotNull] string version) { - ArgumentNullException.ThrowIfNull(updateStream); ArgumentNullException.ThrowIfNull(version); Show(); performAfterFetch(() => { - var build = builds.Find(b => b.Version == version && b.UpdateStream.Name == updateStream) + string versionPart = version.Split('-')[0]; + string updateStream = version.Split('-')[1]; + + var build = builds.Find(b => b.Version == versionPart && b.UpdateStream.Name == updateStream) ?? Streams.Find(s => s.Name == updateStream)?.LatestBuild; if (build != null) diff --git a/osu.Game/Overlays/Settings/SettingsFooter.cs b/osu.Game/Overlays/Settings/SettingsFooter.cs index 307d88e712..f50fca418d 100644 --- a/osu.Game/Overlays/Settings/SettingsFooter.cs +++ b/osu.Game/Overlays/Settings/SettingsFooter.cs @@ -100,7 +100,7 @@ namespace osu.Game.Overlays.Settings [BackgroundDependencyLoader] private void load(ChangelogOverlay? changelog) { - Action = () => changelog?.ShowBuild(OsuGameBase.CLIENT_STREAM_NAME, version); + Action = () => changelog?.ShowBuild(version); Add(new OsuSpriteText { diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index c114e3a8d0..c14c415814 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -111,7 +111,7 @@ namespace osu.Game.Updater Activated = delegate { notificationOverlay.Hide(); - changelog.ShowBuild(OsuGameBase.CLIENT_STREAM_NAME, version); + changelog.ShowBuild(version); return true; }; } diff --git a/osu.Game/Utils/SentryLogger.cs b/osu.Game/Utils/SentryLogger.cs index 95086c501f..4f916f810e 100644 --- a/osu.Game/Utils/SentryLogger.cs +++ b/osu.Game/Utils/SentryLogger.cs @@ -52,7 +52,7 @@ namespace osu.Game.Utils 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)}"; + options.Release = $"osu@{game.Version.Split('-').First()}"; }); Logger.NewEntry += processLogEntry; From 62ec0a15d8367e9a60acf282b5eb2e3dcc2b938a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Jun 2025 18:26:31 +0900 Subject: [PATCH 191/370] Transfer release stream setting from running build --- osu.Game/OsuGame.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 46d9c004c9..e516e56c36 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1024,6 +1024,10 @@ namespace osu.Game if (RuntimeInfo.EntryAssembly.GetCustomAttribute() == null) Logger.Log(NotificationsStrings.NotOfficialBuild.ToString()); + // Make sure the release stream setting matches the build which was just run. + if (Enum.TryParse(Version.Split('-').Last(), true, out var releaseStream)) + LocalConfig.SetValue(OsuSetting.ReleaseStream, releaseStream); + var languages = Enum.GetValues(); var mappings = languages.Select(language => From addd10f4c68e00667130c661dffd6f8ec8c7001b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 13 Jun 2025 20:34:31 +0900 Subject: [PATCH 192/370] Rewrite `UpdateManager` to handle cancellations --- osu.Desktop/Updater/VelopackUpdateManager.cs | 186 ++++++++---------- .../NonVisual/TestSceneUpdateManager.cs | 59 +++--- .../TestSceneNotificationOverlay.cs | 7 +- osu.Game/Updater/MobileUpdateNotifier.cs | 8 +- osu.Game/Updater/NoActionUpdateManager.cs | 10 +- osu.Game/Updater/UpdateManager.cs | 141 ++++++------- 6 files changed, 191 insertions(+), 220 deletions(-) diff --git a/osu.Desktop/Updater/VelopackUpdateManager.cs b/osu.Desktop/Updater/VelopackUpdateManager.cs index 51744345a4..386a62d673 100644 --- a/osu.Desktop/Updater/VelopackUpdateManager.cs +++ b/osu.Desktop/Updater/VelopackUpdateManager.cs @@ -2,11 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Logging; +using osu.Framework.Threading; using osu.Game; -using osu.Game.Configuration; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Screens.Play; @@ -28,132 +29,109 @@ namespace osu.Desktop.Updater private bool isInGameplay => localUserInfo?.PlayingState.Value != LocalUserPlayingState.NotPlaying; - private UpdateManager? updateManager; - private UpdateInfo? pendingUpdate; - private ReleaseStream? lastReleaseStream; + private ScheduledDelegate? scheduledBackgroundCheck; - protected override async Task PerformUpdateCheck() + private void scheduleNextUpdateCheck() { - if (ReleaseStream.Value != lastReleaseStream) + scheduledBackgroundCheck?.Cancel(); + scheduledBackgroundCheck = Scheduler.AddDelayed(() => { - updateManager = new UpdateManager(new GithubSource(@"https://github.com/ppy/osu", null, ReleaseStream.Value == Game.Configuration.ReleaseStream.Tachyon), new UpdateOptions - { - AllowVersionDowngrade = true, - }); - - lastReleaseStream = ReleaseStream.Value; - } - - return await checkForUpdateAsync().ConfigureAwait(false); + Logger.Log("Running scheduled background update check..."); + Task.Run(CheckForUpdateAsync); + }, 60000 * 30); } - private async Task checkForUpdateAsync() + protected override async Task PerformUpdateCheck(CancellationToken cancellationToken) { - // whether to check again in 30 minutes. generally only if there's an error or no update was found (yet). - bool scheduleRecheck = false; + scheduledBackgroundCheck?.Cancel(); - try + if (isInGameplay) { - // Avoid any kind of update checking while gameplay is running. - if (isInGameplay) - { - scheduleRecheck = true; - return true; - } - - // TODO: we should probably be checking if there's a more recent update, rather than shortcutting here. - // Velopack does support this scenario (see https://github.com/ppy/osu/pull/28743#discussion_r1743495975). - if (pendingUpdate != null) - { - // If there is an update pending restart, show the notification to restart again. - notificationOverlay.Post(new UpdateApplicationCompleteNotification - { - Activated = () => - { - Task.Run(restartToApplyUpdate); - return true; - } - }); - - return true; - } - - if (updateManager == null) - { - scheduleRecheck = true; - return false; - } - - pendingUpdate = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false); - - // No update is available. We'll check again later. - if (pendingUpdate == null) - { - scheduleRecheck = true; - return false; - } - - // An update is found, let's notify the user and start downloading it. - UpdateProgressNotification notification = new UpdateProgressNotification - { - CompletionClickAction = () => - { - Task.Run(restartToApplyUpdate); - return true; - }, - }; - - runOutsideOfGameplay(() => notificationOverlay.Post(notification)); - notification.StartDownload(); - - try - { - await updateManager.DownloadUpdatesAsync(pendingUpdate, p => notification.Progress = p / 100f).ConfigureAwait(false); - runOutsideOfGameplay(() => notification.State = ProgressNotificationState.Completed); - } - catch (Exception e) - { - // In the case of an error, a separate notification will be displayed. - scheduleRecheck = true; - notification.FailDownload(); - Logger.Error(e, @"update failed!"); - } - } - catch (Exception e) - { - // we'll ignore this and retry later. can be triggered by no internet connection or thread abortion. - scheduleRecheck = true; - Logger.Log($@"update check failed ({e.Message})"); - } - finally - { - if (scheduleRecheck) - { - Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30); - } + Logger.Log("Update check cancelled - user is in gameplay"); + scheduleNextUpdateCheck(); + return false; } + IUpdateSource updateSource = new GithubSource(@"https://github.com/ppy/osu", null, ReleaseStream.Value == Game.Configuration.ReleaseStream.Tachyon); + UpdateManager updateManager = new UpdateManager(updateSource, new UpdateOptions + { + AllowVersionDowngrade = true + }); + + UpdateInfo? update = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false); + + if (update == null) + { + // No update is available. + Logger.Log("No update found"); + scheduleNextUpdateCheck(); + return false; + } + + Logger.Log($"New update available: {update.TargetFullRelease.Version}"); + + // Download update in the background while notifying awaiters of the update being available. + downloadUpdate(updateManager, update, cancellationToken); return true; } - private void runOutsideOfGameplay(Action action) + private void downloadUpdate(UpdateManager updateManager, UpdateInfo update, CancellationToken cancellationToken) => Task.Run(async () => { + Logger.Log($"Beginning download of update {update.TargetFullRelease.Version}..."); + + UpdateDownloadProgressNotification progressNotification = new UpdateDownloadProgressNotification(cancellationToken) + { + CompletionClickAction = () => + { + restartToApplyUpdate(updateManager, update); + return true; + } + }; + + try + { + using (var cts = CancellationTokenSource.CreateLinkedTokenSource(progressNotification.CancellationToken, cancellationToken)) + { + progressNotification.StartDownload(); + runOutsideOfGameplay(() => notificationOverlay.Post(progressNotification), cts.Token); + + await updateManager.DownloadUpdatesAsync(update, p => progressNotification.Progress = p / 100f, false, cts.Token).ConfigureAwait(false); + runOutsideOfGameplay(() => progressNotification.State = ProgressNotificationState.Completed, cts.Token); + } + } + catch (OperationCanceledException) + { + progressNotification.FailDownload(); + Logger.Log(@"Update cancelled"); + } + catch (Exception e) + { + // In the case of an error, a separate notification will be displayed. + progressNotification.FailDownload(); + Logger.Error(e, @"Update failed!"); + } + + return true; + }, cancellationToken); + + private void runOutsideOfGameplay(Action action, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + return; + if (isInGameplay) { - Scheduler.AddDelayed(() => runOutsideOfGameplay(action), 1000); + Scheduler.AddDelayed(() => runOutsideOfGameplay(action, cancellationToken), 1000); return; } action(); } - private async Task restartToApplyUpdate() + private void restartToApplyUpdate(UpdateManager updateManager, UpdateInfo update) => Task.Run(async () => { - if (updateManager == null) - return; - - await updateManager.WaitExitThenApplyUpdatesAsync(pendingUpdate?.TargetFullRelease).ConfigureAwait(false); + await updateManager.WaitExitThenApplyUpdatesAsync(update.TargetFullRelease).ConfigureAwait(false); Schedule(() => game.AttemptExit()); - } + }); } } diff --git a/osu.Game.Tests/NonVisual/TestSceneUpdateManager.cs b/osu.Game.Tests/NonVisual/TestSceneUpdateManager.cs index fd2a3acb2f..385b8b82cb 100644 --- a/osu.Game.Tests/NonVisual/TestSceneUpdateManager.cs +++ b/osu.Game.Tests/NonVisual/TestSceneUpdateManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; @@ -69,21 +70,18 @@ namespace osu.Game.Tests.NonVisual } /// - /// Updates should be checked once more if the release stream is changed during an going check. + /// Changing the release stream should start a new invocation and cancel the existing one. /// [Test] - public void TestReleaseStreamChangedDuringCheck() + public void TestNewInvocationOnReleaseStreamChanged() { AddStep("change release stream", () => config.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Tachyon)); AddUntilStep("check pending", () => manager.IsPending); AddStep("change release stream", () => config.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Lazer)); + AddUntilStep("3 invocations", () => manager.Invocations, () => Is.EqualTo(3)); AddStep("complete check", () => manager.Complete()); AddUntilStep("2 checks completed", () => manager.Completions, () => Is.EqualTo(2)); - - AddUntilStep("check pending", () => manager.IsPending); - AddStep("complete one check", () => manager.Complete()); - AddUntilStep("3 checks completed", () => manager.Completions, () => Is.EqualTo(3)); AddUntilStep("no check pending", () => !manager.IsPending); } @@ -109,21 +107,18 @@ namespace osu.Game.Tests.NonVisual } /// - /// Any ongoing request should be returned when the user requests a new one. + /// User requests should start a new invocation and cancel the existing one. /// [Test] - public void TestUserRequestReturnsExistingCheck() + public void TestUserRequestOverridesExistingCheck() { - Task task1 = null!; - Task task2 = null!; - // This part covering double user input is not really possible because the settings button is disabled during the check, // but it's kept here for sanity in-case the update manager is used as a standalone object elsewhere. - AddStep("request check", () => task1 = manager.CheckForUpdateAsync()); + AddStep("request check", () => manager.CheckForUpdateAsync()); AddUntilStep("check pending", () => manager.IsPending); - AddStep("request check", () => task2 = manager.CheckForUpdateAsync()); - AddAssert("second request returned original task", () => task2 == task1); + AddStep("request check", () => manager.CheckForUpdateAsync()); + AddUntilStep("3 invocations", () => manager.Invocations, () => Is.EqualTo(3)); AddStep("complete check", () => manager.Complete()); AddUntilStep("2 checks completed", () => manager.Completions, () => Is.EqualTo(2)); @@ -133,12 +128,8 @@ namespace osu.Game.Tests.NonVisual AddStep("change release stream", () => config.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Tachyon)); AddUntilStep("check pending", () => manager.IsPending); - AddStep("request check", () => - { - task1 = manager.CurrentTask; - task2 = manager.CheckForUpdateAsync(); - }); - AddAssert("second request returned original task", () => task2 == task1); + AddStep("request check", () => manager.CheckForUpdateAsync()); + AddUntilStep("5 invocations", () => manager.Invocations, () => Is.EqualTo(5)); AddStep("complete check", () => manager.Complete()); AddUntilStep("3 checks completed", () => manager.Completions, () => Is.EqualTo(3)); @@ -148,28 +139,28 @@ namespace osu.Game.Tests.NonVisual private partial class TestUpdateManager : UpdateManager { public bool IsPending { get; private set; } + public int Invocations { get; private set; } public int Completions { get; private set; } - public Task CurrentTask { get; private set; } = Task.CompletedTask; - private TaskCompletionSource? pendingCheck; - protected override Task PerformUpdateCheck() + protected override async Task PerformUpdateCheck(CancellationToken cancellationToken) { - var task = Task.Run(async () => + Invocations++; + + var check = pendingCheck = new TaskCompletionSource(); + IsPending = true; + + try { - var check = pendingCheck = new TaskCompletionSource(); - IsPending = true; - - bool result = await check.Task.ConfigureAwait(false); - IsPending = false; + bool result = await check.Task.WaitAsync(cancellationToken).ConfigureAwait(false); Completions++; - return result; - }); - - CurrentTask = task; - return task; + } + finally + { + IsPending = false; + } } public void Complete() diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs index 65c8b913d3..3648291816 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using NUnit.Framework; using osu.Framework.Extensions; using osu.Framework.Graphics; @@ -456,7 +457,7 @@ namespace osu.Game.Tests.Visual.UserInterface { applyUpdate = false; - var updateNotification = new UpdateManager.UpdateProgressNotification + var updateNotification = new UpdateManager.UpdateDownloadProgressNotification(CancellationToken.None) { CompletionClickAction = () => applyUpdate = true }; @@ -468,9 +469,9 @@ namespace osu.Game.Tests.Visual.UserInterface checkProgressingCount(1); waitForCompletion(); - UpdateManager.UpdateApplicationCompleteNotification? completionNotification = null; + UpdateManager.UpdateReadyNotification? completionNotification = null; AddUntilStep("wait for completion notification", - () => (completionNotification = notificationOverlay.ChildrenOfType().SingleOrDefault()) != null); + () => (completionNotification = notificationOverlay.ChildrenOfType().SingleOrDefault()) != null); AddStep("click notification", () => completionNotification?.TriggerClick()); AddUntilStep("wait for update applied", () => applyUpdate); diff --git a/osu.Game/Updater/MobileUpdateNotifier.cs b/osu.Game/Updater/MobileUpdateNotifier.cs index 04b54df3c0..02dac00cf4 100644 --- a/osu.Game/Updater/MobileUpdateNotifier.cs +++ b/osu.Game/Updater/MobileUpdateNotifier.cs @@ -4,13 +4,13 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Threading; using System.Threading.Tasks; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; using osu.Framework.Platform; using osu.Game.Online.API; -using osu.Game.Overlays.Notifications; namespace osu.Game.Updater { @@ -31,13 +31,13 @@ namespace osu.Game.Updater version = game.Version; } - protected override async Task PerformUpdateCheck() + protected override async Task PerformUpdateCheck(CancellationToken cancellationToken) { try { var releases = new OsuJsonWebRequest("https://api.github.com/repos/ppy/osu/releases/latest"); - await releases.PerformAsync().ConfigureAwait(false); + await releases.PerformAsync(cancellationToken).ConfigureAwait(false); var latest = releases.ResponseObject; @@ -48,7 +48,7 @@ namespace osu.Game.Updater if (latestTagName != version && tryGetBestUrl(latest, out string? url)) { - Notifications.Post(new SimpleNotification + Notifications.Post(new UpdateAvailableNotification(cancellationToken) { Text = $"A newer release of osu! has been found ({version} → {latestTagName}).\n\n" + "Click here to download the new version, which can be installed over the top of your existing installation", diff --git a/osu.Game/Updater/NoActionUpdateManager.cs b/osu.Game/Updater/NoActionUpdateManager.cs index 4b8a3f000f..3f1e383a50 100644 --- a/osu.Game/Updater/NoActionUpdateManager.cs +++ b/osu.Game/Updater/NoActionUpdateManager.cs @@ -3,12 +3,11 @@ using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; -using osu.Framework.Graphics.Sprites; using osu.Game.Configuration; using osu.Game.Online.API; -using osu.Game.Overlays.Notifications; namespace osu.Game.Updater { @@ -28,7 +27,7 @@ namespace osu.Game.Updater version = game.Version; } - protected override async Task PerformUpdateCheck() + protected override async Task PerformUpdateCheck(CancellationToken cancellationToken) { try { @@ -36,7 +35,7 @@ namespace osu.Game.Updater bool includePrerelease = stream == Configuration.ReleaseStream.Tachyon; OsuJsonWebRequest releasesRequest = new OsuJsonWebRequest("https://api.github.com/repos/ppy/osu/releases?per_page=10&page=1"); - await releasesRequest.PerformAsync().ConfigureAwait(false); + await releasesRequest.PerformAsync(cancellationToken).ConfigureAwait(false); GitHubRelease[] releases = releasesRequest.ResponseObject; GitHubRelease? latest = releases.OrderByDescending(r => r.PublishedAt).FirstOrDefault(r => includePrerelease || !r.Prerelease); @@ -51,11 +50,10 @@ namespace osu.Game.Updater if (latestTagName != version) { - Notifications.Post(new SimpleNotification + Notifications.Post(new UpdateAvailableNotification(cancellationToken) { Text = $"A newer release of osu! has been found ({version} → {latestTagName}).\n\n" + "Check with your package manager / provider to bring osu! up-to-date!", - Icon = FontAwesome.Solid.Download, }); return true; diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index 279517db6c..3855ddd0d6 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Reflection; +using System.Threading; using System.Threading.Tasks; using osu.Framework; using osu.Framework.Allocation; @@ -43,8 +44,6 @@ namespace osu.Game.Updater protected IBindable ReleaseStream => releaseStream; private readonly Bindable releaseStream = new Bindable(); - private bool updateCheckRequested; - private Task updateCheckTask = Task.FromResult(false); protected override void LoadComplete() { @@ -68,41 +67,12 @@ namespace osu.Game.Updater config.SetValue(OsuSetting.Version, version); config.BindWith(OsuSetting.ReleaseStream, releaseStream); - releaseStream.BindValueChanged(_ => scheduleUpdateCheck()); + releaseStream.BindValueChanged(_ => CheckForUpdateAsync()); - scheduleUpdateCheck(); + CheckForUpdateAsync(); } - protected override void Update() - { - base.Update(); - processScheduledUpdateCheck(); - } - - /// - /// Schedules a request to check for new updates to begin as soon as any existing check completes. - /// - private void scheduleUpdateCheck() - { - updateCheckRequested = true; - } - - /// - /// Processes an ongoing request to check for new updates. - /// - private void processScheduledUpdateCheck() - { - if (!updateCheckRequested) - return; - - if (!updateCheckTask.IsCompleted) - return; - - if (CanCheckForUpdate) - updateCheckTask = PerformUpdateCheck(); - - updateCheckRequested = false; - } + private CancellationTokenSource? updateCheckCancellation; /// /// Immediately checks for any available updates, or returns the existing update task. @@ -110,20 +80,22 @@ namespace osu.Game.Updater /// true if any updates are available, false otherwise. public Task CheckForUpdateAsync() { - if (!updateCheckTask.IsCompleted) - return updateCheckTask; - - scheduleUpdateCheck(); - processScheduledUpdateCheck(); - - return updateCheckTask; + updateCheckCancellation?.Cancel(); + updateCheckCancellation = new CancellationTokenSource(); + return PerformUpdateCheck(updateCheckCancellation.Token); } /// /// Performs an asynchronous check for application updates. /// /// Whether any update is waiting. May return true if an error occured (there is potentially an update available). - protected virtual Task PerformUpdateCheck() => Task.FromResult(false); + protected virtual Task PerformUpdateCheck(CancellationToken cancellationToken) => Task.FromResult(false); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + updateCheckCancellation?.Cancel(); + } private partial class UpdateCompleteNotification : SimpleNotification { @@ -150,20 +122,14 @@ namespace osu.Game.Updater } } - public partial class UpdateApplicationCompleteNotification : ProgressCompletionNotification + public partial class UpdateDownloadProgressNotification : ProgressNotification { - public UpdateApplicationCompleteNotification() - { - Text = NotificationsStrings.UpdateReadyToInstall; - } - } + private readonly CancellationToken cancellationToken; - public partial class UpdateProgressNotification : ProgressNotification - { - protected override Notification CreateCompletionNotification() => new UpdateApplicationCompleteNotification + public UpdateDownloadProgressNotification(CancellationToken cancellationToken) { - Activated = CompletionClickAction - }; + this.cancellationToken = cancellationToken; + } [BackgroundDependencyLoader] private void load() @@ -182,24 +148,12 @@ namespace osu.Game.Updater }); } - protected override void LoadComplete() + protected override void Update() { - base.LoadComplete(); - StartDownload(); - } + base.Update(); - public override void Close(bool runFlingAnimation) - { - // cancelling updates is not currently supported by the underlying updater. - // only allow dismissing for now. - - switch (State) - { - case ProgressNotificationState.Cancelled: - case ProgressNotificationState.Completed: - base.Close(runFlingAnimation); - break; - } + if (cancellationToken.IsCancellationRequested) + FailDownload(); } public void StartDownload() @@ -214,6 +168,55 @@ namespace osu.Game.Updater State = ProgressNotificationState.Cancelled; Close(false); } + + protected override Notification CreateCompletionNotification() => new UpdateReadyNotification(cancellationToken) + { + Activated = () => + { + if (cancellationToken.IsCancellationRequested) + return true; + + return CompletionClickAction?.Invoke() ?? true; + } + }; + } + + public partial class UpdateReadyNotification : ProgressCompletionNotification + { + private readonly CancellationToken cancellationToken; + + public UpdateReadyNotification(CancellationToken cancellationToken) + { + this.cancellationToken = cancellationToken; + Text = NotificationsStrings.UpdateReadyToInstall; + } + + protected override void Update() + { + base.Update(); + + if (cancellationToken.IsCancellationRequested) + Close(false); + } + } + + public partial class UpdateAvailableNotification : SimpleNotification + { + private readonly CancellationToken cancellationToken; + + public UpdateAvailableNotification(CancellationToken cancellationToken) + { + this.cancellationToken = cancellationToken; + Icon = FontAwesome.Solid.Download; + } + + protected override void Update() + { + base.Update(); + + if (cancellationToken.IsCancellationRequested) + Close(false); + } } } } From 5f4a9d8e81722d5447a2b048c8b8f291b6d68a39 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 13 Jun 2025 20:47:06 +0900 Subject: [PATCH 193/370] Allow cancelling from initial notification --- osu.Desktop/Updater/VelopackUpdateManager.cs | 12 +++++-- .../Sections/General/UpdateSettings.cs | 7 ++-- osu.Game/Updater/UpdateManager.cs | 36 +++++++++++-------- 3 files changed, 35 insertions(+), 20 deletions(-) diff --git a/osu.Desktop/Updater/VelopackUpdateManager.cs b/osu.Desktop/Updater/VelopackUpdateManager.cs index 386a62d673..12a8b7a05d 100644 --- a/osu.Desktop/Updater/VelopackUpdateManager.cs +++ b/osu.Desktop/Updater/VelopackUpdateManager.cs @@ -37,7 +37,7 @@ namespace osu.Desktop.Updater scheduledBackgroundCheck = Scheduler.AddDelayed(() => { Logger.Log("Running scheduled background update check..."); - Task.Run(CheckForUpdateAsync); + CheckForUpdate(); }, 60000 * 30); } @@ -60,6 +60,13 @@ namespace osu.Desktop.Updater UpdateInfo? update = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false); + if (cancellationToken.IsCancellationRequested) + { + Logger.Log("Update check cancelled"); + scheduleNextUpdateCheck(); + return true; + } + if (update == null) { // No update is available. @@ -68,9 +75,8 @@ namespace osu.Desktop.Updater return false; } - Logger.Log($"New update available: {update.TargetFullRelease.Version}"); - // Download update in the background while notifying awaiters of the update being available. + Logger.Log($"New update available: {update.TargetFullRelease.Version}"); downloadUpdate(updateManager, update, cancellationToken); return true; } diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index ac6215f3ad..b8a77a7688 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -126,7 +126,7 @@ namespace osu.Game.Overlays.Settings.Sections.General try { - bool foundUpdate = await updateManager.CheckForUpdateAsync().ConfigureAwait(true); + bool foundUpdate = await updateManager.CheckForUpdateAsync(checkingNotification.CancellationToken).ConfigureAwait(true); if (!foundUpdate) { @@ -142,8 +142,9 @@ namespace osu.Game.Overlays.Settings.Sections.General } finally { - // This sequence allows the notification to be immediately dismissed. - checkingNotification.State = ProgressNotificationState.Cancelled; + // This sequence allows the notification to be immediately dismissed without posting a continuation message. + checkingNotification.CompletionTarget = null; + checkingNotification.State = ProgressNotificationState.Completed; checkingNotification.Close(false); checkForUpdatesButton.Enabled.Value = true; } diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index 3855ddd0d6..76557a4c77 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -44,6 +44,7 @@ namespace osu.Game.Updater protected IBindable ReleaseStream => releaseStream; private readonly Bindable releaseStream = new Bindable(); + private CancellationTokenSource updateCancellation = new CancellationTokenSource(); protected override void LoadComplete() { @@ -67,22 +68,35 @@ namespace osu.Game.Updater config.SetValue(OsuSetting.Version, version); config.BindWith(OsuSetting.ReleaseStream, releaseStream); - releaseStream.BindValueChanged(_ => CheckForUpdateAsync()); + releaseStream.BindValueChanged(_ => CheckForUpdate()); - CheckForUpdateAsync(); + CheckForUpdate(); } - private CancellationTokenSource? updateCheckCancellation; + /// + /// Immediately checks for any available update. + /// + public void CheckForUpdate() + { + _ = CheckForUpdateAsync(); + } /// - /// Immediately checks for any available updates, or returns the existing update task. + /// Immediately checks for any available update. /// /// true if any updates are available, false otherwise. - public Task CheckForUpdateAsync() + public async Task CheckForUpdateAsync(CancellationToken cancellationToken = default) { - updateCheckCancellation?.Cancel(); - updateCheckCancellation = new CancellationTokenSource(); - return PerformUpdateCheck(updateCheckCancellation.Token); + var cancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var lastCancellation = Interlocked.Exchange(ref updateCancellation, cancellation); + + using (lastCancellation) + { + // This serves a dual purpose of nullifying the last update, closing any existing notifications as stale. + await lastCancellation.CancelAsync().ConfigureAwait(false); + } + + return await PerformUpdateCheck(cancellation.Token).ConfigureAwait(false); } /// @@ -91,12 +105,6 @@ namespace osu.Game.Updater /// Whether any update is waiting. May return true if an error occured (there is potentially an update available). protected virtual Task PerformUpdateCheck(CancellationToken cancellationToken) => Task.FromResult(false); - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - updateCheckCancellation?.Cancel(); - } - private partial class UpdateCompleteNotification : SimpleNotification { private readonly string version; From 101044d7d8ae477df2504c65b58f0a4ca724b08a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Jun 2025 13:40:12 +0200 Subject: [PATCH 194/370] Move blocking / unblocking logic to reusable location --- osu.Game/Users/ConfirmBlockActionDialog.cs | 59 ++++++++++++++++++++++ osu.Game/Users/UserPanel.cs | 44 +--------------- 2 files changed, 61 insertions(+), 42 deletions(-) create mode 100644 osu.Game/Users/ConfirmBlockActionDialog.cs diff --git a/osu.Game/Users/ConfirmBlockActionDialog.cs b/osu.Game/Users/ConfirmBlockActionDialog.cs new file mode 100644 index 0000000000..4dccc77ebc --- /dev/null +++ b/osu.Game/Users/ConfirmBlockActionDialog.cs @@ -0,0 +1,59 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Localisation; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Overlays.Dialog; +using osu.Game.Overlays.Notifications; + +namespace osu.Game.Users +{ + public partial class ConfirmBlockActionDialog : DangerousActionDialog + { + private readonly APIUser user; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private NotificationOverlay? notifications { get; set; } + + private ConfirmBlockActionDialog(APIUser user, LocalisableString text, Action action) + { + this.user = user; + BodyText = text; + DangerousAction = () => action(this); + } + + public static ConfirmBlockActionDialog Block(APIUser user) => new ConfirmBlockActionDialog(user, ContextMenuStrings.ConfirmBlockUser(user.Username), d => d.toggleBlock(true)); + public static ConfirmBlockActionDialog Unblock(APIUser user) => new ConfirmBlockActionDialog(user, ContextMenuStrings.ConfirmUnblockUser(user.Username), d => d.toggleBlock(false)); + + private void toggleBlock(bool block) + { + APIRequest req = block ? new BlockUserRequest(user.OnlineID) : new UnblockUserRequest(user.OnlineID); + + req.Success += () => + { + api.UpdateLocalBlocks(); + }; + + req.Failure += e => + { + notifications?.Post(new SimpleNotification + { + Text = e.Message, + Icon = FontAwesome.Solid.Times, + }); + }; + + api.Queue(req); + } + } +} diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 51550e9f64..9ac40f31c6 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -14,7 +14,6 @@ using osu.Game.Overlays; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Framework.Screens; using osu.Game.Graphics.Containers; @@ -23,11 +22,8 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osu.Game.Resources.Localisation.Web; using osu.Game.Localisation; -using osu.Game.Online.API.Requests; using osu.Game.Online.Metadata; using osu.Game.Online.Multiplayer; -using osu.Game.Overlays.Dialog; -using osu.Game.Overlays.Notifications; using osu.Game.Screens; using osu.Game.Screens.Play; using osu.Game.Users.Drawables; @@ -168,14 +164,8 @@ namespace osu.Game.Users })); items.Add(!isUserBlocked() - ? new OsuMenuItem(UsersStrings.BlocksButtonBlock, MenuItemType.Destructive, () => - { - dialogOverlay?.Push(new ConfirmBlockActionDialog(ContextMenuStrings.ConfirmBlockUser(User.Username), () => toggleBlock(true))); - }) - : new OsuMenuItem(UsersStrings.BlocksButtonUnblock, MenuItemType.Standard, () => - { - dialogOverlay?.Push(new ConfirmBlockActionDialog(ContextMenuStrings.ConfirmUnblockUser(User.Username), () => toggleBlock(false))); - })); + ? new OsuMenuItem(UsersStrings.BlocksButtonBlock, MenuItemType.Destructive, () => dialogOverlay?.Push(ConfirmBlockActionDialog.Block(User))) + : new OsuMenuItem(UsersStrings.BlocksButtonUnblock, MenuItemType.Standard, () => dialogOverlay?.Push(ConfirmBlockActionDialog.Unblock(User)))); if (isUserOnline()) { @@ -203,27 +193,6 @@ namespace osu.Game.Users } } - private void toggleBlock(bool block) - { - APIRequest req = block ? new BlockUserRequest(User.OnlineID) : new UnblockUserRequest(User.OnlineID); - - req.Success += () => - { - api.UpdateLocalBlocks(); - }; - - req.Failure += e => - { - notifications?.Post(new SimpleNotification - { - Text = e.Message, - Icon = FontAwesome.Solid.Times, - }); - }; - - api.Queue(req); - } - public IEnumerable FilterTerms => [User.Username]; public bool MatchingFilter @@ -238,14 +207,5 @@ namespace osu.Game.Users } public bool FilteringActive { get; set; } - - private partial class ConfirmBlockActionDialog : DangerousActionDialog - { - public ConfirmBlockActionDialog(LocalisableString text, Action? action = null) - { - BodyText = text; - DangerousAction = action; - } - } } } From 7690d96b73126eb95ee4341e846d89a1f784ca0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Jun 2025 13:48:29 +0200 Subject: [PATCH 195/370] Add block / unblock option to chat --- osu.Game/Overlays/Chat/DrawableChatUsername.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Overlays/Chat/DrawableChatUsername.cs b/osu.Game/Overlays/Chat/DrawableChatUsername.cs index 57338dde9f..bd39cf0253 100644 --- a/osu.Game/Overlays/Chat/DrawableChatUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableChatUsername.cs @@ -28,6 +28,7 @@ using osu.Game.Online.Multiplayer; using osu.Game.Resources.Localisation.Web; using osu.Game.Screens; using osu.Game.Screens.Play; +using osu.Game.Users; using osuTK; using osuTK.Graphics; using ChatStrings = osu.Game.Localisation.ChatStrings; @@ -92,6 +93,9 @@ namespace osu.Game.Overlays.Chat [Resolved] private Bindable? currentChannel { get; set; } + [Resolved] + private IDialogOverlay? dialogOverlay { get; set; } + private readonly APIUser user; private readonly OsuSpriteText drawableText; @@ -208,6 +212,9 @@ namespace osu.Game.Overlays.Chat items.Add(new OsuMenuItemSpacer()); items.Add(new OsuMenuItem(UsersStrings.ReportButtonText, MenuItemType.Destructive, ReportRequested)); + items.Add(api.Blocks.Any(b => b.TargetID == user.OnlineID) + ? new OsuMenuItem(UsersStrings.BlocksButtonUnblock, MenuItemType.Standard, () => dialogOverlay?.Push(ConfirmBlockActionDialog.Unblock(user))) + : new OsuMenuItem(UsersStrings.BlocksButtonBlock, MenuItemType.Destructive, () => dialogOverlay?.Push(ConfirmBlockActionDialog.Block(user)))); return items.ToArray(); } From b47988e899f6f14e7b4055fd5a1797c20d22f5bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Jun 2025 14:52:54 +0200 Subject: [PATCH 196/370] Add block / unblock option to user profile overlay This is not doing the thing that the website does wherein the entire user profile is replaced by the message that the user is blocked if they're blocked. Someone else can try doing that. I'm also not adding report button to this because it's going to be annoying to make happen because currently reporting is only available as a popover and not as a dialog. Someone else can pick that up as well. --- .../Profile/Header/CentreHeaderContainer.cs | 4 + .../Header/Components/UserActionsButton.cs | 210 ++++++++++++++++++ osu.Game/Overlays/UserProfileOverlay.cs | 15 +- 3 files changed, 224 insertions(+), 5 deletions(-) create mode 100644 osu.Game/Overlays/Profile/Header/Components/UserActionsButton.cs diff --git a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs index d964364510..3f669ebfe3 100644 --- a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs @@ -55,6 +55,10 @@ namespace osu.Game.Overlays.Profile.Header { User = { BindTarget = User } }, + new UserActionsButton + { + User = { BindTarget = User } + } } }, new Container diff --git a/osu.Game/Overlays/Profile/Header/Components/UserActionsButton.cs b/osu.Game/Overlays/Profile/Header/Components/UserActionsButton.cs new file mode 100644 index 0000000000..c959e51e70 --- /dev/null +++ b/osu.Game/Overlays/Profile/Header/Components/UserActionsButton.cs @@ -0,0 +1,210 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Overlays.Profile.Header.Components +{ + public partial class UserActionsButton : OsuHoverContainer, IHasPopover + { + public readonly Bindable User = new Bindable(); + + private Box background = null!; + + protected override IEnumerable EffectTargets => [background]; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + IdleColour = colourProvider.Background2; + HoverColour = colourProvider.Background1; + + Size = new Vector2(40); + Masking = true; + CornerRadius = 20; + + Child = new CircularContainer + { + Masking = true, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + }, + new SpriteIcon + { + Size = new Vector2(12), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.EllipsisV, + }, + } + }; + + Action = this.ShowPopover; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + User.BindValueChanged(_ => Alpha = User.Value?.User.OnlineID == api.LocalUser.Value.OnlineID ? 0 : 1, true); + } + + public Popover GetPopover() => new UserActionPopover(User.Value!.User); + + private partial class UserActionPopover : OsuPopover + { + private readonly APIUser user; + + public UserActionPopover(APIUser user) + : base(false) + { + this.user = user; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider, IAPIProvider api, IDialogOverlay? dialogOverlay) + { + Background.Colour = colourProvider.Background6; + + bool userBlocked = api.Blocks.Any(b => b.TargetID == user.Id); + + AllowableAnchors = [Anchor.BottomCentre, Anchor.TopCentre]; + + Child = new FillFlowContainer + { + Width = 160, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = 5, Vertical = 10 }, + Children = new Drawable[] + { + new UserAction(FontAwesome.Solid.Ban, userBlocked ? UsersStrings.BlocksButtonUnblock : UsersStrings.BlocksButtonBlock) + { + Action = () => + { + dialogOverlay?.Push(userBlocked ? ConfirmBlockActionDialog.Unblock(user) : ConfirmBlockActionDialog.Block(user)); + this.HidePopover(); + } + } + } + }; + } + } + + private partial class UserAction : OsuClickableContainer + { + private readonly IconUsage icon; + private readonly LocalisableString caption; + + private Box background = null!; + private CircularContainer indicator = null!; + + public UserAction(IconUsage icon, LocalisableString caption) + { + this.icon = icon; + this.caption = caption; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + RelativeSizeAxes = Content.RelativeSizeAxes = Axes.X; + AutoSizeAxes = Content.AutoSizeAxes = Axes.Y; + + Masking = true; + CornerRadius = 4; + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + Alpha = 0, + }, + indicator = new Circle + { + Width = 4, + Height = 14, + X = 10, + Colour = colourProvider.Highlight1, + Anchor = Anchor.CentreLeft, + Origin = Anchor.Centre, + Alpha = 0, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Padding = new MarginPadding { Horizontal = 25, Vertical = 5 }, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(3, 0), + Children = new Drawable[] + { + new SpriteIcon + { + Icon = icon, + Size = new Vector2(14), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + new OsuSpriteText + { + Text = caption, + Font = OsuFont.Style.Caption1, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + } + } + }; + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateState(); + base.OnHoverLost(e); + } + + private void updateState() + { + background.Alpha = indicator.Alpha = IsHovered ? 1 : 0; + } + } + } +} diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index 076905819e..edaa1bdc89 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -9,6 +9,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; @@ -55,13 +56,17 @@ namespace osu.Game.Overlays public UserProfileOverlay() : base(OverlayColourScheme.Pink) { - base.Content.AddRange(new Drawable[] + base.Content.Add(new PopoverContainer { - onlineViewContainer = new OnlineViewContainer($"Sign in to view the {Header.Title.Title}") + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both - }, - loadingLayer = new LoadingLayer(true) + onlineViewContainer = new OnlineViewContainer($"Sign in to view the {Header.Title.Title}") + { + RelativeSizeAxes = Axes.Both + }, + loadingLayer = new LoadingLayer(true) + } }); } From 819decde761b9ab706d2479a67c210a530689ddd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 13 Jun 2025 22:13:07 +0900 Subject: [PATCH 197/370] Remove no-longer existing argument I'm not entirely sure how this works, but CI was testing against the updated Velopack package, whereas my local package was outdated and had 4 args. Previous commit merges master to update the package version, this commit fixes the args. --- osu.Desktop/Updater/VelopackUpdateManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/Updater/VelopackUpdateManager.cs b/osu.Desktop/Updater/VelopackUpdateManager.cs index 12a8b7a05d..475d14e1d7 100644 --- a/osu.Desktop/Updater/VelopackUpdateManager.cs +++ b/osu.Desktop/Updater/VelopackUpdateManager.cs @@ -101,7 +101,7 @@ namespace osu.Desktop.Updater progressNotification.StartDownload(); runOutsideOfGameplay(() => notificationOverlay.Post(progressNotification), cts.Token); - await updateManager.DownloadUpdatesAsync(update, p => progressNotification.Progress = p / 100f, false, cts.Token).ConfigureAwait(false); + await updateManager.DownloadUpdatesAsync(update, p => progressNotification.Progress = p / 100f, cts.Token).ConfigureAwait(false); runOutsideOfGameplay(() => progressNotification.State = ProgressNotificationState.Completed, cts.Token); } } From 4f5c9f9713ac7632f503549b718aa91ef5169a6e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 13 Jun 2025 22:36:32 +0900 Subject: [PATCH 198/370] Resolve warnings in tests --- osu.Game.Tests/NonVisual/TestSceneUpdateManager.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/NonVisual/TestSceneUpdateManager.cs b/osu.Game.Tests/NonVisual/TestSceneUpdateManager.cs index 385b8b82cb..bdb4dce354 100644 --- a/osu.Game.Tests/NonVisual/TestSceneUpdateManager.cs +++ b/osu.Game.Tests/NonVisual/TestSceneUpdateManager.cs @@ -91,14 +91,14 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestUserRequest() { - AddStep("request check", () => manager.CheckForUpdateAsync()); + AddStep("request check", () => manager.CheckForUpdate()); AddUntilStep("check pending", () => manager.IsPending); AddStep("complete check", () => manager.Complete()); AddUntilStep("2 checks completed", () => manager.Completions, () => Is.EqualTo(2)); AddUntilStep("no check pending", () => !manager.IsPending); - AddStep("request check", () => manager.CheckForUpdateAsync()); + AddStep("request check", () => manager.CheckForUpdate()); AddUntilStep("check pending", () => manager.IsPending); AddStep("complete check", () => manager.Complete()); @@ -115,9 +115,9 @@ namespace osu.Game.Tests.NonVisual // This part covering double user input is not really possible because the settings button is disabled during the check, // but it's kept here for sanity in-case the update manager is used as a standalone object elsewhere. - AddStep("request check", () => manager.CheckForUpdateAsync()); + AddStep("request check", () => manager.CheckForUpdate()); AddUntilStep("check pending", () => manager.IsPending); - AddStep("request check", () => manager.CheckForUpdateAsync()); + AddStep("request check", () => manager.CheckForUpdate()); AddUntilStep("3 invocations", () => manager.Invocations, () => Is.EqualTo(3)); AddStep("complete check", () => manager.Complete()); @@ -128,7 +128,7 @@ namespace osu.Game.Tests.NonVisual AddStep("change release stream", () => config.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Tachyon)); AddUntilStep("check pending", () => manager.IsPending); - AddStep("request check", () => manager.CheckForUpdateAsync()); + AddStep("request check", () => manager.CheckForUpdate()); AddUntilStep("5 invocations", () => manager.Invocations, () => Is.EqualTo(5)); AddStep("complete check", () => manager.Complete()); From 3be57b90dba4d1a509cfee601f76d936a0f75bb9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 13 Jun 2025 22:51:25 +0900 Subject: [PATCH 199/370] Dispose the last CTS --- osu.Game/Updater/UpdateManager.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index ef53148f67..d48d92bdae 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -105,6 +105,14 @@ namespace osu.Game.Updater /// Whether any update is waiting. May return true if an error occured (there is potentially an update available). protected virtual Task PerformUpdateCheck(CancellationToken cancellationToken) => Task.FromResult(false); + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + updateCancellation.Cancel(); + updateCancellation.Dispose(); + } + private partial class UpdateCompleteNotification : SimpleNotification { private readonly string version; From 5ef3b372ae30b1010eed656a37eb01466adf5e6d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 13 Jun 2025 22:54:34 +0900 Subject: [PATCH 200/370] Don't check for updates in DEBUG --- osu.Game/Updater/UpdateManager.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index d48d92bdae..ed19828998 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -87,6 +87,9 @@ namespace osu.Game.Updater /// true if any updates are available, false otherwise. public async Task CheckForUpdateAsync(CancellationToken cancellationToken = default) { + if (!CanCheckForUpdate) + return false; + var cancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var lastCancellation = Interlocked.Exchange(ref updateCancellation, cancellation); From 977c26d02fefb2b3a7715a8e8a43bbd3ff70476f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 14 Jun 2025 15:05:05 +0300 Subject: [PATCH 201/370] Add localisation support to difficulty range slider --- .../TestSceneDifficultyRangeSlider.cs | 1 - .../TestSceneShearedRangeSlider.cs | 1 - .../UserInterface/ShearedRangeSlider.cs | 41 +++++++++---------- osu.Game/Localisation/SongSelectStrings.cs | 5 +++ .../FilterControl_DifficultyRangeSlider.cs | 16 ++++++-- 5 files changed, 38 insertions(+), 26 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyRangeSlider.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyRangeSlider.cs index 3cadbeb1e3..f97af65fd9 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyRangeSlider.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyRangeSlider.cs @@ -59,7 +59,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Scale = new Vector2(1), LowerBound = customStart, UpperBound = customEnd, - TooltipSuffix = "suffix", NubWidth = 32, MinRange = 0.1f, } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedRangeSlider.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedRangeSlider.cs index 21fa82eda8..fdc5b5948a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedRangeSlider.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedRangeSlider.cs @@ -64,7 +64,6 @@ namespace osu.Game.Tests.Visual.UserInterface Scale = new Vector2(1), LowerBound = customStart, UpperBound = customEnd, - TooltipSuffix = "suffix", NubWidth = 32, DefaultStringLowerBound = "0.0", DefaultStringUpperBound = "∞", diff --git a/osu.Game/Graphics/UserInterface/ShearedRangeSlider.cs b/osu.Game/Graphics/UserInterface/ShearedRangeSlider.cs index 3aaa143987..417474cba3 100644 --- a/osu.Game/Graphics/UserInterface/ShearedRangeSlider.cs +++ b/osu.Game/Graphics/UserInterface/ShearedRangeSlider.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -54,20 +55,14 @@ namespace osu.Game.Graphics.UserInterface } /// - /// Lower bound display for when it is set to its default value. + /// Lower bound display for when it is set to its default value, or null to display the value directly. /// - public string DefaultStringLowerBound { get; init; } = string.Empty; + public LocalisableString? DefaultStringLowerBound { get; init; } /// - /// Upper bound display for when it is set to its default value. + /// Upper bound display for when it is set to its default value, or null to display the value directly. /// - public string DefaultStringUpperBound { get; init; } = string.Empty; - - public LocalisableString DefaultTooltipLowerBound { get; init; } = string.Empty; - - public LocalisableString DefaultTooltipUpperBound { get; init; } = string.Empty; - - public string TooltipSuffix { get; init; } = string.Empty; + public LocalisableString? DefaultStringUpperBound { get; init; } private float minRange = 0.1f; @@ -144,9 +139,7 @@ namespace osu.Game.Graphics.UserInterface { d.KeyboardStep = 0.1f; d.RelativeSizeAxes = Axes.X; - d.TooltipSuffix = TooltipSuffix; d.DefaultString = DefaultStringUpperBound; - d.DefaultTooltip = DefaultTooltipUpperBound; d.NubWidth = NubWidth; d.Current = upperBound; }), @@ -154,9 +147,7 @@ namespace osu.Game.Graphics.UserInterface { d.KeyboardStep = 0.1f; d.RelativeSizeAxes = Axes.X; - d.TooltipSuffix = TooltipSuffix; d.DefaultString = DefaultStringLowerBound; - d.DefaultTooltip = DefaultTooltipLowerBound; d.NubWidth = NubWidth; d.Current = lowerBound; }), @@ -188,14 +179,20 @@ namespace osu.Game.Graphics.UserInterface public new ShearedNub Nub => base.Nub; - public string? DefaultString; - public LocalisableString? DefaultTooltip; - public string? TooltipSuffix; + public LocalisableString? DefaultString; public float NubWidth { get; set; } = ShearedNub.HEIGHT; - public override LocalisableString TooltipText => - (Current.IsDefault ? DefaultTooltip : Current.Value.ToString($@"0.## {TooltipSuffix}")) ?? Current.Value.ToString($@"0.## {TooltipSuffix}"); + public override LocalisableString TooltipText + { + get + { + if (Current.IsDefault) + return string.Empty; + + return Current.Value.ToLocalisableString(@"N1"); + } + } protected OsuSpriteText NubText { get; private set; } = null!; @@ -245,8 +242,10 @@ namespace osu.Game.Graphics.UserInterface protected virtual void UpdateDisplay(double value) { - string defaultString = DefaultString ?? value.ToString("N1"); - NubText.Text = Current.IsDefault ? defaultString : value.ToString("N1"); + if (Current.IsDefault && DefaultString != null) + NubText.Text = DefaultString.Value; + else + NubText.Text = value.ToLocalisableString(@"N1"); } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) diff --git a/osu.Game/Localisation/SongSelectStrings.cs b/osu.Game/Localisation/SongSelectStrings.cs index e715ba8880..6b4527f063 100644 --- a/osu.Game/Localisation/SongSelectStrings.cs +++ b/osu.Game/Localisation/SongSelectStrings.cs @@ -54,6 +54,11 @@ namespace osu.Game.Localisation /// public static LocalisableString EditBeatmap => new TranslatableString(getKey(@"edit_beatmap"), @"Edit beatmap"); + /// + /// "{0} stars" + /// + public static LocalisableString Stars(LocalisableString value) => new TranslatableString(getKey(@"stars"), @"{0} stars", value); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/SelectV2/FilterControl_DifficultyRangeSlider.cs b/osu.Game/Screens/SelectV2/FilterControl_DifficultyRangeSlider.cs index 58c9c60460..52ff41fe63 100644 --- a/osu.Game/Screens/SelectV2/FilterControl_DifficultyRangeSlider.cs +++ b/osu.Game/Screens/SelectV2/FilterControl_DifficultyRangeSlider.cs @@ -5,11 +5,13 @@ using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Layout; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; @@ -35,10 +37,7 @@ namespace osu.Game.Screens.SelectV2 : base("Star Rating") { NubWidth = ShearedNub.HEIGHT * 1.16f; - TooltipSuffix = "stars"; - DefaultStringLowerBound = "0.0"; DefaultStringUpperBound = "∞"; - DefaultTooltipUpperBound = UserInterfaceStrings.NoLimit; AddLayout(drawSizeLayout); } @@ -125,6 +124,17 @@ namespace osu.Game.Screens.SelectV2 protected override bool FocusIndicator => false; + public override LocalisableString TooltipText + { + get + { + if (Current.IsDefault && isUpper) + return UserInterfaceStrings.NoLimit; + + return SongSelectStrings.Stars(Current.Value.ToLocalisableString(@"0.##")); + } + } + public DifficultyBoundSliderBar(ShearedRangeSlider slider, bool isUpper) : base(slider, isUpper) { From b2e936c430f8540ace36479af21592f9b851e9ec Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 14 Jun 2025 15:45:49 +0300 Subject: [PATCH 202/370] Remove unnecessary DI --- osu.Game/Users/UserPanel.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 9ac40f31c6..808958311c 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -83,9 +83,6 @@ namespace osu.Game.Users [Resolved] private MetadataClient? metadataClient { get; set; } - [Resolved] - private INotificationOverlay? notifications { get; set; } - [BackgroundDependencyLoader] private void load() { From 32285f45607a406468d7d52295fec39738a628c2 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 14 Jun 2025 15:46:10 +0300 Subject: [PATCH 203/370] Adjust user actions popover design --- .../Profile/Header/Components/UserActionsButton.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/UserActionsButton.cs b/osu.Game/Overlays/Profile/Header/Components/UserActionsButton.cs index c959e51e70..b8e7e96665 100644 --- a/osu.Game/Overlays/Profile/Header/Components/UserActionsButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/UserActionsButton.cs @@ -167,22 +167,23 @@ namespace osu.Game.Overlays.Profile.Header.Components RelativeSizeAxes = Axes.X, Padding = new MarginPadding { Horizontal = 25, Vertical = 5 }, Direction = FillDirection.Horizontal, - Spacing = new Vector2(3, 0), + Spacing = new Vector2(5, 0), Children = new Drawable[] { new SpriteIcon { Icon = icon, - Size = new Vector2(14), + Size = new Vector2(11), Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, new OsuSpriteText { Text = caption, - Font = OsuFont.Style.Caption1, + Font = OsuFont.Style.Body, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, + UseFullGlyphHeight = false, } } } From 74a173cdfd2f0430c75d47760cf302fdbbae6f7b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 16 Jun 2025 15:36:55 +0900 Subject: [PATCH 204/370] Attempt to fix flaky editor test --- .../Visual/Editing/TestSceneEditorBeatmapCreation.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 2758954907..60898b7ec8 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -274,6 +274,14 @@ namespace osu.Game.Tests.Visual.Editing AddStep("save beatmap", () => Editor.Save()); AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new ManiaRuleset().RulesetInfo)); + AddUntilStep("wait for created", () => + { + string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; + return difficultyName != null && difficultyName != firstDifficultyName; + }); + + ensureEditorLoaded(); + AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName); AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 })); AddStep("add effect points", () => From c88843120149b5b34f2425e364b87f236b17e170 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Jun 2025 15:47:57 +0900 Subject: [PATCH 205/370] Visual pass on loading spinner Fixes regression mentioned [here](https://github.com/ppy/osu/pull/33509#issuecomment-2951271131). Adjust visuals and metrics slightly. --- .../UserInterface/TestSceneLoadingSpinner.cs | 25 ++++++- .../Graphics/UserInterface/LoadingLayer.cs | 2 +- .../Graphics/UserInterface/LoadingSpinner.cs | 68 +++++++++++++++++-- 3 files changed, 87 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingSpinner.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingSpinner.cs index bd36be846b..b3d943b93d 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingSpinner.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingSpinner.cs @@ -11,7 +11,7 @@ namespace osu.Game.Tests.Visual.UserInterface public partial class TestSceneLoadingSpinner : OsuGridTestScene { public TestSceneLoadingSpinner() - : base(2, 2) + : base(2, 3) { LoadingSpinner loading; @@ -52,6 +52,29 @@ namespace osu.Game.Tests.Visual.UserInterface loading.Show(); Cell(3).AddRange(new Drawable[] + { + new Box + { + Colour = Color4.White, + RelativeSizeAxes = Axes.Both + }, + loading = new LoadingSpinner(false, true) + }); + + loading.Show(); + + Cell(4).AddRange(new Drawable[] + { + new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both + }, + loading = new LoadingSpinner(true, true) + }); + loading.Show(); + + Cell(5).AddRange(new Drawable[] { loading = new LoadingSpinner() }); diff --git a/osu.Game/Graphics/UserInterface/LoadingLayer.cs b/osu.Game/Graphics/UserInterface/LoadingLayer.cs index 9059b61a33..fd0cc755a1 100644 --- a/osu.Game/Graphics/UserInterface/LoadingLayer.cs +++ b/osu.Game/Graphics/UserInterface/LoadingLayer.cs @@ -90,7 +90,7 @@ namespace osu.Game.Graphics.UserInterface { base.Update(); - MainContents.Size = new Vector2(Math.Clamp(Math.Min(DrawWidth, DrawHeight) * 0.25f, 20, 100)); + MainContents.Size = new Vector2(Math.Clamp(Math.Min(DrawWidth, DrawHeight) * 0.25f, 20, 80)); } } } diff --git a/osu.Game/Graphics/UserInterface/LoadingSpinner.cs b/osu.Game/Graphics/UserInterface/LoadingSpinner.cs index 92e64d5b78..cb13a730a7 100644 --- a/osu.Game/Graphics/UserInterface/LoadingSpinner.cs +++ b/osu.Game/Graphics/UserInterface/LoadingSpinner.cs @@ -1,10 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics.Backgrounds; using osuTK; using osuTK.Graphics; @@ -25,6 +28,8 @@ namespace osu.Game.Graphics.UserInterface private readonly Container? roundedContent; + private readonly TrianglesV2 triangles; + private const float spin_duration = 900; /// @@ -56,6 +61,17 @@ namespace osu.Game.Graphics.UserInterface RelativeSizeAxes = Axes.Both, Alpha = 0.7f, }, + triangles = new TrianglesV2 + { + RelativeSizeAxes = Axes.Both, + Colour = inverted ? Color4.White : Color4.Black, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0.2f, + ScaleAdjust = 0.4f, + Velocity = 0.8f, + SpawnRatio = 2 + }, spinner = new SpriteIcon { Anchor = Anchor.Centre, @@ -70,13 +86,46 @@ namespace osu.Game.Graphics.UserInterface } else { - Child = MainContents = spinner = new SpriteIcon + Child = MainContents = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Colour = inverted ? Color4.Black : Color4.White, RelativeSizeAxes = Axes.Both, - Icon = FontAwesome.Solid.CircleNotch + Children = new Drawable[] + { + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.8f), + Masking = true, + CornerRadius = 20, + Children = new Drawable[] + { + triangles = new TrianglesV2 + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0.4f, + Colour = ColourInfo.GradientVertical( + inverted ? Color4.Black.Opacity(0) : Color4.White.Opacity(0), + inverted ? Color4.Black : Color4.White), + RelativeSizeAxes = Axes.Both, + ScaleAdjust = 0.4f, + SpawnRatio = 4, + }, + } + }, + spinner = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = inverted ? Color4.Black : Color4.White, + RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.Solid.CircleNotch + } + } }; } } @@ -96,6 +145,13 @@ namespace osu.Game.Graphics.UserInterface roundedContent.CornerRadius = MainContents.DrawWidth / 4; } + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + triangles.Rotation = -MainContents.Rotation; + } + protected override void PopIn() { if (Alpha < 0.5f) @@ -103,13 +159,13 @@ namespace osu.Game.Graphics.UserInterface rotate(); MainContents.ScaleTo(1, TRANSITION_DURATION, Easing.OutQuint); - this.FadeIn(TRANSITION_DURATION * 2, Easing.OutQuint); + this.FadeIn(TRANSITION_DURATION, Easing.OutQuint); } protected override void PopOut() { - MainContents.ScaleTo(0.8f, TRANSITION_DURATION / 2, Easing.In); - this.FadeOut(TRANSITION_DURATION, Easing.OutQuint); + MainContents.ScaleTo(0.6f, TRANSITION_DURATION, Easing.OutQuint); + this.FadeOut(TRANSITION_DURATION / 2, Easing.OutQuint); } private void rotate() From 54ef42a09e61cd4ca4133d0ee57c8d54c603a8e7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 16 Jun 2025 16:21:27 +0900 Subject: [PATCH 206/370] Adjust condition to be more thorough --- osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 60898b7ec8..75b8554cd8 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -773,7 +773,7 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("other audio not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio (1).mp3")); } - private void ensureEditorLoaded() => AddUntilStep("wait for editor load", () => Editor.IsLoaded && DialogOverlay.IsLoaded); + private void ensureEditorLoaded() => AddUntilStep("wait for editor load", () => Editor.ReadyForUse && DialogOverlay.IsLoaded); private void createNewDifficulty() { From 18195120ae0d74f2ceb5b0392d6982840d875fca Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 16 Jun 2025 16:29:08 +0900 Subject: [PATCH 207/370] Ensure editor is loaded in more places While I can't reproduce any test failures because of this, I can reproduce exceptions by simply running the tests with debugger attached. There's one other similar related test failure in `TestSingleAudioFile` but that appears to be an o!f issue. --- .../Visual/Editing/TestSceneEditorBeatmapCreation.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 75b8554cd8..8d7eb41369 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -232,8 +232,8 @@ namespace osu.Game.Tests.Visual.Editing EditorBeatmap.ControlPointInfo.Add(1500, new EffectControlPoint { KiaiMode = false, ScrollSpeed = 0.3 }); }); + ensureEditorLoaded(); AddStep("save beatmap", () => Editor.Save()); - AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new ManiaRuleset().RulesetInfo)); AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); @@ -293,8 +293,8 @@ namespace osu.Game.Tests.Visual.Editing EditorBeatmap.ControlPointInfo.Add(1500, new EffectControlPoint { KiaiMode = false, ScrollSpeed = 0.3 }); }); + ensureEditorLoaded(); AddStep("save beatmap", () => Editor.Save()); - AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new TaikoRuleset().RulesetInfo)); AddUntilStep("wait for created", () => @@ -637,6 +637,8 @@ namespace osu.Game.Tests.Visual.Editing StartTime = 1000 } })); + + ensureEditorLoaded(); AddStep("save beatmap", () => Editor.Save()); AddStep("try to create new catch difficulty", () => Editor.CreateNewDifficulty(new CatchRuleset().RulesetInfo)); From 13df477fc0fc97ea20994777b8b494da14d74fdb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Jun 2025 19:39:37 +0900 Subject: [PATCH 208/370] Fix failing test --- .../UserInterface/TestSceneLoadingLayer.cs | 6 ++---- .../Graphics/UserInterface/LoadingLayer.cs | 19 ++----------------- 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingLayer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingLayer.cs index dc40ecde43..66bf870f90 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingLayer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingLayer.cs @@ -67,11 +67,11 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("show", () => overlay.Show()); - AddUntilStep("wait for content dim", () => overlay.BackgroundDimLayer.Alpha > 0); + AddUntilStep("wait for content dim", () => overlay.Alpha > 0); AddStep("hide", () => overlay.Hide()); - AddUntilStep("wait for content restore", () => Precision.AlmostEquals(overlay.BackgroundDimLayer.Alpha, 0)); + AddUntilStep("wait for content restore", () => Precision.AlmostEquals(overlay.Alpha, 0)); } [Test] @@ -90,8 +90,6 @@ namespace osu.Game.Tests.Visual.UserInterface private partial class TestLoadingLayer : LoadingLayer { - public new Box BackgroundDimLayer => base.BackgroundDimLayer; - public TestLoadingLayer(bool dimBackground = false, bool withBox = true) : base(dimBackground, withBox) { diff --git a/osu.Game/Graphics/UserInterface/LoadingLayer.cs b/osu.Game/Graphics/UserInterface/LoadingLayer.cs index fd0cc755a1..8d7852562a 100644 --- a/osu.Game/Graphics/UserInterface/LoadingLayer.cs +++ b/osu.Game/Graphics/UserInterface/LoadingLayer.cs @@ -22,9 +22,6 @@ namespace osu.Game.Graphics.UserInterface { private readonly bool blockInput; - [CanBeNull] - protected Box BackgroundDimLayer { get; } - /// /// Construct a new loading spinner. /// @@ -42,11 +39,11 @@ namespace osu.Game.Graphics.UserInterface if (dimBackground) { - AddInternal(BackgroundDimLayer = new Box + AddInternal(new Box { Depth = float.MaxValue, Colour = Color4.Black, - Alpha = 0, + Alpha = 0.5f, RelativeSizeAxes = Axes.Both, }); } @@ -74,18 +71,6 @@ namespace osu.Game.Graphics.UserInterface return true; } - protected override void PopIn() - { - BackgroundDimLayer?.FadeTo(0.5f, TRANSITION_DURATION * 2, Easing.OutQuint); - base.PopIn(); - } - - protected override void PopOut() - { - BackgroundDimLayer?.FadeOut(TRANSITION_DURATION, Easing.OutQuint); - base.PopOut(); - } - protected override void Update() { base.Update(); From 42e7a69db6954a3866af5f3c488f7f1bc4517293 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Jun 2025 19:50:12 +0900 Subject: [PATCH 209/370] Fix incorect masking when displayed at small sizes --- .../Graphics/UserInterface/LoadingLayer.cs | 1 - .../Graphics/UserInterface/LoadingSpinner.cs | 103 ++++++++++-------- 2 files changed, 56 insertions(+), 48 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/LoadingLayer.cs b/osu.Game/Graphics/UserInterface/LoadingLayer.cs index 8d7852562a..916b041696 100644 --- a/osu.Game/Graphics/UserInterface/LoadingLayer.cs +++ b/osu.Game/Graphics/UserInterface/LoadingLayer.cs @@ -4,7 +4,6 @@ #nullable disable using System; -using JetBrains.Annotations; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; diff --git a/osu.Game/Graphics/UserInterface/LoadingSpinner.cs b/osu.Game/Graphics/UserInterface/LoadingSpinner.cs index cb13a730a7..b4bc6fb8c3 100644 --- a/osu.Game/Graphics/UserInterface/LoadingSpinner.cs +++ b/osu.Game/Graphics/UserInterface/LoadingSpinner.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.Diagnostics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -24,12 +25,14 @@ namespace osu.Game.Graphics.UserInterface protected override bool StartHidden => true; - protected Drawable MainContents; - - private readonly Container? roundedContent; + protected Container MainContents; private readonly TrianglesV2 triangles; + private readonly Container? trianglesMasking; + + private readonly bool withBox; + private const float spin_duration = 900; /// @@ -39,6 +42,8 @@ namespace osu.Game.Graphics.UserInterface /// Whether colours should be inverted (black spinner instead of white). public LoadingSpinner(bool withBox = false, bool inverted = false) { + this.withBox = withBox; + Size = new Vector2(60); Anchor = Anchor.Centre; @@ -46,7 +51,7 @@ namespace osu.Game.Graphics.UserInterface if (withBox) { - Child = MainContents = roundedContent = new Container + Child = MainContents = new Container { RelativeSizeAxes = Axes.Both, Masking = true, @@ -86,46 +91,49 @@ namespace osu.Game.Graphics.UserInterface } else { - Child = MainContents = new Container + Children = new[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + MainContents = new Container { - new Container + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.8f), - Masking = true, - CornerRadius = 20, - Children = new Drawable[] + spinner = new SpriteIcon { - triangles = new TrianglesV2 - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Alpha = 0.4f, - Colour = ColourInfo.GradientVertical( - inverted ? Color4.Black.Opacity(0) : Color4.White.Opacity(0), - inverted ? Color4.Black : Color4.White), - RelativeSizeAxes = Axes.Both, - ScaleAdjust = 0.4f, - SpawnRatio = 4, - }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = inverted ? Color4.Black : Color4.White, + RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.Solid.CircleNotch } - }, - spinner = new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = inverted ? Color4.Black : Color4.White, - RelativeSizeAxes = Axes.Both, - Icon = FontAwesome.Solid.CircleNotch } - } + }, + trianglesMasking = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.8f), + Masking = true, + CornerRadius = 20, + Children = new Drawable[] + { + triangles = new TrianglesV2 + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0.4f, + Colour = ColourInfo.GradientVertical( + inverted ? Color4.Black.Opacity(0) : Color4.White.Opacity(0), + inverted ? Color4.Black : Color4.White), + RelativeSizeAxes = Axes.Both, + ScaleAdjust = 0.4f, + SpawnRatio = 4, + }, + } + }, }; } } @@ -137,19 +145,20 @@ namespace osu.Game.Graphics.UserInterface rotate(); } - protected override void Update() - { - base.Update(); - - if (roundedContent != null) - roundedContent.CornerRadius = MainContents.DrawWidth / 4; - } - protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); - triangles.Rotation = -MainContents.Rotation; + if (withBox) + { + MainContents.CornerRadius = MainContents.DrawWidth / 4; + triangles.Rotation = -MainContents.Rotation; + } + else + { + Debug.Assert(trianglesMasking != null); + trianglesMasking.CornerRadius = MainContents.DrawWidth / 2; + } } protected override void PopIn() From 05a50f523c037d3b8b9468627b3f847de5f20729 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Jun 2025 19:59:30 +0900 Subject: [PATCH 210/370] Add back random button tests --- .../SongSelectV2/TestSceneSongSelect.cs | 173 +++++++++++------- 1 file changed, 108 insertions(+), 65 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs index 294a33c7e5..69bdb97617 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs @@ -26,6 +26,7 @@ using osu.Game.Screens.SelectV2; using osuTK.Input; using FooterButtonMods = osu.Game.Screens.SelectV2.FooterButtonMods; using FooterButtonOptions = osu.Game.Screens.SelectV2.FooterButtonOptions; +using FooterButtonRandom = osu.Game.Screens.SelectV2.FooterButtonRandom; namespace osu.Game.Tests.Visual.SongSelectV2 { @@ -451,71 +452,113 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("Hide", () => this.ChildrenOfType().Single().Hide()); } - // add these test cases when functionality is implemented. - // [Test] - // public void TestFooterRandom() - // { - // loadSongSelect(); - // - // AddStep("press F2", () => InputManager.Key(Key.F2)); - // AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled); - // } - // - // [Test] - // public void TestFooterRandomViaMouse() - // { - // loadSongSelect(); - // - // AddStep("click button", () => - // { - // InputManager.MoveMouseTo(randomButton); - // InputManager.Click(MouseButton.Left); - // }); - // AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled); - // } - // - // [Test] - // public void TestFooterRewind() - // { - // loadSongSelect(); - // - // AddStep("press Shift+F2", () => - // { - // InputManager.PressKey(Key.LShift); - // InputManager.PressKey(Key.F2); - // InputManager.ReleaseKey(Key.F2); - // InputManager.ReleaseKey(Key.LShift); - // }); - // AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled); - // } - // - // [Test] - // public void TestFooterRewindViaShiftMouseLeft() - // { - // loadSongSelect(); - // - // AddStep("shift + click button", () => - // { - // InputManager.PressKey(Key.LShift); - // InputManager.MoveMouseTo(randomButton); - // InputManager.Click(MouseButton.Left); - // InputManager.ReleaseKey(Key.LShift); - // }); - // AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled); - // } - // - // [Test] - // public void TestFooterRewindViaMouseRight() - // { - // loadSongSelect(); - // - // AddStep("right click button", () => - // { - // InputManager.MoveMouseTo(randomButton); - // InputManager.Click(MouseButton.Right); - // }); - // AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled); - // } + [Test] + public void TestFooterRandom() + { + LoadSongSelect(); + + bool nextRandomCalled = false; + bool previousRandomCalled = false; + AddStep("hook events", () => + { + randomButton.NextRandom = () => nextRandomCalled = true; + randomButton.PreviousRandom = () => previousRandomCalled = true; + }); + + AddStep("press F2", () => InputManager.Key(Key.F2)); + AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled); + } + + [Test] + public void TestFooterRandomViaMouse() + { + LoadSongSelect(); + + bool nextRandomCalled = false; + bool previousRandomCalled = false; + AddStep("hook events", () => + { + randomButton.NextRandom = () => nextRandomCalled = true; + randomButton.PreviousRandom = () => previousRandomCalled = true; + }); + + AddStep("click button", () => + { + InputManager.MoveMouseTo(randomButton); + InputManager.Click(MouseButton.Left); + }); + AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled); + } + + [Test] + public void TestFooterRewind() + { + LoadSongSelect(); + + bool nextRandomCalled = false; + bool previousRandomCalled = false; + AddStep("hook events", () => + { + randomButton.NextRandom = () => nextRandomCalled = true; + randomButton.PreviousRandom = () => previousRandomCalled = true; + }); + + AddStep("press Shift+F2", () => + { + InputManager.PressKey(Key.LShift); + InputManager.PressKey(Key.F2); + InputManager.ReleaseKey(Key.F2); + InputManager.ReleaseKey(Key.LShift); + }); + + AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled); + } + + [Test] + public void TestFooterRewindViaShiftMouseLeft() + { + LoadSongSelect(); + + bool nextRandomCalled = false; + bool previousRandomCalled = false; + AddStep("hook events", () => + { + randomButton.NextRandom = () => nextRandomCalled = true; + randomButton.PreviousRandom = () => previousRandomCalled = true; + }); + + AddStep("shift + click button", () => + { + InputManager.PressKey(Key.LShift); + InputManager.MoveMouseTo(randomButton); + InputManager.Click(MouseButton.Left); + InputManager.ReleaseKey(Key.LShift); + }); + AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled); + } + + [Test] + public void TestFooterRewindViaMouseRight() + { + LoadSongSelect(); + + bool nextRandomCalled = false; + bool previousRandomCalled = false; + AddStep("hook events", () => + { + randomButton.NextRandom = () => nextRandomCalled = true; + randomButton.PreviousRandom = () => previousRandomCalled = true; + }); + + AddStep("right click button", () => + { + InputManager.MoveMouseTo(randomButton); + InputManager.Click(MouseButton.Right); + }); + AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled); + } + + private FooterButtonRandom randomButton => Footer.ChildrenOfType().Single(); [Test] public void TestFooterOptions() From 10c07fdf506f0580526cad424f470c54208e88fb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Jun 2025 20:15:34 +0900 Subject: [PATCH 211/370] Fix crash when random and rewind are run on the same frame --- .../TestSceneBeatmapCarouselRandom.cs | 22 +++++++++++++++++++ osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 5 ++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs index 60cec0c2ec..858c314904 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs @@ -121,6 +121,28 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } } + [Test] + public void TestRandomThenRewindSameFrame() + { + AddBeatmaps(10, 3, true); + WaitForDrawablePanels(); + + BeatmapInfo? originalSelected = null; + + nextRandom(); + + CheckHasSelection(); + AddStep("store selection", () => originalSelected = (BeatmapInfo)Carousel.CurrentSelection!); + + AddStep("random then rewind", () => + { + Carousel.NextRandom(); + Carousel.PreviousRandom(); + }); + + AddAssert("selection not changed", () => Carousel.CurrentSelection, () => Is.EqualTo(originalSelected)); + } + [Test] public void TestRewindToDeletedBeatmap() { diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 4d066e0323..24092b8ecd 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -691,7 +691,10 @@ namespace osu.Game.Screens.SelectV2 if (randomAlgorithm.Value == RandomSelectAlgorithm.RandomPermutation) previouslyVisitedRandomSets.Remove(beatmapInfo.BeatmapSet!); - playSpinSample(distanceBetween(previousBeatmapItem, CurrentSelectionItem!), carouselItems.Count); + if (CurrentSelectionItem == null) + playSpinSample(0, carouselItems.Count); + else + playSpinSample(distanceBetween(previousBeatmapItem, CurrentSelectionItem), carouselItems.Count); } RequestSelection(previousBeatmap); From 36b5da3bd0dade09c22b8ab998ec4f824e16ad41 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Jun 2025 14:32:02 +0900 Subject: [PATCH 212/370] Fix skin layer not hiding when revealing background --- osu.Game/Screens/SelectV2/SongSelect.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 8682576573..fc15090a5b 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -95,6 +95,7 @@ namespace osu.Game.Screens.SelectV2 private FillFlowContainer wedgesContainer = null!; private Box rightGradientBackground = null!; private Container mainContent = null!; + private SkinnableContainer skinnableContent = null!; private NoResultsPlaceholder noResultsPlaceholder = null!; @@ -257,8 +258,10 @@ namespace osu.Game.Screens.SelectV2 }, } }, - new SkinnableContainer(new GlobalSkinnableContainerLookup(GlobalSkinnableContainers.SongSelect)) + skinnableContent = new SkinnableContainer(new GlobalSkinnableContainerLookup(GlobalSkinnableContainers.SongSelect)) { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, }, modSpeedHotkeyHandler = new ModSpeedHotkeyHandler(), @@ -761,6 +764,10 @@ namespace osu.Game.Screens.SelectV2 mainContent.ScaleTo(1.2f, 600, Easing.OutQuint); mainContent.FadeOut(200, Easing.OutQuint); + skinnableContent.ResizeWidthTo(1.2f, 600, Easing.OutQuint); + skinnableContent.ScaleTo(1.2f, 600, Easing.OutQuint); + skinnableContent.FadeOut(200, Easing.OutQuint); + Footer?.Hide(); }, 200); } @@ -785,6 +792,10 @@ namespace osu.Game.Screens.SelectV2 mainContent.ScaleTo(1, 500, Easing.OutQuint); mainContent.FadeIn(500, Easing.OutQuint); + skinnableContent.ResizeWidthTo(1f, 500, Easing.OutQuint); + skinnableContent.ScaleTo(1, 500, Easing.OutQuint); + skinnableContent.FadeIn(500, Easing.OutQuint); + Footer?.Show(); } From bd1002c620fd745007e675f16772fc95f6847ae3 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 16 Jun 2025 22:46:07 -0700 Subject: [PATCH 213/370] Add specific cases to visual test --- osu.Game.Tests/Visual/SongSelectV2/TestSceneFooterButtonMods.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneFooterButtonMods.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneFooterButtonMods.cs index 8e27c395c8..c339f16bb4 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneFooterButtonMods.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneFooterButtonMods.cs @@ -48,6 +48,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("modified", () => changeMods(new List { new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } })); AddStep("modified + one", () => changeMods(new List { new OsuModHidden(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } })); AddStep("modified + two", () => changeMods(new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } })); + AddStep("modified + five", () => changeMods(new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } }, new OsuModClassic(), new OsuModDifficultyAdjust(), new OsuModRandom() })); + AddStep("modified + six", () => changeMods(new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } }, new OsuModClassic(), new OsuModDifficultyAdjust(), new OsuModRandom(), new OsuModAlternate() })); AddStep("clear mods", () => changeMods(Array.Empty())); AddWaitStep("wait", 3); From f9bae1fe2f42ed01f1ee9d9631fe4ea9d397d9d7 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 16 Jun 2025 22:46:13 -0700 Subject: [PATCH 214/370] Fix mod adjustment marker not masking correctly --- osu.Game/Rulesets/UI/ModIcon.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index d3f04e7e74..9ed4f7135f 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -8,7 +8,6 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Utils; @@ -84,7 +83,7 @@ namespace osu.Game.Rulesets.UI private Drawable adjustmentMarker = null!; - private Circle cogBackground = null!; + private SpriteIcon cogBackground = null!; private SpriteIcon cog = null!; private ModSettingChangeTracker? modSettingsChangeTracker; @@ -178,11 +177,12 @@ namespace osu.Game.Rulesets.UI Position = new Vector2(64, 14), Children = new Drawable[] { - cogBackground = new Circle + cogBackground = new SpriteIcon { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.Solid.Circle, }, cog = new SpriteIcon { From e74f687c30d26ef6339602d6e59c17737e4c3744 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Jun 2025 16:49:52 +0900 Subject: [PATCH 215/370] Fix mod button still working after gameplay start if player is not fully loaded --- osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs | 5 +++++ osu.Game/OsuGame.cs | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs index 75996fe158..c704f21fa4 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs @@ -196,7 +196,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2 if (osuScreen.IsLoaded) updateFooterButtons(); else + { + // ensure the current buttons are immediately disabled on screen change (so they can't be pressed). + Footer.SetButtons(Array.Empty()); + osuScreen.OnLoadComplete += _ => updateFooterButtons(); + } void updateFooterButtons() { diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index e516e56c36..394917dc62 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1752,7 +1752,12 @@ namespace osu.Game if (newOsuScreen.IsLoaded) updateFooterButtons(); else + { + // ensure the current buttons are immediately disabled on screen change (so they can't be pressed). + ScreenFooter.SetButtons(Array.Empty()); + newOsuScreen.OnLoadComplete += _ => updateFooterButtons(); + } void updateFooterButtons() { From 27d4ad7991f239d0bc6055cffd36f57639eb2217 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Jun 2025 19:41:05 +0900 Subject: [PATCH 216/370] Fix group selection acting weirdly when only one group is present --- .../TestSceneBeatmapCarouselDifficultyGrouping.cs | 15 +++++++++++++++ .../TestSceneBeatmapCarouselFiltering.cs | 4 ++-- .../TestSceneBeatmapCarouselNoGrouping.cs | 7 ++----- osu.Game/Graphics/Carousel/Carousel.cs | 2 +- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs index 5a03e05344..2ab0eda172 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs @@ -155,6 +155,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2 checkGroupKeyboardSelected(1); } + [Test] + public void TestKeyboardGroupTraversalSingleGroup() + { + RemoveAllBeatmaps(); + AddBeatmaps(1, 1); + + WaitForBeatmapSelection(0, 0); + + SelectNextGroup(); + checkBeatmapIsKeyboardSelected(); + + SelectPrevGroup(); + checkBeatmapIsKeyboardSelected(); + } + [Test] public void TestKeyboardGroupTraversal() { diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs index 8ed1b1745e..78c12e2730 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs @@ -313,9 +313,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 CheckDisplayedBeatmapsCount(4); SelectNextSet(); - WaitForSetSelection(0, 1); - SelectPrevSet(); WaitForSetSelection(1, 1); + SelectPrevSet(); + WaitForSetSelection(0, 1); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs index a6ba6d76a3..c3617b9f16 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs @@ -195,16 +195,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextSet(); WaitForSetSelection(0, 0); - // In the case of a grouped beatmap set, the header gets activated and re-selects the recommended difficulty. - // This is probably fine. - CheckActivationCount(1); - // We don't want it to request present though, which would start gameplay. + CheckActivationCount(0); CheckRequestPresentCount(0); SelectPrevSet(); WaitForSetSelection(0, 0); - CheckActivationCount(1); + CheckActivationCount(0); CheckRequestPresentCount(0); } diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index 231b2958c6..a4aafb269e 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -616,7 +616,7 @@ namespace osu.Game.Graphics.Carousel var newItem = carouselItems[newIndex]; - if (predicate(newItem)) + if (!newItem.IsExpanded && predicate(newItem)) { Activate(newItem); return; From b74db44fdeecfcff4141ca3b56701cf48896879f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Jun 2025 23:15:17 +0900 Subject: [PATCH 217/370] Fix group toggle not working as expected anymore --- osu.Game/Graphics/Carousel/Carousel.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index a4aafb269e..545fac0e98 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -491,8 +491,8 @@ namespace osu.Game.Graphics.Carousel } else { - // If current keyboard selection is not a group, toggle closest group and move keyboard selection to that group. - traverseSelection(-1, CheckValidForGroupSelection, false); + // If current keyboard selection is not a group, toggle the closest group and move keyboard selection to that group. + traverseSelection(-1, CheckValidForGroupSelection, skipFirst: false, activateExpandedItems: true); } return true; @@ -577,7 +577,7 @@ namespace osu.Game.Graphics.Carousel traverseSelection(direction, CheckValidForSetSelection); } - private void traverseSelection(int direction, Func predicate, bool skipFirst = true) + private void traverseSelection(int direction, Func predicate, bool skipFirst = true, bool activateExpandedItems = false) { if (carouselItems == null || carouselItems.Count == 0) return; @@ -616,7 +616,7 @@ namespace osu.Game.Graphics.Carousel var newItem = carouselItems[newIndex]; - if (!newItem.IsExpanded && predicate(newItem)) + if ((activateExpandedItems || !newItem.IsExpanded) && predicate(newItem)) { Activate(newItem); return; From 1ceb59d78e01fcad369c62e842a1c144c5f68df0 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 18 Jun 2025 01:52:26 +0300 Subject: [PATCH 218/370] Adjust rank formatting logic to avoid getting cut in score --- .../Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs | 3 ++- osu.Game/Utils/FormatUtils.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs index 7ef8da7673..0aca2d6a1c 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs @@ -53,6 +53,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { fillFlow = new FillFlowContainer { + X = 100, Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.X, @@ -281,7 +282,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }, new ScoreInfo { - Position = 110000, + Position = 2233, Rank = ScoreRank.D, Accuracy = 1, MaxCombo = 244, diff --git a/osu.Game/Utils/FormatUtils.cs b/osu.Game/Utils/FormatUtils.cs index f7250c6833..122bc6d326 100644 --- a/osu.Game/Utils/FormatUtils.cs +++ b/osu.Game/Utils/FormatUtils.cs @@ -30,7 +30,7 @@ namespace osu.Game.Utils /// Formats the supplied rank/leaderboard position in a consistent, simplified way. /// /// The rank/position to be formatted. - public static string FormatRank(this int rank) => rank.ToMetric(decimals: rank < 100_000 ? 1 : 0); + public static string FormatRank(this int rank) => rank.ToMetric(decimals: rank < 10_000 ? 1 : 0); /// /// Formats the supplied star rating in a consistent, simplified way. From f48cfa7ef6899e6b9083cc8c8ceb107236636950 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Jun 2025 14:27:17 +0900 Subject: [PATCH 219/370] Adjust some button's hover colours to improve visual contrast with text Addresses https://github.com/ppy/osu/discussions/33722. --- osu.Game/Graphics/UserInterfaceV2/FormButton.cs | 2 +- osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormButton.cs b/osu.Game/Graphics/UserInterfaceV2/FormButton.cs index 1c5d4b5d80..85198191b8 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormButton.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormButton.cs @@ -129,7 +129,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 private void load(OverlayColourProvider overlayColourProvider) { DefaultBackgroundColour = overlayColourProvider.Colour3; - triangleGradientSecondColour ??= overlayColourProvider.Colour1; + triangleGradientSecondColour ??= DefaultBackgroundColour.Lighten(0.2f); if (Text == default) { diff --git a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs index 9b57ebb200..bf92f20526 100644 --- a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs +++ b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs @@ -54,7 +54,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 { // Many buttons have local colours, but this provides a sane default for all other cases. DefaultBackgroundColour = overlayColourProvider?.Colour3 ?? colours.Blue3; - triangleGradientSecondColour ??= overlayColourProvider?.Colour1 ?? colours.Blue3.Lighten(0.2f); + triangleGradientSecondColour ??= DefaultBackgroundColour.Lighten(0.2f); } protected override void LoadComplete() From eb8c4a27e5367eca2cccdb1cf4150cb6cba5dfa2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Jun 2025 18:29:36 +0900 Subject: [PATCH 220/370] Update cancellation token naming / inline comment slightly --- osu.Game/Updater/UpdateManager.cs | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index ed19828998..a9b00e8f93 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -44,7 +44,8 @@ namespace osu.Game.Updater protected IBindable ReleaseStream => releaseStream; private readonly Bindable releaseStream = new Bindable(); - private CancellationTokenSource updateCancellation = new CancellationTokenSource(); + + private CancellationTokenSource updateCancellationSource = new CancellationTokenSource(); protected override void LoadComplete() { @@ -90,16 +91,13 @@ namespace osu.Game.Updater if (!CanCheckForUpdate) return false; - var cancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - var lastCancellation = Interlocked.Exchange(ref updateCancellation, cancellation); + var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - using (lastCancellation) - { - // This serves a dual purpose of nullifying the last update, closing any existing notifications as stale. - await lastCancellation.CancelAsync().ConfigureAwait(false); - } + // Cancels the last update and closes any existing notifications as stale. + using (var lastCts = Interlocked.Exchange(ref updateCancellationSource, cts)) + await lastCts.CancelAsync().ConfigureAwait(false); - return await PerformUpdateCheck(cancellation.Token).ConfigureAwait(false); + return await PerformUpdateCheck(cts.Token).ConfigureAwait(false); } /// @@ -112,8 +110,8 @@ namespace osu.Game.Updater { base.Dispose(isDisposing); - updateCancellation.Cancel(); - updateCancellation.Dispose(); + updateCancellationSource.Cancel(); + updateCancellationSource.Dispose(); } private partial class UpdateCompleteNotification : SimpleNotification From 938a3cf3eb56738ffba63ee5708d0f1f5e63a8a2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Jun 2025 18:40:54 +0900 Subject: [PATCH 221/370] Add prefix to log events --- osu.Desktop/Updater/VelopackUpdateManager.cs | 25 +++++++++++--------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/osu.Desktop/Updater/VelopackUpdateManager.cs b/osu.Desktop/Updater/VelopackUpdateManager.cs index 475d14e1d7..3b79313f8c 100644 --- a/osu.Desktop/Updater/VelopackUpdateManager.cs +++ b/osu.Desktop/Updater/VelopackUpdateManager.cs @@ -13,10 +13,11 @@ using osu.Game.Overlays.Notifications; using osu.Game.Screens.Play; using Velopack; using Velopack.Sources; +using UpdateManager = osu.Game.Updater.UpdateManager; namespace osu.Desktop.Updater { - public partial class VelopackUpdateManager : Game.Updater.UpdateManager + public partial class VelopackUpdateManager : UpdateManager { [Resolved] private INotificationOverlay notificationOverlay { get; set; } = null!; @@ -36,7 +37,7 @@ namespace osu.Desktop.Updater scheduledBackgroundCheck?.Cancel(); scheduledBackgroundCheck = Scheduler.AddDelayed(() => { - Logger.Log("Running scheduled background update check..."); + log("Running scheduled background update check..."); CheckForUpdate(); }, 60000 * 30); } @@ -47,13 +48,13 @@ namespace osu.Desktop.Updater if (isInGameplay) { - Logger.Log("Update check cancelled - user is in gameplay"); + log("Update check cancelled - user is in gameplay"); scheduleNextUpdateCheck(); return false; } IUpdateSource updateSource = new GithubSource(@"https://github.com/ppy/osu", null, ReleaseStream.Value == Game.Configuration.ReleaseStream.Tachyon); - UpdateManager updateManager = new UpdateManager(updateSource, new UpdateOptions + Velopack.UpdateManager updateManager = new Velopack.UpdateManager(updateSource, new UpdateOptions { AllowVersionDowngrade = true }); @@ -62,7 +63,7 @@ namespace osu.Desktop.Updater if (cancellationToken.IsCancellationRequested) { - Logger.Log("Update check cancelled"); + log("Update check cancelled"); scheduleNextUpdateCheck(); return true; } @@ -70,20 +71,20 @@ namespace osu.Desktop.Updater if (update == null) { // No update is available. - Logger.Log("No update found"); + log("No update found"); scheduleNextUpdateCheck(); return false; } // Download update in the background while notifying awaiters of the update being available. - Logger.Log($"New update available: {update.TargetFullRelease.Version}"); + log($"New update available: {update.TargetFullRelease.Version}"); downloadUpdate(updateManager, update, cancellationToken); return true; } - private void downloadUpdate(UpdateManager updateManager, UpdateInfo update, CancellationToken cancellationToken) => Task.Run(async () => + private void downloadUpdate(Velopack.UpdateManager updateManager, UpdateInfo update, CancellationToken cancellationToken) => Task.Run(async () => { - Logger.Log($"Beginning download of update {update.TargetFullRelease.Version}..."); + log($"Beginning download of update {update.TargetFullRelease.Version}..."); UpdateDownloadProgressNotification progressNotification = new UpdateDownloadProgressNotification(cancellationToken) { @@ -108,7 +109,7 @@ namespace osu.Desktop.Updater catch (OperationCanceledException) { progressNotification.FailDownload(); - Logger.Log(@"Update cancelled"); + log(@"Update cancelled"); } catch (Exception e) { @@ -134,10 +135,12 @@ namespace osu.Desktop.Updater action(); } - private void restartToApplyUpdate(UpdateManager updateManager, UpdateInfo update) => Task.Run(async () => + private void restartToApplyUpdate(Velopack.UpdateManager updateManager, UpdateInfo update) => Task.Run(async () => { await updateManager.WaitExitThenApplyUpdatesAsync(update.TargetFullRelease).ConfigureAwait(false); Schedule(() => game.AttemptExit()); }); + + private static void log(string text) => Logger.Log($"VelopackUpdateManager: {text}"); } } From 176a85763ce47d56132661b9a6a0feded98e7837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 18 Jun 2025 17:59:54 +0200 Subject: [PATCH 222/370] Fix drawable hold notes continuing to show hit lighting with No Release mod and classic skin Closes https://github.com/ppy/osu/issues/33751. --- .../Objects/Drawables/DrawableHoldNote.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 6c607886ae..23c062164e 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -197,6 +197,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public override void OnKilled() { base.OnKilled(); + // flush the final state of holding on kill. + // this matters because some skin implementations like legacy skin + // insert drawables in the hierarchy that are not a child of this DHO + // (see `LegacyBodyPiece` and related machinations with `lightContainer` being added at column level) + isHolding.Value = Result.IsHolding(Time.Current); (bodyPiece.Drawable as IHoldNoteBody)?.Recycle(); } From 9cb824df6f299d3f8b04753ca6cb9100084a2315 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 18 Jun 2025 19:17:15 +0300 Subject: [PATCH 223/370] Add failing test case --- .../Mods/OsuModMagnetised.cs | 2 +- .../TestSceneSongSelectNavigation.cs | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs index b2553e295c..5038250261 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs @@ -19,7 +19,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Mods { - internal class OsuModMagnetised : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset + public class OsuModMagnetised : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset { public override string Name => "Magnetised"; public override string Acronym => "MG"; diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs index 8dc73af108..14dbd7981c 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs @@ -10,6 +10,7 @@ using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Overlays; +using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens; @@ -126,6 +127,47 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("Ensure time wasn't reset to preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().BeatmapInfo.Metadata.PreviewTime); } + [Test] + public void TestAutoplayShortcutReturnsInitialModsOnExit() + { + PushAndConfirm(() => new SoloSongSelect()); + + AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game, virtualTrack: true).WaitSafely()); + + AddUntilStep("wait for selection", () => !Game.Beatmap.IsDefault); + + AddStep("open mod select", () => InputManager.Key(Key.F1)); + AddStep("search magnetised", () => this.ChildrenOfType().Single().SearchTerm = "MG"); + AddStep("select", () => InputManager.Key(Key.Enter)); + + AddAssert("magnetised selected", () => Game.SelectedMods.Value.Single(), Is.TypeOf); + AddStep("configure mod", () => ((OsuModMagnetised)Game.SelectedMods.Value.Single()).AttractionStrength.Value = 1.0f); + + pushEscape(); + pushEscape(); + + AddStep("press ctrl+enter", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.Enter); + InputManager.ReleaseKey(Key.ControlLeft); + }); + + AddUntilStep("wait for player", () => + { + DismissAnyNotifications(); + return Game.ScreenStack.CurrentScreen is Player; + }); + + AddAssert("only autoplay selected", () => Game.SelectedMods.Value.Single(), Is.TypeOf); + + pushEscape(); + waitForScreen(); + + AddAssert("magnetised selected", () => Game.SelectedMods.Value.Single(), Is.TypeOf); + AddAssert("mod configured", () => ((OsuModMagnetised)Game.SelectedMods.Value.Single()).AttractionStrength.Value, () => Is.EqualTo(1.0f)); + } + private Func playToResults() { var player = playToCompletion(); From ce498c9062c55a504381ffa5fd878073a355177e Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 18 Jun 2025 19:27:32 +0300 Subject: [PATCH 224/370] Deep clone mods when temporarily activating autoplay --- osu.Game/Screens/SelectV2/SoloSongSelect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/SoloSongSelect.cs b/osu.Game/Screens/SelectV2/SoloSongSelect.cs index 3771528a80..4ef73d4c49 100644 --- a/osu.Game/Screens/SelectV2/SoloSongSelect.cs +++ b/osu.Game/Screens/SelectV2/SoloSongSelect.cs @@ -91,7 +91,7 @@ namespace osu.Game.Screens.SelectV2 { if (playerLoader != null) return; - modsAtGameplayStart = Mods.Value; + modsAtGameplayStart = Mods.Value.Select(m => m.DeepClone()).ToArray(); // Ctrl+Enter should start map with autoplay enabled. if (GetContainingInputManager()?.CurrentState?.Keyboard.ControlPressed == true) From 634fd007e1dfcf6580382794c293525e8e507e6b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 19 Jun 2025 15:08:42 +0900 Subject: [PATCH 225/370] Ignore case when parsing `OSU_EXTERNAL_UPDATE_STREAM` --- osu.Game/Updater/NoActionUpdateManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Updater/NoActionUpdateManager.cs b/osu.Game/Updater/NoActionUpdateManager.cs index 3f1e383a50..06189b488c 100644 --- a/osu.Game/Updater/NoActionUpdateManager.cs +++ b/osu.Game/Updater/NoActionUpdateManager.cs @@ -17,7 +17,7 @@ namespace osu.Game.Updater /// public partial class NoActionUpdateManager : UpdateManager { - private static ReleaseStream? externalReleaseStream => Enum.TryParse(Environment.GetEnvironmentVariable("OSU_EXTERNAL_UPDATE_STREAM"), out ReleaseStream stream) ? stream : null; + private static ReleaseStream? externalReleaseStream => Enum.TryParse(Environment.GetEnvironmentVariable("OSU_EXTERNAL_UPDATE_STREAM"), true, out ReleaseStream stream) ? stream : null; private string version = string.Empty; From d200a5990213e1568cb9e9d00da201e1972b3e52 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Jun 2025 19:18:44 +0900 Subject: [PATCH 226/370] SongSelectV2: Fix random button not respecting open group Closes https://github.com/ppy/osu/issues/33569. --- .../TestSceneBeatmapCarouselRandom.cs | 74 ++++++++++++++----- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 20 +++++ 2 files changed, 77 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs index 858c314904..17d31634fc 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Screens.Select.Filter; +using osu.Game.Screens.SelectV2; namespace osu.Game.Tests.Visual.SongSelectV2 { @@ -47,25 +48,42 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddBeatmaps(10, 3, true); WaitForDrawablePanels(); + GroupDefinition? expanded = null; + + for (int i = 0; i < 2; i++) + { + nextRandom(); + expanded ??= storeExpandedGroup(); + + ensureRandomDidNotRepeat(); + checkExpandedGroupUnchanged(); + } + nextRandom(); - ensureRandomDidNotRepeat(); - nextRandom(); - ensureRandomDidNotRepeat(); - nextRandom(); - ensureRandomDidNotRepeat(); + ensureRandomDidRepeat(); + checkExpandedGroupUnchanged(); prevRandom(); checkRewindCorrectSet(); + checkExpandedGroupUnchanged(); prevRandom(); checkRewindCorrectSet(); + checkExpandedGroupUnchanged(); nextRandom(); ensureRandomDidNotRepeat(); + checkExpandedGroupUnchanged(); nextRandom(); - ensureRandomDidNotRepeat(); + ensureRandomDidRepeat(); + checkExpandedGroupUnchanged(); - nextRandom(); - AddAssert("ensure repeat", () => BeatmapSetRequestedSelections.Contains(Carousel.SelectedBeatmapSet!)); + GroupDefinition? storeExpandedGroup() + { + AddStep("store open group", () => expanded = Carousel.ExpandedGroup); + return null; + } + + void checkExpandedGroupUnchanged() => AddAssert("expanded did not change", () => Carousel.ExpandedGroup, () => Is.EqualTo(expanded)); } /// @@ -76,28 +94,47 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { SortAndGroupBy(SortMode.Difficulty, GroupMode.Difficulty); - AddBeatmaps(10, 3, true); + AddBeatmaps(3, 3, true); WaitForDrawablePanels(); + GroupDefinition? expanded = null; + + for (int i = 0; i < 3; i++) + { + nextRandom(); + expanded ??= storeExpandedGroup(); + + ensureRandomDidNotRepeat(); + checkExpandedGroupUnchanged(); + } + nextRandom(); - ensureRandomDidNotRepeat(); - nextRandom(); - ensureRandomDidNotRepeat(); - nextRandom(); - ensureRandomDidNotRepeat(); + ensureRandomDidRepeat(); + checkExpandedGroupUnchanged(); prevRandom(); checkRewindCorrectSet(); + checkExpandedGroupUnchanged(); + prevRandom(); checkRewindCorrectSet(); + checkExpandedGroupUnchanged(); nextRandom(); ensureRandomDidNotRepeat(); - nextRandom(); - ensureRandomDidNotRepeat(); + checkExpandedGroupUnchanged(); nextRandom(); - AddAssert("ensure repeat", () => BeatmapSetRequestedSelections.Contains(Carousel.SelectedBeatmapSet!)); + ensureRandomDidRepeat(); + checkExpandedGroupUnchanged(); + + GroupDefinition? storeExpandedGroup() + { + AddStep("store open group", () => expanded = Carousel.ExpandedGroup); + return null; + } + + void checkExpandedGroupUnchanged() => AddAssert("expanded did not change", () => Carousel.ExpandedGroup, () => Is.EqualTo(expanded)); } [Test] @@ -174,6 +211,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 private void nextRandom() => AddStep("select random next", () => Carousel.NextRandom()); + private void ensureRandomDidRepeat() => + AddAssert("did repeat", () => BeatmapSetRequestedSelections.Distinct().Count(), () => Is.LessThan(BeatmapSetRequestedSelections.Count)); + private void ensureRandomDidNotRepeat() => AddAssert("no repeats", () => BeatmapSetRequestedSelections.Distinct().Count(), () => Is.EqualTo(BeatmapSetRequestedSelections.Count)); diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 24092b8ecd..077a0fb9f8 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -634,6 +634,26 @@ namespace osu.Game.Screens.SelectV2 // This is the fastest way to retrieve sets for randomisation. ICollection visibleSets = grouping.SetItems.Keys; + if (ExpandedGroup != null) + { + // In the case of grouping, users expect random to only operate on the expanded group. + // This is going to incur some overhead as we don't have a group-beatmapset mapping currently. + // + // If this becomes an issue, we could either store a mapping, or run the random algorithm many times + // using the `SetItems` method until we get a group HIT. + if (grouping.BeatmapSetsGroupedTogether) + visibleSets = grouping.GroupItems[ExpandedGroup].Select(i => i.Model).OfType().ToArray(); + else + { + // Note that this is probably not correct in all cases. + // When we aren't grouping sets together, we might want to randomise by beatmaps, not sets. + // + // Imagine the scenario where a single beatmap set has multiple difficulties in the same difficulty grouping, where this + // would always choose the set's user recommended difficulty rather than the visible ones. + visibleSets = grouping.GroupItems[ExpandedGroup].Select(i => i.Model).OfType().Select(b => b.BeatmapSet!).Distinct().ToArray(); + } + } + if (CurrentSelection is BeatmapInfo beatmapInfo) { randomSelectedBeatmaps.Add(beatmapInfo); From f676206331cfa43485f6b62f81282eefc50b3ae6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Jun 2025 19:42:13 +0900 Subject: [PATCH 227/370] SongSelectV2: Fix random selection not working as expected when displaying individual difficulties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, random selection would always be done at a *set* level. The final operation of a random action would be "select the user's recommended difficulty from this randomly selected set". This makes no sense when sets are not grouped together at song select. In fact, it is completely broken with the previous commit which adds group-isolated random support – if we're grouping by difficulty and the user's recommendation is not in the current group it would throw the user into another group unexpectedly. This fixes the issue by splitting out the random implementation into two separate pathways depending on the carousel display mode. --- .../SongSelectV2/BeatmapCarouselTestScene.cs | 3 + .../TestSceneBeatmapCarouselRandom.cs | 113 ++++++++++--- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 150 +++++++++++++----- 3 files changed, 201 insertions(+), 65 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index 3943b13286..e6ba6a904d 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -36,6 +36,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 public abstract partial class BeatmapCarouselTestScene : OsuManualInputManagerTestScene { protected readonly Stack BeatmapSetRequestedSelections = new Stack(); + protected readonly Stack BeatmapRequestedSelections = new Stack(); protected readonly BindableList BeatmapSets = new BindableList(); @@ -73,6 +74,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { AddStep("create components", () => { + BeatmapRequestedSelections.Clear(); BeatmapSetRequestedSelections.Clear(); BeatmapRecommendationFunction = null; NewItemsPresentedInvocationCount = 0; @@ -113,6 +115,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 NewItemsPresented = _ => NewItemsPresentedInvocationCount++, RequestSelection = b => { + BeatmapRequestedSelections.Push(b); Carousel.CurrentSelection = b; }, RequestRecommendedSelection = beatmaps => diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs index 17d31634fc..739fc23ed5 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs @@ -55,26 +55,26 @@ namespace osu.Game.Tests.Visual.SongSelectV2 nextRandom(); expanded ??= storeExpandedGroup(); - ensureRandomDidNotRepeat(); + ensureSetRandomDidNotRepeat(); checkExpandedGroupUnchanged(); } nextRandom(); - ensureRandomDidRepeat(); + ensureSetRandomDidRepeat(); checkExpandedGroupUnchanged(); - prevRandom(); + prevRandomSet(); checkRewindCorrectSet(); checkExpandedGroupUnchanged(); - prevRandom(); + prevRandomSet(); checkRewindCorrectSet(); checkExpandedGroupUnchanged(); nextRandom(); - ensureRandomDidNotRepeat(); + ensureSetRandomDidNotRepeat(); checkExpandedGroupUnchanged(); nextRandom(); - ensureRandomDidRepeat(); + ensureSetRandomDidRepeat(); checkExpandedGroupUnchanged(); GroupDefinition? storeExpandedGroup() @@ -90,7 +90,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 /// Test random non-repeating algorithm /// [Test] - public void TestRandomDifficultyGrouping() + public void TestRandomDifficultyGroupingRewindsCorrectly() { SortAndGroupBy(SortMode.Difficulty, GroupMode.Difficulty); @@ -108,21 +108,19 @@ namespace osu.Game.Tests.Visual.SongSelectV2 checkExpandedGroupUnchanged(); } - nextRandom(); - ensureRandomDidRepeat(); - checkExpandedGroupUnchanged(); + for (int i = 0; i < 2; i++) + { + prevRandom(); + checkRewindCorrect(); + checkExpandedGroupUnchanged(); + } - prevRandom(); - checkRewindCorrectSet(); - checkExpandedGroupUnchanged(); - - prevRandom(); - checkRewindCorrectSet(); - checkExpandedGroupUnchanged(); - - nextRandom(); - ensureRandomDidNotRepeat(); - checkExpandedGroupUnchanged(); + for (int i = 0; i < 2; i++) + { + nextRandom(); + ensureRandomDidNotRepeat(); + checkExpandedGroupUnchanged(); + } nextRandom(); ensureRandomDidRepeat(); @@ -137,6 +135,54 @@ namespace osu.Game.Tests.Visual.SongSelectV2 void checkExpandedGroupUnchanged() => AddAssert("expanded did not change", () => Carousel.ExpandedGroup, () => Is.EqualTo(expanded)); } + /// + /// Test random non-repeating algorithm + /// + [Test] + public void TestRandomDifficultyGroupingRepeatsWhenExhausted() + { + SortAndGroupBy(SortMode.Difficulty, GroupMode.Difficulty); + + AddBeatmaps(3, 3, true); + WaitForDrawablePanels(); + + GroupDefinition? expanded = null; + + for (int i = 0; i < 3; i++) + { + nextRandom(); + expanded ??= storeExpandedGroup(); + + ensureRandomDidNotRepeat(); + checkExpandedGroupUnchanged(); + } + + for (int i = 0; i < 3; i++) + { + nextRandom(); + ensureRandomDidRepeat(); + } + + for (int i = 0; i < 5; i++) + { + prevRandom(); + checkRewindCorrect(); + checkExpandedGroupUnchanged(); + } + + nextRandom(); + checkExpandedGroupUnchanged(); + // can't assert repeat or otherwise as we went through multiple permutations. + + GroupDefinition? storeExpandedGroup() + { + AddStep("store open group", () => expanded = Carousel.ExpandedGroup); + return null; + } + + void checkExpandedGroupUnchanged() => AddAssert("expanded did not change", () => Carousel.ExpandedGroup, () => Is.EqualTo(expanded)); + } + [Test] public void TestRewindOverMultipleIterations() { @@ -153,7 +199,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 for (int i = 0; i < random_select_count; i++) { - prevRandom(); + prevRandomSet(); checkRewindCorrectSet(); } } @@ -204,7 +250,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("selection not changed", () => Carousel.CurrentSelection, () => Is.EqualTo(postRandomSelection)); - prevRandom(); + prevRandomSet(); AddAssert("selection not changed", () => Carousel.CurrentSelection, () => Is.EqualTo(postRandomSelection)); } @@ -212,15 +258,32 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("select random next", () => Carousel.NextRandom()); private void ensureRandomDidRepeat() => - AddAssert("did repeat", () => BeatmapSetRequestedSelections.Distinct().Count(), () => Is.LessThan(BeatmapSetRequestedSelections.Count)); + AddAssert("did repeat", () => BeatmapRequestedSelections.Distinct().Count(), () => Is.LessThan(BeatmapRequestedSelections.Count)); private void ensureRandomDidNotRepeat() => + AddAssert("no repeats", () => BeatmapRequestedSelections.Distinct().Count(), () => Is.EqualTo(BeatmapRequestedSelections.Count)); + + private void ensureSetRandomDidRepeat() => + AddAssert("did repeat", () => BeatmapSetRequestedSelections.Distinct().Count(), () => Is.LessThan(BeatmapSetRequestedSelections.Count)); + + private void ensureSetRandomDidNotRepeat() => AddAssert("no repeats", () => BeatmapSetRequestedSelections.Distinct().Count(), () => Is.EqualTo(BeatmapSetRequestedSelections.Count)); + private void checkRewindCorrect() => + AddAssert("rewind matched expected beatmap", () => BeatmapRequestedSelections.Peek(), () => Is.EqualTo(Carousel.SelectedBeatmapInfo)); + private void checkRewindCorrectSet() => AddAssert("rewind matched expected set", () => BeatmapSetRequestedSelections.Peek(), () => Is.EqualTo(Carousel.SelectedBeatmapSet)); - private void prevRandom() => AddStep("select random last", () => + private void prevRandom() => AddStep("select last random", () => + { + Carousel.PreviousRandom(); + BeatmapRequestedSelections.Pop(); + // Pop twice because the PreviousRandom call also requests selection. + BeatmapRequestedSelections.Pop(); + }); + + private void prevRandomSet() => AddStep("select last random set", () => { Carousel.PreviousRandom(); BeatmapSetRequestedSelections.Pop(); diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 077a0fb9f8..cd5ab68e6f 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -618,8 +618,8 @@ namespace osu.Game.Screens.SelectV2 #region Random selection handling private readonly Bindable randomAlgorithm = new Bindable(); - private readonly List previouslyVisitedRandomSets = new List(); - private readonly List randomSelectedBeatmaps = new List(); + private readonly List previouslyVisitedRandomBeatmaps = new List(); + private readonly List randomHistory = new List(); private Sample? spinSample; private Sample? randomSelectSample; @@ -631,59 +631,129 @@ namespace osu.Game.Screens.SelectV2 if (carouselItems?.Any() != true) return false; - // This is the fastest way to retrieve sets for randomisation. - ICollection visibleSets = grouping.SetItems.Keys; + var selectionBefore = CurrentSelectionItem; + var beatmapBefore = selectionBefore?.Model as BeatmapInfo; - if (ExpandedGroup != null) + bool success; + + if (beatmapBefore != null) { + // keep track of visited beatmaps and sets for rewind + randomHistory.Add(beatmapBefore); + // keep track of visited beatmaps for "RandomPermutation" random tracking. + // note that this is reset when we run out of beatmaps, while `randomHistory` is not. + previouslyVisitedRandomBeatmaps.Add(beatmapBefore); + } + + if (grouping.BeatmapSetsGroupedTogether) + success = nextRandomSet(); + else + success = nextRandomBeatmap(); + + if (!success) + { + if (beatmapBefore != null) + randomHistory.RemoveAt(randomHistory.Count - 1); + return false; + } + + Scheduler.Add(() => + { + if (selectionBefore != null && CurrentSelectionItem != null) + playSpinSample(distanceBetween(selectionBefore, CurrentSelectionItem), carouselItems.Count); + }); + + return true; + } + + private bool nextRandomBeatmap() + { + ICollection visibleBeatmaps = ExpandedGroup != null // In the case of grouping, users expect random to only operate on the expanded group. // This is going to incur some overhead as we don't have a group-beatmapset mapping currently. // // If this becomes an issue, we could either store a mapping, or run the random algorithm many times // using the `SetItems` method until we get a group HIT. - if (grouping.BeatmapSetsGroupedTogether) - visibleSets = grouping.GroupItems[ExpandedGroup].Select(i => i.Model).OfType().ToArray(); - else - { - // Note that this is probably not correct in all cases. - // When we aren't grouping sets together, we might want to randomise by beatmaps, not sets. - // - // Imagine the scenario where a single beatmap set has multiple difficulties in the same difficulty grouping, where this - // would always choose the set's user recommended difficulty rather than the visible ones. - visibleSets = grouping.GroupItems[ExpandedGroup].Select(i => i.Model).OfType().Select(b => b.BeatmapSet!).Distinct().ToArray(); - } - } + ? grouping.GroupItems[ExpandedGroup].Select(i => i.Model).OfType().ToArray() + : GetCarouselItems()!.Select(i => i.Model).OfType().ToArray(); - if (CurrentSelection is BeatmapInfo beatmapInfo) + BeatmapInfo beatmap; + + switch (randomAlgorithm.Value) { - randomSelectedBeatmaps.Add(beatmapInfo); + case RandomSelectAlgorithm.RandomPermutation: + { + ICollection notYetVisitedBeatmaps = visibleBeatmaps.Except(previouslyVisitedRandomBeatmaps).ToList(); - // when performing a random, we want to add the current set to the previously visited list - // else the user may be "randomised" to the existing selection. - if (previouslyVisitedRandomSets.LastOrDefault()?.Equals(beatmapInfo.BeatmapSet) != true) - previouslyVisitedRandomSets.Add(beatmapInfo.BeatmapSet!); + if (!notYetVisitedBeatmaps.Any()) + { + previouslyVisitedRandomBeatmaps.RemoveAll(b => visibleBeatmaps.Contains(b)); + notYetVisitedBeatmaps = visibleBeatmaps; + if (CurrentSelection is BeatmapInfo beatmapInfo) + notYetVisitedBeatmaps = notYetVisitedBeatmaps.Except([beatmapInfo]).ToList(); + } + + if (notYetVisitedBeatmaps.Count == 0) + return false; + + beatmap = notYetVisitedBeatmaps.ElementAt(RNG.Next(notYetVisitedBeatmaps.Count)); + break; + } + + case RandomSelectAlgorithm.Random: + beatmap = visibleBeatmaps.ElementAt(RNG.Next(visibleBeatmaps.Count)); + break; + + default: + throw new ArgumentOutOfRangeException(); } + RequestSelection(beatmap); + return true; + } + + private bool nextRandomSet() + { + ICollection visibleSets = ExpandedGroup != null + // In the case of grouping, users expect random to only operate on the expanded group. + // This is going to incur some overhead as we don't have a group-beatmapset mapping currently. + // + // If this becomes an issue, we could either store a mapping, or run the random algorithm many times + // using the `SetItems` method until we get a group HIT. + ? grouping.GroupItems[ExpandedGroup].Select(i => i.Model).OfType().ToArray() + // This is the fastest way to retrieve sets for randomisation. + : grouping.SetItems.Keys; + BeatmapSetInfo set; - if (randomAlgorithm.Value == RandomSelectAlgorithm.RandomPermutation) + switch (randomAlgorithm.Value) { - ICollection notYetVisitedSets = visibleSets.Except(previouslyVisitedRandomSets).ToList(); - - if (!notYetVisitedSets.Any()) + case RandomSelectAlgorithm.RandomPermutation: { - previouslyVisitedRandomSets.RemoveAll(s => visibleSets.Contains(s)); - notYetVisitedSets = visibleSets; + ICollection notYetVisitedSets = visibleSets.Except(previouslyVisitedRandomBeatmaps.Select(b => b.BeatmapSet!)).ToList(); + + if (!notYetVisitedSets.Any()) + { + previouslyVisitedRandomBeatmaps.RemoveAll(b => visibleSets.Contains(b.BeatmapSet!)); + notYetVisitedSets = visibleSets; + if (CurrentSelection is BeatmapInfo beatmapInfo) + notYetVisitedSets = notYetVisitedSets.Except([beatmapInfo.BeatmapSet!]).ToList(); + } + + if (notYetVisitedSets.Count == 0) + return false; + + set = notYetVisitedSets.ElementAt(RNG.Next(notYetVisitedSets.Count)); + break; } - set = notYetVisitedSets.ElementAt(RNG.Next(notYetVisitedSets.Count)); - previouslyVisitedRandomSets.Add(set); - } - else - set = visibleSets.ElementAt(RNG.Next(visibleSets.Count)); + case RandomSelectAlgorithm.Random: + set = visibleSets.ElementAt(RNG.Next(visibleSets.Count)); + break; - if (CurrentSelectionItem != null) - playSpinSample(distanceBetween(carouselItems.First(i => !ReferenceEquals(i.Model, set)), CurrentSelectionItem), visibleSets.Count); + default: + throw new ArgumentOutOfRangeException(); + } selectRecommendedDifficultyForBeatmapSet(set); return true; @@ -696,10 +766,10 @@ namespace osu.Game.Screens.SelectV2 if (carouselItems?.Any() != true) return; - while (randomSelectedBeatmaps.Any()) + while (randomHistory.Any()) { - var previousBeatmap = randomSelectedBeatmaps[^1]; - randomSelectedBeatmaps.RemoveAt(randomSelectedBeatmaps.Count - 1); + var previousBeatmap = randomHistory[^1]; + randomHistory.RemoveAt(randomHistory.Count - 1); var previousBeatmapItem = carouselItems.FirstOrDefault(i => i.Model is BeatmapInfo b && b.Equals(previousBeatmap)); @@ -709,7 +779,7 @@ namespace osu.Game.Screens.SelectV2 if (CurrentSelection is BeatmapInfo beatmapInfo) { if (randomAlgorithm.Value == RandomSelectAlgorithm.RandomPermutation) - previouslyVisitedRandomSets.Remove(beatmapInfo.BeatmapSet!); + previouslyVisitedRandomBeatmaps.Remove(beatmapInfo); if (CurrentSelectionItem == null) playSpinSample(0, carouselItems.Count); From 873d62291899cfb55605995209c10117b1365b44 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Jun 2025 16:57:32 +0900 Subject: [PATCH 228/370] Fix global beatmap validity potentially being checked on a stale beatmap Noticed in a flaky test with the changes to random, where the debounce may be delayed to the point of thinking the selection is invalid even though it's valid (timing woes, I cannot explain in words but I would highly recommend smiling and nodding approach). --- osu.Game/Screens/SelectV2/SongSelect.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index fc15090a5b..58de111324 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -467,6 +467,9 @@ namespace osu.Game.Screens.SelectV2 if (!this.IsCurrentScreen()) return false; + if (selectionDebounce?.State == ScheduledDelegate.RunState.Waiting) + selectionDebounce?.RunTask(); + // While filtering, let's not ever attempt to change selection. // This will be resolved after the filter completes, see `newItemsPresented`. bool carouselStateIsValid = filterDebounce?.State != ScheduledDelegate.RunState.Waiting && !carousel.IsFiltering; From 10132f19aadc882f6aa542596ec0ea303df5cc9a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Jun 2025 17:49:06 +0900 Subject: [PATCH 229/370] Refactor group deselection logic to avoid adding complexity to `traverseSelection` --- osu.Game/Graphics/Carousel/Carousel.cs | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index 545fac0e98..06751dee80 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -484,7 +484,13 @@ namespace osu.Game.Graphics.Carousel return true; case GlobalAction.ToggleCurrentGroup: - if (currentKeyboardSelection.CarouselItem != null && CheckValidForGroupSelection(currentKeyboardSelection.CarouselItem)) + if (carouselItems == null || carouselItems.Count == 0) + return true; + + if (currentKeyboardSelection.CarouselItem == null || currentKeyboardSelection.Index == null) + return true; + + if (CheckValidForGroupSelection(currentKeyboardSelection.CarouselItem)) { // If keyboard selection is a group, toggle group and then change keyboard selection to actual selection. Activate(currentKeyboardSelection.CarouselItem); @@ -492,7 +498,16 @@ namespace osu.Game.Graphics.Carousel else { // If current keyboard selection is not a group, toggle the closest group and move keyboard selection to that group. - traverseSelection(-1, CheckValidForGroupSelection, skipFirst: false, activateExpandedItems: true); + for (int i = currentKeyboardSelection.Index.Value; i >= 0; i--) + { + var newItem = carouselItems[i]; + + if (CheckValidForGroupSelection(newItem)) + { + Activate(newItem); + return true; + } + } } return true; @@ -577,7 +592,7 @@ namespace osu.Game.Graphics.Carousel traverseSelection(direction, CheckValidForSetSelection); } - private void traverseSelection(int direction, Func predicate, bool skipFirst = true, bool activateExpandedItems = false) + private void traverseSelection(int direction, Func predicate, bool skipFirst = true) { if (carouselItems == null || carouselItems.Count == 0) return; @@ -616,7 +631,7 @@ namespace osu.Game.Graphics.Carousel var newItem = carouselItems[newIndex]; - if ((activateExpandedItems || !newItem.IsExpanded) && predicate(newItem)) + if (!newItem.IsExpanded && predicate(newItem)) { Activate(newItem); return; From 6350e6d1a7b851054c40d91b1ab30087709917ef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Jun 2025 18:30:54 +0900 Subject: [PATCH 230/370] SongSelectV2: Fix pressing multiple traversal keys in same frame causing weirdness Closes #33453. --- .../TestSceneBeatmapCarouselNoGrouping.cs | 32 +++++++++++++++++++ osu.Game/Graphics/Carousel/Carousel.cs | 29 ++++++++++++++--- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs index a6ba6d76a3..c5f7db022a 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs @@ -155,6 +155,38 @@ namespace osu.Game.Tests.Visual.SongSelectV2 WaitForSetSelection(1, 0); } + [Test] + public void TestMultipleKeyboardOperationsPerFrame() + { + AddBeatmaps(10, 3); + WaitForDrawablePanels(); + + SelectNextSet(); + WaitForSetSelection(0, 0); + + SelectNextPanel(); + SelectNextPanel(); + SelectNextPanel(); + + AddStep("Press two keys at once", () => + { + InputManager.Key(Key.Down); + InputManager.Key(Key.Right); + }); + + // Second key is respected, so only set selection changes. + WaitForSetSelection(1, 0); + + AddStep("Press two keys at once", () => + { + InputManager.Key(Key.Left); + InputManager.Key(Key.Up); + }); + + // Second key is respected, so only keyboard selection changes. + WaitForSetSelection(1, 0); + } + [Test] public void TestKeyboardSelection() { diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index ab3e860f8b..a70f5b053a 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -453,25 +453,46 @@ namespace osu.Game.Graphics.Carousel // `refreshAfterSelection()` is the method responsible for updating the index of the selected item here which runs once per frame. case GlobalAction.SelectNext: - Scheduler.AddOnce(traverseKeyboardSelection, 1); + Scheduler.AddOnce(traverseFromKey, new TraversalOperation(TraversalType.Keyboard, 1)); return true; case GlobalAction.SelectPrevious: - Scheduler.AddOnce(traverseKeyboardSelection, -1); + Scheduler.AddOnce(traverseFromKey, new TraversalOperation(TraversalType.Keyboard, -1)); return true; case GlobalAction.ActivateNextSet: - Scheduler.AddOnce(traverseSetSelection, 1); + Scheduler.AddOnce(traverseFromKey, new TraversalOperation(TraversalType.Set, 1)); return true; case GlobalAction.ActivatePreviousSet: - Scheduler.AddOnce(traverseSetSelection, -1); + Scheduler.AddOnce(traverseFromKey, new TraversalOperation(TraversalType.Set, -1)); return true; } return false; + + void traverseFromKey(TraversalOperation traversal) + { + switch (traversal.Type) + { + case TraversalType.Keyboard: + traverseKeyboardSelection(traversal.Direction); + break; + + case TraversalType.Set: + traverseSetSelection(traversal.Direction); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } } + private enum TraversalType { Keyboard, Set } + + private record TraversalOperation(TraversalType Type, int Direction); + public void OnReleased(KeyBindingReleaseEvent e) { } From cb90dee3e82144e02008d3a6b3ef68833c1862cf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Jun 2025 18:44:22 +0900 Subject: [PATCH 231/370] Remove noisy carousel logging Served its purpose, no longer required. --- osu.Game/Graphics/Carousel/Carousel.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index ab3e860f8b..073566b886 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -732,9 +732,7 @@ namespace osu.Game.Graphics.Carousel if (range != displayedRange) { - Logger.Log($"Updating displayed range of carousel from {displayedRange} to {range}"); displayedRange = range; - updateDisplayedRange(range); } From ef5638b6b35b41bffccf1dba09e2d518fdb24528 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Jun 2025 19:08:18 +0900 Subject: [PATCH 232/370] Fix in song select v1 too for good measure --- osu.Game/Screens/Select/PlaySongSelect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index c49b7c2ef2..2f47243b50 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -92,7 +92,7 @@ namespace osu.Game.Screens.Select { if (playerLoader != null) return false; - modsAtGameplayStart = Mods.Value; + modsAtGameplayStart = Mods.Value.Select(m => m.DeepClone()).ToArray(); // Ctrl+Enter should start map with autoplay enabled. if (GetContainingInputManager()?.CurrentState?.Keyboard.ControlPressed == true) From 088659ae04183143c28e8aebd680ceb583555eef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Jun 2025 19:16:12 +0900 Subject: [PATCH 233/370] Adjust test timings to avoid flaky test failing The gameplay clock runs at 1000 ms intervals, and the previous duration meant that the "store" step could cause a missed spinning check in a bad case scenario. --- osu.Game.Rulesets.Osu.Tests/TestSceneCursorParticles.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorParticles.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorParticles.cs index f6e460284b..fd947343e4 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorParticles.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorParticles.cs @@ -110,23 +110,23 @@ namespace osu.Game.Rulesets.Osu.Tests new Spinner { StartTime = 0, - Duration = 1000, + Duration = 3000, Position = OsuPlayfield.BASE_SIZE / 2, }, new Slider { - StartTime = 2500, + StartTime = 4500, RepeatCount = 0, Position = OsuPlayfield.BASE_SIZE / 2, Path = new SliderPath(new[] { new PathControlPoint(Vector2.Zero), - new PathControlPoint(new Vector2(100, 0)), + new PathControlPoint(new Vector2(200, 0)), }) }, new HitCircle { - StartTime = 4500, + StartTime = 10000, Position = OsuPlayfield.BASE_SIZE / 2, }, }, From 9a602345525f275aa1f58b1396d9d7033e4e55db Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Jun 2025 19:27:14 +0900 Subject: [PATCH 234/370] Fix flaky test failures at main menu due to early `ScalingContainer` access See https://github.com/ppy/osu/actions/runs/15754567890/job/44406960692?pr=33775. Logic was running every frame incorrectly. Including transforms doing many allocations. --- osu.Game/Screens/Menu/MainMenu.cs | 71 +++++++++++++++++-------------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 06f62542f8..d87727b797 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -519,16 +519,45 @@ namespace osu.Game.Screens.Menu private void updateSongSelectV2HoldState() { - if (Buttons.State == ButtonSystemState.Play && - inputManager.CurrentState.Mouse.IsPressed(MouseButton.Left) && - inputManager.HoveredDrawables.Any(h => h is OsuLogo || (h is MainMenuButton b && b.TriggerKeys.Contains(Key.P)))) - holdTime += Time.Elapsed; - else + bool isValidHoverState = Buttons.State == ButtonSystemState.Play && + inputManager.CurrentState.Mouse.IsPressed(MouseButton.Left) && + inputManager.HoveredDrawables.Any(h => h is OsuLogo || (h is MainMenuButton b && b.TriggerKeys.Contains(Key.P))); + + if (isValidHoverState) { - var transformTarget = Game.ChildrenOfType().First(); - transformTarget.ScaleTo(1, 200, Easing.OutQuint) - .RotateTo(0, 200, Easing.OutQuint) - .FadeColour(OsuColour.Gray(1f), 200, Easing.OutQuint); + holdTime += Time.Elapsed; + + if (holdTime >= required_hold_time && !ssv2Expanded) + { + var transformTarget = Game.ChildrenOfType().First(); + + transformTarget.Anchor = Anchor.Centre; + transformTarget.Origin = Anchor.Centre; + + transformTarget.ScaleTo(1.2f, 5000, Easing.OutPow10) + .RotateTo(2, 5000, Easing.OutPow10) + .FadeColour(Color4.BlueViolet, 10000, Easing.OutPow10); + + ssv2Duck = musicController.Duck(new DuckParameters + { + DuckDuration = 2000, + DuckVolumeTo = 0.8f, + DuckCutoffTo = 500, + DuckEasing = Easing.OutQuint, + RestoreDuration = 200, + RestoreEasing = Easing.OutQuint + }); + + ssv2Expanded = true; + } + } + else if (holdTime > 0) + { + var transformTarget = Game.ChildrenOfType().FirstOrDefault(); + + transformTarget.ScaleTo(1, 500, Easing.OutQuint) + .RotateTo(0, 500, Easing.OutQuint) + .FadeColour(OsuColour.Gray(1f), 500, Easing.OutQuint); ssv2Duck?.Dispose(); ssv2Duck = null; @@ -536,30 +565,6 @@ namespace osu.Game.Screens.Menu ssv2Expanded = false; holdTime = 0; } - - if (holdTime >= required_hold_time && !ssv2Expanded) - { - var transformTarget = Game.ChildrenOfType().First(); - - transformTarget.Anchor = Anchor.Centre; - transformTarget.Origin = Anchor.Centre; - - transformTarget.ScaleTo(1.2f, 5000, Easing.OutPow10) - .RotateTo(2, 5000, Easing.OutPow10) - .FadeColour(Color4.BlueViolet, 10000, Easing.OutPow10); - - ssv2Duck = musicController.Duck(new DuckParameters - { - DuckDuration = 2000, - DuckVolumeTo = 0.8f, - DuckCutoffTo = 500, - DuckEasing = Easing.OutQuint, - RestoreDuration = 200, - RestoreEasing = Easing.OutQuint - }); - - ssv2Expanded = true; - } } #endregion From 116463e30d92ed3134522e076f0fc3ad2d9a4da9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Jun 2025 23:52:11 +0900 Subject: [PATCH 235/370] Remove unused second parameter --- osu.Game/Graphics/Carousel/Carousel.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index 06751dee80..a2ab35a58f 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -592,7 +592,7 @@ namespace osu.Game.Graphics.Carousel traverseSelection(direction, CheckValidForSetSelection); } - private void traverseSelection(int direction, Func predicate, bool skipFirst = true) + private void traverseSelection(int direction, Func predicate) { if (carouselItems == null || carouselItems.Count == 0) return; @@ -608,15 +608,12 @@ namespace osu.Game.Graphics.Carousel { newIndex = originalIndex = currentKeyboardSelection.Index.Value; - if (skipFirst) + // As a second special case, if we're set selecting backwards and the current selection isn't a set, + // make sure to go back to the set header this item belongs to, so that the block below doesn't find it and stop too early. + if (direction < 0) { - // As a second special case, if we're set selecting backwards and the current selection isn't a set, - // make sure to go back to the set header this item belongs to, so that the block below doesn't find it and stop too early. - if (direction < 0) - { - while (newIndex > 0 && !predicate(carouselItems[newIndex])) - newIndex--; - } + while (newIndex > 0 && !predicate(carouselItems[newIndex])) + newIndex--; } } From a6c7e20ffcc01c44ed82f145831bf6674df10235 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Jun 2025 02:26:46 +0900 Subject: [PATCH 236/370] Add note about reasoning for low battery threshold --- osu.Game/Screens/Play/PlayerLoader.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 94148c13d0..27b6413d0c 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -715,6 +715,10 @@ namespace osu.Game.Screens.Play #region Low battery warning + /// + /// This is intentionally higher than 20%, which is usually when OS level notifications + /// interrupt the active application to warn the user. + /// private const double low_battery_threshold = 0.25; private Bindable batteryWarningShownOnce = null!; From 4177dc395bbb420ffca16f94a7313dc0c454393b Mon Sep 17 00:00:00 2001 From: eyhn Date: Fri, 20 Jun 2025 16:23:18 +0800 Subject: [PATCH 237/370] Fix beatmap set author information --- osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs index c6cf0f735f..d04c59c168 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs @@ -80,6 +80,11 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty("artist_unicode")] public string ArtistUnicode { get; set; } = string.Empty; + /// + /// In the beatmap search API, this property is not provided. + /// In such cases, the following two properties will be used to provide the Author information. + /// + [JsonProperty(@"user")] public APIUser Author = new APIUser(); /// From 131b3f622e55f71b3992bd1c71a01aad33c7880d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Jun 2025 18:08:25 +0900 Subject: [PATCH 238/370] Update group traversal logic to use new debouncing flow --- osu.Game/Graphics/Carousel/Carousel.cs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index cb7dcadf44..66e8aaf008 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -458,29 +458,28 @@ namespace osu.Game.Graphics.Carousel // if the selection is changed more than once during an update frame, // which can happen if repeat inputs are enqueued for processing at a rate faster than the update refresh rate. // `refreshAfterSelection()` is the method responsible for updating the index of the selected item here which runs once per frame. - - case GlobalAction.SelectNext: - Scheduler.AddOnce(traverseFromKey, new TraversalOperation(TraversalType.Keyboard, 1)); - return true; - case GlobalAction.SelectPrevious: Scheduler.AddOnce(traverseFromKey, new TraversalOperation(TraversalType.Keyboard, -1)); return true; - case GlobalAction.ActivateNextSet: - Scheduler.AddOnce(traverseFromKey, new TraversalOperation(TraversalType.Set, 1)); + case GlobalAction.SelectNext: + Scheduler.AddOnce(traverseFromKey, new TraversalOperation(TraversalType.Keyboard, 1)); return true; case GlobalAction.ActivatePreviousSet: Scheduler.AddOnce(traverseFromKey, new TraversalOperation(TraversalType.Set, -1)); return true; + case GlobalAction.ActivateNextSet: + Scheduler.AddOnce(traverseFromKey, new TraversalOperation(TraversalType.Set, 1)); + return true; + case GlobalAction.ExpandPreviousGroup: - Scheduler.AddOnce(traverseGroupSelection, -1); + Scheduler.AddOnce(traverseFromKey, new TraversalOperation(TraversalType.Group, -1)); return true; case GlobalAction.ExpandNextGroup: - Scheduler.AddOnce(traverseGroupSelection, 1); + Scheduler.AddOnce(traverseFromKey, new TraversalOperation(TraversalType.Group, 1)); return true; case GlobalAction.ToggleCurrentGroup: @@ -527,13 +526,17 @@ namespace osu.Game.Graphics.Carousel traverseSetSelection(traversal.Direction); break; + case TraversalType.Group: + traverseGroupSelection(traversal.Direction); + break; + default: throw new ArgumentOutOfRangeException(); } } } - private enum TraversalType { Keyboard, Set } + private enum TraversalType { Keyboard, Set, Group } private record TraversalOperation(TraversalType Type, int Direction); From f3f137dbd43bb30e9fed40706efa406321e61ece Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Jun 2025 17:01:55 +0900 Subject: [PATCH 239/370] Fix incorrectly named variable --- osu.Game/Screens/Footer/ScreenFooter.cs | 4 ++-- osu.Game/Screens/Footer/ScreenFooterButton.cs | 6 +++--- osu.Game/Screens/SelectV2/FooterButtonMods.cs | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index 7fd5e8537a..be9411c858 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -100,7 +100,7 @@ namespace osu.Game.Screens.Footer { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Y = ScreenFooterButton.Y_OFFSET, + Y = ScreenFooterButton.CORNER_RADIUS, Direction = FillDirection.Horizontal, Spacing = new Vector2(7, 0), AutoSizeAxes = Axes.Both, @@ -123,7 +123,7 @@ namespace osu.Game.Screens.Footer hiddenButtonsContainer = new Container { Margin = new MarginPadding { Left = OsuGame.SCREEN_EDGE_MARGIN + ScreenBackButton.BUTTON_WIDTH + padding }, - Y = ScreenFooterButton.Y_OFFSET, + Y = ScreenFooterButton.CORNER_RADIUS, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, AutoSizeAxes = Axes.Both, diff --git a/osu.Game/Screens/Footer/ScreenFooterButton.cs b/osu.Game/Screens/Footer/ScreenFooterButton.cs index 2b23560c26..e877c91d11 100644 --- a/osu.Game/Screens/Footer/ScreenFooterButton.cs +++ b/osu.Game/Screens/Footer/ScreenFooterButton.cs @@ -25,7 +25,7 @@ namespace osu.Game.Screens.Footer { public partial class ScreenFooterButton : OsuClickableContainer, IKeyBindingHandler { - public const int Y_OFFSET = 10; + public const int CORNER_RADIUS = 10; protected const int BUTTON_HEIGHT = 75; protected const int BUTTON_WIDTH = 116; @@ -91,7 +91,7 @@ namespace osu.Game.Screens.Footer }, Shear = OsuGame.SHEAR, Masking = true, - CornerRadius = 10, + CornerRadius = CORNER_RADIUS, RelativeSizeAxes = Axes.Both, Children = new Drawable[] { @@ -138,7 +138,7 @@ namespace osu.Game.Screens.Footer Shear = -OsuGame.SHEAR, Anchor = Anchor.BottomCentre, Origin = Anchor.Centre, - Y = -Y_OFFSET, + Y = -CORNER_RADIUS, Size = new Vector2(100, 5), Masking = true, CornerRadius = 3, diff --git a/osu.Game/Screens/SelectV2/FooterButtonMods.cs b/osu.Game/Screens/SelectV2/FooterButtonMods.cs index 8ea08a0085..9de06988a5 100644 --- a/osu.Game/Screens/SelectV2/FooterButtonMods.cs +++ b/osu.Game/Screens/SelectV2/FooterButtonMods.cs @@ -86,7 +86,7 @@ namespace osu.Game.Screens.SelectV2 Depth = float.MaxValue, Origin = Anchor.BottomLeft, Shear = OsuGame.SHEAR, - CornerRadius = Y_OFFSET, + CornerRadius = CORNER_RADIUS, Size = new Vector2(BUTTON_WIDTH, bar_height), Masking = true, EdgeEffect = new EdgeEffectParameters @@ -122,7 +122,7 @@ namespace osu.Game.Screens.SelectV2 }, modContainer = new Container { - CornerRadius = Y_OFFSET, + CornerRadius = CORNER_RADIUS, RelativeSizeAxes = Axes.Both, Width = mod_display_portion, Masking = true, @@ -304,7 +304,7 @@ namespace osu.Game.Screens.SelectV2 private void load() { AutoSizeAxes = Axes.Both; - CornerRadius = Y_OFFSET; + CornerRadius = CORNER_RADIUS; Masking = true; InternalChildren = new Drawable[] @@ -346,7 +346,7 @@ namespace osu.Game.Screens.SelectV2 Depth = float.MaxValue; Origin = Anchor.BottomLeft; Shear = OsuGame.SHEAR; - CornerRadius = Y_OFFSET; + CornerRadius = CORNER_RADIUS; AutoSizeAxes = Axes.X; Height = bar_height; Masking = true; From 30fb0c530f000029d5f10a5e3fa9ed2fec04df0d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Jun 2025 20:29:35 +0900 Subject: [PATCH 240/370] Remove fade from footer display transition Fades should not be used for these kinds of elements. The opacity changes of multiple elements looks shocking. It's unnecessary. --- osu.Game/Screens/Footer/ScreenFooter.cs | 13 +++++++++---- osu.Game/Screens/Footer/ScreenFooterButton.cs | 4 ++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index be9411c858..ad3aaaa2c9 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -40,7 +40,11 @@ namespace osu.Game.Screens.Footer private const int padding = 60; private const float delay_per_button = 30; - private const double transition_duration = 400; + private const double transition_duration = 500; + + // Disable masking because it breaks due to the height of this container being less than the displayed content. + // The height being set as it is is required for transition purposes. + public override bool UpdateSubTreeMasking() => false; private readonly List overlays = new List(); @@ -162,13 +166,14 @@ namespace osu.Game.Screens.Footer protected override void PopIn() { this.MoveToY(0, transition_duration, Easing.OutQuint) - .FadeIn(transition_duration, Easing.OutQuint); + .FadeIn(); } protected override void PopOut() { - this.MoveToY(HEIGHT, transition_duration, Easing.OutQuint) - .FadeOut(transition_duration, Easing.OutQuint); + this.MoveToY(ScreenFooterButton.HEIGHT, transition_duration, Easing.OutQuint) + .Then() + .FadeOut(); } public void SetButtons(IReadOnlyList buttons) diff --git a/osu.Game/Screens/Footer/ScreenFooterButton.cs b/osu.Game/Screens/Footer/ScreenFooterButton.cs index e877c91d11..5d064670e7 100644 --- a/osu.Game/Screens/Footer/ScreenFooterButton.cs +++ b/osu.Game/Screens/Footer/ScreenFooterButton.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.Footer { public const int CORNER_RADIUS = 10; - protected const int BUTTON_HEIGHT = 75; + public const int HEIGHT = 75; protected const int BUTTON_WIDTH = 116; public Bindable OverlayState = new Bindable(); @@ -75,7 +75,7 @@ namespace osu.Game.Screens.Footer { Overlay = overlay; - Size = new Vector2(BUTTON_WIDTH, BUTTON_HEIGHT); + Size = new Vector2(BUTTON_WIDTH, HEIGHT); Children = new Drawable[] { From 3af0ce7e5e020e3d32682a2484487126b9c7ac1a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Jun 2025 20:19:39 +0900 Subject: [PATCH 241/370] Fix tests --- .../TestSceneMultiplayerMatchSubScreen.cs | 2 +- .../SongSelectV2/TestSceneScreenFooter.cs | 24 +++++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 920a920b9b..e0a0e5a785 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -308,7 +308,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for join", () => RoomJoined); ClickButtonWhenEnabled(); - AddAssert("mod select shows unranked", () => this.ChildrenOfType().Single().Ranked.Value == false); + AddUntilStep("mod select shows unranked", () => this.ChildrenOfType().Single().Ranked.Value == false); AddAssert("score multiplier = 1.20", () => this.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01)); AddStep("select flashlight", () => this.ChildrenOfType().Single().ChildrenOfType().Single(m => m.Mod is ModFlashlight).TriggerClick()); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneScreenFooter.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneScreenFooter.cs index bdecebd64f..e247b92f52 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneScreenFooter.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneScreenFooter.cs @@ -114,11 +114,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddWaitStep("wait for transition", 3); AddStep("show overlay", () => externalOverlay.Show()); - AddAssert("content displayed in footer", () => screenFooter.ChildrenOfType().Single().IsPresent); + contentDisplayed(); AddUntilStep("other buttons hidden", () => screenFooter.ChildrenOfType().Skip(1).All(b => b.Child.Parent!.Y > 0)); AddStep("hide overlay", () => externalOverlay.Hide()); - AddUntilStep("content hidden from footer", () => screenFooter.ChildrenOfType().SingleOrDefault()?.IsPresent != true); + contentHidden(); AddUntilStep("other buttons returned", () => screenFooter.ChildrenOfType().Skip(1).All(b => b.ChildrenOfType().First().Y == 0)); } @@ -133,11 +133,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("add external overlay", () => contentContainer.Add(externalOverlay = new TestShearedOverlayContainer())); AddStep("show external overlay", () => externalOverlay.Show()); AddAssert("footer shown", () => screenFooter.State.Value == Visibility.Visible); - AddAssert("content displayed in footer", () => screenFooter.ChildrenOfType().Single().IsPresent); + contentDisplayed(); AddStep("hide external overlay", () => externalOverlay.Hide()); AddAssert("footer hidden", () => screenFooter.State.Value == Visibility.Hidden); - AddUntilStep("content hidden from footer", () => screenFooter.ChildrenOfType().SingleOrDefault()?.IsPresent != true); + contentHidden(); AddStep("show footer", () => screenFooter.Show()); AddAssert("content still hidden from footer", () => screenFooter.ChildrenOfType().SingleOrDefault()?.IsPresent != true); @@ -216,17 +216,27 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddWaitStep("wait for transition", 3); AddStep("show overlay", () => externalOverlay.Show()); - AddAssert("content displayed in footer", () => screenFooter.ChildrenOfType().Single().IsPresent); + contentDisplayed(); AddUntilStep("other buttons hidden", () => screenFooter.ChildrenOfType().Skip(1).All(b => b.Child.Parent!.Y > 0)); AddStep("resize active button", () => this.ChildrenOfType().First().ResizeWidthTo(240, 300, Easing.OutQuint)); AddStep("resize active button back", () => this.ChildrenOfType().First().ResizeWidthTo(116, 300, Easing.OutQuint)); AddStep("hide overlay", () => externalOverlay.Hide()); - AddUntilStep("content hidden from footer", () => screenFooter.ChildrenOfType().SingleOrDefault()?.IsPresent != true); + contentHidden(); AddUntilStep("other buttons returned", () => screenFooter.ChildrenOfType().Skip(1).All(b => b.ChildrenOfType().First().Y == 0)); } + private void contentHidden() + { + AddUntilStep("content hidden from footer", () => screenFooter.ChildrenOfType().SingleOrDefault()?.IsPresent != true); + } + + private void contentDisplayed() + { + AddUntilStep("content displayed in footer", () => screenFooter.ChildrenOfType().Single().IsPresent); + } + private partial class TestShearedOverlayContainer : ShearedOverlayContainer { public TestShearedOverlayContainer() @@ -261,7 +271,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [BackgroundDependencyLoader] private void load() { - RelativeSizeAxes = Axes.Both; + AutoSizeAxes = Axes.Both; InternalChild = new FillFlowContainer { From fd27d43814fc694cb62fa3f2b92b8bd3d95c926b Mon Sep 17 00:00:00 2001 From: diquoks Date: Fri, 20 Jun 2025 18:40:21 +0300 Subject: [PATCH 242/370] Use localised strings for SSV2 --- .../BeatmapLeaderboardScore_Tooltip.cs | 3 ++- .../Screens/SelectV2/BeatmapMetadataWedge.cs | 19 +++++++++++-------- .../BeatmapTitleWedge_DifficultyDisplay.cs | 2 +- .../FilterControl_DifficultyRangeSlider.cs | 3 ++- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs index 178fb1df00..bc684dfc13 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs @@ -22,6 +22,7 @@ using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Localisation; using osu.Game.Online.Leaderboards; using osu.Game.Overlays; using osu.Game.Resources.Localisation.Web; @@ -126,7 +127,7 @@ namespace osu.Game.Screens.SelectV2 var generalStatistics = new[] { new PerformanceStatisticRow(BeatmapsetsStrings.ShowScoreboardHeaderspp.ToUpper(), colourProvider.Content2, score), - new StatisticRow("Score Multiplier", colourProvider.Content2, ModUtils.FormatScoreMultiplier(multiplier)), + new StatisticRow(ModSelectOverlayStrings.ScoreMultiplier, colourProvider.Content2, ModUtils.FormatScoreMultiplier(multiplier)), new StatisticRow(BeatmapsetsStrings.ShowScoreboardHeadersCombo, colourProvider.Content2, value.MaxCombo.ToLocalisableString(@"0\x")), new StatisticRow(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy, colourProvider.Content2, value.Accuracy.FormatAccuracy()), }; diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs index 5a0222ec20..d5da1d8c25 100644 --- a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs @@ -5,15 +5,18 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Graphics.Containers; +using osu.Game.Localisation; using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; +using osu.Game.Resources.Localisation.Web; using osuTK; namespace osu.Game.Screens.SelectV2 @@ -124,8 +127,8 @@ namespace osu.Game.Screens.SelectV2 Spacing = new Vector2(0f, 10f), Children = new[] { - creator = new MetadataDisplay("Creator"), - genre = new MetadataDisplay("Genre"), + creator = new MetadataDisplay(EditorSetupStrings.Creator), + genre = new MetadataDisplay(BeatmapsetsStrings.ShowInfoGenre), }, }, new FillFlowContainer @@ -136,8 +139,8 @@ namespace osu.Game.Screens.SelectV2 Spacing = new Vector2(0f, 10f), Children = new[] { - source = new MetadataDisplay("Source"), - language = new MetadataDisplay("Language"), + source = new MetadataDisplay(BeatmapsetsStrings.ShowInfoSource), + language = new MetadataDisplay(BeatmapsetsStrings.ShowInfoLanguage), }, }, new FillFlowContainer @@ -148,18 +151,18 @@ namespace osu.Game.Screens.SelectV2 Spacing = new Vector2(0f, 10f), Children = new[] { - submitted = new MetadataDisplay("Submitted"), - ranked = new MetadataDisplay("Ranked"), + submitted = new MetadataDisplay(BeatmapsetsStrings.ShowDetailsDateSubmitted("").ToSentence()), + ranked = new MetadataDisplay(BeatmapsetsStrings.ShowDetailsDateRanked("").ToSentence()), }, }, }, }, }, - userTags = new MetadataDisplay("User Tags") + userTags = new MetadataDisplay(BeatmapsetsStrings.ShowInfoUserTags) { Alpha = 0, }, - mapperTags = new MetadataDisplay("Mapper Tags"), + mapperTags = new MetadataDisplay(BeatmapsetsStrings.ShowInfoMapperTags), }, }, }, diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs index a4be87953c..7595afdbd7 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs @@ -136,7 +136,7 @@ namespace osu.Game.Screens.SelectV2 { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Text = " mapped by ", + Text = BeatmapsStrings.DiscussionsShowTitle("", ""), Font = OsuFont.Style.Body, }, mapperLink = new MapperLinkContainer diff --git a/osu.Game/Screens/SelectV2/FilterControl_DifficultyRangeSlider.cs b/osu.Game/Screens/SelectV2/FilterControl_DifficultyRangeSlider.cs index 52ff41fe63..f65c17bddf 100644 --- a/osu.Game/Screens/SelectV2/FilterControl_DifficultyRangeSlider.cs +++ b/osu.Game/Screens/SelectV2/FilterControl_DifficultyRangeSlider.cs @@ -16,6 +16,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Overlays; +using osu.Game.Resources.Localisation.Web; using osu.Game.Utils; using osuTK.Graphics; @@ -34,7 +35,7 @@ namespace osu.Game.Screens.SelectV2 .Prepend((0.0f, OsuColour.STAR_DIFFICULTY_SPECTRUM.ElementAt(1).Item2)).ToArray(); public DifficultyRangeSlider() - : base("Star Rating") + : base(BeatmapsetsStrings.ShowStatsStars) { NubWidth = ShearedNub.HEIGHT * 1.16f; DefaultStringUpperBound = "∞"; From b603a88043858f2fe1b627534ba309a8be7e4b95 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 20 Jun 2025 20:21:44 +0300 Subject: [PATCH 243/370] Reword documentation --- .../API/Requests/Responses/APIBeatmapSet.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs index d04c59c168..e8e08059b9 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs @@ -81,15 +81,21 @@ namespace osu.Game.Online.API.Requests.Responses public string ArtistUnicode { get; set; } = string.Empty; /// - /// In the beatmap search API, this property is not provided. - /// In such cases, the following two properties will be used to provide the Author information. + /// The creator of this beatmap set. /// + /// + /// This is not included when the set is retrieved via , + /// but the creator's ID and username will be filled in this property from the and properties. + /// [JsonProperty(@"user")] public APIUser Author = new APIUser(); /// - /// Helper property to deserialize a username to . + /// The ID of the beatmap set's creator. /// + /// + /// Helper property to deserialize the ID to . + /// [JsonProperty(@"user_id")] public int AuthorID { @@ -98,8 +104,11 @@ namespace osu.Game.Online.API.Requests.Responses } /// - /// Helper property to deserialize a username to . + /// The username of the beatmap set's creator. /// + /// + /// Helper property to deserialize the username to . + /// [JsonProperty(@"creator")] public string AuthorString { From acc4267a2d5ebf942ac0cdf0f1a477fc993e14c0 Mon Sep 17 00:00:00 2001 From: diquoks Date: Sat, 21 Jun 2025 12:35:02 +0300 Subject: [PATCH 244/370] Use `MarginPadding` instead of leading space, change incorrect `LocalisableString` --- osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs | 4 ++-- .../Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs index d5da1d8c25..d7bc73193d 100644 --- a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs @@ -151,8 +151,8 @@ namespace osu.Game.Screens.SelectV2 Spacing = new Vector2(0f, 10f), Children = new[] { - submitted = new MetadataDisplay(BeatmapsetsStrings.ShowDetailsDateSubmitted("").ToSentence()), - ranked = new MetadataDisplay(BeatmapsetsStrings.ShowDetailsDateRanked("").ToSentence()), + submitted = new MetadataDisplay(BeatmapsetsStrings.ShowDetailsDateSubmitted(string.Empty).ToSentence()), + ranked = new MetadataDisplay(BeatmapsetsStrings.ShowDetailsDateRanked(string.Empty).ToSentence()), }, }, }, diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs index 7595afdbd7..4af5e5846c 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs @@ -130,13 +130,14 @@ namespace osu.Game.Screens.SelectV2 { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, + Padding = new MarginPadding { Right = 3f }, Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold), }, mappedByText = new OsuSpriteText { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Text = BeatmapsStrings.DiscussionsShowTitle("", ""), + Text = BeatmapsetsStrings.ShowDetailsMappedBy(string.Empty), Font = OsuFont.Style.Body, }, mapperLink = new MapperLinkContainer From 3192eaa2a20f20495db8431d3d6f35af7c705a94 Mon Sep 17 00:00:00 2001 From: diquoks Date: Sat, 21 Jun 2025 14:05:20 +0300 Subject: [PATCH 245/370] Add localisation to `Collections` string on `SongSelect` --- osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 2 +- osu.Game/Screens/SelectV2/SongSelect.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index 425ca02e5a..9bdd188a3a 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -232,7 +232,7 @@ namespace osu.Game.Screens.SelectV2 if (manageCollectionsDialog != null) collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show)); - items.Add(new OsuMenuItem("Collections") { Items = collectionItems }); + items.Add(new OsuMenuItem(CommonStrings.Collections) { Items = collectionItems }); if (beatmapSet.Beatmaps.Any(b => b.Hidden)) items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => songSelect?.RestoreAllHidden(beatmapSet))); diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index fc15090a5b..d0b697abc9 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -904,7 +904,7 @@ namespace osu.Game.Screens.SelectV2 collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, () => manageCollectionsDialog?.Show())); - yield return new OsuMenuItem("Collections") { Items = collectionItems }; + yield return new OsuMenuItem(CommonStrings.Collections) { Items = collectionItems }; } public void ManageCollections() => collectionsDialog?.Show(); From 4661cb48188718310abd68ae558b346463df1226 Mon Sep 17 00:00:00 2001 From: diquoks Date: Sat, 21 Jun 2025 18:23:50 +0300 Subject: [PATCH 246/370] Add new strings to `SongSelectStrings.cs` --- osu.Game/Localisation/SongSelectStrings.cs | 10 ++++++++++ osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs | 5 ++--- .../SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs | 1 + 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/osu.Game/Localisation/SongSelectStrings.cs b/osu.Game/Localisation/SongSelectStrings.cs index 6b4527f063..0a031332dd 100644 --- a/osu.Game/Localisation/SongSelectStrings.cs +++ b/osu.Game/Localisation/SongSelectStrings.cs @@ -59,6 +59,16 @@ namespace osu.Game.Localisation /// public static LocalisableString Stars(LocalisableString value) => new TranslatableString(getKey(@"stars"), @"{0} stars", value); + /// + /// "Submitted" + /// + public static LocalisableString Submitted => new TranslatableString(getKey(@"submitted"), @"Submitted"); + + /// + /// "Ranked" + /// + public static LocalisableString Ranked => new TranslatableString(getKey(@"ranked"), @"Ranked"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs index d7bc73193d..b3d3bb6279 100644 --- a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; @@ -151,8 +150,8 @@ namespace osu.Game.Screens.SelectV2 Spacing = new Vector2(0f, 10f), Children = new[] { - submitted = new MetadataDisplay(BeatmapsetsStrings.ShowDetailsDateSubmitted(string.Empty).ToSentence()), - ranked = new MetadataDisplay(BeatmapsetsStrings.ShowDetailsDateRanked(string.Empty).ToSentence()), + submitted = new MetadataDisplay(SongSelectStrings.Submitted), + ranked = new MetadataDisplay(SongSelectStrings.Ranked), }, }, }, diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs index 4af5e5846c..cdf012479e 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs @@ -130,6 +130,7 @@ namespace osu.Game.Screens.SelectV2 { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, + // margin to replicate the missing leading space in `ShowDetailsMappedBy` string Padding = new MarginPadding { Right = 3f }, Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold), }, From cad43823509cda3e45d2f99e86deff5f4cd0695f Mon Sep 17 00:00:00 2001 From: diquoks Date: Sat, 21 Jun 2025 21:01:48 +0300 Subject: [PATCH 247/370] Add localisation usage to `BeatmapStatistic` and revert `ShowDetailsMappedBy` --- .../Beatmaps/CatchBeatmap.cs | 7 +- .../Beatmaps/ManiaBeatmap.cs | 5 +- osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs | 7 +- .../Beatmaps/TaikoBeatmap.cs | 7 +- .../Localisation/BeatmapStatisticStrings.cs | 69 +++++++++++++++++++ .../BeatmapTitleWedge_DifficultyDisplay.cs | 2 +- 6 files changed, 85 insertions(+), 12 deletions(-) create mode 100644 osu.Game/Localisation/BeatmapStatisticStrings.cs diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs index d43290e661..1ff5083aaf 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; +using osu.Game.Localisation; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Objects; @@ -23,21 +24,21 @@ namespace osu.Game.Rulesets.Catch.Beatmaps { new BeatmapStatistic { - Name = @"Fruits", + Name = BeatmapStatisticStrings.Fruits, Content = fruits.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), BarDisplayLength = fruits / (float)sum, }, new BeatmapStatistic { - Name = @"Juice Streams", + Name = BeatmapStatisticStrings.JuiceStreams, Content = juiceStreams.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), BarDisplayLength = juiceStreams / (float)sum, }, new BeatmapStatistic { - Name = @"Banana Showers", + Name = BeatmapStatisticStrings.BananaShowers, Content = bananaShowers.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), BarDisplayLength = Math.Min(bananaShowers / 10f, 1), diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs index 3ee1b63800..1f8380a9f7 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; +using osu.Game.Localisation; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI; @@ -42,14 +43,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps { new BeatmapStatistic { - Name = @"Notes", + Name = BeatmapStatisticStrings.Notes, CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), Content = notes.ToString(), BarDisplayLength = notes / (float)sum, }, new BeatmapStatistic { - Name = @"Hold Notes", + Name = BeatmapStatisticStrings.HoldNotes, CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), Content = holdNotes.ToString(), BarDisplayLength = holdNotes / (float)sum, diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs index d11b4aac3b..87e592a41c 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; +using osu.Game.Localisation; using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Beatmaps @@ -22,21 +23,21 @@ namespace osu.Game.Rulesets.Osu.Beatmaps { new BeatmapStatistic { - Name = "Circles", + Name = BeatmapStatisticStrings.Circles, Content = circles.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), BarDisplayLength = circles / (float)sum, }, new BeatmapStatistic { - Name = "Sliders", + Name = BeatmapStatisticStrings.Sliders, Content = sliders.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), BarDisplayLength = sliders / (float)sum, }, new BeatmapStatistic { - Name = @"Spinners", + Name = BeatmapStatisticStrings.Spinners, Content = spinners.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), BarDisplayLength = Math.Min(spinners / 10f, 1), diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs index 5b0582ab59..4a38381bbe 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; +using osu.Game.Localisation; using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Beatmaps @@ -22,21 +23,21 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps { new BeatmapStatistic { - Name = @"Hits", + Name = BeatmapStatisticStrings.Hits, CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), Content = hits.ToString(), BarDisplayLength = hits / (float)sum, }, new BeatmapStatistic { - Name = @"Drumrolls", + Name = BeatmapStatisticStrings.Drumrolls, CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), Content = drumRolls.ToString(), BarDisplayLength = drumRolls / (float)sum, }, new BeatmapStatistic { - Name = @"Swells", + Name = BeatmapStatisticStrings.Swells, CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), Content = swells.ToString(), BarDisplayLength = Math.Min(swells / 10f, 1), diff --git a/osu.Game/Localisation/BeatmapStatisticStrings.cs b/osu.Game/Localisation/BeatmapStatisticStrings.cs new file mode 100644 index 0000000000..47cc153ac7 --- /dev/null +++ b/osu.Game/Localisation/BeatmapStatisticStrings.cs @@ -0,0 +1,69 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public class BeatmapStatisticStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.BeatmapStatisticStrings"; + + /// + /// "Circles" + /// + public static LocalisableString Circles => new TranslatableString(getKey(@"circles"), @"Circles"); + + /// + /// "Sliders" + /// + public static LocalisableString Sliders => new TranslatableString(getKey(@"sliders"), @"Sliders"); + + /// + /// "Spinners" + /// + public static LocalisableString Spinners => new TranslatableString(getKey(@"spinners"), @"Spinners"); + + /// + /// "Hits" + /// + public static LocalisableString Hits => new TranslatableString(getKey(@"hits"), @"Hits"); + + /// + /// "Drumrolls" + /// + public static LocalisableString Drumrolls => new TranslatableString(getKey(@"drumrolls"), @"Drumrolls"); + + /// + /// "Swells" + /// + public static LocalisableString Swells => new TranslatableString(getKey(@"swells"), @"Swells"); + + /// + /// "Fruits" + /// + public static LocalisableString Fruits => new TranslatableString(getKey(@"fruits"), @"Fruits"); + + /// + /// "Juice Streams" + /// + public static LocalisableString JuiceStreams => new TranslatableString(getKey(@"juice_streams"), @"Juice Streams"); + + /// + /// "Banana Showers" + /// + public static LocalisableString BananaShowers => new TranslatableString(getKey(@"banana_showers"), @"Banana Showers"); + + /// + /// "Notes" + /// + public static LocalisableString Notes => new TranslatableString(getKey(@"notes"), @"Notes"); + + /// + /// "Hold Notes" + /// + public static LocalisableString HoldNotes => new TranslatableString(getKey(@"hold_notes"), @"Hold Notes"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs index cdf012479e..bd3042dc9c 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs @@ -138,7 +138,7 @@ namespace osu.Game.Screens.SelectV2 { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Text = BeatmapsetsStrings.ShowDetailsMappedBy(string.Empty), + Text = " mapped by ", Font = OsuFont.Style.Body, }, mapperLink = new MapperLinkContainer From 71e4f4129ffbd0130c460d818aef78e63707ab1b Mon Sep 17 00:00:00 2001 From: diquoks Date: Sat, 21 Jun 2025 21:06:20 +0300 Subject: [PATCH 248/370] Remove remaining excess `MarginPadding` --- .../Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs index bd3042dc9c..a4be87953c 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs @@ -130,8 +130,6 @@ namespace osu.Game.Screens.SelectV2 { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - // margin to replicate the missing leading space in `ShowDetailsMappedBy` string - Padding = new MarginPadding { Right = 3f }, Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold), }, mappedByText = new OsuSpriteText From 9b7c722d975624941be8c48d08dbe246dec5b1d1 Mon Sep 17 00:00:00 2001 From: StanR Date: Sun, 22 Jun 2025 01:24:58 +0500 Subject: [PATCH 249/370] Add beatmapset `This beatmap contains video` badge --- .../Online/TestSceneBeatmapSetOverlay.cs | 11 ++++ .../BeatmapSet/BeatmapSetHasVideoBadge.cs | 54 +++++++++++++++++++ .../BeatmapSet/BeatmapSetHeaderContent.cs | 33 ++++++++++-- 3 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 osu.Game/Overlays/BeatmapSet/BeatmapSetHasVideoBadge.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs index 5dc6f950a5..4cb9ec4e88 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs @@ -243,6 +243,17 @@ namespace osu.Game.Tests.Visual.Online }); } + [Test] + public void TestBeatmapSetHasVideo() + { + AddStep("show beatmapset with video", () => + { + var beatmapSet = getBeatmapSet(); + beatmapSet.HasVideo = true; + overlay.ShowBeatmapSet(beatmapSet); + }); + } + [Test] public void TestSelectedModsDontAffectStatistics() { diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHasVideoBadge.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHasVideoBadge.cs new file mode 100644 index 0000000000..7281b3970c --- /dev/null +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHasVideoBadge.cs @@ -0,0 +1,54 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Resources.Localisation.Web; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Overlays.BeatmapSet +{ + public partial class BeatmapSetHasVideoBadge : CircularContainer, IHasTooltip + { + public LocalisableString TooltipText => BeatmapsetsStrings.ShowInfoVideo; + + private readonly Box background; + + public BeatmapSetHasVideoBadge() + { + AutoSizeAxes = Axes.Both; + Masking = true; + Alpha = 0; + + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.5f + }, + new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.Film, + Size = new Vector2(14), + Margin = new MarginPadding(10) + }, + }; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + background.Colour = colourProvider.Background6; + } + } +} diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs index 9b10f6156d..7a48a02f58 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs @@ -46,6 +46,7 @@ namespace osu.Game.Overlays.BeatmapSet private readonly Box coverGradient; private readonly LinkFlowContainer title, artist; private readonly AuthorInfo author; + private readonly BeatmapSetHasVideoBadge beatmapSetVideoBadge; private ExternalLinkButton externalLink; @@ -175,13 +176,29 @@ namespace osu.Game.Overlays.BeatmapSet Spacing = new Vector2(10), Children = new Drawable[] { - onlineStatusPill = new BeatmapSetOnlineStatusPill + new FillFlowContainer { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - TextSize = 14, - TextPadding = new MarginPadding { Horizontal = 35, Vertical = 10 } + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10), + Children = new Drawable[] + { + onlineStatusPill = new BeatmapSetOnlineStatusPill + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + TextSize = 14, + TextPadding = new MarginPadding { Horizontal = 35, Vertical = 10 } + }, + beatmapSetVideoBadge = new BeatmapSetHasVideoBadge + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }, + } }, + Details = new Details(), }, }, @@ -218,6 +235,7 @@ namespace osu.Game.Overlays.BeatmapSet if (setInfo.NewValue == null) { onlineStatusPill.FadeTo(0.5f, 500, Easing.OutQuint); + beatmapSetVideoBadge.Hide(); fadeContent.Hide(); loading.Show(); @@ -235,6 +253,11 @@ namespace osu.Game.Overlays.BeatmapSet loading.Hide(); + if (setInfo.NewValue.HasVideo) + beatmapSetVideoBadge.Show(); + else + beatmapSetVideoBadge.Hide(); + var titleText = new RomanisableString(setInfo.NewValue.TitleUnicode, setInfo.NewValue.Title); var artistText = new RomanisableString(setInfo.NewValue.ArtistUnicode, setInfo.NewValue.Artist); From 3e3d984584c84fface7cb32cdf25b6b6e2797358 Mon Sep 17 00:00:00 2001 From: StanR Date: Sun, 22 Jun 2025 02:06:37 +0500 Subject: [PATCH 250/370] Bring `BeatmapPicker` styling closer to web --- osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs index 74b523fdec..9cc9ca87de 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs @@ -38,6 +38,7 @@ namespace osu.Game.Overlays.BeatmapSet public readonly Bindable Beatmap = new Bindable(); private APIBeatmapSet? beatmapSet; + private readonly Box background; public APIBeatmapSet? BeatmapSet { @@ -68,12 +69,31 @@ namespace osu.Game.Overlays.BeatmapSet Direction = FillDirection.Vertical, Children = new Drawable[] { - Difficulties = new DifficultiesContainer + new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Margin = new MarginPadding { Left = -(tile_icon_padding + tile_spacing / 2), Bottom = 10 }, - OnLostHover = () => showBeatmap(Beatmap.Value, withStarRating: false), + Children = new Drawable[] + { + new Container + { + Masking = true, + CornerRadius = 10, + RelativeSizeAxes = Axes.Both, + Child = background = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.5f + } + }, + Difficulties = new DifficultiesContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + OnLostHover = () => showBeatmap(Beatmap.Value, withStarRating: false), + }, + } }, infoContainer = new LinkFlowContainer(t => t.Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 11)) { @@ -108,9 +128,10 @@ namespace osu.Game.Overlays.BeatmapSet private IBindable ruleset { get; set; } = null!; [BackgroundDependencyLoader] - private void load() + private void load(OverlayColourProvider colourProvider) { updateDisplay(); + background.Colour = colourProvider.Background3; } protected override void LoadComplete() @@ -240,8 +261,8 @@ namespace osu.Game.Overlays.BeatmapSet public partial class DifficultySelectorButton : OsuClickableContainer, IStateful { private const float transition_duration = 100; - private const float size = 54; - private const float background_size = size - 2; + private const float size = 40; + private const float background_size = size - 1; private readonly Container background; private readonly Box backgroundBox; @@ -276,7 +297,6 @@ namespace osu.Game.Overlays.BeatmapSet { Beatmap = beatmapInfo; Size = new Vector2(size); - Margin = new MarginPadding { Horizontal = tile_spacing / 2 }; Children = new Drawable[] { @@ -284,7 +304,8 @@ namespace osu.Game.Overlays.BeatmapSet { Size = new Vector2(background_size), Masking = true, - CornerRadius = 4, + CornerRadius = 10, + BorderThickness = 3, Child = backgroundBox = new Box { RelativeSizeAxes = Axes.Both, @@ -338,6 +359,7 @@ namespace osu.Game.Overlays.BeatmapSet private void load(OverlayColourProvider colourProvider) { backgroundBox.Colour = colourProvider.Background6; + background.BorderColour = colourProvider.Light2; } } From 25eb9914a437dbb09a5b2246eb11b7f95ab56e87 Mon Sep 17 00:00:00 2001 From: StanR Date: Sun, 22 Jun 2025 13:45:07 +0500 Subject: [PATCH 251/370] Switch to IconPills, add storyboard icon --- .../Online/TestSceneBeatmapSetOverlay.cs | 15 +++++- osu.Game/Beatmaps/Drawables/Cards/IconPill.cs | 6 +++ .../BeatmapSet/BeatmapSetHasVideoBadge.cs | 54 ------------------- .../BeatmapSet/BeatmapSetHeaderContent.cs | 31 +++++++++-- 4 files changed, 46 insertions(+), 60 deletions(-) delete mode 100644 osu.Game/Overlays/BeatmapSet/BeatmapSetHasVideoBadge.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs index 4cb9ec4e88..f36ef7a8e8 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs @@ -244,7 +244,7 @@ namespace osu.Game.Tests.Visual.Online } [Test] - public void TestBeatmapSetHasVideo() + public void TestBeatmapSetHasVideoOrStoryboard() { AddStep("show beatmapset with video", () => { @@ -252,6 +252,19 @@ namespace osu.Game.Tests.Visual.Online beatmapSet.HasVideo = true; overlay.ShowBeatmapSet(beatmapSet); }); + AddStep("show beatmapset with storyboard", () => + { + var beatmapSet = getBeatmapSet(); + beatmapSet.HasStoryboard = true; + overlay.ShowBeatmapSet(beatmapSet); + }); + AddStep("show beatmapset with video and storyboard", () => + { + var beatmapSet = getBeatmapSet(); + beatmapSet.HasVideo = true; + beatmapSet.HasStoryboard = true; + overlay.ShowBeatmapSet(beatmapSet); + }); } [Test] diff --git a/osu.Game/Beatmaps/Drawables/Cards/IconPill.cs b/osu.Game/Beatmaps/Drawables/Cards/IconPill.cs index 16be57ac95..7cdd50e7ea 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/IconPill.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/IconPill.cs @@ -20,6 +20,12 @@ namespace osu.Game.Beatmaps.Drawables.Cards set => iconContainer.Size = value; } + public MarginPadding IconPadding + { + get => iconContainer.Padding; + set => iconContainer.Padding = value; + } + private readonly Container iconContainer; protected IconPill(IconUsage icon) diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHasVideoBadge.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHasVideoBadge.cs deleted file mode 100644 index 7281b3970c..0000000000 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHasVideoBadge.cs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Localisation; -using osu.Game.Resources.Localisation.Web; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Overlays.BeatmapSet -{ - public partial class BeatmapSetHasVideoBadge : CircularContainer, IHasTooltip - { - public LocalisableString TooltipText => BeatmapsetsStrings.ShowInfoVideo; - - private readonly Box background; - - public BeatmapSetHasVideoBadge() - { - AutoSizeAxes = Axes.Both; - Masking = true; - Alpha = 0; - - Children = new Drawable[] - { - background = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - Alpha = 0.5f - }, - new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Icon = FontAwesome.Solid.Film, - Size = new Vector2(14), - Margin = new MarginPadding(10) - }, - }; - } - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - background.Colour = colourProvider.Background6; - } - } -} diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs index 7a48a02f58..8cdb644cab 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs @@ -16,6 +16,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; +using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -46,7 +47,8 @@ namespace osu.Game.Overlays.BeatmapSet private readonly Box coverGradient; private readonly LinkFlowContainer title, artist; private readonly AuthorInfo author; - private readonly BeatmapSetHasVideoBadge beatmapSetVideoBadge; + private readonly VideoIconPill videoIconPill; + private readonly StoryboardIconPill storyboardIconPill; private ExternalLinkButton externalLink; @@ -191,10 +193,23 @@ namespace osu.Game.Overlays.BeatmapSet TextSize = 14, TextPadding = new MarginPadding { Horizontal = 35, Vertical = 10 } }, - beatmapSetVideoBadge = new BeatmapSetHasVideoBadge + videoIconPill = new VideoIconPill { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, + IconSize = new Vector2(34), + IconPadding = new MarginPadding(10), + }, + storyboardIconPill = new StoryboardIconPill + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + IconSize = new Vector2(34), + IconPadding = new MarginPadding(10), }, } }, @@ -235,7 +250,8 @@ namespace osu.Game.Overlays.BeatmapSet if (setInfo.NewValue == null) { onlineStatusPill.FadeTo(0.5f, 500, Easing.OutQuint); - beatmapSetVideoBadge.Hide(); + videoIconPill.Hide(); + storyboardIconPill.Hide(); fadeContent.Hide(); loading.Show(); @@ -254,9 +270,14 @@ namespace osu.Game.Overlays.BeatmapSet loading.Hide(); if (setInfo.NewValue.HasVideo) - beatmapSetVideoBadge.Show(); + videoIconPill.Show(); else - beatmapSetVideoBadge.Hide(); + videoIconPill.Hide(); + + if (setInfo.NewValue.HasStoryboard) + storyboardIconPill.Show(); + else + storyboardIconPill.Hide(); var titleText = new RomanisableString(setInfo.NewValue.TitleUnicode, setInfo.NewValue.Title); var artistText = new RomanisableString(setInfo.NewValue.ArtistUnicode, setInfo.NewValue.Artist); From c633e3233baffb6cb9f9831843eb6eaa9a322f0d Mon Sep 17 00:00:00 2001 From: diquoks Date: Sun, 22 Jun 2025 15:38:15 +0300 Subject: [PATCH 252/370] Make `DifficultyDisplay` use separate `LocalisableStrings` --- osu.Game/Localisation/SongSelectStrings.cs | 29 +++++++++++++++++-- .../BeatmapTitleWedge_DifficultyDisplay.cs | 11 ++++--- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/osu.Game/Localisation/SongSelectStrings.cs b/osu.Game/Localisation/SongSelectStrings.cs index 0a031332dd..055caccc87 100644 --- a/osu.Game/Localisation/SongSelectStrings.cs +++ b/osu.Game/Localisation/SongSelectStrings.cs @@ -55,9 +55,29 @@ namespace osu.Game.Localisation public static LocalisableString EditBeatmap => new TranslatableString(getKey(@"edit_beatmap"), @"Edit beatmap"); /// - /// "{0} stars" + /// "Circle Size" /// - public static LocalisableString Stars(LocalisableString value) => new TranslatableString(getKey(@"stars"), @"{0} stars", value); + public static LocalisableString CircleSize => new TranslatableString(getKey(@"circle_size"), @"Circle Size"); + + /// + /// "Key Count" + /// + public static LocalisableString KeyCount => new TranslatableString(getKey(@"key_count"), @"Key Count"); + + /// + /// "Approach Rate" + /// + public static LocalisableString ApproachRate => new TranslatableString(getKey(@"approach_rate"), @"Approach Rate"); + + /// + /// "Accuracy" + /// + public static LocalisableString Accuracy => new TranslatableString(getKey(@"accuracy"), @"Accuracy"); + + /// + /// "HP Drain" + /// + public static LocalisableString HPDrain => new TranslatableString(getKey(@"hp_drain"), @"HP Drain"); /// /// "Submitted" @@ -69,6 +89,11 @@ namespace osu.Game.Localisation /// public static LocalisableString Ranked => new TranslatableString(getKey(@"ranked"), @"Ranked"); + /// + /// "{0} stars" + /// + public static LocalisableString Stars(LocalisableString value) => new TranslatableString(getKey(@"stars"), @"{0} stars", value); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs index a4be87953c..7c7c3872cd 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs @@ -24,7 +24,6 @@ using osu.Game.Online; using osu.Game.Online.Chat; using osu.Game.Overlays; using osu.Game.Overlays.Mods; -using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Utils; @@ -322,20 +321,20 @@ namespace osu.Game.Screens.SelectV2 // - Using the difficulty adjustment mod to adjust OD doesn't have an effect on conversion. int keyCount = legacyRuleset.GetKeyCount(beatmap.Value.BeatmapInfo, mods.Value); - firstStatistic = new StatisticDifficulty.Data(BeatmapsetsStrings.ShowStatsCsMania, keyCount, keyCount, 10); + firstStatistic = new StatisticDifficulty.Data(SongSelectStrings.KeyCount, keyCount, keyCount, 10); break; default: - firstStatistic = new StatisticDifficulty.Data(BeatmapsetsStrings.ShowStatsCs, originalDifficulty.CircleSize, adjustedDifficulty.CircleSize, 10); + firstStatistic = new StatisticDifficulty.Data(SongSelectStrings.CircleSize, originalDifficulty.CircleSize, adjustedDifficulty.CircleSize, 10); break; } difficultyStatisticsDisplay.Statistics = new[] { firstStatistic, - new StatisticDifficulty.Data(BeatmapsetsStrings.ShowStatsAr, originalDifficulty.ApproachRate, adjustedDifficulty.ApproachRate, 10), - new StatisticDifficulty.Data(BeatmapsetsStrings.ShowStatsAccuracy, originalDifficulty.OverallDifficulty, adjustedDifficulty.OverallDifficulty, 10), - new StatisticDifficulty.Data(BeatmapsetsStrings.ShowStatsDrain, originalDifficulty.DrainRate, adjustedDifficulty.DrainRate, 10), + new StatisticDifficulty.Data(SongSelectStrings.ApproachRate, originalDifficulty.ApproachRate, adjustedDifficulty.ApproachRate, 10), + new StatisticDifficulty.Data(SongSelectStrings.Accuracy, originalDifficulty.OverallDifficulty, adjustedDifficulty.OverallDifficulty, 10), + new StatisticDifficulty.Data(SongSelectStrings.HPDrain, originalDifficulty.DrainRate, adjustedDifficulty.DrainRate, 10), }; }); From ef9fed47a9e5ab8e0066216e2d007d4d8a1148ab Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 23 Jun 2025 00:41:12 +0300 Subject: [PATCH 253/370] Fix null reference in metadata wedge when accessing beatmap tags --- osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs index b3d3bb6279..8d1dd105a3 100644 --- a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -298,7 +299,11 @@ namespace osu.Game.Screens.SelectV2 else source.Data = ("-", null); - mapperTags.Tags = (metadata.Tags.Split(' '), t => songSelect?.Search(t)); + if (!string.IsNullOrEmpty(metadata.Tags)) + mapperTags.Tags = (metadata.Tags.Split(' '), t => songSelect?.Search(t)); + else + mapperTags.Tags = (Array.Empty(), _ => { }); + submitted.Date = beatmapSetInfo.DateSubmitted; ranked.Date = beatmapSetInfo.DateRanked; From 4203f2cdeb47895526e0d1bf5d51c2a0e48bdaeb Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 23 Jun 2025 03:34:14 +0300 Subject: [PATCH 254/370] Add maximum limit to wedge difficulty statistics --- .../TestSceneDifficultyStatisticsDisplay.cs | 18 ++++++++++++++++++ .../BeatmapTitleWedge_StatisticDifficulty.cs | 3 ++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyStatisticsDisplay.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyStatisticsDisplay.cs index 3dd6fed708..0ee742a09d 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyStatisticsDisplay.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyStatisticsDisplay.cs @@ -162,5 +162,23 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("statistics still visible", () => display.ChildrenOfType().First().Parent!.Alpha == 1); AddAssert("tiny statistics still hidden", () => display.ChildrenOfType().Last().Alpha == 0); } + + [Test] + public void TestMaximumLength() + { + AddStep("setup auto size", () => Child = display = new BeatmapTitleWedge.DifficultyStatisticsDisplay(true) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + + AddStep("set long statistics", () => display.Statistics = new[] + { + new BeatmapTitleWedge.StatisticDifficulty.Data("Very Long Statistic 1", 0.2f, 0.2f, 1f), + new BeatmapTitleWedge.StatisticDifficulty.Data("Very Long Statistic 2", 0.7f, 0.7f, 1f), + new BeatmapTitleWedge.StatisticDifficulty.Data("Very Long Statistic 3", 0.4f, 0.8f, 1f), + new BeatmapTitleWedge.StatisticDifficulty.Data("Very Long Statistic 4", 0.3f, 0.3f, 1f), + }); + } } } diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_StatisticDifficulty.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_StatisticDifficulty.cs index b533d21c1e..d0b6acca88 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_StatisticDifficulty.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_StatisticDifficulty.cs @@ -98,10 +98,11 @@ namespace osu.Game.Screens.SelectV2 }, }, }, - labelText = new OsuSpriteText + labelText = new TruncatingSpriteText { Margin = new MarginPadding { Top = 2f }, Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold), + MaxWidth = 85, }, new FillFlowContainer { From 5436313c86c2e36fc0d6ed8cdaebb26da8024407 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 23 Jun 2025 05:26:12 +0300 Subject: [PATCH 255/370] Flip storyboard/video icon order --- osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs index 8cdb644cab..f75e7b1d3c 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs @@ -193,7 +193,7 @@ namespace osu.Game.Overlays.BeatmapSet TextSize = 14, TextPadding = new MarginPadding { Horizontal = 35, Vertical = 10 } }, - videoIconPill = new VideoIconPill + storyboardIconPill = new StoryboardIconPill { AutoSizeAxes = Axes.X, RelativeSizeAxes = Axes.Y, @@ -202,7 +202,7 @@ namespace osu.Game.Overlays.BeatmapSet IconSize = new Vector2(34), IconPadding = new MarginPadding(10), }, - storyboardIconPill = new StoryboardIconPill + videoIconPill = new VideoIconPill { AutoSizeAxes = Axes.X, RelativeSizeAxes = Axes.Y, From c61ec0b86a56111a77e81221ebd59333d2ac7a86 Mon Sep 17 00:00:00 2001 From: eyhn Date: Mon, 23 Jun 2025 13:37:11 +0800 Subject: [PATCH 256/370] Fix inconsistent rounding strategy for PP --- .../Toolbar/TransientUserStatisticsUpdateDisplay.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Toolbar/TransientUserStatisticsUpdateDisplay.cs b/osu.Game/Overlays/Toolbar/TransientUserStatisticsUpdateDisplay.cs index d5891da936..85b7358d2d 100644 --- a/osu.Game/Overlays/Toolbar/TransientUserStatisticsUpdateDisplay.cs +++ b/osu.Game/Overlays/Toolbar/TransientUserStatisticsUpdateDisplay.cs @@ -83,7 +83,12 @@ namespace osu.Game.Overlays.Toolbar } if (update.After.PP != null) - pp.Display((int)(update.Before.PP ?? update.After.PP.Value), (int)Math.Abs(((int?)update.After.PP - (int?)update.Before.PP) ?? 0M), (int)update.After.PP.Value); + { + int before = (int)Math.Round(update.Before.PP ?? update.After.PP.Value); + int after = (int)Math.Round(update.After.PP.Value); + int delta = Math.Abs(after - before); + pp.Display(before, delta, after); + } this.Delay(5000).FadeOut(500, Easing.OutQuint); }); From cef6445e2b62232d53731edad3cc1788cdb4264f Mon Sep 17 00:00:00 2001 From: jyc76 Date: Mon, 23 Jun 2025 16:15:47 +0900 Subject: [PATCH 257/370] Increase margin in PanelBeatmap to improve legibility --- osu.Game/Screens/SelectV2/PanelBeatmap.cs | 4 ++-- osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 4 +++- osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index 19ff8a0676..4af607c750 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -108,8 +108,8 @@ namespace osu.Game.Screens.SelectV2 AutoSizeAxes = Axes.Both, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Spacing = new Vector2(3), - Margin = new MarginPadding { Left = 5 }, + Spacing = new Vector2(5), + Margin = new MarginPadding { Left = 6.5f, Bottom = 3.5f }, Direction = FillDirection.Horizontal, Children = new Drawable[] { diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index 9bdd188a3a..2864980fce 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -97,7 +97,9 @@ namespace osu.Game.Screens.SelectV2 { AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, - Padding = new MarginPadding { Top = 7.5f, Left = 15, Bottom = 5 }, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Padding = new MarginPadding { Top = 7.5f, Left = 15, Bottom = 13 }, Children = new Drawable[] { titleText = new OsuSpriteText diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs index 287af444ee..bae6483e50 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs @@ -98,8 +98,8 @@ namespace osu.Game.Screens.SelectV2 AutoSizeAxes = Axes.Both, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Spacing = new Vector2(3), - Margin = new MarginPadding { Left = 5 }, + Spacing = new Vector2(5), + Margin = new MarginPadding { Left = 6.5f, Bottom = 2.8f }, Direction = FillDirection.Horizontal, Children = new Drawable[] { From a9347ff8c32fc4e43ae6bdd84b3ba55d495110e7 Mon Sep 17 00:00:00 2001 From: jyc76 Date: Mon, 23 Jun 2025 23:31:20 +0900 Subject: [PATCH 258/370] Fix top and bottom spacing of the local rank display --- osu.Game/Screens/SelectV2/PanelBeatmap.cs | 3 ++- osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index 4af607c750..ca4ef56169 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -109,7 +109,7 @@ namespace osu.Game.Screens.SelectV2 Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Spacing = new Vector2(5), - Margin = new MarginPadding { Left = 6.5f, Bottom = 3.5f }, + Margin = new MarginPadding { Left = 6.5f }, Direction = FillDirection.Horizontal, Children = new Drawable[] { @@ -125,6 +125,7 @@ namespace osu.Game.Screens.SelectV2 Origin = Anchor.CentreLeft, Direction = FillDirection.Vertical, AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Bottom = 3.5f }, Children = new Drawable[] { new FillFlowContainer diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs index bae6483e50..28a6bfc83a 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs @@ -99,7 +99,7 @@ namespace osu.Game.Screens.SelectV2 Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Spacing = new Vector2(5), - Margin = new MarginPadding { Left = 6.5f, Bottom = 2.8f }, + Margin = new MarginPadding { Left = 6.5f }, Direction = FillDirection.Horizontal, Children = new Drawable[] { @@ -114,7 +114,7 @@ namespace osu.Game.Screens.SelectV2 Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Direction = FillDirection.Vertical, - Padding = new MarginPadding { Bottom = 2 }, + Padding = new MarginPadding { Bottom = 4.8f }, AutoSizeAxes = Axes.Both, Children = new Drawable[] { From 96db5677ca21e392097f8690a86f5d5ff9824522 Mon Sep 17 00:00:00 2001 From: jyc76 Date: Mon, 23 Jun 2025 23:49:49 +0900 Subject: [PATCH 259/370] Use padding instead of margin in PanelBeatmap --- osu.Game/Screens/SelectV2/PanelBeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index ca4ef56169..a06c77448f 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -125,7 +125,7 @@ namespace osu.Game.Screens.SelectV2 Origin = Anchor.CentreLeft, Direction = FillDirection.Vertical, AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Bottom = 3.5f }, + Padding = new MarginPadding { Bottom = 3.5f }, Children = new Drawable[] { new FillFlowContainer From 58b9b49c78eec2def26259311f2828e74f363302 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Jun 2025 14:37:34 +0900 Subject: [PATCH 260/370] Fix spectator button not working when user is playing daily challenge --- osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs index 39df3ba22c..02fe681492 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs @@ -197,6 +197,7 @@ namespace osu.Game.Overlays.Dashboard case UserActivity.InSoloGame: case UserActivity.InMultiplayerGame: case UserActivity.InPlaylistGame: + case UserActivity.PlayingDailyChallenge: spectateButton.Enabled.Value = true; break; } From dbdf2d9aca43510fff73cfbdcd13a44b4e05a4a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Jun 2025 14:54:52 +0900 Subject: [PATCH 261/370] Fix very short kiai sections not showing up on editor summary timeline Closes https://github.com/ppy/osu/issues/33836. --- .../Edit/Components/Timelines/Summary/Parts/KiaiPart.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/KiaiPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/KiaiPart.cs index ee44df8598..e856009817 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/KiaiPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/KiaiPart.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.Graphics; using osu.Framework.Graphics.Cursor; @@ -91,7 +92,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts section = value; X = (float)value.StartTime; - Width = (float)value.Duration; + // Minimum width ensures that very short kiai sections still show a slither of colour. + Width = (float)Math.Max(200, value.Duration); } } From 3599269cba188edf1e3aa5bb6ec5b0d3106b7134 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 24 Jun 2025 09:10:13 +0300 Subject: [PATCH 262/370] Fix dropdown search bar not having placeholder text --- osu.Game/Graphics/UserInterface/OsuDropdown.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index af335efdc4..e0179f8bc4 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -18,6 +18,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Input.Bindings; using osu.Game.Overlays; +using osu.Game.Resources.Localisation.Web; using osuTK; using osuTK.Graphics; @@ -440,6 +441,11 @@ namespace osu.Game.Graphics.UserInterface private partial class DropdownSearchTextBox : OsuTextBox { + public DropdownSearchTextBox() + { + PlaceholderText = HomeStrings.SearchPlaceholder; + } + [BackgroundDependencyLoader] private void load(OverlayColourProvider? colourProvider) { From c0a51da11054aba0ffbbbe98555097874cfd0a1e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Jun 2025 15:33:51 +0900 Subject: [PATCH 263/370] Fix player settings overlay potentially disappearing unexpectedly Closes https://github.com/ppy/osu/issues/33793. Can be tested with this diff: ```diff diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index 635d140a4a..b3a827b699 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -60,6 +60,8 @@ public PlayerSettingsOverlay() Origin = Anchor.TopRight; Anchor = Anchor.TopRight; + X = 0.01f; + base.Content.Add(content = new FillFlowContainer { AutoSizeAxes = Axes.Both, ``` --- osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index b285b1b799..635d140a4a 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -6,6 +6,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input; @@ -47,6 +48,12 @@ namespace osu.Game.Screens.Play.HUD [Resolved] private HUDOverlay? hudOverlay { get; set; } + // Player settings are kept off the edge of the screen. + // + // In edge cases, floating point error could result in the whole control getting masked away + // while collapsed down, so let's avoid that. + protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false; + public PlayerSettingsOverlay() : base(0, EXPANDED_WIDTH) { From 0aec52a64ebef23676e50cca9801a045733df44f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Jun 2025 15:57:53 +0900 Subject: [PATCH 264/370] Fix download requests firing too often in multiplayer spectator Closes https://github.com/ppy/osu/issues/33785. Tested on production with debugger attached. --- .../Multiplayer/Match/MultiplayerSpectateButton.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs index 13abe7bb14..46e25fc688 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs @@ -111,6 +111,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private CancellationTokenSource? downloadCheckCancellation; + private int? lastDownloadCheckedBeatmapId; + private void checkForAutomaticDownload() { downloadCheckCancellation?.Cancel(); @@ -132,6 +134,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match MultiplayerPlaylistItem item = client.Room.CurrentPlaylistItem; + // This method is called every time anything changes in the room. + // This could result in download requests firing far too often, when we only expect them to fire once per beatmap. + // + // Without this check, we would see especially egregious behaviour when a user has hit the download rate limit. + if (lastDownloadCheckedBeatmapId == item.BeatmapID) + return; + + lastDownloadCheckedBeatmapId = item.BeatmapID; + // In a perfect world we'd use BeatmapAvailability, but there's no event-driven flow for when a selection changes. // ie. if selection changes from "not downloaded" to another "not downloaded" we wouldn't get a value changed raised. beatmapLookupCache From 4123d4a80d9fa9086579c32ace68bd6787b16914 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Jun 2025 16:36:10 +0900 Subject: [PATCH 265/370] Fix rotating objects in the skin editor not rotating as expected Closes https://github.com/ppy/osu/issues/33845. Not sure if this ever worked but it was definitely wrong for a while now. --- osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs | 2 +- .../Screens/Edit/Compose/Components/SelectionRotationHandler.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs index 9fd28a1cad..c8799ad5ba 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs @@ -59,7 +59,7 @@ namespace osu.Game.Overlays.SkinEditor objectsInRotation = selectedItems.Cast().ToArray(); originalRotations = objectsInRotation.ToDictionary(d => d, d => d.Rotation); originalPositions = objectsInRotation.ToDictionary(d => d, d => d.ToScreenSpace(d.OriginPosition)); - DefaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation.SelectMany(d => d.ScreenSpaceDrawQuad.GetVertices().ToArray())).Centre; + DefaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation.SelectMany(d => ToLocalSpace(d.ScreenSpaceDrawQuad).GetVertices().ToArray())).Centre; base.Begin(); } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs index af3b3d6489..6cd2428b8a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs @@ -30,6 +30,8 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// Implementation-defined origin point to rotate around when no explicit origin is provided. /// This field is only assigned during a rotation operation. + /// + /// Coordinates are in local space for this container. /// public Vector2? DefaultOrigin { get; protected set; } From a1ec71e677a35a4fb453340034db705a6ac629e4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Jun 2025 17:48:38 +0900 Subject: [PATCH 266/370] Fix potential crash when attempting random selection after changing grouping mode --- .../TestSceneBeatmapCarouselRandom.cs | 17 +++++++++++++++++ osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 9 +++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs index 739fc23ed5..b7e169964d 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs @@ -37,6 +37,23 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } } + [Test] + public void TestGroupingModeChangeStillWorks() + { + SortAndGroupBy(SortMode.Artist, GroupMode.Artist); + AddBeatmaps(10, 3, true); + WaitForDrawablePanels(); + + nextRandom(); + ensureRandomDidNotRepeat(); + + SortAndGroupBy(SortMode.Artist, GroupMode.None); + WaitForFiltering(); + + nextRandom(); + ensureRandomDidNotRepeat(); + } + /// /// Test random non-repeating algorithm /// diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index cd5ab68e6f..ccd7e52ed1 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -289,9 +289,14 @@ namespace osu.Game.Screens.SelectV2 // This will update the visual state of the selected item. HandleItemSelected(CurrentSelection); - // If a group was selected that is not the one containing the selection, reselect it. + // If a group was selected that is not the one containing the selection, attempt to reselect it. if (groupForReselection != null) - setExpandedGroup(groupForReselection); + { + if (!grouping.GroupItems.TryGetValue(groupForReselection, out _)) + ExpandedGroup = null; + else + setExpandedGroup(groupForReselection); + } } private void selectRecommendedDifficultyForBeatmapSet(BeatmapSetInfo beatmapSet) From 4fbffc4d660c623cd14c6a2eacfd0d04653da1b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Jun 2025 17:51:39 +0900 Subject: [PATCH 267/370] Move cancellation below new equality check --- .../OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs index 46e25fc688..3f207f6fa1 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs @@ -115,8 +115,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private void checkForAutomaticDownload() { - downloadCheckCancellation?.Cancel(); - if (client.Room == null) return; @@ -143,6 +141,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match lastDownloadCheckedBeatmapId = item.BeatmapID; + downloadCheckCancellation?.Cancel(); + // In a perfect world we'd use BeatmapAvailability, but there's no event-driven flow for when a selection changes. // ie. if selection changes from "not downloaded" to another "not downloaded" we wouldn't get a value changed raised. beatmapLookupCache From a78dc31d8bc278bac25875ff75bcb07192c04cec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 24 Jun 2025 11:37:23 +0200 Subject: [PATCH 268/370] Add testing --- .../Menus/TestSceneToolbarUserButton.cs | 18 +++++++++++++ .../Visual/Ranking/TestSceneOverallRanking.cs | 26 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs index 1af4af8f6b..53d909406f 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs @@ -150,6 +150,24 @@ namespace osu.Game.Tests.Visual.Menus }); }); + // cross-reference: `TestSceneOverallRanking.TestRoundingTreatment()`. + AddStep("Test rounding treatment", () => + { + var transientUpdateDisplay = this.ChildrenOfType().Single(); + transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate( + new ScoreInfo(), + new UserStatistics + { + GlobalRank = 111_111, + PP = 5071.495M + }, + new UserStatistics + { + GlobalRank = 111_111, + PP = 5072.99M + }); + }); + AddStep("No change 1", () => { var transientUpdateDisplay = this.ChildrenOfType().Single(); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs b/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs index fb18cc8a59..e49d23dd80 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs @@ -46,6 +46,32 @@ namespace osu.Game.Tests.Visual.Ranking }); } + // cross-reference: `TestSceneToolbarUserButton.TestTransientUserStatisticsDisplay()`, "Test rounding treatment" step. + [Test] + public void TestRoundingTreatment() + { + createDisplay(); + displayUpdate( + new UserStatistics + { + GlobalRank = 12_345, + Accuracy = 98.99, + MaxCombo = 2_322, + RankedScore = 23_123_543_456, + TotalScore = 123_123_543_456, + PP = 5_071.495M + }, + new UserStatistics + { + GlobalRank = 12_345, + Accuracy = 98.99, + MaxCombo = 2_322, + RankedScore = 23_123_543_456, + TotalScore = 123_123_543_456, + PP = 5_072.99M + }); + } + [Test] public void TestAllDecreased() { From b1e86aa92f8b0465a33391ccb43f2a88682e379f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 24 Jun 2025 11:42:14 +0200 Subject: [PATCH 269/370] Calculate pp difference post-rounding everywhere --- .../Statistics/User/PerformancePointsChangeRow.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/User/PerformancePointsChangeRow.cs b/osu.Game/Screens/Ranking/Statistics/User/PerformancePointsChangeRow.cs index c1faf1a3e3..3af1bdb860 100644 --- a/osu.Game/Screens/Ranking/Statistics/User/PerformancePointsChangeRow.cs +++ b/osu.Game/Screens/Ranking/Statistics/User/PerformancePointsChangeRow.cs @@ -1,25 +1,26 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Diagnostics; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; namespace osu.Game.Screens.Ranking.Statistics.User { - public partial class PerformancePointsChangeRow : RankingChangeRow + public partial class PerformancePointsChangeRow : RankingChangeRow { public PerformancePointsChangeRow() - : base(stats => stats.PP) + : base(stats => stats.PP != null ? (int)Math.Round(stats.PP.Value) : null) { } protected override LocalisableString Label => RankingsStrings.StatPerformance; - protected override LocalisableString FormatCurrentValue(decimal? current) + protected override LocalisableString FormatCurrentValue(int? current) => current == null ? string.Empty : LocalisableString.Interpolate($@"{current:N0}pp"); - protected override int CalculateDifference(decimal? previous, decimal? current, out LocalisableString formattedDifference) + protected override int CalculateDifference(int? previous, int? current, out LocalisableString formattedDifference) { if (previous == null && current == null) { From 7b6ecbd10b4bab72ab8a919a7d0399f126dc27e6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Jun 2025 19:21:04 +0900 Subject: [PATCH 270/370] Expand test to cover more potential weirdness and fix said weirdness --- .../TestSceneBeatmapCarouselRandom.cs | 23 ++++++++++++++++--- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 10 +++----- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs index b7e169964d..ef142e6253 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs @@ -40,6 +40,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] public void TestGroupingModeChangeStillWorks() { + BeatmapInfo originalSelected = null!; + GroupDefinition? expanded = null; + SortAndGroupBy(SortMode.Artist, GroupMode.Artist); AddBeatmaps(10, 3, true); WaitForDrawablePanels(); @@ -47,11 +50,25 @@ namespace osu.Game.Tests.Visual.SongSelectV2 nextRandom(); ensureRandomDidNotRepeat(); - SortAndGroupBy(SortMode.Artist, GroupMode.None); + AddStep("store selection", () => originalSelected = (BeatmapInfo)Carousel.CurrentSelection!); + + SortAndGroupBy(SortMode.Artist, GroupMode.Difficulty); WaitForFiltering(); - nextRandom(); - ensureRandomDidNotRepeat(); + AddAssert("selection not changed", () => Carousel.CurrentSelection, () => Is.EqualTo(originalSelected)); + + storeExpandedGroup(); + + for (int i = 0; i < 5; i++) + { + nextRandom(); + ensureRandomDidNotRepeat(); + checkExpandedGroupUnchanged(); + } + + void storeExpandedGroup() => AddStep("store open group", () => expanded = Carousel.ExpandedGroup); + + void checkExpandedGroupUnchanged() => AddAssert("expanded did not change", () => Carousel.ExpandedGroup, () => Is.EqualTo(expanded)); } /// diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index ccd7e52ed1..c5fb363f3b 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -290,13 +290,9 @@ namespace osu.Game.Screens.SelectV2 HandleItemSelected(CurrentSelection); // If a group was selected that is not the one containing the selection, attempt to reselect it. - if (groupForReselection != null) - { - if (!grouping.GroupItems.TryGetValue(groupForReselection, out _)) - ExpandedGroup = null; - else - setExpandedGroup(groupForReselection); - } + // If the original group was not found, ExpandedGroup will already have been updated to a valid value in `HandleItemSelected` above. + if (groupForReselection != null && grouping.GroupItems.TryGetValue(groupForReselection, out _)) + setExpandedGroup(groupForReselection); } private void selectRecommendedDifficultyForBeatmapSet(BeatmapSetInfo beatmapSet) From 54c2d4207fae1bb50d373f686130d40321dd459e Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 24 Jun 2025 14:11:27 +0300 Subject: [PATCH 271/370] Add link button to multiplayer/playlists room panels --- .../OnlinePlay/Lounge/Components/RoomPanel.cs | 37 +++++++++++++++++-- .../OnlinePlay/Lounge/LoungeRoomPanel.cs | 1 + 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs index acbf5d8462..b94cfb8de7 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs @@ -54,11 +54,15 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components protected readonly Bindable SelectedItem = new Bindable(); protected Container ButtonsContainer { get; private set; } = null!; + protected bool ShowExternalLink { get; init; } = true; + private DrawableRoomParticipantsList? drawableRoomParticipantsList; private RoomSpecialCategoryPill? specialCategoryPill; private PasswordProtectedIcon? passwordIcon; private EndDateInfo? endDateInfo; + private FillFlowContainer? roomNameFlow; private SpriteText? roomName; + private ExternalLinkButton? linkButton; private DelayedLoadWrapper wrapper = null!; private CancellationTokenSource? beatmapLookupCancellation; @@ -204,10 +208,27 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Direction = FillDirection.Vertical, Children = new Drawable[] { - roomName = new TruncatingSpriteText + roomNameFlow = new FillFlowContainer { RelativeSizeAxes = Axes.X, - Font = OsuFont.GetFont(size: 28) + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + roomName = new TruncatingSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Font = OsuFont.GetFont(size: 28), + }, + linkButton = new ExternalLinkButton(formatRoomUrl(Room.RoomID ?? 0)) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding { Horizontal = 6, Bottom = 4 }, + Alpha = ShowExternalLink && Room.RoomID.HasValue ? 1 : 0, + }, + }, }, new RoomStatusText(Room) { @@ -288,6 +309,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components SelectedItem.BindValueChanged(onSelectedItemChanged, true); } + protected override void Update() + { + base.Update(); + + if (roomName != null) + roomName.MaxWidth = (roomNameFlow?.DrawWidth ?? 0) - (linkButton?.LayoutSize.X ?? 0); + } + private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e) { switch (e.PropertyName) @@ -390,11 +419,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } return items.ToArray(); - - string formatRoomUrl(long id) => $@"{api.Endpoints.WebsiteUrl}/multiplayer/rooms/{id}"; } } + private string formatRoomUrl(long id) => $@"{api.Endpoints.WebsiteUrl}/multiplayer/rooms/{id}"; + protected virtual UpdateableBeatmapBackgroundSprite CreateBackground() => new UpdateableBeatmapBackgroundSprite(); protected virtual IEnumerable CreateBottomDetails() diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeRoomPanel.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeRoomPanel.cs index 3ff27a14bb..12b38a9677 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeRoomPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeRoomPanel.cs @@ -67,6 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge public LoungeRoomPanel(Room room) : base(room) { + ShowExternalLink = false; } [BackgroundDependencyLoader] From 0de964b10b461609fdec36eb46c99116e3305d7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 24 Jun 2025 13:32:42 +0200 Subject: [PATCH 272/370] Expand test even further to cover even more potential weirdness --- .../SongSelectV2/TestSceneBeatmapCarouselRandom.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs index ef142e6253..ed694c9e3d 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselRandom.cs @@ -66,6 +66,15 @@ namespace osu.Game.Tests.Visual.SongSelectV2 checkExpandedGroupUnchanged(); } + SortAndGroupBy(SortMode.Artist, GroupMode.None); + WaitForFiltering(); + + for (int i = 0; i < 5; i++) + { + nextRandom(); + ensureRandomDidNotRepeat(); + } + void storeExpandedGroup() => AddStep("store open group", () => expanded = Carousel.ExpandedGroup); void checkExpandedGroupUnchanged() => AddAssert("expanded did not change", () => Carousel.ExpandedGroup, () => Is.EqualTo(expanded)); From 8781901d6bc691a44d99e8bde1f0475afb44b79b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 24 Jun 2025 13:38:32 +0200 Subject: [PATCH 273/370] Ensure expanded group is cleared when grouping is turned off --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index c5fb363f3b..e19cdd20c7 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -267,8 +267,7 @@ namespace osu.Game.Screens.SelectV2 // Find any containing group. There should never be too many groups so iterating is efficient enough. GroupDefinition? containingGroup = grouping.GroupItems.SingleOrDefault(kvp => kvp.Value.Any(i => CheckModelEquality(i.Model, beatmapInfo))).Key; - if (containingGroup != null) - setExpandedGroup(containingGroup); + setExpandedGroup(containingGroup); if (grouping.BeatmapSetsGroupedTogether) setExpandedSet(beatmapInfo); @@ -362,8 +361,11 @@ namespace osu.Game.Screens.SelectV2 { if (ExpandedGroup != null) setExpansionStateOfGroup(ExpandedGroup, false); + ExpandedGroup = group; - setExpansionStateOfGroup(group, true); + + if (ExpandedGroup != null) + setExpansionStateOfGroup(group, true); } private void setExpansionStateOfGroup(GroupDefinition group, bool expanded) From bb15df1ba586b38f745ba9db56f56555d485a04f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Jun 2025 11:13:05 +0900 Subject: [PATCH 274/370] Disallow changing release stream on fixed installs For package-managed solutions that set `OSU_EXTERNAL_UPDATE_STREAM` (which overrides the config value), we should not allow the user to change the release stream themselves. --- .../Localisation/GeneralSettingsStrings.cs | 18 +++++++---- .../Sections/General/UpdateSettings.cs | 21 +++++++++---- osu.Game/Overlays/Settings/SettingsItem.cs | 30 ++++++++++++------- osu.Game/Updater/MobileUpdateNotifier.cs | 3 ++ osu.Game/Updater/NoActionUpdateManager.cs | 2 ++ osu.Game/Updater/UpdateManager.cs | 2 ++ 6 files changed, 55 insertions(+), 21 deletions(-) diff --git a/osu.Game/Localisation/GeneralSettingsStrings.cs b/osu.Game/Localisation/GeneralSettingsStrings.cs index 0f4dd0805e..7c9f78e57f 100644 --- a/osu.Game/Localisation/GeneralSettingsStrings.cs +++ b/osu.Game/Localisation/GeneralSettingsStrings.cs @@ -79,23 +79,31 @@ namespace osu.Game.Localisation /// public static LocalisableString LearnMoreAboutLazerTooltip => new TranslatableString(getKey(@"check_out_the_feature_comparison"), @"Check out the feature comparison and FAQ"); + /// + /// "Check with your package manager / provider for other release streams." + /// + public static LocalisableString ChangeReleaseStreamPackageManagerWarning => new TranslatableString(getKey(@"change_release stream_package_warning"), @"Check with your package manager / provider for other release streams."); + + /// + /// "Check with your app store (testflight, etc) for other release streams." + /// + public static LocalisableString ChangeReleaseStreamMobileWarning => new TranslatableString(getKey(@"change_release stream_mobile_warning"), @"Check with your app store (testflight, etc) for other release streams."); + /// /// "Are you sure you want to run a potentially unstable version of the game?" /// - public static LocalisableString ChangeReleaseStreamConfirmation => new TranslatableString(getKey(@"change_release stream_confirmation"), - @"Are you sure you want to run a potentially unstable version of the game?"); + public static LocalisableString ChangeReleaseStreamConfirmation => new TranslatableString(getKey(@"change_release stream_confirmation"), @"Are you sure you want to run a potentially unstable version of the game?"); /// /// "If you run into issues starting the game, you can usually run the installer from the official site to recover." /// - public static LocalisableString ChangeReleaseStreamConfirmationInfo => new TranslatableString(getKey(@"change_release stream_confirmation_info"), - @"If you run into issues starting the game, you can usually run the installer from the official site to recover."); + public static LocalisableString ChangeReleaseStreamConfirmationInfo => new TranslatableString(getKey(@"change_release stream_confirmation_info"), @"If you run into issues starting the game, you can usually run the installer from the official site to recover."); /// /// "You are running the latest release ({0})" /// public static LocalisableString RunningLatestRelease(string version) => new TranslatableString(getKey(@"running_latest_release"), @"You are running the latest release ({0})", version); - private static string getKey(string key) => $"{prefix}:{key}"; + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index e35fbaee76..63c09cde56 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -28,9 +28,9 @@ namespace osu.Game.Overlays.Settings.Sections.General protected override LocalisableString Header => GeneralSettingsStrings.UpdateHeader; private SettingsButton checkForUpdatesButton = null!; + private SettingsEnumDropdown releaseStreamDropdown = null!; private readonly Bindable configReleaseStream = new Bindable(); - private SettingsEnumDropdown releaseStreamDropdown = null!; [Resolved] private UpdateManager? updateManager { get; set; } @@ -58,11 +58,16 @@ namespace osu.Game.Overlays.Settings.Sections.General Keywords = new[] { @"version" }, }); - Add(checkForUpdatesButton = new SettingsButton + if (updateManager.FixedReleaseStream != null) { - Text = GeneralSettingsStrings.CheckUpdate, - Action = () => checkForUpdates().FireAndForget() - }); + configReleaseStream.Value = updateManager.FixedReleaseStream.Value; + + releaseStreamDropdown.ShowsDefaultIndicator = false; + releaseStreamDropdown.Items = [updateManager.FixedReleaseStream.Value]; + releaseStreamDropdown.SetNoticeText(RuntimeInfo.IsDesktop + ? GeneralSettingsStrings.ChangeReleaseStreamPackageManagerWarning + : GeneralSettingsStrings.ChangeReleaseStreamMobileWarning); + } releaseStreamDropdown.Current.BindValueChanged(stream => { @@ -86,6 +91,12 @@ namespace osu.Game.Overlays.Settings.Sections.General configReleaseStream.Value = stream.NewValue; }); + + Add(checkForUpdatesButton = new SettingsButton + { + Text = GeneralSettingsStrings.CheckUpdate, + Action = () => checkForUpdates().FireAndForget() + }); } if (RuntimeInfo.IsDesktop) diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index 9c6bb5ae60..9186734641 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -45,7 +45,18 @@ namespace osu.Game.Overlays.Settings private OsuTextFlowContainer noticeText; - public bool ShowsDefaultIndicator = true; + private bool showsDefaultIndicator = true; + + public bool ShowsDefaultIndicator + { + get => showsDefaultIndicator; + set + { + showsDefaultIndicator = value; + defaultValueIndicatorContainer.Alpha = value ? 1 : 0; + } + } + private readonly Container defaultValueIndicatorContainer; public LocalisableString TooltipText { get; set; } @@ -214,17 +225,14 @@ namespace osu.Game.Overlays.Settings [BackgroundDependencyLoader] private void load() { - // intentionally done before LoadComplete to avoid overhead. - if (ShowsDefaultIndicator) + defaultValueIndicatorContainer.Child = new RevertToDefaultButton { - defaultValueIndicatorContainer.Add(new RevertToDefaultButton - { - Current = controlWithCurrent.Current, - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }); - updateLayout(); - } + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Current = controlWithCurrent.Current, + }; + + updateLayout(); } private void updateLayout() diff --git a/osu.Game/Updater/MobileUpdateNotifier.cs b/osu.Game/Updater/MobileUpdateNotifier.cs index 02dac00cf4..0b13830046 100644 --- a/osu.Game/Updater/MobileUpdateNotifier.cs +++ b/osu.Game/Updater/MobileUpdateNotifier.cs @@ -10,6 +10,7 @@ using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; using osu.Framework.Platform; +using osu.Game.Configuration; using osu.Game.Online.API; namespace osu.Game.Updater @@ -20,6 +21,8 @@ namespace osu.Game.Updater /// public partial class MobileUpdateNotifier : UpdateManager { + public override ReleaseStream? FixedReleaseStream => Configuration.ReleaseStream.Lazer; + private string version = null!; [Resolved] diff --git a/osu.Game/Updater/NoActionUpdateManager.cs b/osu.Game/Updater/NoActionUpdateManager.cs index 06189b488c..2c6ae459de 100644 --- a/osu.Game/Updater/NoActionUpdateManager.cs +++ b/osu.Game/Updater/NoActionUpdateManager.cs @@ -17,6 +17,8 @@ namespace osu.Game.Updater /// public partial class NoActionUpdateManager : UpdateManager { + public override ReleaseStream? FixedReleaseStream => externalReleaseStream; + private static ReleaseStream? externalReleaseStream => Enum.TryParse(Environment.GetEnvironmentVariable("OSU_EXTERNAL_UPDATE_STREAM"), true, out ReleaseStream stream) ? stream : null; private string version = string.Empty; diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index a9b00e8f93..65b4770174 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -32,6 +32,8 @@ namespace osu.Game.Updater // only implementations will actually check for updates. GetType() != typeof(UpdateManager); + public virtual ReleaseStream? FixedReleaseStream => null; + [Resolved] private OsuConfigManager config { get; set; } = null!; From c52c82ae8a53a607935cb2ad6bb6f62b6710e269 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Jun 2025 11:45:05 +0900 Subject: [PATCH 275/370] Support release streams in `MobileUpdateNotifier` --- osu.Game/Updater/MobileUpdateNotifier.cs | 20 ++++++++++++-------- osu.Game/Updater/NoActionUpdateManager.cs | 5 +---- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/osu.Game/Updater/MobileUpdateNotifier.cs b/osu.Game/Updater/MobileUpdateNotifier.cs index 0b13830046..3a290c9a63 100644 --- a/osu.Game/Updater/MobileUpdateNotifier.cs +++ b/osu.Game/Updater/MobileUpdateNotifier.cs @@ -21,9 +21,10 @@ namespace osu.Game.Updater /// public partial class MobileUpdateNotifier : UpdateManager { - public override ReleaseStream? FixedReleaseStream => Configuration.ReleaseStream.Lazer; + public override ReleaseStream? FixedReleaseStream => stream; private string version = null!; + private ReleaseStream stream; [Resolved] private GameHost host { get; set; } = null!; @@ -31,22 +32,25 @@ namespace osu.Game.Updater [BackgroundDependencyLoader] private void load(OsuGameBase game) { - version = game.Version; + version = game.Version.Split('-').First(); + stream = Enum.TryParse(game.Version.Split('-').Last(), true, out ReleaseStream s) ? s : Configuration.ReleaseStream.Lazer; } protected override async Task PerformUpdateCheck(CancellationToken cancellationToken) { try { - var releases = new OsuJsonWebRequest("https://api.github.com/repos/ppy/osu/releases/latest"); + bool includePrerelease = stream == Configuration.ReleaseStream.Tachyon; - await releases.PerformAsync(cancellationToken).ConfigureAwait(false); + OsuJsonWebRequest releasesRequest = new OsuJsonWebRequest("https://api.github.com/repos/ppy/osu/releases?per_page=10&page=1"); + await releasesRequest.PerformAsync(cancellationToken).ConfigureAwait(false); - var latest = releases.ResponseObject; + GitHubRelease[] releases = releasesRequest.ResponseObject; + GitHubRelease? latest = releases.OrderByDescending(r => r.PublishedAt).FirstOrDefault(r => includePrerelease || !r.Prerelease); + + if (latest == null) + return false; - // avoid any discrepancies due to build suffixes for now. - // eventually we will want to support release streams and consider these. - version = version.Split('-').First(); string latestTagName = latest.TagName.Split('-').First(); if (latestTagName != version && tryGetBestUrl(latest, out string? url)) diff --git a/osu.Game/Updater/NoActionUpdateManager.cs b/osu.Game/Updater/NoActionUpdateManager.cs index 2c6ae459de..0710797b60 100644 --- a/osu.Game/Updater/NoActionUpdateManager.cs +++ b/osu.Game/Updater/NoActionUpdateManager.cs @@ -26,7 +26,7 @@ namespace osu.Game.Updater [BackgroundDependencyLoader] private void load(OsuGameBase game) { - version = game.Version; + version = game.Version.Split('-').First(); } protected override async Task PerformUpdateCheck(CancellationToken cancellationToken) @@ -45,9 +45,6 @@ namespace osu.Game.Updater if (latest == null) return false; - // avoid any discrepancies due to build suffixes for now. - // eventually we will want to support release streams and consider these. - version = version.Split('-').First(); string latestTagName = latest.TagName.Split('-').First(); if (latestTagName != version) From 37df9c5a48329649b65ecf14aab7503ca2c99af1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Jun 2025 12:16:53 +0900 Subject: [PATCH 276/370] Fix incorrect localisation keys --- osu.Game/Localisation/GeneralSettingsStrings.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Localisation/GeneralSettingsStrings.cs b/osu.Game/Localisation/GeneralSettingsStrings.cs index 7c9f78e57f..c806c4eb0a 100644 --- a/osu.Game/Localisation/GeneralSettingsStrings.cs +++ b/osu.Game/Localisation/GeneralSettingsStrings.cs @@ -82,22 +82,22 @@ namespace osu.Game.Localisation /// /// "Check with your package manager / provider for other release streams." /// - public static LocalisableString ChangeReleaseStreamPackageManagerWarning => new TranslatableString(getKey(@"change_release stream_package_warning"), @"Check with your package manager / provider for other release streams."); + public static LocalisableString ChangeReleaseStreamPackageManagerWarning => new TranslatableString(getKey(@"change_release_stream_package_warning"), @"Check with your package manager / provider for other release streams."); /// /// "Check with your app store (testflight, etc) for other release streams." /// - public static LocalisableString ChangeReleaseStreamMobileWarning => new TranslatableString(getKey(@"change_release stream_mobile_warning"), @"Check with your app store (testflight, etc) for other release streams."); + public static LocalisableString ChangeReleaseStreamMobileWarning => new TranslatableString(getKey(@"change_release_stream_mobile_warning"), @"Check with your app store (testflight, etc) for other release streams."); /// /// "Are you sure you want to run a potentially unstable version of the game?" /// - public static LocalisableString ChangeReleaseStreamConfirmation => new TranslatableString(getKey(@"change_release stream_confirmation"), @"Are you sure you want to run a potentially unstable version of the game?"); + public static LocalisableString ChangeReleaseStreamConfirmation => new TranslatableString(getKey(@"change_release_stream_confirmation"), @"Are you sure you want to run a potentially unstable version of the game?"); /// /// "If you run into issues starting the game, you can usually run the installer from the official site to recover." /// - public static LocalisableString ChangeReleaseStreamConfirmationInfo => new TranslatableString(getKey(@"change_release stream_confirmation_info"), @"If you run into issues starting the game, you can usually run the installer from the official site to recover."); + public static LocalisableString ChangeReleaseStreamConfirmationInfo => new TranslatableString(getKey(@"change_release_stream_confirmation_info"), @"If you run into issues starting the game, you can usually run the installer from the official site to recover."); /// /// "You are running the latest release ({0})" From d888572f7f9bab6fdcfe7708b8a66ae469e59f0d Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 25 Jun 2025 08:23:47 +0300 Subject: [PATCH 277/370] Move room name line to own component Fixes test failure caused by accessing components loaded asynchronously, see https://github.com/ppy/osu/actions/runs/15848940420/job/44677458262?pr=33858. --- .../OnlinePlay/Lounge/Components/RoomPanel.cs | 94 ++++++++++++------- 1 file changed, 58 insertions(+), 36 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs index b94cfb8de7..7a4279ef98 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs @@ -17,6 +17,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Database; @@ -60,9 +61,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private RoomSpecialCategoryPill? specialCategoryPill; private PasswordProtectedIcon? passwordIcon; private EndDateInfo? endDateInfo; - private FillFlowContainer? roomNameFlow; - private SpriteText? roomName; - private ExternalLinkButton? linkButton; + private RoomNameLine? roomName; private DelayedLoadWrapper wrapper = null!; private CancellationTokenSource? beatmapLookupCancellation; @@ -208,28 +207,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Direction = FillDirection.Vertical, Children = new Drawable[] { - roomNameFlow = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Children = new Drawable[] - { - roomName = new TruncatingSpriteText - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Font = OsuFont.GetFont(size: 28), - }, - linkButton = new ExternalLinkButton(formatRoomUrl(Room.RoomID ?? 0)) - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Margin = new MarginPadding { Horizontal = 6, Bottom = 4 }, - Alpha = ShowExternalLink && Room.RoomID.HasValue ? 1 : 0, - }, - }, - }, + roomName = new RoomNameLine(getRoomUrl(), ShowExternalLink), new RoomStatusText(Room) { Beatmap = { BindTarget = currentBeatmap } @@ -309,14 +287,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components SelectedItem.BindValueChanged(onSelectedItemChanged, true); } - protected override void Update() - { - base.Update(); - - if (roomName != null) - roomName.MaxWidth = (roomNameFlow?.DrawWidth ?? 0) - (linkButton?.LayoutSize.X ?? 0); - } - private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e) { switch (e.PropertyName) @@ -413,8 +383,8 @@ 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?.CopyToClipboard(formatRoomUrl(Room.RoomID.Value))) + new OsuMenuItem("View in browser", MenuItemType.Standard, () => game?.OpenUrlExternally(getRoomUrl())), + new OsuMenuItem("Copy link", MenuItemType.Standard, () => game?.CopyToClipboard(getRoomUrl())) ]); } @@ -422,7 +392,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } - private string formatRoomUrl(long id) => $@"{api.Endpoints.WebsiteUrl}/multiplayer/rooms/{id}"; + private string? getRoomUrl() => !Room.RoomID.HasValue ? null : $@"{api.Endpoints.WebsiteUrl}/multiplayer/rooms/{Room.RoomID.Value}"; protected virtual UpdateableBeatmapBackgroundSprite CreateBackground() => new UpdateableBeatmapBackgroundSprite(); @@ -585,5 +555,57 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components }; } } + + public partial class RoomNameLine : FillFlowContainer + { + private readonly string? roomUrl; + private readonly bool showExternalLink; + + private TruncatingSpriteText spriteText = null!; + private ExternalLinkButton link = null!; + + public LocalisableString Text + { + get => spriteText.Text; + set => spriteText.Text = value; + } + + public RoomNameLine(string? roomUrl, bool showExternalLink) + { + this.roomUrl = roomUrl; + this.showExternalLink = showExternalLink; + } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Direction = FillDirection.Horizontal; + + Children = new Drawable[] + { + spriteText = new TruncatingSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Font = OsuFont.GetFont(size: 28), + }, + link = new ExternalLinkButton(roomUrl) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding { Horizontal = 6, Bottom = 4 }, + Alpha = showExternalLink ? 1 : 0, + }, + }; + } + + protected override void Update() + { + base.Update(); + spriteText.MaxWidth = DrawWidth - link.LayoutSize.X; + } + } } } From 9febf635f34d199db43bcd9f51cc1401de676ee8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 25 Jun 2025 14:29:57 +0900 Subject: [PATCH 278/370] 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 52cafa5c75..aa7b343f38 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 14863083f5..ab3fc11cca 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 8b542e5442f42ad132af0414a4f7191df9718a99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 9 May 2025 10:43:44 +0200 Subject: [PATCH 279/370] Refactor hit windows class structure to reduce rigidity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change pulls back a significant degree of overspecialisation and rigidity in the class structure of `HitWindows` to make subsequent changes to hit windows, whose purpose is to improve replay playback accuracy, possible to do cleanly. Notably: - `HitWindows` is full abstract now. In a few use cases, and as a reference for ruleset implementors, `DefaultHitWindows` is provided as a separate class instead. This fixes the weirdness wherein `HitWindows` always declared 6 fields for result types but some of them would never be set to a non-zero value or read. - `HitWindow.GetRanges()` is deleted because it is overspecialised and prevents being able to adjust hitwindows by ±0.5ms cleanly which will be required later. The fallout of this is that the assertion that used `GetRanges()` in the `HitWindows` ctor must use something else now, and the closest thing to it was `GetAllAvailableWindows()`, which didn't return the miss window - so I made it return the miss window and fixed the one consumer that didn't want it (bar hit error meter) to skip it. - Diff also contains some clean-up around `DifficultyRange` to unify handling of it. --- .../Scoring/ManiaHitWindows.cs | 59 +++++++- .../TestSceneStartTimeOrderedHitPolicy.cs | 23 +++- osu.Game.Rulesets.Osu/Objects/Spinner.cs | 4 +- osu.Game.Rulesets.Osu/OsuRuleset.cs | 5 +- .../Scoring/OsuHitWindows.cs | 44 ++++-- .../Judgements/TestSceneHitJudgements.cs | 2 +- .../Scoring/TaikoHitWindows.cs | 40 +++++- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 5 +- .../TestSceneDrainingHealthProcessor.cs | 2 +- .../NonVisual/FirstAvailableHitWindowsTest.cs | 8 +- .../TestSceneGameplaySampleTriggerSource.cs | 10 +- osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs | 22 ++- osu.Game/Rulesets/Objects/HitObject.cs | 2 +- .../Rulesets/Scoring/DefaultHitWindows.cs | 66 +++++++++ osu.Game/Rulesets/Scoring/HitWindows.cs | 126 ++---------------- .../HUD/HitErrorMeters/BarHitErrorMeter.cs | 2 +- osu.Game/Screens/Utility/CircleGameplay.cs | 2 +- osu.Game/Screens/Utility/ScrollingGameplay.cs | 2 +- 18 files changed, 254 insertions(+), 170 deletions(-) create mode 100644 osu.Game/Rulesets/Scoring/DefaultHitWindows.cs diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs index 627f48f391..c0ba03d8ed 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs @@ -1,15 +1,30 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; +using System; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Scoring { public class ManiaHitWindows : HitWindows { + private static readonly DifficultyRange perfect_window_range = new DifficultyRange(22.4D, 19.4D, 13.9D); + private static readonly DifficultyRange great_window_range = new DifficultyRange(64, 49, 34); + private static readonly DifficultyRange good_window_range = new DifficultyRange(97, 82, 67); + private static readonly DifficultyRange ok_window_range = new DifficultyRange(127, 112, 97); + private static readonly DifficultyRange meh_window_range = new DifficultyRange(151, 136, 121); + private static readonly DifficultyRange miss_window_range = new DifficultyRange(188, 173, 158); + private readonly double multiplier; + private double perfect; + private double great; + private double good; + private double ok; + private double meh; + private double miss; + public ManiaHitWindows() : this(1) { @@ -36,11 +51,41 @@ namespace osu.Game.Rulesets.Mania.Scoring return false; } - protected override DifficultyRange[] GetRanges() => base.GetRanges().Select(r => - new DifficultyRange( - r.Result, - r.Min * multiplier, - r.Average * multiplier, - r.Max * multiplier)).ToArray(); + public override void SetDifficulty(double difficulty) + { + perfect = IBeatmapDifficultyInfo.DifficultyRange(difficulty, perfect_window_range) * multiplier; + great = IBeatmapDifficultyInfo.DifficultyRange(difficulty, great_window_range) * multiplier; + good = IBeatmapDifficultyInfo.DifficultyRange(difficulty, good_window_range) * multiplier; + ok = IBeatmapDifficultyInfo.DifficultyRange(difficulty, ok_window_range) * multiplier; + meh = IBeatmapDifficultyInfo.DifficultyRange(difficulty, meh_window_range) * multiplier; + miss = IBeatmapDifficultyInfo.DifficultyRange(difficulty, miss_window_range) * multiplier; + } + + public override double WindowFor(HitResult result) + { + switch (result) + { + case HitResult.Perfect: + return perfect; + + case HitResult.Great: + return great; + + case HitResult.Good: + return good; + + case HitResult.Ok: + return ok; + + case HitResult.Meh: + return meh; + + case HitResult.Miss: + return miss; + + default: + throw new ArgumentOutOfRangeException(nameof(result), result, null); + } + } } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs index 895e9bbdee..c637ed45f7 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs @@ -476,15 +476,24 @@ namespace osu.Game.Rulesets.Osu.Tests private class TestHitWindows : HitWindows { - private static readonly DifficultyRange[] ranges = - { - new DifficultyRange(HitResult.Great, 500, 500, 500), - new DifficultyRange(HitResult.Miss, early_miss_window, early_miss_window, early_miss_window), - }; - public override bool IsHitResultAllowed(HitResult result) => result == HitResult.Great || result == HitResult.Miss; - protected override DifficultyRange[] GetRanges() => ranges; + public override void SetDifficulty(double difficulty) { } + + public override double WindowFor(HitResult result) + { + switch (result) + { + case HitResult.Great: + return 500; + + case HitResult.Miss: + return early_miss_window; + + default: + throw new ArgumentOutOfRangeException(nameof(result), result, null); + } + } } private partial class ScoreAccessibleReplayPlayer : ReplayPlayer diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index e3dfe8e69a..31d00a2610 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -21,12 +21,12 @@ namespace osu.Game.Rulesets.Osu.Objects /// /// The RPM required to clear the spinner at ODs [ 0, 5, 10 ]. /// - private static readonly (int min, int mid, int max) clear_rpm_range = (90, 150, 225); + private static readonly DifficultyRange clear_rpm_range = new DifficultyRange(90, 150, 225); /// /// The RPM required to complete the spinner and receive full score at ODs [ 0, 5, 10 ]. /// - private static readonly (int min, int mid, int max) complete_rpm_range = (250, 380, 430); + private static readonly DifficultyRange complete_rpm_range = new DifficultyRange(250, 380, 430); public double EndTime { diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 25b1dd9b12..0edb8046b9 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -373,10 +373,9 @@ namespace osu.Game.Rulesets.Osu preempt /= rate; adjustedDifficulty.ApproachRate = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN); - var greatHitWindowRange = OsuHitWindows.OSU_RANGES.Single(range => range.Result == HitResult.Great); - double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max); + double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, OsuHitWindows.GREAT_WINDOW_RANGE); greatHitWindow /= rate; - adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(greatHitWindow, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max); + adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(greatHitWindow, OsuHitWindows.GREAT_WINDOW_RANGE); return adjustedDifficulty; } diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs index fd86e0eeda..154503c20d 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs @@ -1,24 +1,26 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Scoring { public class OsuHitWindows : HitWindows { + public static readonly DifficultyRange GREAT_WINDOW_RANGE = new DifficultyRange(80, 50, 20); + public static readonly DifficultyRange OK_WINDOW_RANGE = new DifficultyRange(140, 100, 60); + public static readonly DifficultyRange MEH_WINDOW_RANGE = new DifficultyRange(200, 150, 100); + /// /// osu! ruleset has a fixed miss window regardless of difficulty settings. /// public const double MISS_WINDOW = 400; - internal static readonly DifficultyRange[] OSU_RANGES = - { - new DifficultyRange(HitResult.Great, 80, 50, 20), - new DifficultyRange(HitResult.Ok, 140, 100, 60), - new DifficultyRange(HitResult.Meh, 200, 150, 100), - new DifficultyRange(HitResult.Miss, MISS_WINDOW, MISS_WINDOW, MISS_WINDOW), - }; + private double great; + private double ok; + private double meh; public override bool IsHitResultAllowed(HitResult result) { @@ -34,6 +36,32 @@ namespace osu.Game.Rulesets.Osu.Scoring return false; } - protected override DifficultyRange[] GetRanges() => OSU_RANGES; + public override void SetDifficulty(double difficulty) + { + great = IBeatmapDifficultyInfo.DifficultyRange(difficulty, GREAT_WINDOW_RANGE); + ok = IBeatmapDifficultyInfo.DifficultyRange(difficulty, OK_WINDOW_RANGE); + meh = IBeatmapDifficultyInfo.DifficultyRange(difficulty, MEH_WINDOW_RANGE); + } + + public override double WindowFor(HitResult result) + { + switch (result) + { + case HitResult.Great: + return great; + + case HitResult.Ok: + return ok; + + case HitResult.Meh: + return meh; + + case HitResult.Miss: + return MISS_WINDOW; + + default: + throw new ArgumentOutOfRangeException(nameof(result), result, null); + } + } } } diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs index 6fe61e78b7..7008d8d37a 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs @@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 6 }); beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 10 }); - var hitWindows = new HitWindows(); + var hitWindows = new DefaultHitWindows(); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); PerformTest(new List diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs index b44ef8ee93..22d268de3b 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs @@ -1,18 +1,21 @@ // 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.Rulesets.Scoring; namespace osu.Game.Rulesets.Taiko.Scoring { public class TaikoHitWindows : HitWindows { - internal static readonly DifficultyRange[] TAIKO_RANGES = - { - new DifficultyRange(HitResult.Great, 50, 35, 20), - new DifficultyRange(HitResult.Ok, 120, 80, 50), - new DifficultyRange(HitResult.Miss, 135, 95, 70), - }; + public static readonly DifficultyRange GREAT_WINDOW_RANGE = new DifficultyRange(50, 35, 20); + public static readonly DifficultyRange OK_WINDOW_RANGE = new DifficultyRange(120, 80, 50); + public static readonly DifficultyRange MISS_WINDOW_RANGE = new DifficultyRange(135, 95, 70); + + private double great; + private double ok; + private double miss; public override bool IsHitResultAllowed(HitResult result) { @@ -27,6 +30,29 @@ namespace osu.Game.Rulesets.Taiko.Scoring return false; } - protected override DifficultyRange[] GetRanges() => TAIKO_RANGES; + public override void SetDifficulty(double difficulty) + { + great = IBeatmapDifficultyInfo.DifficultyRange(difficulty, GREAT_WINDOW_RANGE); + ok = IBeatmapDifficultyInfo.DifficultyRange(difficulty, OK_WINDOW_RANGE); + miss = IBeatmapDifficultyInfo.DifficultyRange(difficulty, MISS_WINDOW_RANGE); + } + + public override double WindowFor(HitResult result) + { + switch (result) + { + case HitResult.Great: + return great; + + case HitResult.Ok: + return ok; + + case HitResult.Miss: + return miss; + + default: + throw new ArgumentOutOfRangeException(nameof(result), result, null); + } + } } } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 8cc14ca651..1cb41e1299 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -274,10 +274,9 @@ namespace osu.Game.Rulesets.Taiko { BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty); - var greatHitWindowRange = TaikoHitWindows.TAIKO_RANGES.Single(range => range.Result == HitResult.Great); - double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max); + double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, TaikoHitWindows.GREAT_WINDOW_RANGE); greatHitWindow /= rate; - adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(greatHitWindow, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max); + adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(greatHitWindow, TaikoHitWindows.GREAT_WINDOW_RANGE); return adjustedDifficulty; } diff --git a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs index 584a9e09c0..18030d7222 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs @@ -359,7 +359,7 @@ namespace osu.Game.Tests.Gameplay } public override Judgement CreateJudgement() => new TestJudgement(maxResult); - protected override HitWindows CreateHitWindows() => new HitWindows(); + protected override HitWindows CreateHitWindows() => new DefaultHitWindows(); private class TestJudgement : Judgement { diff --git a/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs b/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs index 07d6d68e82..cfe523fdd5 100644 --- a/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs +++ b/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.NonVisual public void TestResultIfOnlyParentHitWindowIsEmpty() { var testObject = new TestHitObject(HitWindows.Empty); - HitObject nested = new TestHitObject(new HitWindows()); + HitObject nested = new TestHitObject(new DefaultHitWindows()); testObject.AddNested(nested); testDrawableRuleset.HitObjects = new List { testObject }; @@ -43,8 +43,8 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestResultIfParentHitWindowsIsNotEmpty() { - var testObject = new TestHitObject(new HitWindows()); - HitObject nested = new TestHitObject(new HitWindows()); + var testObject = new TestHitObject(new DefaultHitWindows()); + HitObject nested = new TestHitObject(new DefaultHitWindows()); testObject.AddNested(nested); testDrawableRuleset.HitObjects = new List { testObject }; @@ -58,7 +58,7 @@ namespace osu.Game.Tests.NonVisual HitObject nested = new TestHitObject(HitWindows.Empty); firstObject.AddNested(nested); - var secondObject = new TestHitObject(new HitWindows()); + var secondObject = new TestHitObject(new DefaultHitWindows()); testDrawableRuleset.HitObjects = new List { firstObject, secondObject }; Assert.AreSame(testDrawableRuleset.FirstAvailableHitWindows, secondObject.HitWindows); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs index 6981591193..894b51ddcb 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs @@ -65,30 +65,30 @@ namespace osu.Game.Tests.Visual.Gameplay { new HitCircle { - HitWindows = new HitWindows(), + HitWindows = new DefaultHitWindows(), StartTime = t += spacing, Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } }, new HitCircle { - HitWindows = new HitWindows(), + HitWindows = new DefaultHitWindows(), StartTime = t += spacing, Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE) } }, new HitCircle { - HitWindows = new HitWindows(), + HitWindows = new DefaultHitWindows(), StartTime = t += spacing, Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT) }, }, new HitCircle { - HitWindows = new HitWindows(), + HitWindows = new DefaultHitWindows(), StartTime = t += spacing, }, new Slider { - HitWindows = new HitWindows(), + HitWindows = new DefaultHitWindows(), StartTime = t += spacing, Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, Vector2.UnitY * 200 }), Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, HitSampleInfo.BANK_SOFT) }, diff --git a/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs b/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs index 48f6564084..2dd73a2541 100644 --- a/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs @@ -92,8 +92,8 @@ namespace osu.Game.Beatmaps /// /// /// Value to which the difficulty value maps in the specified range. - static double DifficultyRange(double difficulty, (double od0, double od5, double od10) range) - => DifficultyRange(difficulty, range.od0, range.od5, range.od10); + static double DifficultyRange(double difficulty, DifficultyRange range) + => DifficultyRange(difficulty, range.Min, range.Mid, range.Max); /// /// Inverse function to . @@ -110,5 +110,23 @@ namespace osu.Game.Beatmaps ? (difficultyValue - diff5) / (diff10 - diff5) * 5 + 5 : (difficultyValue - diff5) / (diff5 - diff0) * 5 + 5; } + + /// + /// Inverse function to . + /// Maps a value returned by the function above back to the difficulty that produced it. + /// + /// The difficulty-dependent value to be unmapped. + /// Minimum of the resulting range which will be achieved by a difficulty value of 0. + /// Value to which the difficulty value maps in the specified range. + static double InverseDifficultyRange(double difficultyValue, DifficultyRange range) + => InverseDifficultyRange(difficultyValue, range.Min, range.Mid, range.Max); } + + /// + /// Represents a piecewise-linear difficulty curve for a given gameplay quantity. + /// + /// Minimum of the resulting range which will be achieved by a difficulty value of 0. + /// Midpoint of the resulting range which will be achieved by a difficulty value of 5. + /// Maximum of the resulting range which will be achieved by a difficulty value of 10. + public record struct DifficultyRange(double Min, double Mid, double Max); } diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index 07e07b25d3..61c6c9f46f 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -192,7 +192,7 @@ namespace osu.Game.Rulesets.Objects /// /// [NotNull] - protected virtual HitWindows CreateHitWindows() => new HitWindows(); + protected virtual HitWindows CreateHitWindows() => new DefaultHitWindows(); /// /// The maximum offset from the end time of at which this can be judged. diff --git a/osu.Game/Rulesets/Scoring/DefaultHitWindows.cs b/osu.Game/Rulesets/Scoring/DefaultHitWindows.cs new file mode 100644 index 0000000000..3048233335 --- /dev/null +++ b/osu.Game/Rulesets/Scoring/DefaultHitWindows.cs @@ -0,0 +1,66 @@ +// 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; + +namespace osu.Game.Rulesets.Scoring +{ + /// + /// An example implementation of . + /// Not meaningfully used, provided mostly as a reference to ruleset implementors. + /// + public class DefaultHitWindows : HitWindows + { + private static readonly DifficultyRange perfect_window_range = new DifficultyRange(22.4D, 19.4D, 13.9D); + private static readonly DifficultyRange great_window_range = new DifficultyRange(64, 49, 34); + private static readonly DifficultyRange good_window_range = new DifficultyRange(97, 82, 67); + private static readonly DifficultyRange ok_window_range = new DifficultyRange(127, 112, 97); + private static readonly DifficultyRange meh_window_range = new DifficultyRange(151, 136, 121); + private static readonly DifficultyRange miss_window_range = new DifficultyRange(188, 173, 158); + + private double perfect; + private double great; + private double good; + private double ok; + private double meh; + private double miss; + + public override void SetDifficulty(double difficulty) + { + perfect = IBeatmapDifficultyInfo.DifficultyRange(difficulty, perfect_window_range); + great = IBeatmapDifficultyInfo.DifficultyRange(difficulty, great_window_range); + good = IBeatmapDifficultyInfo.DifficultyRange(difficulty, good_window_range); + ok = IBeatmapDifficultyInfo.DifficultyRange(difficulty, ok_window_range); + meh = IBeatmapDifficultyInfo.DifficultyRange(difficulty, meh_window_range); + miss = IBeatmapDifficultyInfo.DifficultyRange(difficulty, miss_window_range); + } + + public override double WindowFor(HitResult result) + { + switch (result) + { + case HitResult.Perfect: + return perfect; + + case HitResult.Great: + return great; + + case HitResult.Good: + return good; + + case HitResult.Ok: + return ok; + + case HitResult.Meh: + return meh; + + case HitResult.Miss: + return miss; + + default: + throw new ArgumentOutOfRangeException(nameof(result), result, null); + } + } + } +} diff --git a/osu.Game/Rulesets/Scoring/HitWindows.cs b/osu.Game/Rulesets/Scoring/HitWindows.cs index 94ea51c0b2..e1429f32b2 100644 --- a/osu.Game/Rulesets/Scoring/HitWindows.cs +++ b/osu.Game/Rulesets/Scoring/HitWindows.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Scoring @@ -13,35 +12,19 @@ namespace osu.Game.Rulesets.Scoring /// /// A structure containing timing data for hit window based gameplay. /// - public class HitWindows + public abstract class HitWindows { - private static readonly DifficultyRange[] base_ranges = - { - new DifficultyRange(HitResult.Perfect, 22.4D, 19.4D, 13.9D), - new DifficultyRange(HitResult.Great, 64, 49, 34), - new DifficultyRange(HitResult.Good, 97, 82, 67), - new DifficultyRange(HitResult.Ok, 127, 112, 97), - new DifficultyRange(HitResult.Meh, 151, 136, 121), - new DifficultyRange(HitResult.Miss, 188, 173, 158), - }; - - private double perfect; - private double great; - private double good; - private double ok; - private double meh; - private double miss; - /// /// An empty with only and . /// No time values are provided (meaning instantaneous hit or miss). /// public static HitWindows Empty { get; } = new EmptyHitWindows(); - public HitWindows() + protected HitWindows() { - Debug.Assert(GetRanges().Any(r => r.Result == HitResult.Miss), $"{nameof(GetRanges)} should always contain {nameof(HitResult.Miss)}"); - Debug.Assert(GetRanges().Any(r => r.Result != HitResult.Miss), $"{nameof(GetRanges)} should always contain at least one result type other than {nameof(HitResult.Miss)}."); + var availableWindows = GetAllAvailableWindows(); + Debug.Assert(availableWindows.Any(r => r.result == HitResult.Miss), $"{nameof(GetAllAvailableWindows)} should always contain {nameof(HitResult.Miss)}"); + Debug.Assert(availableWindows.Any(r => r.result != HitResult.Miss), $"{nameof(GetAllAvailableWindows)} should always contain at least one result type other than {nameof(HitResult.Miss)}."); } /// @@ -64,7 +47,7 @@ namespace osu.Game.Rulesets.Scoring /// public IEnumerable<(HitResult result, double length)> GetAllAvailableWindows() { - for (var result = HitResult.Meh; result <= HitResult.Perfect; ++result) + for (var result = HitResult.Miss; result <= HitResult.Perfect; ++result) { if (IsHitResultAllowed(result)) yield return (result, WindowFor(result)); @@ -82,40 +65,7 @@ namespace osu.Game.Rulesets.Scoring /// Sets hit windows with values that correspond to a difficulty parameter. /// /// The parameter. - public void SetDifficulty(double difficulty) - { - foreach (var range in GetRanges()) - { - double value = IBeatmapDifficultyInfo.DifficultyRange(difficulty, (range.Min, range.Average, range.Max)); - - switch (range.Result) - { - case HitResult.Miss: - miss = value; - break; - - case HitResult.Meh: - meh = value; - break; - - case HitResult.Ok: - ok = value; - break; - - case HitResult.Good: - good = value; - break; - - case HitResult.Great: - great = value; - break; - - case HitResult.Perfect: - perfect = value; - break; - } - } - } + public abstract void SetDifficulty(double difficulty); /// /// Retrieves the for a time offset. @@ -141,35 +91,7 @@ namespace osu.Game.Rulesets.Scoring /// /// The expected . /// One half of the hit window for . - public double WindowFor(HitResult result) - { - if (!IsHitResultAllowed(result)) - throw new ArgumentOutOfRangeException(nameof(result), result, $@"{result} is not an allowed result."); - - switch (result) - { - case HitResult.Perfect: - return perfect; - - case HitResult.Great: - return great; - - case HitResult.Good: - return good; - - case HitResult.Ok: - return ok; - - case HitResult.Meh: - return meh; - - case HitResult.Miss: - return miss; - - default: - throw new ArgumentOutOfRangeException(nameof(result), result, null); - } - } + public abstract double WindowFor(HitResult result); /// /// Given a time offset, whether the can ever be hit in the future with a non- result. @@ -179,41 +101,13 @@ namespace osu.Game.Rulesets.Scoring /// Whether the can be hit at any point in the future from this time offset. public bool CanBeHit(double timeOffset) => timeOffset <= WindowFor(LowestSuccessfulHitResult()); - /// - /// Retrieve a valid list of s representing hit windows. - /// Defaults are provided but can be overridden to customise for a ruleset. - /// - protected virtual DifficultyRange[] GetRanges() => base_ranges; - private class EmptyHitWindows : HitWindows { - private static readonly DifficultyRange[] ranges = - { - new DifficultyRange(HitResult.Perfect, 0, 0, 0), - new DifficultyRange(HitResult.Miss, 0, 0, 0), - }; - public override bool IsHitResultAllowed(HitResult result) => true; - protected override DifficultyRange[] GetRanges() => ranges; - } - } + public override void SetDifficulty(double difficulty) { } - public struct DifficultyRange - { - public readonly HitResult Result; - - public double Min; - public double Average; - public double Max; - - public DifficultyRange(HitResult result, double min, double average, double max) - { - Result = result; - - Min = min; - Average = average; - Max = max; + public override double WindowFor(HitResult result) => 0; } } } diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs index a71a46ec2a..e27a7544c9 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs @@ -87,7 +87,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters const int bar_width = 2; const float chevron_size = 8; - hitWindows = HitWindows.GetAllAvailableWindows().ToArray(); + hitWindows = HitWindows.GetAllAvailableWindows().Where(w => w.result.IsHit()).ToArray(); InternalChild = new Container { diff --git a/osu.Game/Screens/Utility/CircleGameplay.cs b/osu.Game/Screens/Utility/CircleGameplay.cs index 0f328d04fb..c5c4d7d5b2 100644 --- a/osu.Game/Screens/Utility/CircleGameplay.cs +++ b/osu.Game/Screens/Utility/CircleGameplay.cs @@ -226,7 +226,7 @@ namespace osu.Game.Screens.Utility HitEvent = new HitEvent(Clock.CurrentTime - HitTime, 1.0, HitResult.Good, new HitObject { - HitWindows = new HitWindows(), + HitWindows = new DefaultHitWindows(), }, null, null); Hit?.Invoke(HitEvent.Value); diff --git a/osu.Game/Screens/Utility/ScrollingGameplay.cs b/osu.Game/Screens/Utility/ScrollingGameplay.cs index c0264f5734..3e0969b625 100644 --- a/osu.Game/Screens/Utility/ScrollingGameplay.cs +++ b/osu.Game/Screens/Utility/ScrollingGameplay.cs @@ -188,7 +188,7 @@ namespace osu.Game.Screens.Utility HitEvent = new HitEvent(Clock.CurrentTime - HitTime, 1.0, HitResult.Good, new HitObject { - HitWindows = new HitWindows(), + HitWindows = new DefaultHitWindows(), }, null, null); Hit?.Invoke(HitEvent.Value); From f1e23595e7024f326b1750494d05cd58cd1d59ef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 25 Jun 2025 15:45:07 +0900 Subject: [PATCH 280/370] Fix flaky carousel test due to out of range async filter operation See https://github.com/ppy/osu/actions/runs/15868654672/job/44740248052?pr=33873#step:5:49. --- .../Visual/SongSelectV2/BeatmapCarouselTestScene.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index 69b5de09c2..bc507fbffa 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -273,9 +273,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { var groupingFilter = Carousel.Filters.OfType().Single(); - GroupDefinition g = groupingFilter.GroupItems.Keys.ElementAt(group); + GroupDefinition? groupDefinition = groupingFilter.GroupItems.Keys.ElementAtOrDefault(group); + + if (groupDefinition == null) + return false; + // offset by one because the group itself is included in the items list. - CarouselItem item = groupingFilter.GroupItems[g].ElementAt(panel + 1); + CarouselItem item = groupingFilter.GroupItems[groupDefinition].ElementAt(panel + 1); return (Carousel.CurrentSelection as BeatmapInfo)? .Equals(item.Model as BeatmapInfo) == true; From 57bfb378887f4ab588f412ea2e99c045b21298f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 25 Jun 2025 09:46:54 +0200 Subject: [PATCH 281/370] Add failing test --- .../TestSceneSongSelectNavigation.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs index 14dbd7981c..676be8fccf 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs @@ -9,6 +9,7 @@ using osu.Framework.Extensions; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Online.Leaderboards; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Mods; @@ -168,6 +169,30 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("mod configured", () => ((OsuModMagnetised)Game.SelectedMods.Value.Single()).AttractionStrength.Value, () => Is.EqualTo(1.0f)); } + [Test] + public void TestLeaderboardCorrectInPlayer() + { + IWorkingBeatmap beatmap() => Game.Beatmap.Value; + + PushAndConfirm(() => new SoloSongSelect()); + + AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game, virtualTrack: true).WaitSafely()); + + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + AddStep("switch to next difficulty and immediately press enter", () => + { + InputManager.Key(Key.Down); + Schedule(() => InputManager.Key(Key.Enter)); + }); + + AddUntilStep("wait for player", () => + { + DismissAnyNotifications(); + return Game.ScreenStack.CurrentScreen is Player; + }); + AddAssert("leaderboard matches gameplay beatmap", () => Game.ChildrenOfType().Single().CurrentCriteria?.Beatmap, () => Is.EqualTo(beatmap().BeatmapInfo)); + } + private Func playToResults() { var player = playToCompletion(); From dee4ede306118431bc4f761d53492dc4d49f1dc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 25 Jun 2025 09:50:36 +0200 Subject: [PATCH 282/370] Ensure global leaderboard state matches beatmap when loading player Closes https://github.com/ppy/osu/issues/33835. I love fixing issues multiple times. Notably the refetch is not forced so this should be a no-op if the global state is already correct. https://github.com/ppy/osu/issues/33835#issuecomment-2998897932 says > but we should also consider adding a force `RunTask` of the pending debounce before entering gameplay, probably around here: > > https://github.com/ppy/osu/blob/3192eaa2a20f20495db8431d3d6f35af7c705a94/osu.Game/Screens/SelectV2/SongSelect.cs#L420 but I am not confident in making that change as I have no idea whether it is correct or not. --- osu.Game/Screens/Play/PlayerLoader.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 27b6413d0c..f1a31b809f 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -26,12 +26,14 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Input; using osu.Game.Localisation; +using osu.Game.Online.Leaderboards; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Volume; using osu.Game.Performance; using osu.Game.Screens.Menu; using osu.Game.Screens.Play.PlayerSettings; +using osu.Game.Screens.Select.Leaderboards; using osu.Game.Skinning; using osu.Game.Users; using osu.Game.Utils; @@ -175,6 +177,9 @@ namespace osu.Game.Screens.Play [Resolved] private IHighPerformanceSessionManager? highPerformanceSessionManager { get; set; } + [Resolved] + private LeaderboardManager? leaderboardManager { get; set; } + public PlayerLoader(Func createPlayer) { this.createPlayer = createPlayer; @@ -269,6 +274,12 @@ namespace osu.Game.Screens.Play showStoryboards.BindValueChanged(val => epilepsyWarning?.FadeTo(val.NewValue ? 1 : 0, 250, Easing.OutQuint), true); epilepsyWarning?.FinishTransforms(true); + + leaderboardManager?.FetchWithCriteria(new LeaderboardCriteria( + Beatmap.Value.BeatmapInfo, + Ruleset.Value, + leaderboardManager?.CurrentCriteria?.Scope ?? BeatmapLeaderboardScope.Global, + leaderboardManager?.CurrentCriteria?.ExactMods)); } #region Screen handling From 55ff5a744cad9d7efdf40b7d0117958bc22bb8d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 25 Jun 2025 10:05:46 +0200 Subject: [PATCH 283/370] Make assertion completely dead in release mode --- osu.Game/Rulesets/Scoring/HitWindows.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HitWindows.cs b/osu.Game/Rulesets/Scoring/HitWindows.cs index e1429f32b2..f4d1fe1e14 100644 --- a/osu.Game/Rulesets/Scoring/HitWindows.cs +++ b/osu.Game/Rulesets/Scoring/HitWindows.cs @@ -22,9 +22,16 @@ namespace osu.Game.Rulesets.Scoring protected HitWindows() { - var availableWindows = GetAllAvailableWindows(); + ensureValidHitWindows(); + } + + [Conditional("DEBUG")] + private void ensureValidHitWindows() + { + var availableWindows = GetAllAvailableWindows().ToList(); Debug.Assert(availableWindows.Any(r => r.result == HitResult.Miss), $"{nameof(GetAllAvailableWindows)} should always contain {nameof(HitResult.Miss)}"); - Debug.Assert(availableWindows.Any(r => r.result != HitResult.Miss), $"{nameof(GetAllAvailableWindows)} should always contain at least one result type other than {nameof(HitResult.Miss)}."); + Debug.Assert(availableWindows.Any(r => r.result != HitResult.Miss), + $"{nameof(GetAllAvailableWindows)} should always contain at least one result type other than {nameof(HitResult.Miss)}."); } /// From 8fb8772282215a3af3a13e46c44f9ad3ab6fd328 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Jun 2025 17:13:39 +0900 Subject: [PATCH 284/370] Hide entire section on mobile instead --- osu.Game/Localisation/GeneralSettingsStrings.cs | 5 ----- .../Overlays/Settings/Sections/General/UpdateSettings.cs | 6 ++---- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/osu.Game/Localisation/GeneralSettingsStrings.cs b/osu.Game/Localisation/GeneralSettingsStrings.cs index c806c4eb0a..20db5983fd 100644 --- a/osu.Game/Localisation/GeneralSettingsStrings.cs +++ b/osu.Game/Localisation/GeneralSettingsStrings.cs @@ -84,11 +84,6 @@ namespace osu.Game.Localisation /// public static LocalisableString ChangeReleaseStreamPackageManagerWarning => new TranslatableString(getKey(@"change_release_stream_package_warning"), @"Check with your package manager / provider for other release streams."); - /// - /// "Check with your app store (testflight, etc) for other release streams." - /// - public static LocalisableString ChangeReleaseStreamMobileWarning => new TranslatableString(getKey(@"change_release_stream_mobile_warning"), @"Check with your app store (testflight, etc) for other release streams."); - /// /// "Are you sure you want to run a potentially unstable version of the game?" /// diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 63c09cde56..156a3db6eb 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -49,7 +49,7 @@ namespace osu.Game.Overlays.Settings.Sections.General { config.BindWith(OsuSetting.ReleaseStream, configReleaseStream); - if (updateManager?.CanCheckForUpdate == true) + if (updateManager?.CanCheckForUpdate == true && !RuntimeInfo.IsMobile) { Add(releaseStreamDropdown = new SettingsEnumDropdown { @@ -64,9 +64,7 @@ namespace osu.Game.Overlays.Settings.Sections.General releaseStreamDropdown.ShowsDefaultIndicator = false; releaseStreamDropdown.Items = [updateManager.FixedReleaseStream.Value]; - releaseStreamDropdown.SetNoticeText(RuntimeInfo.IsDesktop - ? GeneralSettingsStrings.ChangeReleaseStreamPackageManagerWarning - : GeneralSettingsStrings.ChangeReleaseStreamMobileWarning); + releaseStreamDropdown.SetNoticeText(GeneralSettingsStrings.ChangeReleaseStreamPackageManagerWarning); } releaseStreamDropdown.Current.BindValueChanged(stream => From e05c716d70af2cd72e8c60e7f8703bf4cd2d9a7e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 25 Jun 2025 17:35:57 +0900 Subject: [PATCH 285/370] Always show update button on mobile so section isn't empty --- .../Sections/General/UpdateSettings.cs | 76 ++++++++++--------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 156a3db6eb..ea18e5fd19 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -44,51 +44,40 @@ namespace osu.Game.Overlays.Settings.Sections.General [Resolved] private OsuGame? game { get; set; } + [Resolved] + private IDialogOverlay? dialogOverlay { get; set; } + [BackgroundDependencyLoader] - private void load(OsuConfigManager config, IDialogOverlay? dialogOverlay) + private void load(OsuConfigManager config) { config.BindWith(OsuSetting.ReleaseStream, configReleaseStream); - if (updateManager?.CanCheckForUpdate == true && !RuntimeInfo.IsMobile) + bool isDesktop = RuntimeInfo.IsDesktop; + bool canCheckUpdates = updateManager?.CanCheckForUpdate == true; + + if (canCheckUpdates) { - Add(releaseStreamDropdown = new SettingsEnumDropdown + // For simplicity, hide the concept of release streams from mobile users. + if (isDesktop) { - LabelText = GeneralSettingsStrings.ReleaseStream, - Current = { Value = configReleaseStream.Value }, - Keywords = new[] { @"version" }, - }); - - if (updateManager.FixedReleaseStream != null) - { - configReleaseStream.Value = updateManager.FixedReleaseStream.Value; - - releaseStreamDropdown.ShowsDefaultIndicator = false; - releaseStreamDropdown.Items = [updateManager.FixedReleaseStream.Value]; - releaseStreamDropdown.SetNoticeText(GeneralSettingsStrings.ChangeReleaseStreamPackageManagerWarning); - } - - releaseStreamDropdown.Current.BindValueChanged(stream => - { - if (stream.NewValue == ReleaseStream.Tachyon) + Add(releaseStreamDropdown = new SettingsEnumDropdown { - dialogOverlay?.Push(new ConfirmDialog(GeneralSettingsStrings.ChangeReleaseStreamConfirmation, - () => - { - configReleaseStream.Value = ReleaseStream.Tachyon; - }, - () => - { - releaseStreamDropdown.Current.Value = ReleaseStream.Lazer; - }) - { - BodyText = GeneralSettingsStrings.ChangeReleaseStreamConfirmationInfo - }); + LabelText = GeneralSettingsStrings.ReleaseStream, + Current = { Value = configReleaseStream.Value }, + Keywords = new[] { @"version" }, + }); - return; + if (updateManager!.FixedReleaseStream != null) + { + configReleaseStream.Value = updateManager.FixedReleaseStream.Value; + + releaseStreamDropdown.ShowsDefaultIndicator = false; + releaseStreamDropdown.Items = [updateManager.FixedReleaseStream.Value]; + releaseStreamDropdown.SetNoticeText(GeneralSettingsStrings.ChangeReleaseStreamPackageManagerWarning); } - configReleaseStream.Value = stream.NewValue; - }); + releaseStreamDropdown.Current.BindValueChanged(releaseStreamChanged); + } Add(checkForUpdatesButton = new SettingsButton { @@ -97,7 +86,8 @@ namespace osu.Game.Overlays.Settings.Sections.General }); } - if (RuntimeInfo.IsDesktop) + // Loosely update-related maintenance buttons. + if (isDesktop) { Add(new SettingsButton { @@ -121,6 +111,20 @@ namespace osu.Game.Overlays.Settings.Sections.General } } + private void releaseStreamChanged(ValueChangedEvent stream) + { + if (stream.NewValue == ReleaseStream.Tachyon) + { + dialogOverlay?.Push( + new ConfirmDialog(GeneralSettingsStrings.ChangeReleaseStreamConfirmation, () => { configReleaseStream.Value = ReleaseStream.Tachyon; }, + () => { releaseStreamDropdown.Current.Value = ReleaseStream.Lazer; }) { BodyText = GeneralSettingsStrings.ChangeReleaseStreamConfirmationInfo }); + + return; + } + + configReleaseStream.Value = stream.NewValue; + } + private async Task checkForUpdates() { if (updateManager == null || game == null) From a369061c15f45f02126e673f844e416024abcdcf Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Jun 2025 17:41:04 +0900 Subject: [PATCH 286/370] Fix code quality issue --- .../Overlays/Settings/Sections/General/UpdateSettings.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index ea18e5fd19..f0428a4c92 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -116,8 +116,12 @@ namespace osu.Game.Overlays.Settings.Sections.General if (stream.NewValue == ReleaseStream.Tachyon) { dialogOverlay?.Push( - new ConfirmDialog(GeneralSettingsStrings.ChangeReleaseStreamConfirmation, () => { configReleaseStream.Value = ReleaseStream.Tachyon; }, - () => { releaseStreamDropdown.Current.Value = ReleaseStream.Lazer; }) { BodyText = GeneralSettingsStrings.ChangeReleaseStreamConfirmationInfo }); + new ConfirmDialog(GeneralSettingsStrings.ChangeReleaseStreamConfirmation, + () => configReleaseStream.Value = ReleaseStream.Tachyon, + () => releaseStreamDropdown.Current.Value = ReleaseStream.Lazer) + { + BodyText = GeneralSettingsStrings.ChangeReleaseStreamConfirmationInfo + }); return; } From 49a9652fa5359732d95dcff8e9caf0a93222f274 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 25 Jun 2025 17:58:54 +0900 Subject: [PATCH 287/370] Rewrite and add commentary to selection debounce logic Hopefully a bit easier to maintain going forward? Not sure. --- osu.Game/Screens/SelectV2/SongSelect.cs | 43 +++++++++++++++++-------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 744c990317..4c30662bd4 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -233,8 +233,8 @@ namespace osu.Game.Screens.SelectV2 BleedBottom = ScreenFooter.HEIGHT + 5, RelativeSizeAxes = Axes.Both, RequestPresentBeatmap = b => SelectAndRun(b, OnStart), - RequestSelection = selectBeatmap, - RequestRecommendedSelection = selectRecommendedBeatmap, + RequestSelection = queueBeatmapSelection, + RequestRecommendedSelection = b => queueBeatmapSelection(difficultyRecommender?.GetRecommendedBeatmap(b) ?? b.First()), NewItemsPresented = newItemsPresented, }, noResultsPlaceholder = new NoResultsPlaceholder @@ -417,8 +417,6 @@ namespace osu.Game.Screens.SelectV2 /// The action to perform if conditions are met to be able to proceed. May not be invoked if in an invalid state. public void SelectAndRun(BeatmapInfo beatmap, Action startAction) { - selectionDebounce?.Cancel(); - if (!this.IsCurrentScreen()) return; @@ -427,6 +425,10 @@ namespace osu.Game.Screens.SelectV2 if (!checkBeatmapValidForSelection(beatmap, carousel.Criteria)) return; + // To ensure sanity, cancel any pending selection as we are about to force a selection. + // Carousel selection will update to the forced selection via a call of `ensureGlobalBeatmapValid` below, or when song select becomes current again. + selectionDebounce?.Cancel(); + // Forced refetch is important here to guarantee correct invalidation across all difficulties (editor specific). Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap, true); @@ -439,12 +441,18 @@ namespace osu.Game.Screens.SelectV2 startAction(); } - private void selectRecommendedBeatmap(IEnumerable beatmaps) - { - selectBeatmap(difficultyRecommender?.GetRecommendedBeatmap(beatmaps) ?? beatmaps.First()); - } - - private void selectBeatmap(BeatmapInfo beatmap) + /// + /// Prepares the proposed beatmap for global selection based on a carousel user-performed action. + /// + /// + /// Calling this method will: + /// - Immediately update the selection the carousel. + /// - After , update the global beatmap. This in turn causes song select visuals (title, details, leaderboard) to update. + /// This debounce is intended to avoid high overheads from churning lookups while a user is changing selection via rapid keyboard operations. + /// To complete the operation immediately, call . + /// + /// The beatmap to be selected. + private void queueBeatmapSelection(BeatmapInfo beatmap) { if (!this.IsCurrentScreen()) return; @@ -462,13 +470,21 @@ namespace osu.Game.Screens.SelectV2 }, SELECTION_DEBOUNCE); } + /// + /// If any pending selection exists from , run it immediately. + /// + private void finaliseBeatmapSelection() + { + if (selectionDebounce?.State == ScheduledDelegate.RunState.Waiting) + selectionDebounce?.RunTask(); + } + private bool ensureGlobalBeatmapValid() { if (!this.IsCurrentScreen()) return false; - if (selectionDebounce?.State == ScheduledDelegate.RunState.Waiting) - selectionDebounce?.RunTask(); + finaliseBeatmapSelection(); // While filtering, let's not ever attempt to change selection. // This will be resolved after the filter completes, see `newItemsPresented`. @@ -483,8 +499,7 @@ namespace osu.Game.Screens.SelectV2 if (Beatmap.IsDefault || !validSelection) { validSelection = carousel.NextRandom(); - if (selectionDebounce?.State == ScheduledDelegate.RunState.Waiting) - selectionDebounce?.RunTask(); + finaliseBeatmapSelection(); } if (validSelection) From ee2a247c6681e7b82a7e30b50d72ad991b27bebc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 25 Jun 2025 10:56:10 +0200 Subject: [PATCH 288/370] Add failing test case --- .../Visual/Ranking/TestSceneUserTagControl.cs | 55 ++++++++++++++++++- .../Ranking/UserTagControl_DrawableUserTag.cs | 2 +- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs b/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs index c546c9727c..b63f8ca31c 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs @@ -2,8 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Testing; using osu.Game.Beatmaps; @@ -14,10 +16,12 @@ using osu.Game.Overlays; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Taiko; using osu.Game.Screens.Ranking; +using osuTK; +using osuTK.Input; namespace osu.Game.Tests.Visual.Ranking { - public partial class TestSceneUserTagControl : OsuTestScene + public partial class TestSceneUserTagControl : OsuManualInputManagerTestScene { [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); @@ -63,6 +67,8 @@ namespace osu.Game.Tests.Visual.Ranking beatmapSet.Beatmaps.Single().TopTags = [ new APIBeatmapTag { TagId = 3, VoteCount = 9 }, + new APIBeatmapTag { TagId = 2, VoteCount = 8 }, + new APIBeatmapTag { TagId = 0, VoteCount = 7 }, ]; Scheduler.AddDelayed(() => getBeatmapSetRequest.TriggerSuccess(beatmapSet), 500); return true; @@ -79,6 +85,11 @@ namespace osu.Game.Tests.Visual.Ranking return false; }; }); + } + + [Test] + public void TestRulesetSupport() + { AddStep("show for osu! beatmap", () => { var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); @@ -86,6 +97,7 @@ namespace osu.Game.Tests.Visual.Ranking Beatmap.Value = working; recreateControl(); }); + AddStep("show for taiko beatmap", () => { var working = CreateWorkingBeatmap(new TaikoRuleset().RulesetInfo); @@ -95,6 +107,47 @@ namespace osu.Game.Tests.Visual.Ranking }); } + [Test] + public void TestTagsDoNotMoveUntilMouseMovesAway() + { + AddStep("show", () => + { + var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); + working.BeatmapInfo.OnlineID = 42; + Beatmap.Value = working; + recreateControl(); + }); + AddUntilStep("wait for ready", () => getTagFlow().Count, () => Is.EqualTo(4)); + AddAssert("tag 2 is second", () => getTagFlow().GetLayoutPosition(getDrawableTagById(2)), () => Is.EqualTo(1)); + AddStep("vote for tag 2", () => + { + InputManager.MoveMouseTo(getDrawableTagById(2)); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("tag 2 voted for", () => getDrawableTagById(2).UserTag.VoteCount.Value, () => Is.EqualTo(9)); + + AddStep("remove vote for tag 2", () => + { + InputManager.MoveMouseTo(getDrawableTagById(2)); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("tag 2 not voted for", () => getDrawableTagById(2).UserTag.VoteCount.Value, () => Is.EqualTo(8)); + AddAssert("tag 2 is still second", () => getTagFlow().GetLayoutPosition(getDrawableTagById(2)), () => Is.EqualTo(1)); + + AddStep("vote for tag 2", () => + { + InputManager.MoveMouseTo(getDrawableTagById(2)); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("tag 2 voted for", () => getDrawableTagById(2).UserTag.VoteCount.Value, () => Is.EqualTo(9)); + AddStep("move mouse away", () => InputManager.MoveMouseTo(Vector2.Zero)); + AddAssert("tag 2 reordered to first", () => getTagFlow().GetLayoutPosition(getDrawableTagById(2)), () => Is.EqualTo(0)); + + FillFlowContainer getTagFlow() => this.ChildrenOfType>().Single(); + + UserTagControl.DrawableUserTag getDrawableTagById(long id) => getTagFlow().Single(t => t.UserTag.Id == id); + } + private void recreateControl() { Child = new PopoverContainer diff --git a/osu.Game/Screens/Ranking/UserTagControl_DrawableUserTag.cs b/osu.Game/Screens/Ranking/UserTagControl_DrawableUserTag.cs index e54d88bca2..ff3c0711c0 100644 --- a/osu.Game/Screens/Ranking/UserTagControl_DrawableUserTag.cs +++ b/osu.Game/Screens/Ranking/UserTagControl_DrawableUserTag.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.Ranking { public partial class UserTagControl { - private partial class DrawableUserTag : OsuAnimatedButton + public partial class DrawableUserTag : OsuAnimatedButton { public readonly UserTag UserTag; From 7975120d5d67c488dc4a75966098616abe20405e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 25 Jun 2025 11:02:16 +0200 Subject: [PATCH 289/370] Fix user tags moving in the control after voting Closes https://github.com/ppy/osu/issues/33877. Most likely regressed when the user tags were changed such that the loading spinner that shows on adding/removing a vote was introdiced to every individual tag separately. This in turn means that the `LoadingLayer` responsible for showing the spinner also briefly consumes all input when visible, which also means that the control briefly becomes unhovered, breaking the logic. This probably doesn't work on mobile because mobile input sucks. On iOS simulator it looks somewhat fine in that the tags don't move until you touch the screen anywhere else which seems okay if that's what actually what happens on device as well. And if it isn't I'm not sure I can do anything sane about it anyway. --- osu.Game/Screens/Ranking/UserTagControl.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index e323107783..5618dd2490 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -44,6 +45,8 @@ namespace osu.Game.Screens.Ranking private AddNewTagUserTag addNewTagUserTag = null!; + private InputManager inputManager = null!; + [Resolved] private IAPIProvider api { get; set; } = null!; @@ -124,6 +127,8 @@ namespace osu.Game.Screens.Ranking updateTags(); displayedTags.BindCollectionChanged(displayTags, true); + + inputManager = GetContainingInputManager()!; } private void updateTags() @@ -251,7 +256,7 @@ namespace osu.Game.Screens.Ranking { base.Update(); - if (!layout.IsValid && !IsHovered) + if (!layout.IsValid && !Contains(inputManager.CurrentState.Mouse.Position)) { var sortedTags = new Dictionary( displayedTags.OrderByDescending(t => t.VoteCount.Value) From d42fcabcf41b06e7aa2287c6ec0e51dc39172375 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Jun 2025 18:27:49 +0900 Subject: [PATCH 290/370] Remove no longer used method --- osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs | 4 ---- osu.Game.Tests/Mods/ModDifficultyAdjustTest.cs | 4 ---- osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs | 2 -- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 4 ---- osu.Game/Rulesets/Mods/ModEasy.cs | 4 ---- osu.Game/Rulesets/Mods/ModHardRock.cs | 4 ---- 6 files changed, 22 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs index 72422a0ae8..71080e3d8e 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs @@ -115,10 +115,6 @@ namespace osu.Game.Rulesets.Osu.Mods #region Reduce AR (IApplicableToDifficulty) - public void ReadFromDifficulty(IBeatmapDifficultyInfo difficulty) - { - } - public void ApplyToDifficulty(BeatmapDifficulty difficulty) { // Decrease AR to increase preempt time diff --git a/osu.Game.Tests/Mods/ModDifficultyAdjustTest.cs b/osu.Game.Tests/Mods/ModDifficultyAdjustTest.cs index e31a3dbdf0..2230763984 100644 --- a/osu.Game.Tests/Mods/ModDifficultyAdjustTest.cs +++ b/osu.Game.Tests/Mods/ModDifficultyAdjustTest.cs @@ -166,11 +166,7 @@ namespace osu.Game.Tests.Mods /// private BeatmapDifficulty applyDifficulty(BeatmapDifficulty difficulty) { - // ensure that ReadFromDifficulty doesn't pollute the values. var newDifficulty = difficulty.Clone(); - - testMod.ReadFromDifficulty(difficulty); - testMod.ApplyToDifficulty(newDifficulty); return newDifficulty; } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs index ca6c4998d1..5cf503f21e 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs @@ -119,7 +119,6 @@ namespace osu.Game.Tests.Visual.SongSelect { var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance().AsNonNull(); var difficultyAdjustMod = ruleset.CreateMod().AsNonNull(); - difficultyAdjustMod.ReadFromDifficulty(advancedStats.BeatmapInfo.Difficulty); advancedStats.Mods.Value = new[] { difficultyAdjustMod }; }); @@ -140,7 +139,6 @@ namespace osu.Game.Tests.Visual.SongSelect var difficultyAdjustMod = ruleset.CreateMod().AsNonNull(); var originalDifficulty = advancedStats.BeatmapInfo.Difficulty; - difficultyAdjustMod.ReadFromDifficulty(originalDifficulty); difficultyAdjustMod.DrainRate.Value = originalDifficulty.DrainRate - 0.5f; difficultyAdjustMod.ApproachRate.Value = originalDifficulty.ApproachRate + 2.2f; advancedStats.Mods.Value = new[] { difficultyAdjustMod }; diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index 15ce583413..4fd9916b89 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -96,10 +96,6 @@ namespace osu.Game.Rulesets.Mods } } - public void ReadFromDifficulty(IBeatmapDifficultyInfo difficulty) - { - } - public void ApplyToDifficulty(BeatmapDifficulty difficulty) => ApplySettings(difficulty); /// diff --git a/osu.Game/Rulesets/Mods/ModEasy.cs b/osu.Game/Rulesets/Mods/ModEasy.cs index b0ac0d5cce..3ee4d7846e 100644 --- a/osu.Game/Rulesets/Mods/ModEasy.cs +++ b/osu.Game/Rulesets/Mods/ModEasy.cs @@ -19,10 +19,6 @@ namespace osu.Game.Rulesets.Mods public override bool Ranked => UsesDefaultConfiguration; public override bool ValidForFreestyleAsRequiredMod => true; - public virtual void ReadFromDifficulty(BeatmapDifficulty difficulty) - { - } - public virtual void ApplyToDifficulty(BeatmapDifficulty difficulty) { const float ratio = 0.5f; diff --git a/osu.Game/Rulesets/Mods/ModHardRock.cs b/osu.Game/Rulesets/Mods/ModHardRock.cs index ce40e6e075..6149a9c712 100644 --- a/osu.Game/Rulesets/Mods/ModHardRock.cs +++ b/osu.Game/Rulesets/Mods/ModHardRock.cs @@ -22,10 +22,6 @@ namespace osu.Game.Rulesets.Mods protected const float ADJUST_RATIO = 1.4f; - public void ReadFromDifficulty(IBeatmapDifficultyInfo difficulty) - { - } - public virtual void ApplyToDifficulty(BeatmapDifficulty difficulty) { difficulty.DrainRate = Math.Min(difficulty.DrainRate * ADJUST_RATIO, 10.0f); From 0f078ee55040c3a55ed3dbbac61f4c6e0ddc0129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 09:56:53 +0200 Subject: [PATCH 291/370] Apply flooring and half-millisecond-adjustments to hit windows This is a "two-birds-with-one-stone" change, which addresses both https://github.com/ppy/osu/issues/28744 and https://github.com/ppy/osu/issues/11311 simultaneously. - The replay stability issue caused by time instants being rounded to nearest integer is fixed by this, because flooring and subtracting/adding 0.5 from the hit window threshold makes it impossible for a judgement to switch to anything else after replay rounding is applied - all hit windows are always a full integer plus 0.5 milliseconds, which immunizes them to rounding-to-full-ms issues. - The direction of applying the 0.5 adjustment additionally fixes the disparity with stable - in osu! and taiko 0.5 is subtracted as hit window ranges in those rulesets are exclusive on stable, while in mania 0.5 is added, as the hit window ranges there are *inclusive* on stable. As should be obvious, this materially changes hit windows. To what degree this is a *significant* change is up for discussion; I would say "no" since hitting half a millisecond changes would require 2000fps input recording, and we're still timestamping inputs using the update thread's clock, that gives a 1ms resolution at best. In the worst case, in osu! and taiko, this can change a hit window range by 1.5ms (e.g. 300.9ms -> floored to 300ms -> 299.5ms after subtraction of the half). It's more than the best-case resolution of input timestamps, but not by much. Considering how cleanly this resolves the issues in question, I see it as an acceptable tradeoff. --- .../Scoring/ManiaHitWindows.cs | 12 +++--- .../TestSceneSliderLateHitJudgement.cs | 40 +++++++++---------- .../Scoring/OsuHitWindows.cs | 6 +-- .../Judgements/TestSceneHitJudgements.cs | 2 +- .../Scoring/TaikoHitWindows.cs | 6 +-- 5 files changed, 33 insertions(+), 33 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs index c0ba03d8ed..96dbd957ae 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs @@ -53,12 +53,12 @@ namespace osu.Game.Rulesets.Mania.Scoring public override void SetDifficulty(double difficulty) { - perfect = IBeatmapDifficultyInfo.DifficultyRange(difficulty, perfect_window_range) * multiplier; - great = IBeatmapDifficultyInfo.DifficultyRange(difficulty, great_window_range) * multiplier; - good = IBeatmapDifficultyInfo.DifficultyRange(difficulty, good_window_range) * multiplier; - ok = IBeatmapDifficultyInfo.DifficultyRange(difficulty, ok_window_range) * multiplier; - meh = IBeatmapDifficultyInfo.DifficultyRange(difficulty, meh_window_range) * multiplier; - miss = IBeatmapDifficultyInfo.DifficultyRange(difficulty, miss_window_range) * multiplier; + perfect = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, perfect_window_range) * multiplier) + 0.5; + great = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, great_window_range) * multiplier) + 0.5; + good = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, good_window_range) * multiplier) + 0.5; + ok = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, ok_window_range) * multiplier) + 0.5; + meh = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, meh_window_range) * multiplier) + 0.5; + miss = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, miss_window_range) * multiplier) + 0.5; } public override double WindowFor(HitResult result) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderLateHitJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderLateHitJudgement.cs index d089e924ca..3276516d0a 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderLateHitJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderLateHitJudgement.cs @@ -52,8 +52,8 @@ namespace osu.Game.Rulesets.Osu.Tests { performTest(new List { - new OsuReplayFrame(time_slider_start + 100, slider_start_position, OsuAction.LeftButton), - new OsuReplayFrame(time_slider_end + 100, slider_end_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_start + 99, slider_start_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_end + 99, slider_end_position, OsuAction.LeftButton), }); assertHeadJudgement(HitResult.Ok); @@ -70,8 +70,8 @@ namespace osu.Game.Rulesets.Osu.Tests { performTest(new List { - new OsuReplayFrame(time_slider_start + 100, slider_start_position, OsuAction.LeftButton), - new OsuReplayFrame(time_slider_end + 100, slider_end_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_start + 99, slider_start_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_end + 99, slider_end_position, OsuAction.LeftButton), }, s => { s.SliderVelocityMultiplier = 2; @@ -91,8 +91,8 @@ namespace osu.Game.Rulesets.Osu.Tests { performTest(new List { - new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton), - new OsuReplayFrame(time_slider_end + 150, slider_end_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_start + 149, slider_start_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_end + 149, slider_end_position, OsuAction.LeftButton), }, s => { s.TickDistanceMultiplier = 0.2f; @@ -116,8 +116,8 @@ namespace osu.Game.Rulesets.Osu.Tests { performTest(new List { - new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton), - new OsuReplayFrame(time_slider_end + 150, slider_end_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_start + 149, slider_start_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_end + 149, slider_end_position, OsuAction.LeftButton), }, s => { s.SliderVelocityMultiplier = 2; @@ -165,8 +165,8 @@ namespace osu.Game.Rulesets.Osu.Tests { performTest(new List { - new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton), - new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_start + 149, slider_start_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_end + 149, slider_start_position, OsuAction.LeftButton), }, s => { s.Path = new SliderPath(PathType.LINEAR, new[] @@ -195,8 +195,8 @@ namespace osu.Game.Rulesets.Osu.Tests { performTest(new List { - new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton), - new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_start + 149, slider_start_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_end + 149, slider_start_position, OsuAction.LeftButton), }, s => { s.Path = new SliderPath(PathType.LINEAR, new[] @@ -224,8 +224,8 @@ namespace osu.Game.Rulesets.Osu.Tests { performTest(new List { - new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton), - new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_start + 149, slider_start_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_end + 149, slider_start_position, OsuAction.LeftButton), }, s => { s.Path = new SliderPath(PathType.PERFECT_CURVE, new[] @@ -259,8 +259,8 @@ namespace osu.Game.Rulesets.Osu.Tests { performTest(new List { - new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton), - new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_start + 149, slider_start_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_end + 149, slider_start_position, OsuAction.LeftButton), }, s => { s.Path = new SliderPath(PathType.PERFECT_CURVE, new[] @@ -289,8 +289,8 @@ namespace osu.Game.Rulesets.Osu.Tests { performTest(new List { - new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton), - new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_start + 149, slider_start_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_end + 149, slider_start_position, OsuAction.LeftButton), }, s => { s.Path = new SliderPath(PathType.PERFECT_CURVE, new[] @@ -320,8 +320,8 @@ namespace osu.Game.Rulesets.Osu.Tests { performTest(new List { - new OsuReplayFrame(time_slider_start + 150, slider_start_position - new Vector2(20), OsuAction.LeftButton), - new OsuReplayFrame(time_slider_end + 150, slider_start_position - new Vector2(20), OsuAction.LeftButton), + new OsuReplayFrame(time_slider_start + 149, slider_start_position - new Vector2(20), OsuAction.LeftButton), + new OsuReplayFrame(time_slider_end + 149, slider_start_position - new Vector2(20), OsuAction.LeftButton), }, s => { s.Path = new SliderPath(PathType.PERFECT_CURVE, new[] diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs index 154503c20d..a0f235c8c7 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs @@ -38,9 +38,9 @@ namespace osu.Game.Rulesets.Osu.Scoring public override void SetDifficulty(double difficulty) { - great = IBeatmapDifficultyInfo.DifficultyRange(difficulty, GREAT_WINDOW_RANGE); - ok = IBeatmapDifficultyInfo.DifficultyRange(difficulty, OK_WINDOW_RANGE); - meh = IBeatmapDifficultyInfo.DifficultyRange(difficulty, MEH_WINDOW_RANGE); + great = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, GREAT_WINDOW_RANGE)) - 0.5; + ok = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, OK_WINDOW_RANGE)) - 0.5; + meh = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, MEH_WINDOW_RANGE)) - 0.5; } public override double WindowFor(HitResult result) diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs index 7008d8d37a..c175e3342b 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs @@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements PerformTest(new List { new TaikoReplayFrame(0), - new TaikoReplayFrame(hit_time - hitWindows.WindowFor(HitResult.Great), TaikoAction.LeftCentre), + new TaikoReplayFrame(hit_time - (hitWindows.WindowFor(HitResult.Great) + 0.1), TaikoAction.LeftCentre), }, beatmap); AssertJudgementCount(1); diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs index 22d268de3b..f3a478f592 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs @@ -32,9 +32,9 @@ namespace osu.Game.Rulesets.Taiko.Scoring public override void SetDifficulty(double difficulty) { - great = IBeatmapDifficultyInfo.DifficultyRange(difficulty, GREAT_WINDOW_RANGE); - ok = IBeatmapDifficultyInfo.DifficultyRange(difficulty, OK_WINDOW_RANGE); - miss = IBeatmapDifficultyInfo.DifficultyRange(difficulty, MISS_WINDOW_RANGE); + great = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, GREAT_WINDOW_RANGE)) - 0.5; + ok = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, OK_WINDOW_RANGE)) - 0.5; + miss = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, MISS_WINDOW_RANGE)) - 0.5; } public override double WindowFor(HitResult result) From 89c48451cd969054af60cf588a20f36145309ee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 25 Jun 2025 12:11:53 +0200 Subject: [PATCH 292/370] Uncomment & adjust relevant replay test cases The replay stability tests needed adjustments because hit windows have been materially changed with the previous commit. What matters in the replay stability tests is covering the time instants near the hit window edges and ensuring that re-encode doesn't mutate the resulting judgements, not what the particular numbers used are. --- .../TestSceneLegacyReplayPlayback.cs | 187 ++++++++++-------- .../TestSceneReplayStability.cs | 51 ++--- .../TestSceneLegacyReplayPlayback.cs | 1 - .../TestSceneReplayStability.cs | 55 +++--- .../TestSceneLegacyReplayPlayback.cs | 3 +- .../TestSceneReplayStability.cs | 43 ++-- 6 files changed, 169 insertions(+), 171 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneLegacyReplayPlayback.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneLegacyReplayPlayback.cs index 2a7f2dc7ea..2c17cd8015 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneLegacyReplayPlayback.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneLegacyReplayPlayback.cs @@ -19,7 +19,6 @@ using osuTK; namespace osu.Game.Rulesets.Mania.Tests { - [Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")] public partial class TestSceneLegacyReplayPlayback : LegacyReplayPlaybackTestScene { protected override Ruleset CreateRuleset() => new ManiaRuleset(); @@ -72,13 +71,14 @@ namespace osu.Game.Rulesets.Mania.Tests new object[] { 5f, -137d, HitResult.Miss }, new object[] { 5f, -138d, HitResult.Miss }, new object[] { 5f, 111d, HitResult.Ok }, - new object[] { 5f, 112d, HitResult.Miss }, - new object[] { 5f, 113d, HitResult.Miss }, - new object[] { 5f, 114d, HitResult.Miss }, - new object[] { 5f, 135d, HitResult.Miss }, - new object[] { 5f, 136d, HitResult.Miss }, - new object[] { 5f, 137d, HitResult.Miss }, - new object[] { 5f, 138d, HitResult.Miss }, + // coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced + // new object[] { 5f, 112d, HitResult.Miss }, + // new object[] { 5f, 113d, HitResult.Miss }, + // new object[] { 5f, 114d, HitResult.Miss }, + // new object[] { 5f, 135d, HitResult.Miss }, + // new object[] { 5f, 136d, HitResult.Miss }, + // new object[] { 5f, 137d, HitResult.Miss }, + // new object[] { 5f, 138d, HitResult.Miss }, // OD = 9.3 test cases. // PERFECT hit window is [ -14ms, 14ms] @@ -99,13 +99,14 @@ namespace osu.Game.Rulesets.Mania.Tests new object[] { 9.3f, 70d, HitResult.Ok }, new object[] { 9.3f, 71d, HitResult.Ok }, new object[] { 9.3f, 98d, HitResult.Ok }, - new object[] { 9.3f, 99d, HitResult.Miss }, - new object[] { 9.3f, 100d, HitResult.Miss }, - new object[] { 9.3f, 101d, HitResult.Miss }, - new object[] { 9.3f, 122d, HitResult.Miss }, - new object[] { 9.3f, 123d, HitResult.Miss }, - new object[] { 9.3f, 124d, HitResult.Miss }, - new object[] { 9.3f, 125d, HitResult.Miss }, + // coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced + // new object[] { 9.3f, 99d, HitResult.Miss }, + // new object[] { 9.3f, 100d, HitResult.Miss }, + // new object[] { 9.3f, 101d, HitResult.Miss }, + // new object[] { 9.3f, 122d, HitResult.Miss }, + // new object[] { 9.3f, 123d, HitResult.Miss }, + // new object[] { 9.3f, 124d, HitResult.Miss }, + // new object[] { 9.3f, 125d, HitResult.Miss }, new object[] { 9.3f, -98d, HitResult.Ok }, new object[] { 9.3f, -99d, HitResult.Ok }, new object[] { 9.3f, -100d, HitResult.Meh }, @@ -145,13 +146,14 @@ namespace osu.Game.Rulesets.Mania.Tests new object[] { 5f, -137d, HitResult.Miss }, new object[] { 5f, -138d, HitResult.Miss }, new object[] { 5f, 111d, HitResult.Ok }, - new object[] { 5f, 112d, HitResult.Miss }, - new object[] { 5f, 113d, HitResult.Miss }, - new object[] { 5f, 114d, HitResult.Miss }, - new object[] { 5f, 135d, HitResult.Miss }, - new object[] { 5f, 136d, HitResult.Miss }, - new object[] { 5f, 137d, HitResult.Miss }, - new object[] { 5f, 138d, HitResult.Miss }, + // coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced + // new object[] { 5f, 112d, HitResult.Miss }, + // new object[] { 5f, 113d, HitResult.Miss }, + // new object[] { 5f, 114d, HitResult.Miss }, + // new object[] { 5f, 135d, HitResult.Miss }, + // new object[] { 5f, 136d, HitResult.Miss }, + // new object[] { 5f, 137d, HitResult.Miss }, + // new object[] { 5f, 138d, HitResult.Miss }, // OD = 9.3 test cases. // PERFECT hit window is [ -16ms, 16ms] @@ -172,13 +174,14 @@ namespace osu.Game.Rulesets.Mania.Tests new object[] { 9.3f, 70d, HitResult.Ok }, new object[] { 9.3f, 71d, HitResult.Ok }, new object[] { 9.3f, 98d, HitResult.Ok }, - new object[] { 9.3f, 99d, HitResult.Miss }, - new object[] { 9.3f, 100d, HitResult.Miss }, - new object[] { 9.3f, 101d, HitResult.Miss }, - new object[] { 9.3f, 122d, HitResult.Miss }, - new object[] { 9.3f, 123d, HitResult.Miss }, - new object[] { 9.3f, 124d, HitResult.Miss }, - new object[] { 9.3f, 125d, HitResult.Miss }, + // coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced + // new object[] { 9.3f, 99d, HitResult.Miss }, + // new object[] { 9.3f, 100d, HitResult.Miss }, + // new object[] { 9.3f, 101d, HitResult.Miss }, + // new object[] { 9.3f, 122d, HitResult.Miss }, + // new object[] { 9.3f, 123d, HitResult.Miss }, + // new object[] { 9.3f, 124d, HitResult.Miss }, + // new object[] { 9.3f, 125d, HitResult.Miss }, new object[] { 9.3f, -98d, HitResult.Ok }, new object[] { 9.3f, -99d, HitResult.Ok }, new object[] { 9.3f, -100d, HitResult.Meh }, @@ -207,13 +210,14 @@ namespace osu.Game.Rulesets.Mania.Tests new object[] { 3.1f, 88d, HitResult.Ok }, new object[] { 3.1f, 89d, HitResult.Ok }, new object[] { 3.1f, 116d, HitResult.Ok }, - new object[] { 3.1f, 117d, HitResult.Miss }, - new object[] { 3.1f, 118d, HitResult.Miss }, - new object[] { 3.1f, 119d, HitResult.Miss }, - new object[] { 3.1f, 140d, HitResult.Miss }, - new object[] { 3.1f, 141d, HitResult.Miss }, - new object[] { 3.1f, 142d, HitResult.Miss }, - new object[] { 3.1f, 143d, HitResult.Miss }, + // coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced + // new object[] { 3.1f, 117d, HitResult.Miss }, + // new object[] { 3.1f, 118d, HitResult.Miss }, + // new object[] { 3.1f, 119d, HitResult.Miss }, + // new object[] { 3.1f, 140d, HitResult.Miss }, + // new object[] { 3.1f, 141d, HitResult.Miss }, + // new object[] { 3.1f, 142d, HitResult.Miss }, + // new object[] { 3.1f, 143d, HitResult.Miss }, new object[] { 3.1f, -116d, HitResult.Ok }, new object[] { 3.1f, -117d, HitResult.Ok }, new object[] { 3.1f, -118d, HitResult.Meh }, @@ -253,13 +257,14 @@ namespace osu.Game.Rulesets.Mania.Tests new object[] { 5f, -122d, HitResult.Miss }, new object[] { 5f, -123d, HitResult.Miss }, new object[] { 5f, 96d, HitResult.Ok }, - new object[] { 5f, 97d, HitResult.Miss }, - new object[] { 5f, 98d, HitResult.Miss }, - new object[] { 5f, 99d, HitResult.Miss }, - new object[] { 5f, 120d, HitResult.Miss }, - new object[] { 5f, 121d, HitResult.Miss }, - new object[] { 5f, 122d, HitResult.Miss }, - new object[] { 5f, 123d, HitResult.Miss }, + // coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced + // new object[] { 5f, 97d, HitResult.Miss }, + // new object[] { 5f, 98d, HitResult.Miss }, + // new object[] { 5f, 99d, HitResult.Miss }, + // new object[] { 5f, 120d, HitResult.Miss }, + // new object[] { 5f, 121d, HitResult.Miss }, + // new object[] { 5f, 122d, HitResult.Miss }, + // new object[] { 5f, 123d, HitResult.Miss }, // OD = 3.1 test cases. // PERFECT hit window is [ -16ms, 16ms] @@ -280,13 +285,14 @@ namespace osu.Game.Rulesets.Mania.Tests new object[] { 3.1f, 78d, HitResult.Ok }, new object[] { 3.1f, 79d, HitResult.Ok }, new object[] { 3.1f, 96d, HitResult.Ok }, - new object[] { 3.1f, 97d, HitResult.Miss }, - new object[] { 3.1f, 98d, HitResult.Miss }, - new object[] { 3.1f, 99d, HitResult.Miss }, - new object[] { 3.1f, 120d, HitResult.Miss }, - new object[] { 3.1f, 121d, HitResult.Miss }, - new object[] { 3.1f, 122d, HitResult.Miss }, - new object[] { 3.1f, 123d, HitResult.Miss }, + // coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced + // new object[] { 3.1f, 97d, HitResult.Miss }, + // new object[] { 3.1f, 98d, HitResult.Miss }, + // new object[] { 3.1f, 99d, HitResult.Miss }, + // new object[] { 3.1f, 120d, HitResult.Miss }, + // new object[] { 3.1f, 121d, HitResult.Miss }, + // new object[] { 3.1f, 122d, HitResult.Miss }, + // new object[] { 3.1f, 123d, HitResult.Miss }, new object[] { 3.1f, -96d, HitResult.Ok }, new object[] { 3.1f, -97d, HitResult.Ok }, new object[] { 3.1f, -98d, HitResult.Meh }, @@ -327,13 +333,14 @@ namespace osu.Game.Rulesets.Mania.Tests new object[] { 5f, -98d, HitResult.Miss }, new object[] { 5f, -99d, HitResult.Miss }, new object[] { 5f, 79d, HitResult.Ok }, - new object[] { 5f, 80d, HitResult.Miss }, - new object[] { 5f, 81d, HitResult.Miss }, - new object[] { 5f, 82d, HitResult.Miss }, - new object[] { 5f, 96d, HitResult.Miss }, - new object[] { 5f, 97d, HitResult.Miss }, - new object[] { 5f, 98d, HitResult.Miss }, - new object[] { 5f, 99d, HitResult.Miss }, + // coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced + // new object[] { 5f, 80d, HitResult.Miss }, + // new object[] { 5f, 81d, HitResult.Miss }, + // new object[] { 5f, 82d, HitResult.Miss }, + // new object[] { 5f, 96d, HitResult.Miss }, + // new object[] { 5f, 97d, HitResult.Miss }, + // new object[] { 5f, 98d, HitResult.Miss }, + // new object[] { 5f, 99d, HitResult.Miss }, // OD = 9.3 test cases. // This leads to "effective" OD of 13.02. @@ -356,13 +363,14 @@ namespace osu.Game.Rulesets.Mania.Tests new object[] { 9.3f, 50d, HitResult.Ok }, new object[] { 9.3f, 51d, HitResult.Ok }, new object[] { 9.3f, 69d, HitResult.Ok }, - new object[] { 9.3f, 70d, HitResult.Miss }, - new object[] { 9.3f, 71d, HitResult.Miss }, - new object[] { 9.3f, 72d, HitResult.Miss }, - new object[] { 9.3f, 86d, HitResult.Miss }, - new object[] { 9.3f, 87d, HitResult.Miss }, - new object[] { 9.3f, 88d, HitResult.Miss }, - new object[] { 9.3f, 89d, HitResult.Miss }, + // coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced + // new object[] { 9.3f, 70d, HitResult.Miss }, + // new object[] { 9.3f, 71d, HitResult.Miss }, + // new object[] { 9.3f, 72d, HitResult.Miss }, + // new object[] { 9.3f, 86d, HitResult.Miss }, + // new object[] { 9.3f, 87d, HitResult.Miss }, + // new object[] { 9.3f, 88d, HitResult.Miss }, + // new object[] { 9.3f, 89d, HitResult.Miss }, new object[] { 9.3f, -69d, HitResult.Ok }, new object[] { 9.3f, -70d, HitResult.Ok }, new object[] { 9.3f, -71d, HitResult.Meh }, @@ -402,13 +410,14 @@ namespace osu.Game.Rulesets.Mania.Tests new object[] { 5f, -191d, HitResult.Miss }, new object[] { 5f, -192d, HitResult.Miss }, new object[] { 5f, 155d, HitResult.Ok }, - new object[] { 5f, 156d, HitResult.Miss }, - new object[] { 5f, 157d, HitResult.Miss }, - new object[] { 5f, 158d, HitResult.Miss }, - new object[] { 5f, 189d, HitResult.Miss }, - new object[] { 5f, 190d, HitResult.Miss }, - new object[] { 5f, 191d, HitResult.Miss }, - new object[] { 5f, 192d, HitResult.Miss }, + // coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced + // new object[] { 5f, 156d, HitResult.Miss }, + // new object[] { 5f, 157d, HitResult.Miss }, + // new object[] { 5f, 158d, HitResult.Miss }, + // new object[] { 5f, 189d, HitResult.Miss }, + // new object[] { 5f, 190d, HitResult.Miss }, + // new object[] { 5f, 191d, HitResult.Miss }, + // new object[] { 5f, 192d, HitResult.Miss }, }; private static readonly object[][] score_v1_non_convert_double_time_test_cases = @@ -440,13 +449,14 @@ namespace osu.Game.Rulesets.Mania.Tests new object[] { 5f, -205d, HitResult.Miss }, new object[] { 5f, -206d, HitResult.Miss }, new object[] { 5f, 167d, HitResult.Ok }, - new object[] { 5f, 168d, HitResult.Miss }, - new object[] { 5f, 169d, HitResult.Miss }, - new object[] { 5f, 170d, HitResult.Miss }, - new object[] { 5f, 203d, HitResult.Miss }, - new object[] { 5f, 204d, HitResult.Miss }, - new object[] { 5f, 205d, HitResult.Miss }, - new object[] { 5f, 206d, HitResult.Miss }, + // coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced + // new object[] { 5f, 168d, HitResult.Miss }, + // new object[] { 5f, 169d, HitResult.Miss }, + // new object[] { 5f, 170d, HitResult.Miss }, + // new object[] { 5f, 203d, HitResult.Miss }, + // new object[] { 5f, 204d, HitResult.Miss }, + // new object[] { 5f, 205d, HitResult.Miss }, + // new object[] { 5f, 206d, HitResult.Miss }, }; private static readonly object[][] score_v1_non_convert_half_time_test_cases = @@ -478,13 +488,14 @@ namespace osu.Game.Rulesets.Mania.Tests new object[] { 5f, -103d, HitResult.Miss }, new object[] { 5f, -104d, HitResult.Miss }, new object[] { 5f, 83d, HitResult.Ok }, - new object[] { 5f, 84d, HitResult.Miss }, - new object[] { 5f, 85d, HitResult.Miss }, - new object[] { 5f, 86d, HitResult.Miss }, - new object[] { 5f, 101d, HitResult.Miss }, - new object[] { 5f, 102d, HitResult.Miss }, - new object[] { 5f, 103d, HitResult.Miss }, - new object[] { 5f, 104d, HitResult.Miss }, + // coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced + // new object[] { 5f, 84d, HitResult.Miss }, + // new object[] { 5f, 85d, HitResult.Miss }, + // new object[] { 5f, 86d, HitResult.Miss }, + // new object[] { 5f, 101d, HitResult.Miss }, + // new object[] { 5f, 102d, HitResult.Miss }, + // new object[] { 5f, 103d, HitResult.Miss }, + // new object[] { 5f, 104d, HitResult.Miss }, }; private const double note_time = 300; @@ -517,6 +528,7 @@ namespace osu.Game.Rulesets.Mania.Tests RunTest($@"SV2 single note @ OD{overallDifficulty}", beatmap, $@"SV2 {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]); } + [Ignore("Tests expected to fail until stable's detailed treatment of hit windows in mania is reproduced.")] [TestCaseSource(nameof(score_v1_non_convert_test_cases))] public void TestHitWindowTreatmentWithScoreV1NonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult) { @@ -544,6 +556,7 @@ namespace osu.Game.Rulesets.Mania.Tests RunTest($@"SV1 single note @ OD{overallDifficulty}", beatmap, $@"SV1 {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]); } + [Ignore("Tests expected to fail until stable's detailed treatment of hit windows in mania is reproduced.")] [TestCaseSource(nameof(score_v1_convert_test_cases))] public void TestHitWindowTreatmentWithScoreV1Convert(float overallDifficulty, double hitOffset, HitResult expectedResult) { @@ -572,6 +585,7 @@ namespace osu.Game.Rulesets.Mania.Tests RunTest($@"SV1 convert single note @ OD{overallDifficulty}", beatmap, $@"SV1 convert {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]); } + [Ignore("Tests expected to fail until stable's detailed treatment of hit windows in mania is reproduced.")] [TestCaseSource(nameof(score_v1_non_convert_hard_rock_test_cases))] public void TestHitWindowTreatmentWithScoreV1AndHardRockNonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult) { @@ -600,6 +614,7 @@ namespace osu.Game.Rulesets.Mania.Tests RunTest($@"SV1+HR single note @ OD{overallDifficulty}", beatmap, $@"SV1+HR {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]); } + [Ignore("Tests expected to fail until stable's detailed treatment of hit windows in mania is reproduced.")] [TestCaseSource(nameof(score_v1_non_convert_easy_test_cases))] public void TestHitWindowTreatmentWithScoreV1AndEasyNonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult) { @@ -628,6 +643,7 @@ namespace osu.Game.Rulesets.Mania.Tests RunTest($@"SV1+EZ single note @ OD{overallDifficulty}", beatmap, $@"SV1+EZ {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]); } + [Ignore("Tests expected to fail until stable's detailed treatment of hit windows in mania is reproduced.")] [TestCaseSource(nameof(score_v1_non_convert_double_time_test_cases))] public void TestHitWindowTreatmentWithScoreV1AndDoubleTimeNonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult) { @@ -656,6 +672,7 @@ namespace osu.Game.Rulesets.Mania.Tests RunTest($@"SV1+DT single note @ OD{overallDifficulty}", beatmap, $@"SV1+DT {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]); } + [Ignore("Tests expected to fail until stable's detailed treatment of hit windows in mania is reproduced.")] [TestCaseSource(nameof(score_v1_non_convert_half_time_test_cases))] public void TestHitWindowTreatmentWithScoreV1AndHalfTimeNonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult) { diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs index 64496d7628..a8160d3373 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs @@ -12,7 +12,6 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Mania.Tests { - [Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")] public partial class TestSceneReplayStability : ReplayStabilityTestScene { private static readonly object[][] test_cases = @@ -22,87 +21,79 @@ namespace osu.Game.Rulesets.Mania.Tests // while round brackets `()` represent *open* or *exclusive* bounds. // OD = 5 test cases. - // PERFECT hit window is [ -19.4ms, 19.4ms] - // GREAT hit window is [ -49.0ms, 49.0ms] - // GOOD hit window is [ -82.0ms, 82.0ms] - // OK hit window is [-112.0ms, 112.0ms] - // MEH hit window is [-136.0ms, 136.0ms] - // MISS hit window is [-173.0ms, 173.0ms] + // PERFECT hit window is [ -19.5ms, 19.5ms] + // GREAT hit window is [ -49.5ms, 49.5ms] + // GOOD hit window is [ -82.5ms, 82.5ms] + // OK hit window is [-112.5ms, 112.5ms] + // MEH hit window is [-136.5ms, 136.5ms] + // MISS hit window is [-173.5ms, 173.5ms] new object[] { 5f, -19d, HitResult.Perfect }, new object[] { 5f, -19.2d, HitResult.Perfect }, - new object[] { 5f, -19.38d, HitResult.Perfect }, - // new object[] { 5f, -19.4d, HitResult.Perfect }, <- in theory this should work, in practice it does not (fails even before encode & rounding due to floating point precision issues) - new object[] { 5f, -19.44d, HitResult.Great }, new object[] { 5f, -19.7d, HitResult.Great }, new object[] { 5f, -20d, HitResult.Great }, new object[] { 5f, -48d, HitResult.Great }, new object[] { 5f, -48.4d, HitResult.Great }, new object[] { 5f, -48.7d, HitResult.Great }, new object[] { 5f, -49d, HitResult.Great }, - new object[] { 5f, -49.2d, HitResult.Good }, + new object[] { 5f, -49.2d, HitResult.Great }, new object[] { 5f, -49.7d, HitResult.Good }, new object[] { 5f, -50d, HitResult.Good }, new object[] { 5f, -81d, HitResult.Good }, new object[] { 5f, -81.2d, HitResult.Good }, new object[] { 5f, -81.7d, HitResult.Good }, new object[] { 5f, -82d, HitResult.Good }, - new object[] { 5f, -82.2d, HitResult.Ok }, + new object[] { 5f, -82.2d, HitResult.Good }, new object[] { 5f, -82.7d, HitResult.Ok }, new object[] { 5f, -83d, HitResult.Ok }, new object[] { 5f, -111d, HitResult.Ok }, new object[] { 5f, -111.2d, HitResult.Ok }, new object[] { 5f, -111.7d, HitResult.Ok }, new object[] { 5f, -112d, HitResult.Ok }, - new object[] { 5f, -112.2d, HitResult.Meh }, + new object[] { 5f, -112.2d, HitResult.Ok }, new object[] { 5f, -112.7d, HitResult.Meh }, new object[] { 5f, -113d, HitResult.Meh }, new object[] { 5f, -135d, HitResult.Meh }, new object[] { 5f, -135.2d, HitResult.Meh }, new object[] { 5f, -135.8d, HitResult.Meh }, new object[] { 5f, -136d, HitResult.Meh }, - new object[] { 5f, -136.2d, HitResult.Miss }, + new object[] { 5f, -136.2d, HitResult.Meh }, new object[] { 5f, -136.7d, HitResult.Miss }, new object[] { 5f, -137d, HitResult.Miss }, // OD = 9.3 test cases. - // PERFECT hit window is [ -14.67ms, 14.67ms] - // GREAT hit window is [ -36.10ms, 36.10ms] - // GOOD hit window is [ -69.10ms, 69.10ms] - // OK hit window is [ -99.10ms, 99.10ms] - // MEH hit window is [-123.10ms, 123.10ms] - // MISS hit window is [-160.10ms, 160.10ms] + // PERFECT hit window is [ -14.5ms, 14.5ms] + // GREAT hit window is [ -36.5ms, 36.5ms] + // GOOD hit window is [ -69.5ms, 69.5ms] + // OK hit window is [ -99.5ms, 99.5ms] + // MEH hit window is [-123.5ms, 123.5ms] + // MISS hit window is [-160.5ms, 160.5ms] new object[] { 9.3f, 14d, HitResult.Perfect }, new object[] { 9.3f, 14.2d, HitResult.Perfect }, - new object[] { 9.3f, 14.6d, HitResult.Perfect }, - // new object[] { 9.3f, 14.67d, HitResult.Perfect }, <- in theory this should work, in practice it does not (fails even before encode & rounding due to floating point precision issues) new object[] { 9.3f, 14.7d, HitResult.Great }, new object[] { 9.3f, 15d, HitResult.Great }, new object[] { 9.3f, 35d, HitResult.Great }, new object[] { 9.3f, 35.3d, HitResult.Great }, new object[] { 9.3f, 35.8d, HitResult.Great }, - new object[] { 9.3f, 36.05d, HitResult.Great }, - new object[] { 9.3f, 36.3d, HitResult.Good }, + new object[] { 9.3f, 36.3d, HitResult.Great }, new object[] { 9.3f, 36.7d, HitResult.Good }, new object[] { 9.3f, 37d, HitResult.Good }, new object[] { 9.3f, 68d, HitResult.Good }, new object[] { 9.3f, 68.4d, HitResult.Good }, new object[] { 9.3f, 68.9d, HitResult.Good }, - new object[] { 9.3f, 69.07d, HitResult.Good }, - new object[] { 9.3f, 69.25d, HitResult.Ok }, + new object[] { 9.3f, 69.25d, HitResult.Good }, new object[] { 9.3f, 69.85d, HitResult.Ok }, new object[] { 9.3f, 70d, HitResult.Ok }, new object[] { 9.3f, 98d, HitResult.Ok }, new object[] { 9.3f, 98.3d, HitResult.Ok }, new object[] { 9.3f, 98.6d, HitResult.Ok }, new object[] { 9.3f, 99d, HitResult.Ok }, - new object[] { 9.3f, 99.3d, HitResult.Meh }, + new object[] { 9.3f, 99.3d, HitResult.Ok }, new object[] { 9.3f, 99.7d, HitResult.Meh }, new object[] { 9.3f, 100d, HitResult.Meh }, new object[] { 9.3f, 122d, HitResult.Meh }, new object[] { 9.3f, 122.34d, HitResult.Meh }, new object[] { 9.3f, 122.57d, HitResult.Meh }, - new object[] { 9.3f, 123.04d, HitResult.Meh }, - new object[] { 9.3f, 123.45d, HitResult.Miss }, + new object[] { 9.3f, 123.45d, HitResult.Meh }, new object[] { 9.3f, 123.95d, HitResult.Miss }, new object[] { 9.3f, 124d, HitResult.Miss }, }; @@ -110,7 +101,7 @@ namespace osu.Game.Rulesets.Mania.Tests [TestCaseSource(nameof(test_cases))] public void TestHitWindowStability(float overallDifficulty, double hitOffset, HitResult expectedResult) { - const double note_time = 100; + const double note_time = 300; var beatmap = new ManiaBeatmap(new StageDefinition(1)) { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyReplayPlayback.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyReplayPlayback.cs index 379699b276..404ca0c79e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyReplayPlayback.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyReplayPlayback.cs @@ -17,7 +17,6 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Tests { - [Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")] public partial class TestSceneLegacyReplayPlayback : LegacyReplayPlaybackTestScene { protected override Ruleset CreateRuleset() => new OsuRuleset(); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs index 2303b17d96..320fdcff2c 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs @@ -13,7 +13,6 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests { - [Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")] public partial class TestSceneReplayStability : ReplayStabilityTestScene { private static readonly object[][] test_cases = @@ -23,53 +22,49 @@ namespace osu.Game.Rulesets.Osu.Tests // while round brackets `()` represent *open* or *exclusive* bounds. // OD = 5 test cases. - // GREAT hit window is [ -50ms, 50ms] - // OK hit window is [-100ms, 100ms] - // MEH hit window is [-150ms, 150ms] - // MISS hit window is [-400ms, 400ms] + // GREAT hit window is [ -49.5ms, 49.5ms] + // OK hit window is [ -99.5ms, 99.5ms] + // MEH hit window is [-149.5ms, 149.5ms] new object[] { 5f, 49d, HitResult.Great }, new object[] { 5f, 49.2d, HitResult.Great }, - new object[] { 5f, 49.7d, HitResult.Great }, - new object[] { 5f, 50d, HitResult.Great }, + new object[] { 5f, 49.7d, HitResult.Ok }, + new object[] { 5f, 50d, HitResult.Ok }, new object[] { 5f, 50.4d, HitResult.Ok }, new object[] { 5f, 50.9d, HitResult.Ok }, new object[] { 5f, 51d, HitResult.Ok }, new object[] { 5f, 99d, HitResult.Ok }, new object[] { 5f, 99.2d, HitResult.Ok }, - new object[] { 5f, 99.7d, HitResult.Ok }, - new object[] { 5f, 100d, HitResult.Ok }, + new object[] { 5f, 99.7d, HitResult.Meh }, + new object[] { 5f, 100d, HitResult.Meh }, new object[] { 5f, 100.4d, HitResult.Meh }, new object[] { 5f, 100.9d, HitResult.Meh }, new object[] { 5f, 101d, HitResult.Meh }, new object[] { 5f, 149d, HitResult.Meh }, new object[] { 5f, 149.2d, HitResult.Meh }, - new object[] { 5f, 149.7d, HitResult.Meh }, - new object[] { 5f, 150d, HitResult.Meh }, + new object[] { 5f, 149.7d, HitResult.Miss }, + new object[] { 5f, 150d, HitResult.Miss }, new object[] { 5f, 150.4d, HitResult.Miss }, new object[] { 5f, 150.9d, HitResult.Miss }, new object[] { 5f, 151d, HitResult.Miss }, // OD = 5.7 test cases. - // GREAT hit window is [ -45.8ms, 45.8ms] - // OK hit window is [ -94.4ms, 94.4ms] - // MEH hit window is [-143.0ms, 143.0ms] - // MISS hit window is [-400.0ms, 400.0ms] - new object[] { 5.7f, 45d, HitResult.Great }, - new object[] { 5.7f, 45.2d, HitResult.Great }, - new object[] { 5.7f, 45.8d, HitResult.Great }, - new object[] { 5.7f, 45.9d, HitResult.Ok }, - new object[] { 5.7f, 46d, HitResult.Ok }, - new object[] { 5.7f, 46.4d, HitResult.Ok }, - new object[] { 5.7f, 94d, HitResult.Ok }, - new object[] { 5.7f, 94.2d, HitResult.Ok }, - new object[] { 5.7f, 94.4d, HitResult.Ok }, - new object[] { 5.7f, 94.48d, HitResult.Ok }, - new object[] { 5.7f, 94.9d, HitResult.Meh }, - new object[] { 5.7f, 95d, HitResult.Meh }, - new object[] { 5.7f, 95.4d, HitResult.Meh }, + // GREAT hit window is [ -44.5ms, 44.5ms] + // OK hit window is [ -93.5ms, 93.5ms] + // MEH hit window is [-142.5ms, 142.5ms] + new object[] { 5.7f, 44d, HitResult.Great }, + new object[] { 5.7f, 44.2d, HitResult.Great }, + new object[] { 5.7f, 44.8d, HitResult.Ok }, + new object[] { 5.7f, 45d, HitResult.Ok }, + new object[] { 5.7f, 45.4d, HitResult.Ok }, + new object[] { 5.7f, 93d, HitResult.Ok }, + new object[] { 5.7f, 93.4d, HitResult.Ok }, + new object[] { 5.7f, 93.9d, HitResult.Meh }, + new object[] { 5.7f, 94d, HitResult.Meh }, + new object[] { 5.7f, 94.4d, HitResult.Meh }, new object[] { 5.7f, 142d, HitResult.Meh }, - new object[] { 5.7f, 142.7d, HitResult.Meh }, - new object[] { 5.7f, 143d, HitResult.Meh }, + new object[] { 5.7f, 142.2d, HitResult.Meh }, + new object[] { 5.7f, 142.7d, HitResult.Miss }, + new object[] { 5.7f, 143d, HitResult.Miss }, new object[] { 5.7f, 143.4d, HitResult.Miss }, new object[] { 5.7f, 143.9d, HitResult.Miss }, new object[] { 5.7f, 144d, HitResult.Miss }, diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneLegacyReplayPlayback.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneLegacyReplayPlayback.cs index 5e71f974d8..40a426b360 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneLegacyReplayPlayback.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneLegacyReplayPlayback.cs @@ -15,7 +15,6 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Taiko.Tests { - [Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")] public partial class TestSceneLegacyReplayPlayback : LegacyReplayPlaybackTestScene { protected override string? ExportLocation => null; @@ -177,7 +176,7 @@ namespace osu.Game.Rulesets.Taiko.Tests ScoreInfo = new ScoreInfo { Ruleset = CreateRuleset().RulesetInfo, - Mods = [new TaikoModHardRock()] + Mods = [new TaikoModEasy()] } }; diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs index 62bbebcf0b..c61ae8ecc7 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs @@ -12,7 +12,6 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Taiko.Tests { - [Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")] public partial class TestSceneReplayStability : ReplayStabilityTestScene { private static readonly object[][] test_cases = @@ -22,40 +21,38 @@ namespace osu.Game.Rulesets.Taiko.Tests // while round brackets `()` represent *open* or *exclusive* bounds. // OD = 5 test cases. - // GREAT hit window is [-35ms, 35ms] - // OK hit window is [-80ms, 80ms] - // MISS hit window is [-95ms, 95ms] + // GREAT hit window is [-34.5ms, 34.5ms] + // OK hit window is [-79.5ms, 79.5ms] + // MISS hit window is [-94.5ms, 94.5ms] new object[] { 5f, -34d, HitResult.Great }, new object[] { 5f, -34.2d, HitResult.Great }, - new object[] { 5f, -34.7d, HitResult.Great }, - new object[] { 5f, -35d, HitResult.Great }, + new object[] { 5f, -34.7d, HitResult.Ok }, + new object[] { 5f, -35d, HitResult.Ok }, new object[] { 5f, -35.2d, HitResult.Ok }, new object[] { 5f, -35.8d, HitResult.Ok }, new object[] { 5f, -36d, HitResult.Ok }, new object[] { 5f, -79d, HitResult.Ok }, new object[] { 5f, -79.3d, HitResult.Ok }, - new object[] { 5f, -79.7d, HitResult.Ok }, - new object[] { 5f, -80d, HitResult.Ok }, + new object[] { 5f, -79.7d, HitResult.Miss }, + new object[] { 5f, -80d, HitResult.Miss }, new object[] { 5f, -80.2d, HitResult.Miss }, new object[] { 5f, -80.8d, HitResult.Miss }, new object[] { 5f, -81d, HitResult.Miss }, // OD = 7.8 test cases. - // GREAT hit window is [-26.6ms, 26.6ms] - // OK hit window is [-63.2ms, 63.2ms] - // MISS hit window is [-81.0ms, 81.0ms] - new object[] { 7.8f, -26d, HitResult.Great }, - new object[] { 7.8f, -26.4d, HitResult.Great }, - new object[] { 7.8f, -26.59d, HitResult.Great }, - new object[] { 7.8f, -26.8d, HitResult.Ok }, - new object[] { 7.8f, -27d, HitResult.Ok }, - new object[] { 7.8f, -27.1d, HitResult.Ok }, - new object[] { 7.8f, -63d, HitResult.Ok }, - new object[] { 7.8f, -63.18d, HitResult.Ok }, - new object[] { 7.8f, -63.4d, HitResult.Ok }, - new object[] { 7.8f, -63.7d, HitResult.Miss }, - new object[] { 7.8f, -64d, HitResult.Miss }, - new object[] { 7.8f, -64.2d, HitResult.Miss }, + // GREAT hit window is [-25.5ms, 25.5ms] + // OK hit window is [-62.5ms, 62.5ms] + // MISS hit window is [-80.5ms, 80.5ms] + new object[] { 7.8f, -25d, HitResult.Great }, + new object[] { 7.8f, -25.4d, HitResult.Great }, + new object[] { 7.8f, -25.8d, HitResult.Ok }, + new object[] { 7.8f, -26d, HitResult.Ok }, + new object[] { 7.8f, -26.1d, HitResult.Ok }, + new object[] { 7.8f, -62d, HitResult.Ok }, + new object[] { 7.8f, -62.4d, HitResult.Ok }, + new object[] { 7.8f, -62.7d, HitResult.Miss }, + new object[] { 7.8f, -63d, HitResult.Miss }, + new object[] { 7.8f, -63.2d, HitResult.Miss }, }; [TestCaseSource(nameof(test_cases))] From 5d69af55f07cee0699bb0d8590d8da116607b231 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Jun 2025 21:16:42 +0900 Subject: [PATCH 293/370] Add test hitting next circle during tail window --- .../TestSceneSliderInput.cs | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index 286e4bd775..362a86065d 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -484,6 +484,50 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("no miss judgements recorded", () => judgementResults.All(r => r.Type.IsHit())); } + /// + /// Sliders are common to by 1/2 or 1/4 beat length in order to place the circle on the next beat. + /// This tests a user pressing the next circle in the window between the last tick and the end of the slider (). + /// + [Test] + public void TestHitNextCircleDuringTailLeniency() + { + const double bpm = 240; + const double beat_length = 60000 / bpm; + const double slider_start = time_slider_start; + const double slider_end = slider_start + beat_length; + const double last_tick_time = slider_end + SliderEventGenerator.TAIL_LENIENCY; + const double next_circle_time = slider_end + beat_length / 4; + + performTest(new List + { + new OsuReplayFrame { Position = Vector2.Zero, Actions = { OsuAction.LeftButton }, Time = time_slider_start }, + // This frame is weird because the "up" frame can be skipped if the current time passes it (and is thus no longer in an important section). + // So the idea is to instead generate another important frame at the intended time without yet hitting the next circle. + new OsuReplayFrame { Position = new Vector2(100, 0), Actions = { OsuAction.RightButton }, Time = last_tick_time + 5 }, + new OsuReplayFrame { Position = new Vector2(140, 0), Actions = { OsuAction.LeftButton }, Time = last_tick_time + 20 }, + }, + [ + new Slider + { + StartTime = slider_start, + Position = new Vector2(0, 0), + TickDistanceMultiplier = 10, // no ticks + Path = new SliderPath(PathType.PERFECT_CURVE, new[] + { + Vector2.Zero, + new Vector2(100, 0), + }, 100), + }, + new HitCircle + { + StartTime = next_circle_time, + Position = new Vector2(140, 0) + } + ], bpm: bpm); + + AddAssert("all judgements are hit", () => judgementResults.All(j => j.Type.IsHit())); + } + private void assertAllMaxJudgements() { AddAssert("All judgements max", () => @@ -522,6 +566,11 @@ namespace osu.Game.Rulesets.Osu.Tests }, slider_path_length), }; + performTest(frames, [slider], bpm, tickRate); + } + + private void performTest(List frames, List objects, double? bpm = null, int? tickRate = null) + { AddStep("load player", () => { var cpi = new ControlPointInfo(); @@ -531,7 +580,7 @@ namespace osu.Game.Rulesets.Osu.Tests Beatmap.Value = CreateWorkingBeatmap(new Beatmap { - HitObjects = { slider }, + HitObjects = objects, BeatmapInfo = { Difficulty = new BeatmapDifficulty From 5dc08d4351c5348f79cc5afa599aad0b93c3d860 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Jun 2025 21:50:04 +0900 Subject: [PATCH 294/370] Simplify important frame management --- osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index 362a86065d..0842f8f14f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -501,10 +501,7 @@ namespace osu.Game.Rulesets.Osu.Tests performTest(new List { new OsuReplayFrame { Position = Vector2.Zero, Actions = { OsuAction.LeftButton }, Time = time_slider_start }, - // This frame is weird because the "up" frame can be skipped if the current time passes it (and is thus no longer in an important section). - // So the idea is to instead generate another important frame at the intended time without yet hitting the next circle. - new OsuReplayFrame { Position = new Vector2(100, 0), Actions = { OsuAction.RightButton }, Time = last_tick_time + 5 }, - new OsuReplayFrame { Position = new Vector2(140, 0), Actions = { OsuAction.LeftButton }, Time = last_tick_time + 20 }, + new OsuReplayFrame { Position = new Vector2(140, 0), Actions = { OsuAction.RightButton }, Time = last_tick_time + 20 }, }, [ new Slider From 75a3cdcfe2ed730be99f576275d42541cb60206e Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 26 Jun 2025 02:56:43 +0300 Subject: [PATCH 295/370] Move room URL formatting to extension method --- osu.Game/Online/Rooms/RoomExtensions.cs | 21 +++++++++++++++++++ .../OnlinePlay/Lounge/Components/RoomPanel.cs | 10 ++++----- 2 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 osu.Game/Online/Rooms/RoomExtensions.cs diff --git a/osu.Game/Online/Rooms/RoomExtensions.cs b/osu.Game/Online/Rooms/RoomExtensions.cs new file mode 100644 index 0000000000..b7348e8997 --- /dev/null +++ b/osu.Game/Online/Rooms/RoomExtensions.cs @@ -0,0 +1,21 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Online.API; + +namespace osu.Game.Online.Rooms +{ + public static class RoomExtensions + { + /// + /// Get the room page URL, or null if unavailable. + /// + public static string? GetOnlineURL(this Room room, IAPIProvider api) + { + if (!room.RoomID.HasValue) + return null; + + return $@"{api.Endpoints.WebsiteUrl}/multiplayer/rooms/{room.RoomID.Value}"; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs index 7a4279ef98..ae593cd3cb 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs @@ -380,11 +380,13 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { var items = new List(); - if (Room.RoomID.HasValue) + string? url = Room.GetOnlineURL(api); + + if (url != null) { items.AddRange([ - new OsuMenuItem("View in browser", MenuItemType.Standard, () => game?.OpenUrlExternally(getRoomUrl())), - new OsuMenuItem("Copy link", MenuItemType.Standard, () => game?.CopyToClipboard(getRoomUrl())) + new OsuMenuItem("View in browser", MenuItemType.Standard, () => game?.OpenUrlExternally(url)), + new OsuMenuItem("Copy link", MenuItemType.Standard, () => game?.CopyToClipboard(url)) ]); } @@ -392,8 +394,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } - private string? getRoomUrl() => !Room.RoomID.HasValue ? null : $@"{api.Endpoints.WebsiteUrl}/multiplayer/rooms/{Room.RoomID.Value}"; - protected virtual UpdateableBeatmapBackgroundSprite CreateBackground() => new UpdateableBeatmapBackgroundSprite(); protected virtual IEnumerable CreateBottomDetails() From 8b67747735347a21867ecac7c9e5fe8d7459fb8b Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 26 Jun 2025 02:57:18 +0300 Subject: [PATCH 296/370] Simplify code and fix link not updating on changes to room ID --- .../OnlinePlay/Lounge/Components/RoomPanel.cs | 47 ++++++++++++++----- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs index ae593cd3cb..b9f84b4fa4 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs @@ -207,7 +207,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Direction = FillDirection.Vertical, Children = new Drawable[] { - roomName = new RoomNameLine(getRoomUrl(), ShowExternalLink), + roomName = new RoomNameLine(), new RoomStatusText(Room) { Beatmap = { BindTarget = currentBeatmap } @@ -278,6 +278,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components wrapper.FadeInFromZero(200); + updateRoomID(); updateRoomName(); updateRoomCategory(); updateRoomType(); @@ -291,6 +292,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { switch (e.PropertyName) { + case nameof(Room.RoomID): + updateRoomID(); + break; + case nameof(Room.Name): updateRoomName(); break; @@ -334,6 +339,12 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components }), cancellationSource.Token); } + private void updateRoomID() + { + if (roomName != null && ShowExternalLink) + roomName.Link = Room.GetOnlineURL(api); + } + private void updateRoomName() { if (roomName != null) @@ -558,11 +569,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components public partial class RoomNameLine : FillFlowContainer { - private readonly string? roomUrl; - private readonly bool showExternalLink; - private TruncatingSpriteText spriteText = null!; - private ExternalLinkButton link = null!; + private ExternalLinkButton linkButton = null!; public LocalisableString Text { @@ -570,10 +578,16 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components set => spriteText.Text = value; } - public RoomNameLine(string? roomUrl, bool showExternalLink) + private string? link; + + public string? Link { - this.roomUrl = roomUrl; - this.showExternalLink = showExternalLink; + get => link; + set + { + link = value; + updateLink(); + } } [BackgroundDependencyLoader] @@ -591,20 +605,31 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Origin = Anchor.BottomLeft, Font = OsuFont.GetFont(size: 28), }, - link = new ExternalLinkButton(roomUrl) + linkButton = new ExternalLinkButton { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Margin = new MarginPadding { Horizontal = 6, Bottom = 4 }, - Alpha = showExternalLink ? 1 : 0, + Alpha = 0f, }, }; } + private void updateLink() + { + if (link == null) + linkButton.Hide(); + else + { + linkButton.Show(); + linkButton.Link = link; + } + } + protected override void Update() { base.Update(); - spriteText.MaxWidth = DrawWidth - link.LayoutSize.X; + spriteText.MaxWidth = DrawWidth - linkButton.LayoutSize.X; } } } From 3ac557907e4d2d280043109e7083dacee59b3621 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 26 Jun 2025 02:58:55 +0300 Subject: [PATCH 297/370] Add test coverage --- .../Visual/Multiplayer/TestSceneRoomPanel.cs | 56 ++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs index fee5e62958..037c5faae3 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs @@ -165,23 +165,75 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = "A host-only room", QueueMode = QueueMode.HostOnly, Type = MatchType.HeadToHead, + RoomID = 1337, }), new MultiplayerRoomPanel(new Room { Name = "An all-players, team-versus room", QueueMode = QueueMode.AllPlayers, - Type = MatchType.TeamVersus + Type = MatchType.TeamVersus, + RoomID = 1338, }), new MultiplayerRoomPanel(new Room { Name = "A round-robin room", QueueMode = QueueMode.AllPlayersRoundRobin, - Type = MatchType.HeadToHead + Type = MatchType.HeadToHead, + RoomID = 1339, }), } }); } + [Test] + public void TestRoomWithLongTitle() + { + AddStep("create rooms", () => Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5), + Children = new[] + { + new MultiplayerRoomPanel(new Room + { + Name = "This room has a very very long title enough to make the external link button reach the participants list on the right side unless the test window is very wide, at which point I don't know, hi.", + QueueMode = QueueMode.HostOnly, + Type = MatchType.HeadToHead, + RoomID = 1337, + }), + } + }); + } + + [Test] + public void TestRoomWithUpdatedRoomID() + { + Room room = null!; + + AddStep("create rooms", () => Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5), + Children = new[] + { + new MultiplayerRoomPanel(room = new Room + { + Name = "This room has a very very long title enough to make the external link button reach the participants list on the right side unless the test window is very wide, at which point I don't know, hi.", + QueueMode = QueueMode.HostOnly, + Type = MatchType.HeadToHead, + }), + } + }); + AddWaitStep("wait", 3); + AddStep("set room ID", () => room.RoomID = 1337); + AddWaitStep("wait", 3); + AddStep("clear room ID", () => room.RoomID = null); + } + private RoomPanel createLoungeRoom(Room room) { room.Host ??= new APIUser { Username = "peppy", Id = 2 }; From fa508245a1fa50b14a602a758d24581b3c6e9f40 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 26 Jun 2025 07:23:29 +0300 Subject: [PATCH 298/370] Support changing weight in text-based elements --- osu.Game/Graphics/OsuFont.cs | 8 ++ osu.Game/Localisation/FontStrings.cs | 44 +++++++++++ .../SkinnableComponentStrings.cs | 10 +++ .../Skinning/FontAdjustableSkinComponent.cs | 76 +++++++++++++++++-- 4 files changed, 130 insertions(+), 8 deletions(-) create mode 100644 osu.Game/Localisation/FontStrings.cs diff --git a/osu.Game/Graphics/OsuFont.cs b/osu.Game/Graphics/OsuFont.cs index b314c602f5..22a2c9f37b 100644 --- a/osu.Game/Graphics/OsuFont.cs +++ b/osu.Game/Graphics/OsuFont.cs @@ -5,6 +5,8 @@ using System.ComponentModel; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Localisation; namespace osu.Game.Graphics { @@ -177,31 +179,37 @@ namespace osu.Game.Graphics /// /// Equivalent to weight 300. /// + [LocalisableDescription(typeof(FontStrings), nameof(FontStrings.Light))] Light = 300, /// /// Equivalent to weight 400. /// + [LocalisableDescription(typeof(FontStrings), nameof(FontStrings.Regular))] Regular = 400, /// /// Equivalent to weight 500. /// + [LocalisableDescription(typeof(FontStrings), nameof(FontStrings.Medium))] Medium = 500, /// /// Equivalent to weight 600. /// + [LocalisableDescription(typeof(FontStrings), nameof(FontStrings.SemiBold))] SemiBold = 600, /// /// Equivalent to weight 700. /// + [LocalisableDescription(typeof(FontStrings), nameof(FontStrings.Bold))] Bold = 700, /// /// Equivalent to weight 900. /// + [LocalisableDescription(typeof(FontStrings), nameof(FontStrings.Black))] Black = 900 } } diff --git a/osu.Game/Localisation/FontStrings.cs b/osu.Game/Localisation/FontStrings.cs new file mode 100644 index 0000000000..72e3f3eaba --- /dev/null +++ b/osu.Game/Localisation/FontStrings.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class FontStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.Font"; + + /// + /// "Light" + /// + public static LocalisableString Light => new TranslatableString(getKey(@"light"), @"Light"); + + /// + /// "Regular" + /// + public static LocalisableString Regular => new TranslatableString(getKey(@"regular"), @"Regular"); + + /// + /// "Medium" + /// + public static LocalisableString Medium => new TranslatableString(getKey(@"medium"), @"Medium"); + + /// + /// "Semibold" + /// + public static LocalisableString SemiBold => new TranslatableString(getKey(@"semi_bold"), @"Semibold"); + + /// + /// "Bold" + /// + public static LocalisableString Bold => new TranslatableString(getKey(@"bold"), @"Bold"); + + /// + /// "Black" + /// + public static LocalisableString Black => new TranslatableString(getKey(@"black"), @"Black"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs index 66abf2bfd5..61d1137e6a 100644 --- a/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs +++ b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs @@ -79,6 +79,16 @@ namespace osu.Game.Localisation.SkinComponents /// public static LocalisableString TextColourDescription => new TranslatableString(getKey(@"text_colour_description"), @"The colour of the text."); + /// + /// "Text weight" + /// + public static LocalisableString TextWeight => new TranslatableString(getKey(@"text_weight"), @"Text weight"); + + /// + /// "The weight of the text." + /// + public static LocalisableString TextWeightDescription => new TranslatableString(getKey(@"text_weight_description"), @"The weight of the text."); + /// /// "Use relative size" /// diff --git a/osu.Game/Skinning/FontAdjustableSkinComponent.cs b/osu.Game/Skinning/FontAdjustableSkinComponent.cs index 0821edf7fc..1fda31afb7 100644 --- a/osu.Game/Skinning/FontAdjustableSkinComponent.cs +++ b/osu.Game/Skinning/FontAdjustableSkinComponent.cs @@ -1,13 +1,16 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Configuration; using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; using osu.Game.Localisation.SkinComponents; +using osu.Game.Overlays.Settings; namespace osu.Game.Skinning { @@ -21,6 +24,10 @@ namespace osu.Game.Skinning [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))] public Bindable Font { get; } = new Bindable(Typeface.Torus); + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextWeight), nameof(SkinnableComponentStrings.TextWeightDescription), + SettingControlType = typeof(WeightDropdown))] + public Bindable TextWeight { get; } = new Bindable(FontWeight.Regular); + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour), nameof(SkinnableComponentStrings.TextColourDescription))] public BindableColour4 TextColour { get; } = new BindableColour4(Colour4.White); @@ -35,16 +42,69 @@ namespace osu.Game.Skinning { base.LoadComplete(); - Font.BindValueChanged(e => - { - // We only have bold weight for venera, so let's force that. - FontWeight fontWeight = e.NewValue == Typeface.Venera ? FontWeight.Bold : FontWeight.Regular; - - FontUsage f = OsuFont.GetFont(e.NewValue, weight: fontWeight); - SetFont(f); - }, true); + Font.BindValueChanged(_ => updateFont()); + TextWeight.BindValueChanged(_ => updateFont(), true); TextColour.BindValueChanged(e => SetTextColour(e.NewValue), true); } + + private void updateFont() => SetFont(OsuFont.GetFont(Font.Value, weight: TextWeight.Value)); + + private partial class WeightDropdown : SettingsDropdown + { + public FontAdjustableSkinComponent FontComponent => (FontAdjustableSkinComponent)SettingSourceObject; + protected override OsuDropdown CreateDropdown() => new DropdownControl(this); + + private new partial class DropdownControl : SettingsDropdown.DropdownControl + { + private readonly WeightDropdown settingsDropdown; + + private IBindable font = null!; + + public DropdownControl(WeightDropdown settingsDropdown) + { + this.settingsDropdown = settingsDropdown; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + font = settingsDropdown.FontComponent.Font.GetBoundCopy(); + font.BindValueChanged(_ => updateItems(), true); + } + + private void updateItems() + { + ClearItems(); + + switch (font.Value) + { + case Typeface.Venera: + AddDropdownItem(FontWeight.Light); + AddDropdownItem(FontWeight.Bold); + AddDropdownItem(FontWeight.Black); + + Current.Default = FontWeight.Bold; + + if (!Items.Contains(Current.Value)) + Current.SetDefault(); + break; + + default: + AddDropdownItem(FontWeight.Light); + AddDropdownItem(FontWeight.Regular); + AddDropdownItem(FontWeight.SemiBold); + AddDropdownItem(FontWeight.Bold); + + Current.Default = FontWeight.Regular; + + if (!Items.Contains(Current.Value)) + Current.SetDefault(); + break; + } + } + } + } } } From cd354a0de827a0601d8f78fdd74c6e61d64c55da Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 27 Jun 2025 00:58:10 +0300 Subject: [PATCH 299/370] Remove font weight localisation --- osu.Game/Graphics/OsuFont.cs | 8 ----- osu.Game/Localisation/FontStrings.cs | 44 ---------------------------- 2 files changed, 52 deletions(-) delete mode 100644 osu.Game/Localisation/FontStrings.cs diff --git a/osu.Game/Graphics/OsuFont.cs b/osu.Game/Graphics/OsuFont.cs index 22a2c9f37b..b314c602f5 100644 --- a/osu.Game/Graphics/OsuFont.cs +++ b/osu.Game/Graphics/OsuFont.cs @@ -5,8 +5,6 @@ using System.ComponentModel; using osu.Framework.Graphics.Sprites; -using osu.Framework.Localisation; -using osu.Game.Localisation; namespace osu.Game.Graphics { @@ -179,37 +177,31 @@ namespace osu.Game.Graphics /// /// Equivalent to weight 300. /// - [LocalisableDescription(typeof(FontStrings), nameof(FontStrings.Light))] Light = 300, /// /// Equivalent to weight 400. /// - [LocalisableDescription(typeof(FontStrings), nameof(FontStrings.Regular))] Regular = 400, /// /// Equivalent to weight 500. /// - [LocalisableDescription(typeof(FontStrings), nameof(FontStrings.Medium))] Medium = 500, /// /// Equivalent to weight 600. /// - [LocalisableDescription(typeof(FontStrings), nameof(FontStrings.SemiBold))] SemiBold = 600, /// /// Equivalent to weight 700. /// - [LocalisableDescription(typeof(FontStrings), nameof(FontStrings.Bold))] Bold = 700, /// /// Equivalent to weight 900. /// - [LocalisableDescription(typeof(FontStrings), nameof(FontStrings.Black))] Black = 900 } } diff --git a/osu.Game/Localisation/FontStrings.cs b/osu.Game/Localisation/FontStrings.cs deleted file mode 100644 index 72e3f3eaba..0000000000 --- a/osu.Game/Localisation/FontStrings.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Localisation; - -namespace osu.Game.Localisation -{ - public static class FontStrings - { - private const string prefix = @"osu.Game.Resources.Localisation.Font"; - - /// - /// "Light" - /// - public static LocalisableString Light => new TranslatableString(getKey(@"light"), @"Light"); - - /// - /// "Regular" - /// - public static LocalisableString Regular => new TranslatableString(getKey(@"regular"), @"Regular"); - - /// - /// "Medium" - /// - public static LocalisableString Medium => new TranslatableString(getKey(@"medium"), @"Medium"); - - /// - /// "Semibold" - /// - public static LocalisableString SemiBold => new TranslatableString(getKey(@"semi_bold"), @"Semibold"); - - /// - /// "Bold" - /// - public static LocalisableString Bold => new TranslatableString(getKey(@"bold"), @"Bold"); - - /// - /// "Black" - /// - public static LocalisableString Black => new TranslatableString(getKey(@"black"), @"Black"); - - private static string getKey(string key) => $@"{prefix}:{key}"; - } -} From 3dbb1e15b643f17442a94edcbc159af5cf5a8069 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 27 Jun 2025 03:52:09 +0300 Subject: [PATCH 300/370] Fix potential null reference in `RoomNameLine` --- osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs index b9f84b4fa4..3610995b2c 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs @@ -569,8 +569,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components public partial class RoomNameLine : FillFlowContainer { - private TruncatingSpriteText spriteText = null!; - private ExternalLinkButton linkButton = null!; + private readonly TruncatingSpriteText spriteText; + private readonly ExternalLinkButton linkButton; public LocalisableString Text { @@ -590,8 +590,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } - [BackgroundDependencyLoader] - private void load() + public RoomNameLine() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; From e713a68c4936b7f4d9f84d2904dd5e995f7b8820 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Jun 2025 14:32:58 +0900 Subject: [PATCH 301/370] Attempt to fix flaky test `TestExitWithHoldDisabled` See https://github.com/ppy/osu/actions/runs/15915618971/job/44892620536#step:5:203. Note that the `DialogOverlay` only finishes loading after the until step failure. --- osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 8ba914c05f..2a755b46b3 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -1201,6 +1201,8 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestExitWithHoldDisabled() { + AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType().SingleOrDefault() != null); + AddStep("set hold delay to 0", () => Game.LocalConfig.SetValue(OsuSetting.UIHoldActivationDelay, 0.0)); AddStep("press escape twice rapidly", () => From 5485abd3a53102c6bca1e3eec790b304cba9fec6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Jun 2025 17:09:29 +0900 Subject: [PATCH 302/370] Remove pointless tooltip I agree that these are pointless and we should probably remove the others and stop wasting localiser's time on these. --- .../Localisation/SkinComponents/SkinnableComponentStrings.cs | 5 ----- osu.Game/Skinning/FontAdjustableSkinComponent.cs | 3 +-- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs index 61d1137e6a..35ed1ea55c 100644 --- a/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs +++ b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs @@ -84,11 +84,6 @@ namespace osu.Game.Localisation.SkinComponents /// public static LocalisableString TextWeight => new TranslatableString(getKey(@"text_weight"), @"Text weight"); - /// - /// "The weight of the text." - /// - public static LocalisableString TextWeightDescription => new TranslatableString(getKey(@"text_weight_description"), @"The weight of the text."); - /// /// "Use relative size" /// diff --git a/osu.Game/Skinning/FontAdjustableSkinComponent.cs b/osu.Game/Skinning/FontAdjustableSkinComponent.cs index 1fda31afb7..f2d8c9e440 100644 --- a/osu.Game/Skinning/FontAdjustableSkinComponent.cs +++ b/osu.Game/Skinning/FontAdjustableSkinComponent.cs @@ -24,8 +24,7 @@ namespace osu.Game.Skinning [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))] public Bindable Font { get; } = new Bindable(Typeface.Torus); - [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextWeight), nameof(SkinnableComponentStrings.TextWeightDescription), - SettingControlType = typeof(WeightDropdown))] + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextWeight), SettingControlType = typeof(WeightDropdown))] public Bindable TextWeight { get; } = new Bindable(FontWeight.Regular); [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour), nameof(SkinnableComponentStrings.TextColourDescription))] From 52fad813713ab6ad4605b84e7f608dd9c11f50bf Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 27 Jun 2025 17:19:11 +0900 Subject: [PATCH 303/370] Fix lag when checking for update --- osu.Game/Updater/UpdateManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index 65b4770174..335f6085a9 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -88,7 +88,7 @@ namespace osu.Game.Updater /// Immediately checks for any available update. /// /// true if any updates are available, false otherwise. - public async Task CheckForUpdateAsync(CancellationToken cancellationToken = default) + public async Task CheckForUpdateAsync(CancellationToken cancellationToken = default) => await Task.Run(async () => { if (!CanCheckForUpdate) return false; @@ -100,7 +100,7 @@ namespace osu.Game.Updater await lastCts.CancelAsync().ConfigureAwait(false); return await PerformUpdateCheck(cts.Token).ConfigureAwait(false); - } + }, cancellationToken).ConfigureAwait(false); /// /// Performs an asynchronous check for application updates. From c53a7aa2fcbc1a0f523b2c7c0cf66322943b87f8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 27 Jun 2025 19:36:55 +0900 Subject: [PATCH 304/370] Fix flaky tests due to async disposal --- .../Mods/TestSceneOsuModRelax.cs | 46 ++++++++++--------- .../Tests/Visual/ReplayStabilityTestScene.cs | 5 ++ 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRelax.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRelax.cs index 1bb2f24c1c..b4298344b8 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRelax.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRelax.cs @@ -13,7 +13,6 @@ using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Replays; -using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Tests.Visual; using osuTK; @@ -22,21 +21,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { public partial class TestSceneOsuModRelax : OsuModTestScene { - private readonly HitCircle hitObject; - private readonly HitWindows hitWindows = new OsuHitWindows(); - - public TestSceneOsuModRelax() - { - hitWindows.SetDifficulty(9); - - hitObject = new HitCircle - { - StartTime = 1000, - Position = new Vector2(100, 100), - HitWindows = hitWindows - }; - } - protected override TestPlayer CreateModPlayer(Ruleset ruleset) => new ModRelaxTestPlayer(CurrentTestData, AllowFail); [Test] @@ -46,12 +30,21 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods Autoplay = false, CreateBeatmap = () => new Beatmap { - HitObjects = new List { hitObject } + Difficulty = { OverallDifficulty = 9 }, + HitObjects = new List + { + new HitCircle + { + StartTime = 1000, + Position = new Vector2(100, 100), + HitWindows = new OsuHitWindows() + } + } }, ReplayFrames = new List { new OsuReplayFrame(0, new Vector2()), - new OsuReplayFrame(hitObject.StartTime, hitObject.Position), + new OsuReplayFrame(100, new Vector2(100)), }, PassCondition = () => Player.ScoreProcessor.Combo.Value == 1 }); @@ -63,13 +56,22 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods Autoplay = false, CreateBeatmap = () => new Beatmap { - HitObjects = new List { hitObject } + Difficulty = { OverallDifficulty = 9 }, + HitObjects = new List + { + new HitCircle + { + StartTime = 1000, + Position = new Vector2(100, 100), + HitWindows = new OsuHitWindows() + } + } }, ReplayFrames = new List { - new OsuReplayFrame(0, new Vector2(hitObject.X - 22, hitObject.Y - 22)), // must be an edge hit for the cursor to not stay on the object for too long - new OsuReplayFrame(hitObject.StartTime - OsuModRelax.RELAX_LENIENCY, new Vector2(hitObject.X - 22, hitObject.Y - 22)), - new OsuReplayFrame(hitObject.StartTime, new Vector2(0)), + new OsuReplayFrame(0, new Vector2(78, 78)), // must be an edge hit for the cursor to not stay on the object for too long + new OsuReplayFrame(1000 - OsuModRelax.RELAX_LENIENCY, new Vector2(78, 78)), + new OsuReplayFrame(1000, new Vector2(0)), }, PassCondition = () => Player.ScoreProcessor.Combo.Value == 1 }); diff --git a/osu.Game/Tests/Visual/ReplayStabilityTestScene.cs b/osu.Game/Tests/Visual/ReplayStabilityTestScene.cs index af41617a7b..a84fb86200 100644 --- a/osu.Game/Tests/Visual/ReplayStabilityTestScene.cs +++ b/osu.Game/Tests/Visual/ReplayStabilityTestScene.cs @@ -57,6 +57,11 @@ namespace osu.Game.Tests.Visual AddStep(@"exit player", () => currentPlayer.Exit()); + // The incoming beatmap is ruleset-typed in every usage, so the incoming hitobjects will be used as-is rather than being converted. + // Because we'll be re-using the beatmap (thus also the hitobjects), we need to make sure the previous player has been fully disposed. + AddUntilStep("player exited", () => !currentPlayer.IsCurrentScreen()); + AddStep("dispose player", () => currentPlayer.Dispose()); + AddStep(@"encode and decode score", () => { var encoder = new LegacyScoreEncoder(originalScore, beatmap); From c7cd3a984285b6770ff69983186e16908bbb507e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Jun 2025 12:44:50 +0200 Subject: [PATCH 305/370] Fix beatmap skin sample lookups falling back to non-custom sample banks if the custom sample bank sample was not found Closes https://github.com/ppy/osu/issues/33900. I think. Stable's sample lookup logic is horrible. The user in the issue claimed they were hearing `drum-hitfinish2`, but they were really hearing `drum-hitfinish`, because they're the same `.wav` file in the beatmap. Now the reason *why* they were hearind `drum-hitfinish` is that the sample control point was specifying something like: 23946,-200,4,2,4,40,0,0 To decipher, this is: - default sample bank of soft - custom sample bank of 4 Taking one of the objects affected, namely 00:23:946 (2) - that's a slider with finish addition and drum addition bank on the slider head. The slider head is thus attempting to play `soft-hitnormal4` and `drum-hitfinish4`. Neither `soft-hitnormal4` or `soft-hitnormal` exist in the beatmap, so that plays fine via falling back to user skin's `soft-hitnormal`, but `drum-hitfinish4` ends up falling back to `drum-hitfinish` which *does* exist in the beatmap skin and thus plays wrongly from the beatmap skin rather than the user skin. I have no idea how to ensure this is correct across every beatmap and skin out there so my approach is to just spray and pray (and rely on issue reports I guess). I *think* this matches the stable logic which is nestled within https://github.com/peppy/osu-stable-reference/blob/a5e5fe6ef240505d13526cf32783cad261e9bd8b/osu!/Audio/AudioEngine.cs#L1136-L1230 but honestly if you put a gun to my head I couldn't be sure if it matches completely in every possible circumstance or not. --- osu.Game/Skinning/LegacySkin.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index bbed434b3a..b648299787 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -594,12 +594,17 @@ namespace osu.Game.Skinning { var lookupNames = hitSample.LookupNames.SelectMany(getFallbackSampleNames); - if (!UseCustomSampleBanks && !string.IsNullOrEmpty(hitSample.Suffix)) + if (!string.IsNullOrEmpty(hitSample.Suffix)) { - // for compatibility with stable, exclude the lookup names with the custom sample bank suffix, if they are not valid for use in this skin. + // for compatibility with stable: + // - if the skin can use custom sample banks, it MUST use the custom sample bank suffix. it is not allowed to fall back to a non-custom sound. + // - if the skin cannot use custom sample banks, it MUST NOT use the custom sample bank suffix. // using .EndsWith() is intentional as it ensures parity in all edge cases - // (see LegacyTaikoSampleInfo for an example of one - prioritising the taiko prefix should still apply, but the sample bank should not). - lookupNames = lookupNames.Where(name => !name.EndsWith(hitSample.Suffix, StringComparison.Ordinal)); + // (see LegacyTaikoSampleInfo for an example of one - prioritising the taiko prefix should still apply). + if (UseCustomSampleBanks) + lookupNames = lookupNames.Where(name => name.EndsWith(hitSample.Suffix, StringComparison.Ordinal)); + else + lookupNames = lookupNames.Where(name => !name.EndsWith(hitSample.Suffix, StringComparison.Ordinal)); } foreach (string l in lookupNames) From 360ba548dc3048f5a2ae3eb2bf233a282aa258c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Jun 2025 13:24:45 +0200 Subject: [PATCH 306/370] Explicitly explain to users that failed plays do not give pp on results screen Addresses https://osu.ppy.sh/community/forums/topics/2096912. --- osu.Game/Localisation/ResultsScreenStrings.cs | 5 +++++ .../Ranking/Expanded/Statistics/PerformanceStatistic.cs | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/osu.Game/Localisation/ResultsScreenStrings.cs b/osu.Game/Localisation/ResultsScreenStrings.cs index 54e7717af9..143d5b70bc 100644 --- a/osu.Game/Localisation/ResultsScreenStrings.cs +++ b/osu.Game/Localisation/ResultsScreenStrings.cs @@ -19,6 +19,11 @@ namespace osu.Game.Localisation /// public static LocalisableString NoPPForUnrankedMods => new TranslatableString(getKey(@"no_pp_for_unranked_mods"), @"Performance points are not granted for this score because of unranked mods."); + /// + /// "Performance points are not granted for failed scores." + /// + public static LocalisableString NoPPForFailedScores => new TranslatableString(getKey(@"no_pp_for_failed_scores"), @"Performance points are not granted for failed scores."); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index 7d155e32b0..8a84501a17 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -79,6 +79,11 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics Alpha = 0.5f; TooltipText = ResultsScreenStrings.NoPPForUnrankedMods; } + else if (scoreInfo.Rank == ScoreRank.F) + { + Alpha = 0.5f; + TooltipText = ResultsScreenStrings.NoPPForFailedScores; + } else { Alpha = 1f; From 19abd4fbcab047153f6cd64eeb18c7b6256a0a69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Jun 2025 13:48:19 +0200 Subject: [PATCH 307/370] Make Flashlight test case exercising playfield scaling not useless It was doing a "if I touch the game in this very specific manner everything works" which will light up in very nice green colours but is actually useless for preventing regressions. --- .../Mods/TestSceneOsuModFlashlight.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs index 33ae2c68e6..496e7610ff 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Testing; -using osu.Framework.Utils; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Beatmaps; @@ -36,22 +35,21 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods [Test] public void TestPlayfieldBasedSize() { - ModFlashlight mod = new OsuModFlashlight(); + OsuModFlashlight flashlight; CreateModTest(new ModTestData { - Mod = mod, + Mods = [flashlight = new OsuModFlashlight(), new OsuModBarrelRoll()], PassCondition = () => { var flashlightOverlay = Player.DrawableRuleset.Overlays .ChildrenOfType.Flashlight>() .First(); - return Precision.AlmostEquals(mod.DefaultFlashlightSize * .5f, flashlightOverlay.GetSize()); + // the combo check is here because the flashlight radius decreases for the first time at 100 combo + // and hardcoding it here eliminates the need to meddle in flashlight internals further by e.g. exposing `GetComboScaleFor()` + return flashlightOverlay.GetSize() < flashlight.DefaultFlashlightSize && Player.GameplayState.ScoreProcessor.Combo.Value < 100; } }); - - AddStep("adjust playfield scale", () => - Player.DrawableRuleset.Playfield.Scale = new Vector2(.5f)); } [Test] From 96569e78295123f6c27d60aa94edecfe63536c89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Jun 2025 13:40:15 +0200 Subject: [PATCH 308/370] Fix Flashlight having increased radius when Barrel Roll is active Closes https://github.com/ppy/osu/issues/33893. Regressed in https://github.com/ppy/osu/pull/29841. This sucks but I don't have better ideas. --- osu.Game/Rulesets/Mods/ModFlashlight.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index 64c193d25f..a88d714dce 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs @@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Mods flashlight.Colour = Color4.Black; flashlight.Combo.BindTo(Combo); - flashlight.GetPlayfieldScale = () => drawableRuleset.Playfield.Scale; + flashlight.GetPlayfieldScale = () => drawableRuleset.PlayfieldAdjustmentContainer.Scale; drawableRuleset.Overlays.Add(new Container { From edafac2aaa348eda4d0e0b71d3b066db90eaf9ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Jun 2025 14:06:30 +0200 Subject: [PATCH 309/370] Adjust tests to pass (and add test coverage of fail case) --- .../TestSceneOsuHitObjectSamples.cs | 21 ++++++++++--------- .../TestSceneTaikoHitObjectSamples.cs | 21 ++++++++++--------- .../Gameplay/TestSceneHitObjectSamples.cs | 3 --- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuHitObjectSamples.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuHitObjectSamples.cs index 61cc10f284..ddea5eed87 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuHitObjectSamples.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuHitObjectSamples.cs @@ -14,26 +14,27 @@ namespace osu.Game.Rulesets.Osu.Tests protected override IResourceStore RulesetResources => new DllResourceStore(Assembly.GetAssembly(typeof(TestSceneOsuHitObjectSamples))); - [TestCase("normal-hitnormal")] - [TestCase("hitnormal")] - public void TestDefaultCustomSampleFromBeatmap(string expectedSample) + [TestCase("normal-hitnormal2", "normal-hitnormal")] + [TestCase("hitnormal", "hitnormal")] + public void TestDefaultCustomSampleFromBeatmap(string beatmapSkinSampleName, string userSkinSampleName) { - SetupSkins(expectedSample, expectedSample); + SetupSkins(beatmapSkinSampleName, userSkinSampleName); CreateTestWithBeatmap("osu-hitobject-beatmap-custom-sample-bank.osu"); - AssertBeatmapLookup(expectedSample); + AssertBeatmapLookup(beatmapSkinSampleName); } - [TestCase("normal-hitnormal")] - [TestCase("hitnormal")] - public void TestDefaultCustomSampleFromUserSkinFallback(string expectedSample) + [TestCase("", "normal-hitnormal")] + [TestCase("normal-hitnormal", "normal-hitnormal")] + [TestCase("", "hitnormal")] + public void TestDefaultCustomSampleFromUserSkinFallback(string beatmapSkinSampleName, string userSkinSampleName) { - SetupSkins(string.Empty, expectedSample); + SetupSkins(beatmapSkinSampleName, userSkinSampleName); CreateTestWithBeatmap("osu-hitobject-beatmap-custom-sample-bank.osu"); - AssertUserLookup(expectedSample); + AssertUserLookup(userSkinSampleName); } [TestCase("normal-hitnormal2")] diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectSamples.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectSamples.cs index 1d1e82fb07..b1df133c30 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectSamples.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectSamples.cs @@ -14,26 +14,27 @@ namespace osu.Game.Rulesets.Taiko.Tests protected override IResourceStore RulesetResources => new DllResourceStore(Assembly.GetAssembly(typeof(TestSceneTaikoHitObjectSamples))); - [TestCase("taiko-normal-hitnormal")] - [TestCase("hitnormal")] - public void TestDefaultCustomSampleFromBeatmap(string expectedSample) + [TestCase("taiko-normal-hitnormal2", "taiko-normal-hitnormal")] + [TestCase("hitnormal", "hitnormal")] + public void TestDefaultCustomSampleFromBeatmap(string beatmapSkinSampleName, string userSkinSampleName) { - SetupSkins(expectedSample, expectedSample); + SetupSkins(beatmapSkinSampleName, userSkinSampleName); CreateTestWithBeatmap("taiko-hitobject-beatmap-custom-sample-bank.osu"); - AssertBeatmapLookup(expectedSample); + AssertBeatmapLookup(beatmapSkinSampleName); } - [TestCase("taiko-normal-hitnormal")] - [TestCase("hitnormal")] - public void TestDefaultCustomSampleFromUserSkinFallback(string expectedSample) + [TestCase("", "taiko-normal-hitnormal")] + [TestCase("taiko-normal-hitnormal", "taiko-normal-hitnormal")] + [TestCase("", "hitnormal")] + public void TestDefaultCustomSampleFromUserSkinFallback(string beatmapSkinSampleName, string userSkinSampleName) { - SetupSkins(string.Empty, expectedSample); + SetupSkins(beatmapSkinSampleName, userSkinSampleName); CreateTestWithBeatmap("taiko-hitobject-beatmap-custom-sample-bank.osu"); - AssertUserLookup(expectedSample); + AssertUserLookup(userSkinSampleName); } [TestCase("taiko-normal-hitnormal2")] diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs index d198ef5074..c9f5f50232 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs @@ -64,11 +64,9 @@ namespace osu.Game.Tests.Gameplay /// /// Tests that a hitobject which provides a custom sample set of 2 retrieves the following samples from the beatmap skin: /// normal-hitnormal2 - /// normal-hitnormal /// hitnormal /// [TestCase("normal-hitnormal2")] - [TestCase("normal-hitnormal")] [TestCase("hitnormal")] public void TestDefaultCustomSampleFromBeatmap(string expectedSample) { @@ -162,7 +160,6 @@ namespace osu.Game.Tests.Gameplay /// Tests that a control point that provides a custom sample of 2 causes . /// [TestCase("normal-hitnormal2")] - [TestCase("normal-hitnormal")] [TestCase("hitnormal")] public void TestControlPointCustomSampleFromBeatmap(string sampleName) { From 5c89644d5893f15990a4f12dfc8a0d50f9c8c5ed Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 29 Jun 2025 00:24:12 +0900 Subject: [PATCH 310/370] Fix flaky `TestSceneGameplaySamplePlayback` test See: https://github.com/ppy/osu/actions/runs/15924307907/job/44920278903 Similar null-checks within `Update()` are present in the `PlayerTestScene` superclass. --- .../Visual/Gameplay/TestSceneGameplaySamplePlayback.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs index 2334b1c6d6..84b312d5ee 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs @@ -9,7 +9,6 @@ using osu.Game.Audio; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Screens.Play; using osu.Game.Skinning; namespace osu.Game.Tests.Visual.Gameplay @@ -74,8 +73,8 @@ namespace osu.Game.Tests.Visual.Gameplay // // We want to keep seeking while asserting various test conditions, so // continue to seek until we unset the flag. - var gameplayClockContainer = Player.ChildrenOfType().First(); - gameplayClockContainer.Seek(gameplayClockContainer.CurrentTime > 30000 ? 0 : 60000); + var gameplayClockContainer = Player?.GameplayClockContainer; + gameplayClockContainer?.Seek(gameplayClockContainer.CurrentTime > 30000 ? 0 : 60000); } } From 9155f566ecb11cf9da75729c93161f4ae2d38fec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Jun 2025 16:09:23 +0900 Subject: [PATCH 311/370] Fix random sound effect not playing correctly --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index b09490ce32..d33d5dbd87 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -667,7 +667,9 @@ namespace osu.Game.Screens.SelectV2 return false; } - Scheduler.Add(() => + // CurrentSelectionItem won't be valid until UpdaterAfterChildren. + // We probably want to fix this at some point since a few places are working-around this quirk. + ScheduleAfterChildren(() => { if (selectionBefore != null && CurrentSelectionItem != null) playSpinSample(distanceBetween(selectionBefore, CurrentSelectionItem), carouselItems.Count); From bf8b6754dc3f54d205af30dc12ccc0aba9593e21 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Jun 2025 16:09:34 +0900 Subject: [PATCH 312/370] Play error sound when random selection fails --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 10 ++++++---- osu.Game/Screens/SelectV2/SongSelect.cs | 20 +++++++++++++++++--- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index d33d5dbd87..d343bd9e12 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -771,12 +771,12 @@ namespace osu.Game.Screens.SelectV2 return true; } - public void PreviousRandom() + public bool PreviousRandom() { var carouselItems = GetCarouselItems(); if (carouselItems?.Any() != true) - return; + return false; while (randomHistory.Any()) { @@ -786,7 +786,7 @@ namespace osu.Game.Screens.SelectV2 var previousBeatmapItem = carouselItems.FirstOrDefault(i => i.Model is BeatmapInfo b && b.Equals(previousBeatmap)); if (previousBeatmapItem == null) - return; + return false; if (CurrentSelection is BeatmapInfo beatmapInfo) { @@ -800,8 +800,10 @@ namespace osu.Game.Screens.SelectV2 } RequestSelection(previousBeatmap); - break; + return true; } + + return false; } private double distanceBetween(CarouselItem item1, CarouselItem item2) => Math.Ceiling(Math.Abs(item1.CarouselYPosition - item2.CarouselYPosition) / PanelBeatmapSet.HEIGHT); diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 4c30662bd4..e26c72575a 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -6,6 +6,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -103,6 +105,8 @@ namespace osu.Game.Screens.SelectV2 public override bool ShowFooter => true; + private Sample? errorSample; + [Resolved] private OsuGameBase? game { get; set; } @@ -128,8 +132,10 @@ namespace osu.Game.Screens.SelectV2 private IDialogOverlay? dialogOverlay { get; set; } [BackgroundDependencyLoader] - private void load() + private void load(AudioManager audio) { + errorSample = audio.Samples.Get(@"UI/generic-error"); + AddRangeInternal(new Drawable[] { new GlobalScrollAdjustsVolume(), @@ -286,8 +292,16 @@ namespace osu.Game.Screens.SelectV2 }, new FooterButtonRandom { - NextRandom = () => carousel.NextRandom(), - PreviousRandom = () => carousel.PreviousRandom() + NextRandom = () => + { + if (!carousel.NextRandom()) + errorSample?.Play(); + }, + PreviousRandom = () => + { + if (!carousel.PreviousRandom()) + errorSample?.Play(); + } }, new FooterButtonOptions { From d7b76400553ffab3ab3f1179bbf9366faf0f2ee1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Jun 2025 16:21:41 +0900 Subject: [PATCH 313/370] Refactor distance-between-panels implementation The new version wasn't really working as expected, because the Y position measurement only considered visible panels, while it was being divided over all panels (including non-expanded groups or sets). Rather than trying to divide across all panels, just choose a sane number for the "highest pitch" sound and work with that as a constant. --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index d343bd9e12..cf7403972c 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -672,7 +672,7 @@ namespace osu.Game.Screens.SelectV2 ScheduleAfterChildren(() => { if (selectionBefore != null && CurrentSelectionItem != null) - playSpinSample(distanceBetween(selectionBefore, CurrentSelectionItem), carouselItems.Count); + playSpinSample(visiblePanelCountBetweenItems(selectionBefore, CurrentSelectionItem)); }); return true; @@ -794,9 +794,9 @@ namespace osu.Game.Screens.SelectV2 previouslyVisitedRandomBeatmaps.Remove(beatmapInfo); if (CurrentSelectionItem == null) - playSpinSample(0, carouselItems.Count); + playSpinSample(0); else - playSpinSample(distanceBetween(previousBeatmapItem, CurrentSelectionItem), carouselItems.Count); + playSpinSample(visiblePanelCountBetweenItems(previousBeatmapItem, CurrentSelectionItem)); } RequestSelection(previousBeatmap); @@ -806,15 +806,15 @@ namespace osu.Game.Screens.SelectV2 return false; } - private double distanceBetween(CarouselItem item1, CarouselItem item2) => Math.Ceiling(Math.Abs(item1.CarouselYPosition - item2.CarouselYPosition) / PanelBeatmapSet.HEIGHT); + private double visiblePanelCountBetweenItems(CarouselItem item1, CarouselItem item2) => Math.Ceiling(Math.Abs(item1.CarouselYPosition - item2.CarouselYPosition) / PanelBeatmapSet.HEIGHT); - private void playSpinSample(double distance, int count) + private void playSpinSample(double distance) { var chan = spinSample?.GetChannel(); if (chan != null) { - chan.Frequency.Value = 1f + Math.Min(1f, distance / count); + chan.Frequency.Value = 1f + Math.Clamp(distance / 200, 0, 1); chan.Play(); } From d1c0d58f2e33f0535563f25f324f0d8b4bf73269 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Jun 2025 16:49:43 +0900 Subject: [PATCH 314/370] Fix player settings no longer collapsing correctly Regressed with https://github.com/ppy/osu/pull/33621 for obvious reasons. Tachyon doing its job, caught this before hitting a proper release. --- .../Visual/Gameplay/TestSceneReplayPlayer.cs | 15 +++++++++++ .../Graphics/Containers/ExpandingContainer.cs | 25 ++++++++----------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs index 5db7a78983..b3ed4135a9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs @@ -12,6 +12,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Scoring; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; using osuTK; @@ -192,6 +193,20 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("player failed after 10000", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(10000)); } + [Test] + public void TestPlayerLoaderSettingsHover() + { + loadPlayerWithBeatmap(); + + AddUntilStep("wait for settings overlay hidden", () => settingsOverlay().Expanded.Value, () => Is.False); + AddStep("move mouse to right of screen", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopRight)); + AddUntilStep("wait for settings overlay visible", () => settingsOverlay().Expanded.Value, () => Is.True); + AddStep("move mouse to centre of screen", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre)); + AddUntilStep("wait for settings overlay hidden", () => settingsOverlay().Expanded.Value, () => Is.False); + + PlayerSettingsOverlay settingsOverlay() => Player.ChildrenOfType().Single(); + } + private void loadPlayerWithBeatmap(IBeatmap? beatmap = null) { AddStep("create player", () => diff --git a/osu.Game/Graphics/Containers/ExpandingContainer.cs b/osu.Game/Graphics/Containers/ExpandingContainer.cs index ad5c65c10e..4b70fd6987 100644 --- a/osu.Game/Graphics/Containers/ExpandingContainer.cs +++ b/osu.Game/Graphics/Containers/ExpandingContainer.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -41,24 +40,20 @@ namespace osu.Game.Graphics.Containers RelativeSizeAxes = Axes.Y; Width = contractedWidth; - FillFlow = new FillFlowContainer - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - }; - } - - [BackgroundDependencyLoader] - private void load() - { InternalChild = CreateScrollContainer().With(s => { s.RelativeSizeAxes = Axes.Both; s.ScrollbarVisible = false; - }).WithChild(FillFlow); + }).WithChild( + FillFlow = new FillFlowContainer + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + } + ); } protected virtual OsuScrollContainer CreateScrollContainer() => new OsuScrollContainer(); From d4a863d00abb036c4c694d255ae016cd2b37afef Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 30 Jun 2025 11:10:43 +0300 Subject: [PATCH 315/370] Use `MaximumSize` to limit picker background width Co-authored-by: Joseph Madamba --- osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs index 9cc9ca87de..f2630caa83 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs @@ -71,8 +71,7 @@ namespace osu.Game.Overlays.BeatmapSet { new Container { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + AutoSizeAxes = Axes.Both, Margin = new MarginPadding { Left = -(tile_icon_padding + tile_spacing / 2), Bottom = 10 }, Children = new Drawable[] { @@ -89,8 +88,7 @@ namespace osu.Game.Overlays.BeatmapSet }, Difficulties = new DifficultiesContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + AutoSizeAxes = Axes.Both, OnLostHover = () => showBeatmap(Beatmap.Value, withStarRating: false), }, } @@ -144,6 +142,12 @@ namespace osu.Game.Overlays.BeatmapSet Beatmap.TriggerChange(); } + protected override void Update() + { + base.Update(); + Difficulties.MaximumSize = new Vector2(DrawWidth, float.MaxValue); + } + private void updateDisplay() { Difficulties.Clear(); From 2b92b59504eab5e29b2d2951a09bedb8b5ef2f00 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 30 Jun 2025 11:58:38 +0300 Subject: [PATCH 316/370] Remove unnecessary skin settings descriptions --- .../BeatmapAttributeTextStrings.cs | 5 ---- .../SkinnableComponentStrings.cs | 30 ------------------- .../Screens/Play/HUD/ArgonAccuracyCounter.cs | 2 +- .../Screens/Play/HUD/ArgonComboCounter.cs | 2 +- .../Play/HUD/ArgonPerformancePointsCounter.cs | 2 +- .../Screens/Play/HUD/ArgonScoreCounter.cs | 2 +- .../Screens/Play/HUD/ArgonSongProgress.cs | 2 +- osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs | 2 +- .../Screens/Play/HUD/DefaultSongProgress.cs | 2 +- .../Components/BeatmapAttributeText.cs | 2 +- osu.Game/Skinning/Components/BoxElement.cs | 2 +- osu.Game/Skinning/Components/TextElement.cs | 2 +- .../Skinning/FontAdjustableSkinComponent.cs | 4 +-- osu.Game/Skinning/SkinnableSprite.cs | 2 +- 14 files changed, 13 insertions(+), 48 deletions(-) diff --git a/osu.Game/Localisation/SkinComponents/BeatmapAttributeTextStrings.cs b/osu.Game/Localisation/SkinComponents/BeatmapAttributeTextStrings.cs index 390a6f9ca4..4ddffe615f 100644 --- a/osu.Game/Localisation/SkinComponents/BeatmapAttributeTextStrings.cs +++ b/osu.Game/Localisation/SkinComponents/BeatmapAttributeTextStrings.cs @@ -14,11 +14,6 @@ namespace osu.Game.Localisation.SkinComponents /// public static LocalisableString Attribute => new TranslatableString(getKey(@"attribute"), @"Attribute"); - /// - /// "The attribute to be displayed." - /// - public static LocalisableString AttributeDescription => new TranslatableString(getKey(@"attribute_description"), @"The attribute to be displayed."); - /// /// "Template" /// diff --git a/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs index 35ed1ea55c..2f34987e8e 100644 --- a/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs +++ b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs @@ -14,31 +14,16 @@ namespace osu.Game.Localisation.SkinComponents /// public static LocalisableString SpriteName => new TranslatableString(getKey(@"sprite_name"), @"Sprite name"); - /// - /// "The filename of the sprite" - /// - public static LocalisableString SpriteNameDescription => new TranslatableString(getKey(@"sprite_name_description"), @"The filename of the sprite"); - /// /// "Font" /// public static LocalisableString Font => new TranslatableString(getKey(@"font"), @"Font"); - /// - /// "The font to use." - /// - public static LocalisableString FontDescription => new TranslatableString(getKey(@"font_description"), @"The font to use."); - /// /// "Text" /// public static LocalisableString TextElementText => new TranslatableString(getKey(@"text_element_text"), @"Text"); - /// - /// "The text to be displayed." - /// - public static LocalisableString TextElementTextDescription => new TranslatableString(getKey(@"text_element_text_description"), @"The text to be displayed."); - /// /// "Corner radius" /// @@ -54,31 +39,16 @@ namespace osu.Game.Localisation.SkinComponents /// public static LocalisableString ShowLabel => new TranslatableString(getKey(@"show_label"), @"Show label"); - /// - /// "Whether the component's label should be shown." - /// - public static LocalisableString ShowLabelDescription => new TranslatableString(getKey(@"show_label_description"), @"Whether the component's label should be shown."); - /// /// "Colour" /// public static LocalisableString Colour => new TranslatableString(getKey(@"colour"), @"Colour"); - /// - /// "The colour of the component." - /// - public static LocalisableString ColourDescription => new TranslatableString(getKey(@"colour_description"), @"The colour of the component."); - /// /// "Text colour" /// public static LocalisableString TextColour => new TranslatableString(getKey(@"text_colour"), @"Text colour"); - /// - /// "The colour of the text." - /// - public static LocalisableString TextColourDescription => new TranslatableString(getKey(@"text_colour_description"), @"The colour of the text."); - /// /// "Text weight" /// diff --git a/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs index d7fe1f52ff..c4cf52c254 100644 --- a/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.Play.HUD MaxValue = 1, }; - [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.ShowLabel), nameof(SkinnableComponentStrings.ShowLabelDescription))] + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.ShowLabel))] public Bindable ShowLabel { get; } = new BindableBool(true); public bool UsesFixedAnchor { get; set; } diff --git a/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs b/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs index e82e8f4b6f..22d65601cd 100644 --- a/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Play.HUD MaxValue = 1, }; - [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.ShowLabel), nameof(SkinnableComponentStrings.ShowLabelDescription))] + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.ShowLabel))] public Bindable ShowLabel { get; } = new BindableBool(true); [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Play/HUD/ArgonPerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/ArgonPerformancePointsCounter.cs index 1620da2f2e..8e9360920c 100644 --- a/osu.Game/Screens/Play/HUD/ArgonPerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonPerformancePointsCounter.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Play.HUD MaxValue = 1, }; - [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.ShowLabel), nameof(SkinnableComponentStrings.ShowLabelDescription))] + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.ShowLabel))] public Bindable ShowLabel { get; } = new BindableBool(true); public override bool IsValid diff --git a/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs index 8658651407..f000a5977c 100644 --- a/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs @@ -28,7 +28,7 @@ namespace osu.Game.Screens.Play.HUD MaxValue = 1, }; - [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.ShowLabel), nameof(SkinnableComponentStrings.ShowLabelDescription))] + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.ShowLabel))] public Bindable ShowLabel { get; } = new BindableBool(true); public bool UsesFixedAnchor { get; set; } diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs index 8dc5d60352..5b2efb447b 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Play.HUD [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.UseRelativeSize))] public BindableBool UseRelativeSize { get; } = new BindableBool(true); - [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))] + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour))] public BindableColour4 AccentColour { get; } = new BindableColour4(Colour4.White); [Resolved] diff --git a/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs b/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs index 46a658cd1c..810100532b 100644 --- a/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs +++ b/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs @@ -22,7 +22,7 @@ namespace osu.Game.Screens.Play.HUD [SettingSource("Inverted shear")] public BindableBool InvertShear { get; } = new BindableBool(); - [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))] + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour))] public BindableColour4 AccentColour { get; } = new BindableColour4(Color4Extensions.FromHex("#66CCFF")); public ArgonWedgePiece() diff --git a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs index 672017750d..06d541d838 100644 --- a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.Play.HUD [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.UseRelativeSize))] public BindableBool UseRelativeSize { get; } = new BindableBool(true); - [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))] + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour))] public BindableColour4 AccentColour { get; } = new BindableColour4(Colour4.White); [Resolved] diff --git a/osu.Game/Skinning/Components/BeatmapAttributeText.cs b/osu.Game/Skinning/Components/BeatmapAttributeText.cs index 76c8d54f50..58821f869a 100644 --- a/osu.Game/Skinning/Components/BeatmapAttributeText.cs +++ b/osu.Game/Skinning/Components/BeatmapAttributeText.cs @@ -29,7 +29,7 @@ namespace osu.Game.Skinning.Components [UsedImplicitly] public partial class BeatmapAttributeText : FontAdjustableSkinComponent { - [SettingSource(typeof(BeatmapAttributeTextStrings), nameof(BeatmapAttributeTextStrings.Attribute), nameof(BeatmapAttributeTextStrings.AttributeDescription))] + [SettingSource(typeof(BeatmapAttributeTextStrings), nameof(BeatmapAttributeTextStrings.Attribute))] public Bindable Attribute { get; } = new Bindable(BeatmapAttribute.StarRating); [SettingSource(typeof(BeatmapAttributeTextStrings), nameof(BeatmapAttributeTextStrings.Template), nameof(BeatmapAttributeTextStrings.TemplateDescription))] diff --git a/osu.Game/Skinning/Components/BoxElement.cs b/osu.Game/Skinning/Components/BoxElement.cs index 7f052a8523..ddfa1aa446 100644 --- a/osu.Game/Skinning/Components/BoxElement.cs +++ b/osu.Game/Skinning/Components/BoxElement.cs @@ -27,7 +27,7 @@ namespace osu.Game.Skinning.Components Precision = 0.01f }; - [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))] + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour))] public BindableColour4 AccentColour { get; } = new BindableColour4(Colour4.White); public BoxElement() diff --git a/osu.Game/Skinning/Components/TextElement.cs b/osu.Game/Skinning/Components/TextElement.cs index 6e875c5590..a271857c03 100644 --- a/osu.Game/Skinning/Components/TextElement.cs +++ b/osu.Game/Skinning/Components/TextElement.cs @@ -15,7 +15,7 @@ namespace osu.Game.Skinning.Components [UsedImplicitly] public partial class TextElement : FontAdjustableSkinComponent { - [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextElementText), nameof(SkinnableComponentStrings.TextElementTextDescription))] + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextElementText))] public Bindable Text { get; } = new Bindable("Circles!"); private readonly OsuSpriteText text; diff --git a/osu.Game/Skinning/FontAdjustableSkinComponent.cs b/osu.Game/Skinning/FontAdjustableSkinComponent.cs index f2d8c9e440..eba29e9b79 100644 --- a/osu.Game/Skinning/FontAdjustableSkinComponent.cs +++ b/osu.Game/Skinning/FontAdjustableSkinComponent.cs @@ -21,13 +21,13 @@ namespace osu.Game.Skinning { public bool UsesFixedAnchor { get; set; } - [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))] + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font))] public Bindable Font { get; } = new Bindable(Typeface.Torus); [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextWeight), SettingControlType = typeof(WeightDropdown))] public Bindable TextWeight { get; } = new Bindable(FontWeight.Regular); - [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour), nameof(SkinnableComponentStrings.TextColourDescription))] + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour))] public BindableColour4 TextColour { get; } = new BindableColour4(Colour4.White); /// diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs index 47618f6296..49ce7e48ab 100644 --- a/osu.Game/Skinning/SkinnableSprite.cs +++ b/osu.Game/Skinning/SkinnableSprite.cs @@ -29,7 +29,7 @@ namespace osu.Game.Skinning [Resolved] private TextureStore textures { get; set; } = null!; - [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.SpriteName), nameof(SkinnableComponentStrings.SpriteNameDescription), SettingControlType = typeof(SpriteSelectorControl))] + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.SpriteName), SettingControlType = typeof(SpriteSelectorControl))] public Bindable SpriteName { get; } = new Bindable(string.Empty); [Resolved] From a8e3ce9af15bc392866a57d5503ede8d7005398f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Jun 2025 11:03:06 +0200 Subject: [PATCH 317/370] Fix typo --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index cf7403972c..ce7bd7582e 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -667,7 +667,7 @@ namespace osu.Game.Screens.SelectV2 return false; } - // CurrentSelectionItem won't be valid until UpdaterAfterChildren. + // CurrentSelectionItem won't be valid until UpdateAfterChildren. // We probably want to fix this at some point since a few places are working-around this quirk. ScheduleAfterChildren(() => { From 535d9f5b589f6987d7ca337d24ef5b238f338c1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Jun 2025 11:50:31 +0200 Subject: [PATCH 318/370] Read & output combo indices in timestamps in catch editor Addresses https://github.com/ppy/osu/discussions/33912 for catch specifically. Code copy-pasted from osu! ruleset. I'm leaving taiko be because new combo still doesn't make any sense in taiko. It'd probably either have to be object index in beatmap period instead of index in combo, or the `kdkdkdkdkkkdkdkkkdkkdkd` notation people use. --- .../Edit/CatchHitObjectComposer.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index dfe9dc9dd8..370eb37d16 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -219,5 +220,40 @@ namespace osu.Game.Rulesets.Catch.Edit distanceSnapGrid.StartTime = sourceHitObject.GetEndTime(); distanceSnapGrid.StartX = sourceHitObject.EffectiveX; } + + #region Clipboard handling + + public override string ConvertSelectionToString() + => string.Join(',', EditorBeatmap.SelectedHitObjects.Cast().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString())); + + // 1,2,3,4 ... + private static readonly Regex selection_regex = new Regex(@"^\d+(,\d+)*$", RegexOptions.Compiled); + + public override void SelectFromTimestamp(double timestamp, string objectDescription) + { + if (!selection_regex.IsMatch(objectDescription)) + return; + + List remainingHitObjects = EditorBeatmap.HitObjects.Cast().Where(h => h.StartTime >= timestamp).ToList(); + string[] splitDescription = objectDescription.Split(','); + + for (int i = 0; i < splitDescription.Length; i++) + { + if (!int.TryParse(splitDescription[i], out int combo) || combo < 1) + continue; + + CatchHitObject? current = remainingHitObjects.FirstOrDefault(h => h.IndexInCurrentCombo + 1 == combo); + + if (current == null) + continue; + + EditorBeatmap.SelectedHitObjects.Add(current); + + if (i < splitDescription.Length - 1) + remainingHitObjects = remainingHitObjects.Where(h => h != current && h.StartTime >= current.StartTime).ToList(); + } + } + + #endregion } } From 28caa03d21fc1c9b0f2d84c90d1813821c3f7bc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Jun 2025 12:19:54 +0200 Subject: [PATCH 319/370] Fix Difficulty Adjust extended mod icon information not showing with extended limits active - Closes https://github.com/ppy/osu/issues/33522. - Alternative to / closes https://github.com/ppy/osu/pull/33561. --- .../Mods/CatchModDifficultyAdjust.cs | 2 +- .../Mods/OsuModDifficultyAdjust.cs | 2 +- .../Mods/TaikoModDifficultyAdjust.cs | 2 +- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 41 +++++++++---------- 4 files changed, 22 insertions(+), 25 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index c300afa79f..e4a910700c 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Mods { get { - if (UserAdjustedSettingsCount != 1) + if (!IsExactlyOneSettingChanged(CircleSize, ApproachRate, OverallDifficulty, DrainRate)) return string.Empty; if (!CircleSize.IsDefault) return format("CS", CircleSize); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index 1d94ac6335..1c3b7360bc 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Mods { get { - if (UserAdjustedSettingsCount != 1) + if (!IsExactlyOneSettingChanged(CircleSize, ApproachRate, OverallDifficulty, DrainRate)) return string.Empty; if (!CircleSize.IsDefault) return format("CS", CircleSize); diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs index 57b57555c2..296342ed97 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Mods { get { - if (UserAdjustedSettingsCount != 1) + if (!IsExactlyOneSettingChanged(ScrollSpeed, OverallDifficulty, DrainRate)) return string.Empty; if (!ScrollSpeed.IsDefault) return format("SC", ScrollSpeed); diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index 4fd9916b89..dbc690bd15 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Mods { get { - if (UserAdjustedSettingsCount != 1) + if (!IsExactlyOneSettingChanged(OverallDifficulty, DrainRate)) return string.Empty; if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty); @@ -84,6 +84,24 @@ namespace osu.Game.Rulesets.Mods } } + protected bool IsExactlyOneSettingChanged(params DifficultyBindable[] difficultySettings) + { + DifficultyBindable? changedSetting = null; + + foreach (var setting in difficultySettings) + { + if (setting.IsDefault) + continue; + + if (changedSetting != null) + return false; + + changedSetting = setting; + } + + return changedSetting != null; + } + public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription { get @@ -107,26 +125,5 @@ namespace osu.Game.Rulesets.Mods if (DrainRate.Value != null) difficulty.DrainRate = DrainRate.Value.Value; if (OverallDifficulty.Value != null) difficulty.OverallDifficulty = OverallDifficulty.Value.Value; } - - /// - /// The number of settings on this mod instance which have been adjusted by the user from their default values. - /// - protected int UserAdjustedSettingsCount - { - get - { - int count = 0; - - foreach (var (_, property) in this.GetSettingsSourceProperties()) - { - var bindable = (IBindable)property.GetValue(this)!; - - if (!bindable.IsDefault) - count++; - } - - return count; - } - } } } From cae1f8bb88d4270c794259302d265301132ce375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Jun 2025 12:24:52 +0200 Subject: [PATCH 320/370] Fix taiko Difficulty Adjust scroll speed value getting truncated on extended mod icon information --- osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs index 296342ed97..b06d1fe5ac 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs @@ -28,13 +28,13 @@ namespace osu.Game.Rulesets.Taiko.Mods if (!IsExactlyOneSettingChanged(ScrollSpeed, OverallDifficulty, DrainRate)) return string.Empty; - if (!ScrollSpeed.IsDefault) return format("SC", ScrollSpeed); - if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty); - if (!DrainRate.IsDefault) return format("HP", DrainRate); + if (!ScrollSpeed.IsDefault) return format("SC", ScrollSpeed, 2); + if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty, 1); + if (!DrainRate.IsDefault) return format("HP", DrainRate, 1); return string.Empty; - string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}"; + string format(string acronym, DifficultyBindable bindable, int digits) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(digits)}"; } } From a95987aadfbe876ec0fddfea8846ecc3007fa547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Jun 2025 12:54:16 +0200 Subject: [PATCH 321/370] Move external edit overlay to more proper namespace --- osu.Game/Overlays/{ => SkinEditor}/ExternalEditOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename osu.Game/Overlays/{ => SkinEditor}/ExternalEditOverlay.cs (99%) diff --git a/osu.Game/Overlays/ExternalEditOverlay.cs b/osu.Game/Overlays/SkinEditor/ExternalEditOverlay.cs similarity index 99% rename from osu.Game/Overlays/ExternalEditOverlay.cs rename to osu.Game/Overlays/SkinEditor/ExternalEditOverlay.cs index e9b3590626..89b36476ec 100644 --- a/osu.Game/Overlays/ExternalEditOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/ExternalEditOverlay.cs @@ -29,7 +29,7 @@ using osu.Game.Skinning; using osuTK; using osuTK.Graphics; -namespace osu.Game.Overlays +namespace osu.Game.Overlays.SkinEditor { public partial class ExternalEditOverlay : OsuFocusedOverlayContainer { From bccdf4213308a05d23d6606e3c0e9fd0b9e71ef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Jun 2025 12:59:37 +0200 Subject: [PATCH 322/370] Eliminate weird parameter passing --- .../Overlays/SkinEditor/ExternalEditOverlay.cs | 18 ++++++------------ osu.Game/Overlays/SkinEditor/SkinEditor.cs | 2 +- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/ExternalEditOverlay.cs b/osu.Game/Overlays/SkinEditor/ExternalEditOverlay.cs index 89b36476ec..edf7db2f7d 100644 --- a/osu.Game/Overlays/SkinEditor/ExternalEditOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/ExternalEditOverlay.cs @@ -5,7 +5,6 @@ using System; using System.IO; using System.Threading.Tasks; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -42,10 +41,10 @@ namespace osu.Game.Overlays.SkinEditor [Resolved] private GameHost gameHost { get; set; } = null!; - private ExternalEditOperation? editOperation; + [Resolved] + private SkinManager skinManager { get; set; } = null!; - private Bindable? skinBindable; - private SkinManager? skinManager; + private ExternalEditOperation? editOperation; protected override bool DimMainContent => false; @@ -96,7 +95,7 @@ namespace osu.Game.Overlays.SkinEditor }; } - public async Task Begin(SkinInfo skinInfo, Bindable skinBindable, SkinManager skinManager) + public async Task Begin(SkinInfo skinInfo) { Show(); showSpinner("Mounting external skin..."); @@ -115,9 +114,6 @@ namespace osu.Game.Overlays.SkinEditor Hide(); } - this.skinBindable = skinBindable; - this.skinManager = skinManager; - Schedule(() => { flow.Children = new Drawable[] @@ -194,12 +190,12 @@ namespace osu.Game.Overlays.SkinEditor Schedule(() => { - var oldSkin = skinBindable!.Value; + var oldSkin = skinManager.CurrentSkin!.Value; var newSkinInfo = oldSkin.SkinInfo.PerformRead(s => s); // Create a new skin instance to ensure the skin is reloaded // If there's a better way to reload the skin, this should be replaced with it. - skinBindable.Value = newSkinInfo.CreateInstance(skinManager!); + skinManager.CurrentSkin.Value = newSkinInfo.CreateInstance(skinManager!); oldSkin.Dispose(); @@ -218,8 +214,6 @@ namespace osu.Game.Overlays.SkinEditor { // Set everything to a clean state editOperation = null; - skinManager = null; - skinBindable = null; flow.Children = Array.Empty(); }); } diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 3aade5edc4..22ef80be84 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -284,7 +284,7 @@ namespace osu.Game.Overlays.SkinEditor { var skin = currentSkin.Value.SkinInfo.PerformRead(s => s.Detach()); - await externalEditOverlay!.Begin(skin, currentSkin, skins).ConfigureAwait(false); + await externalEditOverlay!.Begin(skin).ConfigureAwait(false); } public bool OnPressed(KeyBindingPressEvent e) From b82bf228aba313a39bc92039b6fd00e390c3bac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Jun 2025 13:23:49 +0200 Subject: [PATCH 323/370] Use better method of disallowing skin changes during external edit --- osu.Game/OsuGame.cs | 17 ++--------------- .../Overlays/Settings/Sections/SkinSection.cs | 15 ++++++++++++--- .../Overlays/SkinEditor/ExternalEditOverlay.cs | 12 +++++++++++- osu.Game/Skinning/SkinManager.cs | 4 ++++ 4 files changed, 29 insertions(+), 19 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 831a24bbd5..9e524878dc 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -138,8 +138,6 @@ namespace osu.Game private SkinEditorOverlay skinEditor; - private ExternalEditOverlay externalEditOverlay; - private Container overlayContent; private Container rightFloatingOverlayContent; @@ -1227,7 +1225,7 @@ namespace osu.Game loadComponentSingleFile(beatmapSetOverlay = new BeatmapSetOverlay(), overlayContent.Add, true); loadComponentSingleFile(wikiOverlay = new WikiOverlay(), overlayContent.Add, true); loadComponentSingleFile(skinEditor = new SkinEditorOverlay(ScreenContainer), overlayContent.Add, true); - loadComponentSingleFile(externalEditOverlay = new ExternalEditOverlay(), overlayContent.Add, true); + loadComponentSingleFile(new ExternalEditOverlay(), overlayContent.Add, true); loadComponentSingleFile(new LoginOverlay { @@ -1278,17 +1276,6 @@ namespace osu.Game }; } - Settings.State.ValueChanged += state => - { - if (state.NewValue == Visibility.Hidden) - return; - - if (externalEditOverlay.State.Value == Visibility.Visible) - { - Scheduler.Add(() => Settings.Hide()); - } - }; - // ensure only one of these overlays are open at once. var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, news, dashboard, beatmapListing, changelogOverlay, rankingsOverlay, wikiOverlay }; @@ -1600,7 +1587,7 @@ namespace osu.Game // Don't allow random skin selection while in the skin editor. // This is mainly to stop many "osu! default (modified)" skins being created via the SkinManager.EnsureMutableSkin() path. // If people want this to work we can potentially avoid selecting default skins when the editor is open, or allow a maximum of one mutable skin somehow. - if (skinEditor.State.Value == Visibility.Visible || externalEditOverlay.State.Value == Visibility.Visible) + if (skinEditor.State.Value == Visibility.Visible) return false; SkinManager.SelectRandomSkin(); diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 84767c8619..eef8030121 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -175,9 +175,12 @@ namespace osu.Game.Overlays.Settings.Sections base.LoadComplete(); currentSkin = skins.CurrentSkin.GetBoundCopy(); - currentSkin.BindValueChanged(skin => Enabled.Value = skin.NewValue.SkinInfo.PerformRead(s => !s.Protected), true); + currentSkin.BindValueChanged(_ => updateState()); + currentSkin.BindDisabledChanged(_ => updateState(), true); } + private void updateState() => Enabled.Value = !currentSkin.Disabled && currentSkin.Value.SkinInfo.PerformRead(s => !s.Protected); + public Popover GetPopover() { return new RenameSkinPopover(); @@ -203,9 +206,12 @@ namespace osu.Game.Overlays.Settings.Sections base.LoadComplete(); currentSkin = skins.CurrentSkin.GetBoundCopy(); - currentSkin.BindValueChanged(skin => Enabled.Value = skin.NewValue.SkinInfo.PerformRead(s => !s.Protected), true); + currentSkin.BindValueChanged(_ => updateState()); + currentSkin.BindDisabledChanged(_ => updateState(), true); } + private void updateState() => Enabled.Value = !currentSkin.Disabled && currentSkin.Value.SkinInfo.PerformRead(s => !s.Protected); + private void export() { try @@ -241,9 +247,12 @@ namespace osu.Game.Overlays.Settings.Sections base.LoadComplete(); currentSkin = skins.CurrentSkin.GetBoundCopy(); - currentSkin.BindValueChanged(skin => Enabled.Value = skin.NewValue.SkinInfo.PerformRead(s => !s.Protected), true); + currentSkin.BindValueChanged(_ => updateState()); + currentSkin.BindDisabledChanged(_ => updateState(), true); } + private void updateState() => Enabled.Value = !currentSkin.Disabled && currentSkin.Value.SkinInfo.PerformRead(s => !s.Protected); + private void delete() { dialogOverlay?.Push(new SkinDeleteDialog(currentSkin.Value)); diff --git a/osu.Game/Overlays/SkinEditor/ExternalEditOverlay.cs b/osu.Game/Overlays/SkinEditor/ExternalEditOverlay.cs index edf7db2f7d..d8dc01362c 100644 --- a/osu.Game/Overlays/SkinEditor/ExternalEditOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/ExternalEditOverlay.cs @@ -99,6 +99,7 @@ namespace osu.Game.Overlays.SkinEditor { Show(); showSpinner("Mounting external skin..."); + setGlobalSkinDisabled(true); await Task.Delay(500).ConfigureAwait(true); @@ -111,6 +112,7 @@ namespace osu.Game.Overlays.SkinEditor Logger.Log($"Failed to initialize external edit operation: {ex}", LoggingTarget.Database, LogLevel.Error); Schedule(() => showSpinner("Export failed!")); await Task.Delay(1000).ConfigureAwait(true); + setGlobalSkinDisabled(false); Hide(); } @@ -186,6 +188,7 @@ namespace osu.Game.Overlays.SkinEditor showSpinner("Import failed!"); await Task.Delay(1000).ConfigureAwait(true); Hide(); + setGlobalSkinDisabled(false); } Schedule(() => @@ -195,7 +198,8 @@ namespace osu.Game.Overlays.SkinEditor // Create a new skin instance to ensure the skin is reloaded // If there's a better way to reload the skin, this should be replaced with it. - skinManager.CurrentSkin.Value = newSkinInfo.CreateInstance(skinManager!); + setGlobalSkinDisabled(false); + skinManager.CurrentSkin.Value = newSkinInfo.CreateInstance(skinManager); oldSkin.Dispose(); @@ -203,6 +207,12 @@ namespace osu.Game.Overlays.SkinEditor }); } + private void setGlobalSkinDisabled(bool disabled) + { + skinManager.CurrentSkin.Disabled = disabled; + skinManager.CurrentSkinInfo.Disabled = disabled; + } + protected override void PopIn() { this.FadeIn(transition_duration, Easing.OutQuint); diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 9018c2e2c3..825d2f59c5 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -131,6 +131,10 @@ namespace osu.Game.Skinning { Realm.Run(r => { + // can be the case when the current skin is externally mounted for editing + if (CurrentSkinInfo.Disabled) + return; + // Required local for iOS. Will cause runtime crash if inlined. Guid currentSkinId = CurrentSkinInfo.Value.ID; From f63dc2dcea2cf0bb8ed380b404b266bf1cd7b966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Jun 2025 13:25:42 +0200 Subject: [PATCH 324/370] Ensure pending changes are saved before initiating external edit operation --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 22ef80be84..140c011e3c 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -282,6 +282,8 @@ namespace osu.Game.Overlays.SkinEditor private async Task editExternally() { + Save(); + var skin = currentSkin.Value.SkinInfo.PerformRead(s => s.Detach()); await externalEditOverlay!.Begin(skin).ConfigureAwait(false); From 17cfa7fcf3dd53e346e50028b2e1300f574264ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Jun 2025 13:35:53 +0200 Subject: [PATCH 325/370] Prevent hiding skin editor during external edit operation --- .../Overlays/SkinEditor/ExternalEditOverlay.cs | 16 +++++++++++++++- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 6 +++++- .../Overlays/SkinEditor/SkinEditorOverlay.cs | 8 ++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/ExternalEditOverlay.cs b/osu.Game/Overlays/SkinEditor/ExternalEditOverlay.cs index d8dc01362c..cbf85c6f2b 100644 --- a/osu.Game/Overlays/SkinEditor/ExternalEditOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/ExternalEditOverlay.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using System.IO; using System.Threading.Tasks; using osu.Framework.Allocation; @@ -45,6 +46,7 @@ namespace osu.Game.Overlays.SkinEditor private SkinManager skinManager { get; set; } = null!; private ExternalEditOperation? editOperation; + private TaskCompletionSource? taskCompletionSource; protected override bool DimMainContent => false; @@ -95,8 +97,11 @@ namespace osu.Game.Overlays.SkinEditor }; } - public async Task Begin(SkinInfo skinInfo) + public async Task Begin(SkinInfo skinInfo) { + if (taskCompletionSource != null) + throw new InvalidOperationException("Cannot start multiple concurrent external edits!"); + Show(); showSpinner("Mounting external skin..."); setGlobalSkinDisabled(true); @@ -114,6 +119,7 @@ namespace osu.Game.Overlays.SkinEditor await Task.Delay(1000).ConfigureAwait(true); setGlobalSkinDisabled(false); Hide(); + return Task.FromException(ex); } Schedule(() => @@ -163,6 +169,7 @@ namespace osu.Game.Overlays.SkinEditor b.Enabled.Value = true; openDirectory(); }, 1000); + return (taskCompletionSource = new TaskCompletionSource()).Task; } private void openDirectory() @@ -175,6 +182,8 @@ namespace osu.Game.Overlays.SkinEditor private async Task finish() { + Debug.Assert(taskCompletionSource != null); + showSpinner("Cleaning up..."); await Task.Delay(500).ConfigureAwait(true); @@ -189,6 +198,9 @@ namespace osu.Game.Overlays.SkinEditor await Task.Delay(1000).ConfigureAwait(true); Hide(); setGlobalSkinDisabled(false); + taskCompletionSource.SetException(ex); + taskCompletionSource = null; + return; } Schedule(() => @@ -205,6 +217,8 @@ namespace osu.Game.Overlays.SkinEditor Hide(); }); + taskCompletionSource.SetResult(); + taskCompletionSource = null; } private void setGlobalSkinDisabled(bool disabled) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 140c011e3c..f4a1bb7562 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -48,6 +48,8 @@ namespace osu.Game.Overlays.SkinEditor public readonly BindableList SelectedComponents = new BindableList(); + public bool ExternalEditInProgress => externalEditOperation != null && !externalEditOperation.IsCompleted; + protected override bool StartHidden => true; private Drawable? targetScreen; @@ -107,6 +109,8 @@ namespace osu.Game.Overlays.SkinEditor [Resolved] private ExternalEditOverlay? externalEditOverlay { get; set; } + private Task? externalEditOperation; + public SkinEditor() { } @@ -286,7 +290,7 @@ namespace osu.Game.Overlays.SkinEditor var skin = currentSkin.Value.SkinInfo.PerformRead(s => s.Detach()); - await externalEditOverlay!.Begin(skin).ConfigureAwait(false); + externalEditOperation = await externalEditOverlay!.Begin(skin).ConfigureAwait(false); } public bool OnPressed(KeyBindingPressEvent e) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index 571f99bd08..344dcc0d66 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -334,6 +334,14 @@ namespace osu.Game.Overlays.SkinEditor leasedBeatmapSkins = null; } + public new void ToggleVisibility() + { + if (skinEditor?.ExternalEditInProgress == true) + return; + + base.ToggleVisibility(); + } + private partial class EndlessPlayer : ReplayPlayer { protected override UserActivity? InitialActivity => null; From 5fb044956945915ef28054a04bcc1ceae8a9b9eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Jun 2025 13:47:13 +0200 Subject: [PATCH 326/370] Fix error code paths just completely falling apart You can't hide a drawable outside of update thread. --- osu.Game/Overlays/SkinEditor/ExternalEditOverlay.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/ExternalEditOverlay.cs b/osu.Game/Overlays/SkinEditor/ExternalEditOverlay.cs index cbf85c6f2b..e4ac157936 100644 --- a/osu.Game/Overlays/SkinEditor/ExternalEditOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/ExternalEditOverlay.cs @@ -115,10 +115,9 @@ namespace osu.Game.Overlays.SkinEditor catch (Exception ex) { Logger.Log($"Failed to initialize external edit operation: {ex}", LoggingTarget.Database, LogLevel.Error); - Schedule(() => showSpinner("Export failed!")); - await Task.Delay(1000).ConfigureAwait(true); setGlobalSkinDisabled(false); - Hide(); + Schedule(() => showSpinner("Export failed!")); + Scheduler.AddDelayed(Hide, 1000); return Task.FromException(ex); } @@ -195,8 +194,7 @@ namespace osu.Game.Overlays.SkinEditor { Logger.Log($"Failed to finish external edit operation: {ex}", LoggingTarget.Database, LogLevel.Error); showSpinner("Import failed!"); - await Task.Delay(1000).ConfigureAwait(true); - Hide(); + Scheduler.AddDelayed(Hide, 1000); setGlobalSkinDisabled(false); taskCompletionSource.SetException(ex); taskCompletionSource = null; From 2e0e7ff3c2c21c8718fae9ffcf954f5fb9b10e96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Jun 2025 14:13:39 +0200 Subject: [PATCH 327/370] Fix dodgy threading The fact that the stuff "just worked" previously due to one load-bearing detach in a random location is really scary because a lot of this was just not written the way it is supposed to be. --- osu.Game/Database/RealmAccess.cs | 38 +++++++++++++++++++ .../Database/RealmArchiveModelImporter.cs | 4 +- osu.Game/Database/RealmObjectExtensions.cs | 2 + osu.Game/Skinning/SkinImporter.cs | 33 ++++++++-------- 4 files changed, 57 insertions(+), 20 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 49bde7c505..59cbfcb1e3 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -543,6 +543,44 @@ namespace osu.Game.Database return writeTask; } + /// + /// Write changes to realm asynchronously, guaranteeing order of execution. + /// + /// The work to run. + public Task WriteAsync(Func action) + { + ObjectDisposedException.ThrowIf(isDisposed, this); + + // Required to ensure the write is tracked and accounted for before disposal. + // Can potentially be avoided if we have a need to do so in the future. + if (!ThreadSafety.IsUpdateThread) + throw new InvalidOperationException(@$"{nameof(WriteAsync)} must be called from the update thread."); + + // CountdownEvent will fail if already at zero. + if (!pendingAsyncWrites.TryAddCount()) + pendingAsyncWrites.Reset(1); + + // Regardless of calling Realm.GetInstance or Realm.GetInstanceAsync, there is a blocking overhead on retrieval. + // Adding a forced Task.Run resolves this. + var writeTask = Task.Run(async () => + { + T result; + total_writes_async.Value++; + + // Not attempting to use Realm.GetInstanceAsync as there's seemingly no benefit to us (for now) and it adds complexity due to locking + // concerns in getRealmInstance(). On a quick check, it looks to be more suited to cases where realm is connecting to an online sync + // server, which we don't use. May want to report upstream or revisit in the future. + using (var realm = getRealmInstance()) + // ReSharper disable once AccessToDisposedClosure (WriteAsync should be marked as [InstantHandle]). + result = await realm.WriteAsync(() => action(realm)).ConfigureAwait(false); + + pendingAsyncWrites.Signal(); + return result; + }); + + return writeTask; + } + /// /// Subscribe to a realm collection and begin watching for asynchronous changes. /// diff --git a/osu.Game/Database/RealmArchiveModelImporter.cs b/osu.Game/Database/RealmArchiveModelImporter.cs index 6f613267d6..a3cdc2dc77 100644 --- a/osu.Game/Database/RealmArchiveModelImporter.cs +++ b/osu.Game/Database/RealmArchiveModelImporter.cs @@ -205,9 +205,7 @@ namespace osu.Game.Database Directory.CreateDirectory(mountedPath); - // Detach files from the model to avoid realm contention when copying to the external location. - // This is safe as we are not modifying the model in any way. - foreach (var realmFile in model.Files.Detach()) + foreach (var realmFile in model.Files) { string sourcePath = Files.Storage.GetFullPath(realmFile.File.GetStoragePath()); string destinationPath = Path.Join(mountedPath, realmFile.Filename); diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index 538ac1dff7..d43f90c292 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -14,6 +14,7 @@ using osu.Game.Input.Bindings; using osu.Game.Models; using osu.Game.Rulesets; using osu.Game.Scoring; +using osu.Game.Skinning; using Realms; namespace osu.Game.Database @@ -177,6 +178,7 @@ namespace osu.Game.Database c.CreateMap(); c.CreateMap(); c.CreateMap(); + c.CreateMap(); } /// diff --git a/osu.Game/Skinning/SkinImporter.cs b/osu.Game/Skinning/SkinImporter.cs index e1bdcaff0c..382a7b56c2 100644 --- a/osu.Game/Skinning/SkinImporter.cs +++ b/osu.Game/Skinning/SkinImporter.cs @@ -53,12 +53,11 @@ namespace osu.Game.Skinning /// The to update the with /// The to update /// - public override Task?> ImportAsUpdate(ProgressNotification notification, ImportTask task, SkinInfo original) + public override async Task?> ImportAsUpdate(ProgressNotification notification, ImportTask task, SkinInfo original) { - var skinInfoLive = original.ToLive(Realm); - - skinInfoLive.PerformWrite(skinInfo => + return await Realm.WriteAsync?>(r => { + var skinInfo = r.Find(original.ID)!; skinInfo.Files.Clear(); string[] filesInMountedDirectory = Directory.EnumerateFiles(task.Path, "*.*", SearchOption.AllDirectories).Select(f => Path.GetRelativePath(task.Path, f)).ToArray(); @@ -67,28 +66,28 @@ namespace osu.Game.Skinning { using var stream = File.OpenRead(Path.Combine(task.Path, file)); - modelManager.AddFile(original, stream, file); + modelManager.AddFile(skinInfo, stream, file, r); } string skinIniPath = Path.Combine(task.Path, "skin.ini"); - if (!File.Exists(skinIniPath)) - return; - - using (var stream = File.OpenRead(skinIniPath)) - using (var lineReader = new LineBufferedReader(stream)) + if (File.Exists(skinIniPath)) { - var decodedSkinIni = new LegacySkinDecoder().Decode(lineReader); + using (var stream = File.OpenRead(skinIniPath)) + using (var lineReader = new LineBufferedReader(stream)) + { + var decodedSkinIni = new LegacySkinDecoder().Decode(lineReader); - if (!string.IsNullOrEmpty(decodedSkinIni.SkinInfo.Name)) - skinInfo.Name = decodedSkinIni.SkinInfo.Name; + if (!string.IsNullOrEmpty(decodedSkinIni.SkinInfo.Name)) + skinInfo.Name = decodedSkinIni.SkinInfo.Name; - if (!string.IsNullOrEmpty(decodedSkinIni.SkinInfo.Creator)) - skinInfo.Creator = decodedSkinIni.SkinInfo.Creator; + if (!string.IsNullOrEmpty(decodedSkinIni.SkinInfo.Creator)) + skinInfo.Creator = decodedSkinIni.SkinInfo.Creator; + } } - }); - return Task.FromResult(skinInfoLive)!; + return skinInfo.ToLive(Realm); + }).ConfigureAwait(false); } protected override void Populate(SkinInfo model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) From ccc1a7ae6f01df3ac2db00d4c547d09a987fe2d3 Mon Sep 17 00:00:00 2001 From: marvin Date: Mon, 30 Jun 2025 18:43:37 +0200 Subject: [PATCH 328/370] Change icon colour of BeatDivisorControl to be light --- osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index 5386b39190..5883b6d89d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -375,7 +375,8 @@ namespace osu.Game.Screens.Edit.Compose.Components [BackgroundDependencyLoader] private void load(OsuColour colours) { - IconColour = Color4.Black; + IconColour = colours.GrayB; + IconHoverColour = Color4.White; HoverColour = colours.Gray7; FlashColour = colours.Gray9; } From 32d478e19cec9c1989ee3a46e89aeeaea8e4180e Mon Sep 17 00:00:00 2001 From: Chris Ehmann Date: Mon, 30 Jun 2025 18:55:27 -0700 Subject: [PATCH 329/370] Use floored star rating in BeatmapCarouselFilterGrouping and AdvancedStats This commit changes BeatmapCarouselFilterGrouping to now use floored star rating when determining which group a beatmap belongs to, to be consistent with changes introduced here: https://github.com/ppy/osu/pull/33679. The AdvancedStats section of the original song select is also updated to show the floored star rating (rather than rounded). --- osu.Game/Screens/Select/Details/AdvancedStats.cs | 2 +- osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index b7086d2416..152398dee3 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -251,7 +251,7 @@ namespace osu.Game.Screens.Select.Details if (normalDifficulty == null || moddedDifficulty == null) return; - starDifficulty.Value = ((float)normalDifficulty.Value.Stars, (float)moddedDifficulty.Value.Stars); + starDifficulty.Value = ((float)normalDifficulty.Value.Stars.FloorToDecimalDigits(2), (float)moddedDifficulty.Value.Stars.FloorToDecimalDigits(2)); }), starDifficultyCancellationSource.Token, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Current); }); diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index c68f377fbb..d9c2571bfb 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -11,6 +11,7 @@ using osu.Game.Beatmaps; using osu.Game.Graphics.Carousel; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; +using osu.Game.Utils; namespace osu.Game.Screens.SelectV2 { @@ -189,7 +190,7 @@ namespace osu.Game.Screens.SelectV2 }, items); case GroupMode.Difficulty: - return getGroupsBy(b => defineGroupByStars(b.StarRating), items); + return getGroupsBy(b => defineGroupByStars(b.StarRating.FloorToDecimalDigits(2)), items); case GroupMode.Length: return getGroupsBy(b => From 8ae8d847d504c542c2d398a2c0428961d089858f Mon Sep 17 00:00:00 2001 From: Chris Ehmann Date: Mon, 30 Jun 2025 19:52:40 -0700 Subject: [PATCH 330/370] Update TestGroupingByDifficulty to account for floored star rating --- .../Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs index 7f34d7a901..29b4955d02 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs @@ -263,8 +263,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 var results = await runGrouping(GroupMode.Difficulty, beatmapSets); assertGroup(results, 0, "Below 1 Star", new[] { beatmapBelow1 }, ref total); - assertGroup(results, 1, "1 Star", new[] { beatmapAbove1 }, ref total); - assertGroup(results, 2, "2 Stars", new[] { beatmapAlmost2, beatmap2, beatmapAbove2 }, ref total); + assertGroup(results, 1, "1 Star", new[] { beatmapAbove1, beatmapAlmost2 }, ref total); + assertGroup(results, 2, "2 Stars", new[] { beatmap2, beatmapAbove2 }, ref total); assertGroup(results, 3, "7 Stars", new[] { beatmap7 }, ref total); assertTotal(results, total); } From 87357f8ba425378879b8e923298a7db0fa64cf47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Jul 2025 09:25:29 +0200 Subject: [PATCH 331/370] Apply flooring directly rather than flooring and then rounding (down, anyway) Just a bit weird. Especially so that the function isn't even generic, it's called `defineGroupByStars()`, so flooring locally is 200% warranted. --- osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index d9c2571bfb..cef08370f7 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -11,7 +11,6 @@ using osu.Game.Beatmaps; using osu.Game.Graphics.Carousel; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; -using osu.Game.Utils; namespace osu.Game.Screens.SelectV2 { @@ -190,7 +189,7 @@ namespace osu.Game.Screens.SelectV2 }, items); case GroupMode.Difficulty: - return getGroupsBy(b => defineGroupByStars(b.StarRating.FloorToDecimalDigits(2)), items); + return getGroupsBy(b => defineGroupByStars(b.StarRating), items); case GroupMode.Length: return getGroupsBy(b => @@ -324,7 +323,7 @@ namespace osu.Game.Screens.SelectV2 private GroupDefinition defineGroupByStars(double stars) { - int starInt = (int)Math.Round(stars, 2); + int starInt = (int)stars; var starDifficulty = new StarDifficulty(starInt, 0); if (starInt == 0) From 6084863aef9cc49f61f060755f1d5437509c1988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Jul 2025 09:26:43 +0200 Subject: [PATCH 332/370] Leave inline comment for posterity --- osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index cef08370f7..772d4123c2 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -323,6 +323,7 @@ namespace osu.Game.Screens.SelectV2 private GroupDefinition defineGroupByStars(double stars) { + // truncation is intentional - compare `FormatUtils.FormatStarRating()` int starInt = (int)stars; var starDifficulty = new StarDifficulty(starInt, 0); From 654faa553a95f83bf6804f22995ca4728702d1d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Jul 2025 10:14:27 +0200 Subject: [PATCH 333/370] Fix very old lazer replays failing to decode See https://discord.com/channels/188630481301012481/1097318920991559880/1389240665061462047. I hope this was worth it. --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index cf6819b086..ec2b567a7b 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -286,7 +286,23 @@ namespace osu.Game.Scoring.Legacy // In mania, mouseX encodes the pressed keys in the lower 20 bits int mouseXParseLimit = currentRuleset.RulesetInfo.OnlineID == 3 ? (1 << 20) - 1 : Parsing.MAX_COORDINATE_VALUE; - int diff = Parsing.ParseInt(split[0]); + // the legacy replay format as defined by stable expects frame delta times + // ('delta time' here meaning the amount of time between consecutive frames) + // to be integral and does not allow fractional values. + // one particular reason why this matters is that integral deltas + // avoid nasty floating point traps like accumulation error from summation or round-off error. + // however, there was a period in lazer's lifetime wherein lazer emitted replays + // with fractional (float) frame deltas, up until https://github.com/ppy/osu/pull/12583. + // despite the fact that gameplay mechanics changed multiple times since + // and the replay isn't going to play back anywhere near accurately anyway, + // no mistakes are ever forgiven, thus this attempts to parse the delta as an integer once, + // and if that fails, tries again as float. + // notably this cannot just be `(int)Parsing.ParseFloat(split[0])`, because that can lose information + // (`float` numbers have 24 bits of significand precision, which is not enough to accurately represent every possible value of `int`). + int diff; + if (!int.TryParse(split[0], out diff)) + diff = (int)Math.Round(Parsing.ParseFloat(split[0])); + float mouseX = Parsing.ParseFloat(split[1], mouseXParseLimit); float mouseY = Parsing.ParseFloat(split[2], Parsing.MAX_COORDINATE_VALUE); From 6f10aa5d9ce3e1b6f35e572d166889e367b8c1a8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Jun 2025 18:05:06 +0900 Subject: [PATCH 334/370] Default to Song Select V2 --- osu.Game/Screens/Menu/MainMenu.cs | 102 ++---------------------------- 1 file changed, 7 insertions(+), 95 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index d87727b797..bc3bcbd800 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -14,13 +14,11 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Screens; -using osu.Framework.Testing; using osu.Framework.Threading; using osu.Game.Audio; using osu.Game.Beatmaps; @@ -41,12 +39,10 @@ using osu.Game.Screens.Edit; using osu.Game.Screens.OnlinePlay.DailyChallenge; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Playlists; -using osu.Game.Screens.Select; using osu.Game.Screens.SelectV2; using osu.Game.Seasonal; using osuTK; using osuTK.Graphics; -using osuTK.Input; namespace osu.Game.Screens.Menu { @@ -93,8 +89,6 @@ namespace osu.Game.Screens.Menu IBindable ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled; private readonly Bindable samplePlaybackDisabled = new Bindable(); - private InputManager inputManager; - protected override BackgroundScreen CreateBackground() => new BackgroundScreenDefault(); protected override bool PlayExitSound => false; @@ -121,6 +115,9 @@ namespace osu.Game.Screens.Menu [Resolved(canBeNull: true)] private SkinEditorOverlay skinEditor { get; set; } + [CanBeNull] + private IDisposable logoProxy; + [BackgroundDependencyLoader(true)] private void load(BeatmapListingOverlay beatmapListing, SettingsOverlay settings, OsuConfigManager config, SessionStatics statics, AudioManager audio) { @@ -160,7 +157,7 @@ namespace osu.Game.Screens.Menu { skinEditor?.Show(); }, - OnSolo = loadPreferredSongSelect, + OnSolo = loadSongSelect, OnMultiplayer = () => this.Push(new Multiplayer()), OnPlaylists = () => this.Push(new Playlists()), OnDailyChallenge = room => @@ -241,19 +238,12 @@ namespace osu.Game.Screens.Menu Buttons.OnBeatmapListing = () => beatmapListing?.ToggleVisibility(); reappearSampleSwoosh = audio.Samples.Get(@"Menu/reappear-swoosh"); - loadSongSelectV2Samples(audio); - } - - protected override void Update() - { - base.Update(); - updateSongSelectV2HoldState(); } protected override void LoadComplete() { base.LoadComplete(); - inputManager = GetContainingInputManager(); + GetContainingInputManager(); } public void ReturnToOsuLogo() => Buttons.State = ButtonSystemState.Initial; @@ -465,7 +455,7 @@ namespace osu.Game.Screens.Menu Beatmap.Value = beatmap; Ruleset.Value = ruleset; - Schedule(loadPreferredSongSelect); + Schedule(loadSongSelect); } public bool OnPressed(KeyBindingPressEvent e) @@ -489,85 +479,7 @@ namespace osu.Game.Screens.Menu { } - #region TEMPORARY: Song Select v2 easter egg - - private const double required_hold_time = 500; - - private double holdTime; - private bool ssv2Expanded; - private IDisposable ssv2Duck; - private Sample ssv2Sample; - - [CanBeNull] - private IDisposable logoProxy; - - private void loadPreferredSongSelect() - { - if (holdTime >= required_hold_time) - { - ssv2Sample?.Play(); - this.Push(new SoloSongSelect()); - } - else - this.Push(new PlaySongSelect()); - } - - private void loadSongSelectV2Samples(AudioManager audio) - { - ssv2Sample = audio.Samples.Get(@"UI/bss-complete"); - } - - private void updateSongSelectV2HoldState() - { - bool isValidHoverState = Buttons.State == ButtonSystemState.Play && - inputManager.CurrentState.Mouse.IsPressed(MouseButton.Left) && - inputManager.HoveredDrawables.Any(h => h is OsuLogo || (h is MainMenuButton b && b.TriggerKeys.Contains(Key.P))); - - if (isValidHoverState) - { - holdTime += Time.Elapsed; - - if (holdTime >= required_hold_time && !ssv2Expanded) - { - var transformTarget = Game.ChildrenOfType().First(); - - transformTarget.Anchor = Anchor.Centre; - transformTarget.Origin = Anchor.Centre; - - transformTarget.ScaleTo(1.2f, 5000, Easing.OutPow10) - .RotateTo(2, 5000, Easing.OutPow10) - .FadeColour(Color4.BlueViolet, 10000, Easing.OutPow10); - - ssv2Duck = musicController.Duck(new DuckParameters - { - DuckDuration = 2000, - DuckVolumeTo = 0.8f, - DuckCutoffTo = 500, - DuckEasing = Easing.OutQuint, - RestoreDuration = 200, - RestoreEasing = Easing.OutQuint - }); - - ssv2Expanded = true; - } - } - else if (holdTime > 0) - { - var transformTarget = Game.ChildrenOfType().FirstOrDefault(); - - transformTarget.ScaleTo(1, 500, Easing.OutQuint) - .RotateTo(0, 500, Easing.OutQuint) - .FadeColour(OsuColour.Gray(1f), 500, Easing.OutQuint); - - ssv2Duck?.Dispose(); - ssv2Duck = null; - - ssv2Expanded = false; - holdTime = 0; - } - } - - #endregion + private void loadSongSelect() => this.Push(new SoloSongSelect()); private partial class MobileDisclaimerDialog : PopupDialog { From 1048bd9edebba44a29a8887cdaa2155cb6198b3a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Jun 2025 01:52:46 +0900 Subject: [PATCH 335/370] Remove Song Select v1 implementation and update auxiliary usages --- .../Overlays/FirstRunSetup/ScreenUIScale.cs | 13 +- .../Overlays/SkinEditor/SkinEditorOverlay.cs | 6 +- .../SkinEditor/SkinEditorSceneLibrary.cs | 5 +- osu.Game/Screens/Select/PlaySongSelect.cs | 166 ------------------ 4 files changed, 14 insertions(+), 176 deletions(-) delete mode 100644 osu.Game/Screens/Select/PlaySongSelect.cs diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs index fc64408775..aec1859176 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs @@ -25,7 +25,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Screens; using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; -using osu.Game.Screens.Select; +using osu.Game.Screens.SelectV2; using osu.Game.Tests.Visual; using osuTK; @@ -101,11 +101,14 @@ namespace osu.Game.Overlays.FirstRunSetup } } - private partial class NestedSongSelect : PlaySongSelect + private partial class NestedSongSelect : SoloSongSelect { - protected override bool ControlGlobalMusic => false; - public override bool? ApplyModTrackAdjustments => false; + + public NestedSongSelect() + { + ControlGlobalMusic = false; + } } private partial class UIScaleSlider : RoundedSliderBar @@ -148,7 +151,7 @@ namespace osu.Game.Overlays.FirstRunSetup [BackgroundDependencyLoader] private void load(AudioManager audio, TextureStore textures, RulesetStore rulesets) { - Beatmap.Value = new DummyWorkingBeatmap(audio, textures); + Beatmap.Default = Beatmap.Value = new DummyWorkingBeatmap(audio, textures); Ruleset.Value = rulesets.AvailableRulesets.First(); diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index 571f99bd08..29158a3880 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -28,7 +28,7 @@ using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Menu; using osu.Game.Screens.Play; -using osu.Game.Screens.Select; +using osu.Game.Screens.SelectV2; using osu.Game.Users; using osu.Game.Utils; @@ -180,7 +180,7 @@ namespace osu.Game.Overlays.SkinEditor // the validity of the current game-wide beatmap + ruleset combination is enforced by song select. // if we're anywhere else, the state is unknown and may not make sense, so forcibly set something that does. - if (screen is not PlaySongSelect) + if (screen is not SoloSongSelect) ruleset.Value = beatmap.Value.BeatmapInfo.Ruleset; var replayGeneratingMod = ruleset.Value.CreateInstance().GetAutoplayMod(); @@ -194,7 +194,7 @@ namespace osu.Game.Overlays.SkinEditor if (replayGeneratingMod != null) screen.Push(new EndlessPlayer((beatmap, mods) => replayGeneratingMod.CreateScoreFromReplayData(beatmap, mods))); - }, new[] { typeof(Player), typeof(PlaySongSelect) }); + }, new[] { typeof(Player), typeof(SoloSongSelect) }); } protected override void Update() diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs b/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs index 5a283c0e8d..f8d5213622 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs @@ -12,8 +12,9 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Screens; -using osu.Game.Screens.Select; +using osu.Game.Screens.SelectV2; using osuTK; +using SongSelect = osu.Game.Screens.Select.SongSelect; namespace osu.Game.Overlays.SkinEditor { @@ -78,7 +79,7 @@ namespace osu.Game.Overlays.SkinEditor if (screen is SongSelect) return; - screen.Push(new PlaySongSelect()); + screen.Push(new SoloSongSelect()); }, new[] { typeof(SongSelect) }) }, new SceneButton diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs deleted file mode 100644 index 2f47243b50..0000000000 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Extensions.LocalisationExtensions; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input.Events; -using osu.Framework.Screens; -using osu.Game.Beatmaps; -using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; -using osu.Game.Localisation; -using osu.Game.Overlays; -using osu.Game.Overlays.Notifications; -using osu.Game.Rulesets.Mods; -using osu.Game.Scoring; -using osu.Game.Screens.Play; -using osu.Game.Screens.Ranking; -using osu.Game.Users; -using osu.Game.Utils; -using osuTK.Input; - -namespace osu.Game.Screens.Select -{ - public partial class PlaySongSelect : SongSelect - { - private OsuScreen? playerLoader; - - [Resolved] - private INotificationOverlay? notifications { get; set; } - - public override bool AllowExternalScreenChange => true; - - public override MenuItem[] CreateForwardNavigationMenuItemsForBeatmap(Func getBeatmap) => new MenuItem[] - { - new OsuMenuItem(ButtonSystemStrings.Play.ToSentence(), MenuItemType.Highlighted, () => FinaliseSelection(getBeatmap())), - new OsuMenuItem(ButtonSystemStrings.Edit.ToSentence(), MenuItemType.Standard, () => Edit(getBeatmap())) - }; - - protected override UserActivity InitialActivity => new UserActivity.ChoosingBeatmap(); - - private PlayBeatmapDetailArea playBeatmapDetailArea = null!; - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - BeatmapOptions.AddButton(ButtonSystemStrings.Edit.ToSentence(), @"beatmap", FontAwesome.Solid.PencilAlt, colours.Yellow, () => Edit()); - - AddInternal(new SongSelectTouchInputDetector()); - } - - protected void PresentScore(ScoreInfo score) => - FinaliseSelection(score.BeatmapInfo, score.Ruleset, () => this.Push(new SoloResultsScreen(score))); - - protected override BeatmapDetailArea CreateBeatmapDetailArea() - { - playBeatmapDetailArea = new PlayBeatmapDetailArea - { - Leaderboard = - { - ScoreSelected = PresentScore - } - }; - - return playBeatmapDetailArea; - } - - protected override bool OnKeyDown(KeyDownEvent e) - { - switch (e.Key) - { - case Key.Enter: - case Key.KeypadEnter: - // this is a special hard-coded case; we can't rely on OnPressed (of SongSelect) as GlobalActionContainer is - // matching with exact modifier consideration (so Ctrl+Enter would be ignored). - FinaliseSelection(); - return true; - } - - return base.OnKeyDown(e); - } - - private IReadOnlyList? modsAtGameplayStart; - - private ModAutoplay? getAutoplayMod() => Ruleset.Value.CreateInstance().GetAutoplayMod(); - - protected override bool OnStart() - { - if (playerLoader != null) return false; - - modsAtGameplayStart = Mods.Value.Select(m => m.DeepClone()).ToArray(); - - // Ctrl+Enter should start map with autoplay enabled. - if (GetContainingInputManager()?.CurrentState?.Keyboard.ControlPressed == true) - { - var autoInstance = getAutoplayMod(); - - if (autoInstance == null) - { - notifications?.Post(new SimpleNotification - { - Text = NotificationsStrings.NoAutoplayMod - }); - return false; - } - - var mods = Mods.Value.Append(autoInstance).ToArray(); - - if (!ModUtils.CheckCompatibleSet(mods, out var invalid)) - mods = mods.Except(invalid).Append(autoInstance).ToArray(); - - Mods.Value = mods; - } - - SampleConfirm?.Play(); - - this.Push(playerLoader = new PlayerLoader(createPlayer)); - return true; - - Player createPlayer() - { - Player player; - - var replayGeneratingMod = Mods.Value.OfType().FirstOrDefault(); - - if (replayGeneratingMod != null) - { - player = new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateScoreFromReplayData(beatmap, mods)); - } - else - { - player = new SoloPlayer(); - } - - return player; - } - } - - public override void OnResuming(ScreenTransitionEvent e) - { - base.OnResuming(e); - revertMods(); - } - - public override bool OnExiting(ScreenExitEvent e) - { - if (base.OnExiting(e)) - return true; - - revertMods(); - return false; - } - - private void revertMods() - { - if (playerLoader == null) return; - - Mods.Value = modsAtGameplayStart; - playerLoader = null; - } - } -} From 397b9b39945274402e600b29978216254c9c3b30 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Jun 2025 17:00:07 +0900 Subject: [PATCH 336/370] Update existing tests to work with Song Select v2 --- .../Background/TestSceneUserDimBackgrounds.cs | 12 +- .../Visual/Editing/TestSceneEditorSaving.cs | 4 +- .../Editing/TestSceneOpenEditorTimestamp.cs | 10 +- .../TestSceneBeatmapEditorNavigation.cs | 20 +- .../TestSceneButtonSystemNavigation.cs | 6 +- .../TestSceneChangeAndUseGameplayBindings.cs | 8 +- .../TestSceneMouseWheelVolumeAdjust.cs | 7 +- .../Navigation/TestScenePerformFromScreen.cs | 16 +- .../Navigation/TestScenePresentBeatmap.cs | 16 +- .../Navigation/TestScenePresentScore.cs | 17 +- .../Navigation/TestSceneScreenNavigation.cs | 241 +-- .../TestSceneSkinEditorNavigation.cs | 15 +- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 548 ------- .../TestSceneBeatmapRecommendations.cs | 3 +- .../SongSelect/TestScenePlaySongSelect.cs | 1445 ----------------- .../TestSceneBeatmapLeaderboardWedge.cs | 224 ++- osu.Game/Graphics/Carousel/Carousel.cs | 4 +- .../SelectV2/BeatmapLeaderboardScore.cs | 57 +- osu.Game/Screens/SelectV2/FilterControl.cs | 2 +- osu.Game/Screens/SelectV2/SoloSongSelect.cs | 4 +- osu.Game/Screens/SelectV2/SongSelect.cs | 9 +- .../Tests/Visual/EditorSavingTestScene.cs | 11 +- 22 files changed, 456 insertions(+), 2223 deletions(-) delete mode 100644 osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs delete mode 100644 osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index eeaa68e2ee..58fb02c90c 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -32,7 +32,7 @@ using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Play; using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Ranking; -using osu.Game.Screens.Select; +using osu.Game.Screens.SelectV2; using osu.Game.Storyboards.Drawables; using osu.Game.Tests.Resources; using osuTK; @@ -325,7 +325,7 @@ namespace osu.Game.Tests.Visual.Background private void setupUserSettings() { AddUntilStep("Song select is current", () => songSelect.IsCurrentScreen()); - AddUntilStep("Song select has selection", () => songSelect.Carousel?.SelectedBeatmapInfo != null); + AddUntilStep("Song select has selection", () => songSelect.Carousel?.CurrentSelection != null); AddStep("Set default user settings", () => { SelectedMods.Value = new[] { new OsuModNoFail() }; @@ -340,7 +340,7 @@ namespace osu.Game.Tests.Visual.Background rulesets?.Dispose(); } - private partial class DummySongSelect : PlaySongSelect + private partial class DummySongSelect : SoloSongSelect { private FadeAccessibleBackground background; @@ -355,7 +355,7 @@ namespace osu.Game.Tests.Visual.Background public readonly Bindable DimLevel = new BindableDouble(); public readonly Bindable BlurLevel = new BindableDouble(); - public new BeatmapCarousel Carousel => base.Carousel; + public BeatmapCarousel Carousel => this.ChildrenOfType().SingleOrDefault(); [BackgroundDependencyLoader] private void load(OsuConfigManager config) @@ -368,7 +368,7 @@ namespace osu.Game.Tests.Visual.Background public bool IsBackgroundDimmed() => background.CurrentColour == OsuColour.Gray(1f - background.CurrentDim); - public bool IsBackgroundUndimmed() => background.CurrentColour == Color4.White; + public bool IsBackgroundUndimmed() => background.CurrentColour == new Color4(0.9f, 0.9f, 0.9f, 1f); public bool IsUserBlurApplied() => Precision.AlmostEquals(background.CurrentBlur, new Vector2((float)BlurLevel.Value * BackgroundScreenBeatmap.USER_BLUR_FACTOR), 0.1f); @@ -376,7 +376,7 @@ namespace osu.Game.Tests.Visual.Background public bool IsBackgroundVisible() => background.CurrentAlpha == 1; - public bool IsBackgroundBlur() => Precision.AlmostEquals(background.CurrentBlur, new Vector2(BACKGROUND_BLUR), 0.1f); + public bool IsBackgroundBlur() => Precision.AlmostBigger(background.CurrentBlur.X, 0, 0.1f); public bool CheckBackgroundBlur(Vector2 expected) => Precision.AlmostEquals(background.CurrentBlur, expected, 0.1f); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs index 2e7b55ab49..7f40da5bab 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs @@ -15,7 +15,7 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Overlays; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components.Timeline; -using osu.Game.Screens.Select; +using osu.Game.Screens.SelectV2; using osuTK.Input; namespace osu.Game.Tests.Visual.Editing @@ -190,7 +190,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("Set tags again", () => EditorBeatmap.BeatmapInfo.Metadata.Tags = tags_to_discard); AddStep("Exit editor", () => Editor.Exit()); - AddUntilStep("Wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); + AddUntilStep("Wait for song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect); AddAssert("Tags reverted correctly", () => Game.Beatmap.Value.BeatmapInfo.Metadata.Tags == tags_to_save); } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs index 955ded97af..e3b79d4053 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs @@ -14,7 +14,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Edit; using osu.Game.Screens.Menu; -using osu.Game.Screens.Select; +using osu.Game.Screens.SelectV2; using osu.Game.Tests.Resources; namespace osu.Game.Tests.Visual.Editing @@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Editing () => Is.EqualTo(1)); AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo?.Invoke()); - AddUntilStep("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded); + AddUntilStep("entered song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect && songSelect.CarouselItemsPresented); addStepClickLink("00:00:000 (1)", waitForSeek: false); AddUntilStep("received 'must be in edit'", @@ -151,12 +151,12 @@ namespace osu.Game.Tests.Visual.Editing AddStep("Present beatmap", () => Game.PresentBeatmap(beatmapSet)); AddUntilStep("Wait for song select", () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet) - && Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect - && songSelect.BeatmapSetsLoaded + && Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect + && songSelect.CarouselItemsPresented ); AddStep("Switch ruleset", () => Game.Ruleset.Value = ruleset); AddStep("Open editor for ruleset", () => - ((PlaySongSelect)Game.ScreenStack.CurrentScreen) + ((SoloSongSelect)Game.ScreenStack.CurrentScreen) .Edit(beatmapSet.Beatmaps.Last(beatmap => beatmap.Ruleset.Name == ruleset.Name)) ); AddUntilStep("Wait for editor open", () => editor?.ReadyForUse == true); diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs index ee5b1797ed..c7499c98b5 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs @@ -26,8 +26,8 @@ using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.GameplayTest; using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Menu; -using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; +using osu.Game.Screens.SelectV2; using osu.Game.Tests.Resources; using osuTK.Input; @@ -110,7 +110,7 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("exit", () => getEditor().Exit()); - AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect + AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect && songSelect.Beatmap.Value is DummyWorkingBeatmap); } @@ -171,7 +171,7 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("switch ruleset at song select", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo); - AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0))); + AddStep("open editor", () => ((SoloSongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0))); AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); AddAssert("editor ruleset is osu!", () => Game.Ruleset.Value, () => Is.EqualTo(new OsuRuleset().RulesetInfo)); @@ -187,8 +187,8 @@ namespace osu.Game.Tests.Visual.Navigation }); AddAssert("gameplay ruleset is osu!", () => Game.Ruleset.Value, () => Is.EqualTo(new OsuRuleset().RulesetInfo)); - AddStep("exit to song select", () => Game.PerformFromScreen(_ => { }, typeof(PlaySongSelect).Yield())); - AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); + AddStep("exit to song select", () => Game.PerformFromScreen(_ => { }, typeof(SoloSongSelect).Yield())); + AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect); AddAssert("previous ruleset restored", () => Game.Ruleset.Value.Equals(new ManiaRuleset().RulesetInfo)); } @@ -289,8 +289,8 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("user request play", () => Game.MusicController.Play(requestedByUser: true)); AddUntilStep("music still stopped", () => !Game.MusicController.IsPlaying); - AddStep("exit to song select", () => Game.PerformFromScreen(_ => { }, typeof(PlaySongSelect).Yield())); - AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); + AddStep("exit to song select", () => Game.PerformFromScreen(_ => { }, typeof(SoloSongSelect).Yield())); + AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect); AddUntilStep("wait for music playing", () => Game.MusicController.IsPlaying); AddStep("user request stop", () => Game.MusicController.Stop(requestedByUser: true)); @@ -352,13 +352,13 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet)); AddUntilStep("wait for song select", () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet) - && Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect - && songSelect.BeatmapSetsLoaded); + && Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect + && songSelect.CarouselItemsPresented); } private void openEditor() { - AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0))); + AddStep("open editor", () => ((SoloSongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0))); AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); } diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneButtonSystemNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneButtonSystemNavigation.cs index 43b160250c..0ccfb5a4e3 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneButtonSystemNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneButtonSystemNavigation.cs @@ -5,7 +5,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Screens.Menu; -using osu.Game.Screens.Select; +using osu.Game.Screens.SelectV2; using osuTK.Input; namespace osu.Game.Tests.Visual.Navigation @@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.Navigation InputManager.Key(Key.P); }); - AddAssert("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); + AddAssert("entered song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect); } [Test] @@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("state is play", () => buttons.State == ButtonSystemState.Play); AddStep("press P", () => InputManager.Key(Key.P)); - AddAssert("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); + AddAssert("entered song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect); } } } diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs index 3a3af43cb1..4f27d9b323 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs @@ -15,7 +15,7 @@ using osu.Game.Input.Bindings; using osu.Game.Overlays.Settings.Sections.Input; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; -using osu.Game.Screens.Select; +using osu.Game.Screens.SelectV2; using osu.Game.Tests.Beatmaps.IO; using osuTK.Input; @@ -54,10 +54,10 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); - PushAndConfirm(() => new PlaySongSelect()); + PushAndConfirm(() => new SoloSongSelect()); AddUntilStep("wait for selection", () => !Game.Beatmap.IsDefault); - AddUntilStep("wait for carousel load", () => songSelect.BeatmapSetsLoaded); + AddUntilStep("wait for carousel load", () => songSelect.CarouselItemsPresented); AddStep("enter gameplay", () => InputManager.Key(Key.Enter)); @@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.Navigation .AsEnumerable() .First(k => k.RulesetName == "osu" && k.ActionInt == 0); - private Screens.Select.SongSelect songSelect => Game.ScreenStack.CurrentScreen as Screens.Select.SongSelect; + private SoloSongSelect songSelect => Game.ScreenStack.CurrentScreen as SoloSongSelect; private Player player => Game.ScreenStack.CurrentScreen as Player; diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs b/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs index 26a37fa211..0a4349d73f 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using osu.Framework.Extensions; using osu.Game.Configuration; using osu.Game.Screens.Play; +using osu.Game.Screens.SelectV2; using osu.Game.Tests.Beatmaps.IO; using osuTK.Input; @@ -83,9 +84,9 @@ namespace osu.Game.Tests.Visual.Navigation private void loadToPlayerNonBreakTime() { Player? player = null; - Screens.Select.SongSelect songSelect = null!; - PushAndConfirm(() => songSelect = new TestSceneScreenNavigation.TestPlaySongSelect()); - AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + SoloSongSelect songSelect = null!; + PushAndConfirm(() => songSelect = new SoloSongSelect()); + AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented); AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game, virtualTrack: true).WaitSafely()); AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs index 5fe4bb9340..04d7b15295 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs @@ -17,9 +17,9 @@ using osu.Game.Rulesets.Mods; using osu.Game.Screens; using osu.Game.Screens.Menu; using osu.Game.Screens.Play; +using osu.Game.Screens.SelectV2; using osu.Game.Tests.Beatmaps.IO; using osuTK.Input; -using static osu.Game.Tests.Visual.Navigation.TestSceneScreenNavigation; namespace osu.Game.Tests.Visual.Navigation { @@ -44,17 +44,17 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestPerformAtSongSelect() { - PushAndConfirm(() => new TestPlaySongSelect()); + PushAndConfirm(() => new SoloSongSelect()); - AddStep("perform immediately", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(TestPlaySongSelect) })); + AddStep("perform immediately", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(SoloSongSelect) })); AddAssert("did perform", () => actionPerformed); - AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen is TestPlaySongSelect); + AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen is SoloSongSelect); } [Test] public void TestPerformAtMenuFromSongSelect() { - PushAndConfirm(() => new TestPlaySongSelect()); + PushAndConfirm(() => new SoloSongSelect()); AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true)); AddUntilStep("returned to menu", () => Game.ScreenStack.CurrentScreen is MainMenu); @@ -69,8 +69,8 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("Press enter", () => InputManager.Key(Key.Enter)); AddUntilStep("Wait for new screen", () => Game.ScreenStack.CurrentScreen is PlayerLoader); - AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(TestPlaySongSelect) })); - AddUntilStep("returned to song select", () => Game.ScreenStack.CurrentScreen is TestPlaySongSelect); + AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(SoloSongSelect) })); + AddUntilStep("returned to song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect); AddAssert("did perform", () => actionPerformed); } @@ -257,7 +257,7 @@ namespace osu.Game.Tests.Visual.Navigation private void importAndWaitForSongSelect() { AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); - PushAndConfirm(() => new TestPlaySongSelect()); + PushAndConfirm(() => new SoloSongSelect()); AddUntilStep("beatmap updated", () => Game.Beatmap.Value.BeatmapSetInfo.OnlineID == 241526); } diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs index f036b4b3ef..e7172cacbf 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs @@ -16,7 +16,7 @@ using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Menu; -using osu.Game.Screens.Select; +using osu.Game.Screens.SelectV2; namespace osu.Game.Tests.Visual.Navigation { @@ -81,11 +81,9 @@ namespace osu.Game.Tests.Visual.Navigation presentAndConfirm(osuImport); var maniaImport = importBeatmap(2, new ManiaRuleset().RulesetInfo); - confirmBeatmapInSongSelect(maniaImport); presentAndConfirm(maniaImport); var catchImport = importBeatmap(3, new CatchRuleset().RulesetInfo); - confirmBeatmapInSongSelect(catchImport); presentAndConfirm(catchImport); // Ruleset is always changed. @@ -103,11 +101,9 @@ namespace osu.Game.Tests.Visual.Navigation presentAndConfirm(osuImport); var maniaImport = importBeatmap(2, new ManiaRuleset().RulesetInfo); - confirmBeatmapInSongSelect(maniaImport); presentAndConfirm(maniaImport); var catchImport = importBeatmap(3, new CatchRuleset().RulesetInfo); - confirmBeatmapInSongSelect(catchImport); presentAndConfirm(catchImport); // force ruleset to osu!mania @@ -178,14 +174,14 @@ namespace osu.Game.Tests.Visual.Navigation { AddUntilStep("wait for carousel loaded", () => { - var songSelect = (Screens.Select.SongSelect)Game.ScreenStack.CurrentScreen; + var songSelect = (SoloSongSelect)Game.ScreenStack.CurrentScreen; return songSelect.ChildrenOfType().SingleOrDefault()?.IsLoaded == true; }); AddUntilStep("beatmap in song select", () => { - var songSelect = (Screens.Select.SongSelect)Game.ScreenStack.CurrentScreen; - return songSelect.ChildrenOfType().Single().BeatmapSets.Any(b => b.MatchesOnlineID(getImport())); + var songSelect = (SoloSongSelect)Game.ScreenStack.CurrentScreen; + return songSelect.ChildrenOfType().Single().GetCarouselItems()!.Any(i => i.Model is BeatmapSetInfo bsi && bsi.MatchesOnlineID(getImport())); }); } @@ -193,7 +189,7 @@ namespace osu.Game.Tests.Visual.Navigation { AddStep("present beatmap", () => Game.PresentBeatmap(getImport())); - AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect songSelect && songSelect.BeatmapSetsLoaded); + AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect && songSelect.CarouselItemsPresented); AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapSetInfo.OnlineID, () => Is.EqualTo(getImport().OnlineID)); AddAssert("correct ruleset selected", () => Game.Ruleset.Value, () => Is.EqualTo(getImport().Beatmaps.First().Ruleset)); } @@ -203,7 +199,7 @@ namespace osu.Game.Tests.Visual.Navigation Predicate pred = b => b.OnlineID == importedID * 1024 + 2; AddStep("present difficulty", () => Game.PresentBeatmap(getImport(), pred)); - AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect songSelect && songSelect.BeatmapSetsLoaded); + AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect && songSelect.CarouselItemsPresented); AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(importedID * 1024 + 2)); AddAssert("correct ruleset selected", () => Game.Ruleset.Value.OnlineID, () => Is.EqualTo(expectedRulesetOnlineID ?? getImport().Beatmaps.First().Ruleset.OnlineID)); } diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs index 2c2335de13..fa337a3ec2 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs @@ -18,7 +18,8 @@ using osu.Game.Screens; using osu.Game.Screens.Menu; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; -using osu.Game.Screens.Select; +using osu.Game.Screens.SelectV2; +using FilterControl = osu.Game.Screens.SelectV2.FilterControl; namespace osu.Game.Tests.Visual.Navigation { @@ -96,9 +97,9 @@ namespace osu.Game.Tests.Visual.Navigation public void TestFromSongSelectWithFilter([Values] ScorePresentType type) { AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo?.Invoke()); - AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded); + AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect && songSelect.CarouselItemsPresented); - AddStep("filter to nothing", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).FilterControl.CurrentTextSearch.Value = "fdsajkl;fgewq"); + AddStep("filter to nothing", () => ((SoloSongSelect)Game.ScreenStack.CurrentScreen).ChildrenOfType().Single().Search("fdsajkl;fgewq")); AddUntilStep("wait for no results", () => Beatmap.IsDefault); var firstImport = importScore(1, new CatchRuleset().RulesetInfo); @@ -109,7 +110,7 @@ namespace osu.Game.Tests.Visual.Navigation public void TestFromSongSelectWithConvertRulesetChange([Values] ScorePresentType type) { AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo?.Invoke()); - AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded); + AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect && songSelect.CarouselItemsPresented); AddStep("set convert to false", () => Game.LocalConfig.SetValue(OsuSetting.ShowConvertedBeatmaps, false)); @@ -121,7 +122,7 @@ namespace osu.Game.Tests.Visual.Navigation public void TestFromSongSelect([Values] ScorePresentType type) { AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo?.Invoke()); - AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded); + AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect && songSelect.CarouselItemsPresented); var firstImport = importScore(1); presentAndConfirm(firstImport, type); @@ -134,7 +135,7 @@ namespace osu.Game.Tests.Visual.Navigation public void TestFromSongSelectDifferentRuleset([Values] ScorePresentType type) { AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo?.Invoke()); - AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded); + AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect && songSelect.CarouselItemsPresented); var firstImport = importScore(1); presentAndConfirm(firstImport, type); @@ -147,7 +148,7 @@ namespace osu.Game.Tests.Visual.Navigation public void TestPresentTwoImportsWithSameOnlineIDButDifferentHashes([Values] ScorePresentType type) { AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo?.Invoke()); - AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded); + AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect && songSelect.CarouselItemsPresented); var firstImport = importScore(1); presentAndConfirm(firstImport, type); @@ -160,7 +161,7 @@ namespace osu.Game.Tests.Visual.Navigation public void TestScoreRefetchIgnoresEmptyHash() { AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo?.Invoke()); - AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded); + AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is SoloSongSelect songSelect && songSelect.CarouselItemsPresented); importScore(-1, hash: string.Empty); importScore(3, hash: @"deadbeef"); diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 2a755b46b3..bcab3c7672 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -26,7 +26,6 @@ using osu.Game.Graphics.Carousel; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; -using osu.Game.Online.Leaderboards; using osu.Game.Online.Notifications.WebSocket; using osu.Game.Online.Notifications.WebSocket.Events; using osu.Game.Overlays; @@ -49,20 +48,13 @@ using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Ranking; -using osu.Game.Screens.Select; -using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Leaderboards; -using osu.Game.Screens.Select.Options; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Resources; using osu.Game.Utils; using osuTK; using osuTK.Input; -using BeatmapCarousel = osu.Game.Screens.Select.BeatmapCarousel; -using CollectionDropdown = osu.Game.Collections.CollectionDropdown; -using FilterControl = osu.Game.Screens.Select.FilterControl; -using FooterButtonRandom = osu.Game.Screens.Select.FooterButtonRandom; namespace osu.Game.Tests.Visual.Navigation { @@ -146,62 +138,70 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestExitSongSelectWithEscape() { - TestPlaySongSelect songSelect = null; + SoloSongSelect songSelect = null; + ModSelectOverlay modSelect = null; - PushAndConfirm(() => songSelect = new TestPlaySongSelect()); - AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show()); - AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible); + PushAndConfirm(() => songSelect = new SoloSongSelect()); + AddStep("Show mods overlay", () => + { + modSelect = songSelect!.ChildrenOfType().Single(); + modSelect.Show(); + }); + AddAssert("Overlay was shown", () => modSelect.State.Value == Visibility.Visible); pushEscape(); - AddAssert("Overlay was hidden", () => songSelect.ModSelectOverlay.State.Value == Visibility.Hidden); + AddAssert("Overlay was hidden", () => modSelect.State.Value == Visibility.Hidden); exitViaEscapeAndConfirm(); } [Test] public void TestEnterGameplayWhileFilteringToNoSelection() { - TestPlaySongSelect songSelect = null; + SoloSongSelect songSelect = null; - PushAndConfirm(() => songSelect = new TestPlaySongSelect()); - AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + PushAndConfirm(() => songSelect = new SoloSongSelect()); + AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented); AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); - AddStep("force selection", () => + AddStep("force selection and change filter immediately", () => { - songSelect.FinaliseSelection(); - songSelect.FilterControl.CurrentTextSearch.Value = "test"; + InputManager.Key(Key.Enter); + songSelect.ChildrenOfType().Single().Search("test"); }); AddUntilStep("wait for player", () => !songSelect.IsCurrentScreen()); AddStep("return to song select", () => songSelect.MakeCurrent()); - AddUntilStep("wait for selection lost", () => songSelect.Beatmap.IsDefault); + AddUntilStep("selection not lost", () => !songSelect.Beatmap.IsDefault); + AddUntilStep("placeholder visible", () => songSelect.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Visible)); } [Test] public void TestSongSelectBackActionHandling() { - TestPlaySongSelect songSelect = null; + SoloSongSelect songSelect = null; - PushAndConfirm(() => songSelect = new TestPlaySongSelect()); + PushAndConfirm(() => songSelect = new SoloSongSelect()); + + AddUntilStep("wait for filter control", () => filterControlTextBox().IsLoaded); AddStep("set filter", () => filterControlTextBox().Current.Value = "test"); AddStep("press back", () => InputManager.Click(MouseButton.Button1)); - AddAssert("still at song select", () => Game.ScreenStack.CurrentScreen == songSelect); + AddAssert("still at song select", () => Game.ScreenStack.CurrentScreen, () => Is.EqualTo(songSelect)); AddAssert("filter cleared", () => string.IsNullOrEmpty(filterControlTextBox().Current.Value)); AddStep("set filter again", () => filterControlTextBox().Current.Value = "test"); AddStep("open collections dropdown", () => { - InputManager.MoveMouseTo(songSelect.ChildrenOfType().Single()); + InputManager.MoveMouseTo(songSelect.ChildrenOfType().Single()); InputManager.Click(MouseButton.Left); }); AddStep("press back once", () => InputManager.Click(MouseButton.Button1)); AddAssert("still at song select", () => Game.ScreenStack.CurrentScreen == songSelect); AddAssert("collections dropdown closed", () => songSelect - .ChildrenOfType().Single() + .ChildrenOfType().Single() .ChildrenOfType.DropdownMenu>().Single().State == MenuState.Closed); AddStep("press back a second time", () => InputManager.Click(MouseButton.Button1)); @@ -210,17 +210,17 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("press back a third time", () => InputManager.Click(MouseButton.Button1)); ConfirmAtMainMenu(); - TextBox filterControlTextBox() => songSelect.ChildrenOfType().Single(); + FilterControl.SongSelectSearchTextBox filterControlTextBox() => songSelect.ChildrenOfType().Single(); } [Test] public void TestSongSelectRandomRewindButton() { Guid? originalSelection = null; - TestPlaySongSelect songSelect = null; + SoloSongSelect songSelect = null; - PushAndConfirm(() => songSelect = new TestPlaySongSelect()); - AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + PushAndConfirm(() => songSelect = new SoloSongSelect()); + AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented); AddStep("Add two beatmaps", () => { @@ -248,20 +248,30 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestSongSelectScrollHandling() { - TestPlaySongSelect songSelect = null; + SoloSongSelect songSelect = null; double scrollPosition = 0; AddStep("set game volume to max", () => Game.Dependencies.Get().SetValue(FrameworkSetting.VolumeUniversal, 1d)); AddUntilStep("wait for volume overlay to hide", () => Game.ChildrenOfType().SingleOrDefault()?.State.Value, () => Is.EqualTo(Visibility.Hidden)); - PushAndConfirm(() => songSelect = new TestPlaySongSelect()); - AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + PushAndConfirm(() => songSelect = new SoloSongSelect()); + AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented); AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); - AddStep("store scroll position", () => scrollPosition = getCarouselScrollPosition()); + AddUntilStep("store scroll position", () => + { + double s = getCarouselScrollPosition(); + + // TODO: this logic can likely be removed when we fix https://github.com/ppy/osu/issues/33379 + if (scrollPosition == s) + return true; + + scrollPosition = s; + return false; + }); AddStep("move to left side", () => InputManager.MoveMouseTo( - songSelect.ChildrenOfType().Single().ScreenSpaceDrawQuad.TopLeft + new Vector2(1))); + songSelect.ChildrenOfType().Single().ScreenSpaceDrawQuad.Centre)); AddStep("scroll down", () => InputManager.ScrollVerticalBy(-1)); AddAssert("carousel didn't move", getCarouselScrollPosition, () => Is.EqualTo(scrollPosition)); @@ -277,7 +287,7 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("scroll down", () => InputManager.ScrollVerticalBy(-1)); AddAssert("carousel moved", getCarouselScrollPosition, () => Is.Not.EqualTo(scrollPosition)); - double getCarouselScrollPosition() => Game.ChildrenOfType>().Single().Current; + double getCarouselScrollPosition() => Game.ChildrenOfType.CarouselScrollContainer>().Single().Current; } [Test] @@ -325,7 +335,7 @@ namespace osu.Game.Tests.Visual.Navigation }, 5); AddAssert("game volume decreased", () => Game.Dependencies.Get().Get(FrameworkSetting.VolumeUniversal), () => Is.LessThan(1)); - AddStep("move to carousel", () => InputManager.MoveMouseTo(songSelect.ChildrenOfType().Single())); + AddStep("move to carousel", () => InputManager.MoveMouseTo(songSelect.ChildrenOfType().Single())); AddStep("scroll down", () => InputManager.ScrollVerticalBy(-1)); AddAssert("carousel moved", getCarouselScrollPosition, () => Is.Not.EqualTo(scrollPosition)); @@ -339,21 +349,21 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestOpenModSelectOverlayUsingAction() { - TestPlaySongSelect songSelect = null; + SoloSongSelect songSelect = null; - PushAndConfirm(() => songSelect = new TestPlaySongSelect()); + PushAndConfirm(() => songSelect = new SoloSongSelect()); AddStep("Show mods overlay", () => InputManager.Key(Key.F1)); - AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible); + AddAssert("Overlay was shown", () => songSelect!.ChildrenOfType().Single().State.Value == Visibility.Visible); } [Test] public void TestAttemptPlayBeatmapWrongHashFails() { - Screens.Select.SongSelect songSelect = null; + Screens.SelectV2.SongSelect songSelect = null; AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).GetResultSafely()); - PushAndConfirm(() => songSelect = new TestPlaySongSelect()); - AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + PushAndConfirm(() => songSelect = new SoloSongSelect()); + AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented); AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); @@ -384,11 +394,11 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestAttemptPlayBeatmapMissingFails() { - Screens.Select.SongSelect songSelect = null; + Screens.SelectV2.SongSelect songSelect = null; AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).GetResultSafely()); - PushAndConfirm(() => songSelect = new TestPlaySongSelect()); - AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + PushAndConfirm(() => songSelect = new SoloSongSelect()); + AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented); AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); @@ -418,9 +428,9 @@ namespace osu.Game.Tests.Visual.Navigation { Player player = null; - Screens.Select.SongSelect songSelect = null; - PushAndConfirm(() => songSelect = new TestPlaySongSelect()); - AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + Screens.SelectV2.SongSelect songSelect = null; + PushAndConfirm(() => songSelect = new SoloSongSelect()); + AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented); AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); @@ -461,9 +471,9 @@ namespace osu.Game.Tests.Visual.Navigation { Player player = null; - Screens.Select.SongSelect songSelect = null; - PushAndConfirm(() => songSelect = new TestPlaySongSelect()); - AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + Screens.SelectV2.SongSelect songSelect = null; + PushAndConfirm(() => songSelect = new SoloSongSelect()); + AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented); AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game).WaitSafely()); @@ -515,9 +525,9 @@ namespace osu.Game.Tests.Visual.Navigation { Player player = null; - Screens.Select.SongSelect songSelect = null; - PushAndConfirm(() => songSelect = new TestPlaySongSelect()); - AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + Screens.SelectV2.SongSelect songSelect = null; + PushAndConfirm(() => songSelect = new SoloSongSelect()); + AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented); AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game).WaitSafely()); @@ -558,9 +568,9 @@ namespace osu.Game.Tests.Visual.Navigation { Player player = null; - Screens.Select.SongSelect songSelect = null; - PushAndConfirm(() => songSelect = new TestPlaySongSelect()); - AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + Screens.SelectV2.SongSelect songSelect = null; + PushAndConfirm(() => songSelect = new SoloSongSelect()); + AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented); AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); @@ -663,7 +673,7 @@ namespace osu.Game.Tests.Visual.Navigation playToResults(); ScoreInfo score = null; - LeaderboardScore scorePanel = null; + BeatmapLeaderboardScore scorePanel = null; AddStep("get score", () => score = ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score); @@ -672,18 +682,11 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("press back button", () => Game.ChildrenOfType().First().Action!.Invoke()); AddStep("show local scores", - () => Game.ChildrenOfType().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Local)); + () => Game.ChildrenOfType>().First().Current.Value = BeatmapLeaderboardScope.Local); - AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType().FirstOrDefault(s => s.Score.Equals(score))) != null); + AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType().FirstOrDefault(s => s.Score.Equals(score))) != null); - AddStep("open options", () => InputManager.Key(Key.F3)); - - AddStep("choose clear all scores", () => InputManager.Key(Key.Number4)); - - AddUntilStep("wait for dialog display", () => ((Drawable)Game.Dependencies.Get()).IsLoaded); - AddUntilStep("wait for dialog", () => Game.Dependencies.Get().CurrentDialog != null); - AddStep("confirm deletion", () => InputManager.Key(Key.Number1)); - AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get().CurrentDialog == null); + AddStep("Clear all scores", () => Game.Dependencies.Get().Delete()); AddUntilStep("ensure score is pending deletion", () => Game.Realm.Run(r => r.Find(score.ID)?.DeletePending == true)); @@ -696,7 +699,7 @@ namespace osu.Game.Tests.Visual.Navigation playToResults(); ScoreInfo score = null; - LeaderboardScore scorePanel = null; + BeatmapLeaderboardScore scorePanel = null; AddStep("get score", () => score = ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score); @@ -705,9 +708,9 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("press back button", () => Game.ChildrenOfType().First().Action!.Invoke()); AddStep("show local scores", - () => Game.ChildrenOfType().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Local)); + () => Game.ChildrenOfType>().First().Current.Value = BeatmapLeaderboardScope.Local); - AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType().FirstOrDefault(s => s.Score.Equals(score))) != null); + AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType().FirstOrDefault(s => s.Score.Equals(score))) != null); AddStep("right click panel", () => { @@ -718,7 +721,7 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("click delete", () => { var dropdownItem = Game - .ChildrenOfType().First() + .ChildrenOfType().First() .ChildrenOfType().First() .ChildrenOfType().First(i => i.Item.Text.ToString() == "Delete"); @@ -744,9 +747,9 @@ namespace osu.Game.Tests.Visual.Navigation IWorkingBeatmap beatmap() => Game.Beatmap.Value; - Screens.Select.SongSelect songSelect = null; - PushAndConfirm(() => songSelect = new TestPlaySongSelect()); - AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + Screens.SelectV2.SongSelect songSelect = null; + PushAndConfirm(() => songSelect = new SoloSongSelect()); + AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented); AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game, virtualTrack: true).WaitSafely()); @@ -777,9 +780,9 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestMenuMakesMusic() { - TestPlaySongSelect songSelect = null; + SoloSongSelect songSelect = null; - PushAndConfirm(() => songSelect = new TestPlaySongSelect()); + PushAndConfirm(() => songSelect = new SoloSongSelect()); AddUntilStep("wait for no track", () => Game.MusicController.CurrentTrack.IsDummyDevice); @@ -791,7 +794,7 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestPushSongSelectAndPressBackButtonImmediately() { - AddStep("push song select", () => Game.ScreenStack.Push(new TestPlaySongSelect())); + AddStep("push song select", () => Game.ScreenStack.Push(new SoloSongSelect())); AddStep("press back button", () => Game.ChildrenOfType().First().Action!.Invoke()); ConfirmAtMainMenu(); @@ -800,18 +803,23 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestExitSongSelectWithClick() { - TestPlaySongSelect songSelect = null; + SoloSongSelect songSelect = null; + ModSelectOverlay modSelect = null; - PushAndConfirm(() => songSelect = new TestPlaySongSelect()); - AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show()); - AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible); + PushAndConfirm(() => songSelect = new SoloSongSelect()); + AddStep("Show mods overlay", () => + { + modSelect = songSelect!.ChildrenOfType().Single(); + modSelect.Show(); + }); + AddAssert("Overlay was shown", () => modSelect.State.Value == Visibility.Visible); AddStep("Move mouse to dimmed area", () => InputManager.MoveMouseTo(new Vector2( songSelect.ScreenSpaceDrawQuad.TopLeft.X + 1, songSelect.ScreenSpaceDrawQuad.TopLeft.Y + songSelect.ScreenSpaceDrawQuad.Height / 2))); AddStep("Click left mouse button", () => InputManager.Click(MouseButton.Left)); - AddUntilStep("Overlay was hidden", () => songSelect.ModSelectOverlay.State.Value == Visibility.Hidden); + AddUntilStep("Overlay was hidden", () => modSelect.State.Value == Visibility.Hidden); exitViaBackButtonAndConfirm(); } @@ -876,10 +884,18 @@ namespace osu.Game.Tests.Visual.Navigation { AddUntilStep("Wait for toolbar to load", () => Game.Toolbar.IsLoaded); - TestPlaySongSelect songSelect = null; - PushAndConfirm(() => songSelect = new TestPlaySongSelect()); + SoloSongSelect songSelect = null; + ModSelectOverlay modSelect = null; - AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show()); + PushAndConfirm(() => songSelect = new SoloSongSelect()); + AddStep("Show mods overlay", () => + { + modSelect = songSelect!.ChildrenOfType().Single(); + modSelect.Show(); + }); + AddAssert("Overlay was shown", () => modSelect.State.Value == Visibility.Visible); + + AddStep("Show mods overlay", () => modSelect.Show()); AddStep("Change ruleset to osu!taiko", () => { @@ -890,7 +906,7 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("Ruleset changed to osu!taiko", () => Game.Toolbar.ChildrenOfType().Single().Current.Value.OnlineID == 1); - AddAssert("Mods overlay still visible", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible); + AddAssert("Mods overlay still visible", () => modSelect.State.Value == Visibility.Visible); } [Test] @@ -900,10 +916,12 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); - TestPlaySongSelect songSelect = null; - PushAndConfirm(() => songSelect = new TestPlaySongSelect()); + SoloSongSelect songSelect = null; + PushAndConfirm(() => songSelect = new SoloSongSelect()); + AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented); - AddStep("Show options overlay", () => songSelect.BeatmapOptionsOverlay.Show()); + AddStep("Show options overlay", () => InputManager.Key(Key.F3)); + AddUntilStep("Options overlay visible", () => this.ChildrenOfType().SingleOrDefault()?.State.Value == Visibility.Visible); AddStep("Change ruleset to osu!taiko", () => { @@ -914,7 +932,7 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("Ruleset changed to osu!taiko", () => Game.Toolbar.ChildrenOfType().Single().Current.Value.OnlineID == 1); - AddAssert("Options overlay still visible", () => songSelect.BeatmapOptionsOverlay.State.Value == Visibility.Visible); + AddAssert("Options overlay still visible", () => this.ChildrenOfType().Single().State.Value == Visibility.Visible); } [Test] @@ -1186,7 +1204,7 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestExitGameFromSongSelect() { - PushAndConfirm(() => new TestPlaySongSelect()); + PushAndConfirm(() => new SoloSongSelect()); exitViaEscapeAndConfirm(); pushEscape(); // returns to osu! logo @@ -1258,10 +1276,10 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("close settings sidebar", () => InputManager.Key(Key.Escape)); - Screens.Select.SongSelect songSelect = null; + Screens.SelectV2.SongSelect songSelect = null; AddRepeatStep("go to solo", () => InputManager.Key(Key.P), 3); - AddUntilStep("wait for song select", () => (songSelect = Game.ScreenStack.CurrentScreen as Screens.Select.SongSelect) != null); - AddUntilStep("wait for beatmap sets loaded", () => songSelect.BeatmapSetsLoaded); + AddUntilStep("wait for song select", () => (songSelect = Game.ScreenStack.CurrentScreen as Screens.SelectV2.SongSelect) != null); + AddUntilStep("wait for beatmap sets loaded", () => songSelect.CarouselItemsPresented); AddStep("switch to osu! ruleset", () => { @@ -1271,7 +1289,7 @@ namespace osu.Game.Tests.Visual.Navigation }); AddStep("touch beatmap wedge", () => { - var wedge = Game.ChildrenOfType().Single(); + var wedge = Game.ChildrenOfType().Single(); var touch = new Touch(TouchSource.Touch2, wedge.ScreenSpaceDrawQuad.Centre); InputManager.BeginTouch(touch); InputManager.EndTouch(touch); @@ -1287,7 +1305,7 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("touch device mod not activated", () => Game.SelectedMods.Value, () => Has.None.InstanceOf()); AddStep("touch beatmap wedge", () => { - var wedge = Game.ChildrenOfType().Single(); + var wedge = Game.ChildrenOfType().Single(); var touch = new Touch(TouchSource.Touch2, wedge.ScreenSpaceDrawQuad.Centre); InputManager.BeginTouch(touch); InputManager.EndTouch(touch); @@ -1304,7 +1322,7 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("click beatmap wedge", () => { - InputManager.MoveMouseTo(Game.ChildrenOfType().Single()); + InputManager.MoveMouseTo(Game.ChildrenOfType().Single()); InputManager.Click(MouseButton.Left); }); AddUntilStep("touch device mod not activated", () => Game.SelectedMods.Value, () => Has.None.InstanceOf()); @@ -1315,7 +1333,7 @@ namespace osu.Game.Tests.Visual.Navigation { BeatmapSetInfo beatmapSet = null; - PushAndConfirm(() => new TestPlaySongSelect()); + PushAndConfirm(() => new SoloSongSelect()); AddStep("import beatmap", () => beatmapSet = BeatmapImportHelper.LoadQuickOszIntoOsu(Game).GetResultSafely()); AddUntilStep("wait for selected", () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)); AddStep("select", () => InputManager.Key(Key.Enter)); @@ -1345,9 +1363,9 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestExitSongSelectAndImmediatelyClickLogo() { - Screens.Select.SongSelect songSelect = null; - PushAndConfirm(() => songSelect = new TestPlaySongSelect()); - AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + Screens.SelectV2.SongSelect songSelect = null; + PushAndConfirm(() => songSelect = new SoloSongSelect()); + AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented); AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); @@ -1376,9 +1394,9 @@ namespace osu.Game.Tests.Visual.Navigation { BeatmapSetInfo beatmap = null; - Screens.Select.SongSelect songSelect = null; - PushAndConfirm(() => songSelect = new TestPlaySongSelect()); - AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + Screens.SelectV2.SongSelect songSelect = null; + PushAndConfirm(() => songSelect = new SoloSongSelect()); + AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented); AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); @@ -1407,9 +1425,9 @@ namespace osu.Game.Tests.Visual.Navigation IWorkingBeatmap beatmap() => Game.Beatmap.Value; - Screens.Select.SongSelect songSelect = null; - PushAndConfirm(() => songSelect = new TestPlaySongSelect()); - AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + Screens.SelectV2.SongSelect songSelect = null; + PushAndConfirm(() => songSelect = new SoloSongSelect()); + AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented); AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); @@ -1447,12 +1465,5 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("Click back button", () => InputManager.Click(MouseButton.Left)); ConfirmAtMainMenu(); } - - public partial class TestPlaySongSelect : PlaySongSelect - { - public ModSelectOverlay ModSelectOverlay => ModSelect; - - public BeatmapOptionsOverlay BeatmapOptionsOverlay => BeatmapOptions; - } } } diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs index 622c85774a..fe76b74bcb 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs @@ -17,6 +17,7 @@ using osu.Framework.Testing; using osu.Framework.Threading; using osu.Game.Online.API; using osu.Game.Beatmaps; +using osu.Game.Overlays.Mods; using osu.Game.Overlays.Settings; using osu.Game.Overlays.SkinEditor; using osu.Game.Rulesets.Mods; @@ -26,17 +27,19 @@ using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.HitErrorMeters; +using osu.Game.Screens.SelectV2; using osu.Game.Skinning; using osu.Game.Tests.Beatmaps.IO; using osuTK; using osuTK.Input; -using static osu.Game.Tests.Visual.Navigation.TestSceneScreenNavigation; namespace osu.Game.Tests.Visual.Navigation { public partial class TestSceneSkinEditorNavigation : OsuGameTestScene { - private TestPlaySongSelect songSelect; + private SoloSongSelect songSelect; + private ModSelectOverlay modSelect => songSelect.ChildrenOfType().First(); + private SkinEditor skinEditor => Game.ChildrenOfType().FirstOrDefault(); [Test] @@ -331,10 +334,10 @@ namespace osu.Game.Tests.Visual.Navigation public void TestModOverlayClosesOnOpeningSkinEditor() { advanceToSongSelect(); - AddStep("open mod overlay", () => songSelect.ModSelectOverlay.Show()); + AddStep("open mod overlay", () => modSelect.Show()); openSkinEditor(); - AddUntilStep("mod overlay closed", () => songSelect.ModSelectOverlay.State.Value == Visibility.Hidden); + AddUntilStep("mod overlay closed", () => modSelect.State.Value == Visibility.Hidden); } [Test] @@ -448,8 +451,8 @@ namespace osu.Game.Tests.Visual.Navigation private void advanceToSongSelect() { - PushAndConfirm(() => songSelect = new TestPlaySongSelect()); - AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + PushAndConfirm(() => songSelect = new SoloSongSelect()); + AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented); } private void openSkinEditor() diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs deleted file mode 100644 index 44f64365f0..0000000000 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ /dev/null @@ -1,548 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Extensions; -using osu.Framework.Graphics; -using osu.Framework.Platform; -using osu.Framework.Testing; -using osu.Game.Beatmaps; -using osu.Game.Graphics.Cursor; -using osu.Game.Graphics.UserInterface; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Leaderboards; -using osu.Game.Overlays; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Scoring; -using osu.Game.Screens.Select; -using osu.Game.Screens.Select.Leaderboards; -using osu.Game.Tests.Resources; -using osu.Game.Users; -using osuTK; -using osuTK.Input; - -namespace osu.Game.Tests.Visual.SongSelect -{ - public partial class TestSceneBeatmapLeaderboard : OsuManualInputManagerTestScene - { - private readonly FailableLeaderboard leaderboard; - - [Cached(typeof(IDialogOverlay))] - private readonly DialogOverlay dialogOverlay; - - private ScoreManager scoreManager = null!; - private RulesetStore rulesetStore = null!; - private BeatmapManager beatmapManager = null!; - private PlaySongSelect songSelect = null!; - - private LeaderboardManager leaderboardManager = null!; - - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - - dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm)); - dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); - dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, API)); - dependencies.CacheAs(songSelect = new PlaySongSelect()); - dependencies.Cache(leaderboardManager = new LeaderboardManager()); - - Dependencies.Cache(Realm); - - return dependencies; - } - - [BackgroundDependencyLoader] - private void load() - { - LoadComponent(songSelect); - LoadComponent(leaderboardManager); - } - - public TestSceneBeatmapLeaderboard() - { - Add(new OsuContextMenuContainer - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - dialogOverlay = new DialogOverlay - { - Depth = -1 - }, - leaderboard = new FailableLeaderboard - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Size = new Vector2(550f, 450f), - Scope = BeatmapLeaderboardScope.Global, - } - } - }); - } - - [Test] - public void TestLocalScoresDisplay() - { - BeatmapInfo beatmapInfo = null!; - - AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Local); - - AddStep(@"Set beatmap", () => - { - beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); - beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); - - leaderboard.BeatmapInfo = beatmapInfo; - }); - - clearScores(); - checkDisplayedCount(0); - - importMoreScores(() => beatmapInfo); - checkDisplayedCount(10); - - importMoreScores(() => beatmapInfo); - checkDisplayedCount(20); - - clearScores(); - checkDisplayedCount(0); - } - - [Test] - public void TestLocalScoresDisplayWorksWhenStartingOffline() - { - BeatmapInfo beatmapInfo = null!; - - AddStep("Log out", () => API.Logout()); - AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Local); - - AddStep(@"Set beatmap", () => - { - beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); - beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); - - leaderboard.BeatmapInfo = beatmapInfo; - }); - - clearScores(); - importMoreScores(() => beatmapInfo); - checkDisplayedCount(10); - } - - [Test] - public void TestLocalScoresDisplayOnBeatmapEdit() - { - BeatmapInfo beatmapInfo = null!; - string originalHash = string.Empty; - - AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Local); - - AddStep(@"Import beatmap", () => - { - beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); - beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); - - leaderboard.BeatmapInfo = beatmapInfo; - }); - - clearScores(); - checkDisplayedCount(0); - - AddStep(@"Perform initial save to guarantee stable hash", () => - { - IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(beatmapInfo).Beatmap; - beatmapManager.Save(beatmapInfo, beatmap); - - originalHash = beatmapInfo.Hash; - }); - - importMoreScores(() => beatmapInfo); - - checkDisplayedCount(10); - checkStoredCount(10); - - AddStep(@"Save with changes", () => - { - IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(beatmapInfo).Beatmap; - beatmap.Difficulty.ApproachRate = 12; - beatmapManager.Save(beatmapInfo, beatmap); - }); - - AddAssert("Hash changed", () => beatmapInfo.Hash, () => Is.Not.EqualTo(originalHash)); - checkDisplayedCount(0); - checkStoredCount(10); - - importMoreScores(() => beatmapInfo); - importMoreScores(() => beatmapInfo); - checkDisplayedCount(20); - checkStoredCount(30); - - AddStep(@"Revert changes", () => - { - IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(beatmapInfo).Beatmap; - beatmap.Difficulty.ApproachRate = 8; - beatmapManager.Save(beatmapInfo, beatmap); - }); - - AddAssert("Hash restored", () => beatmapInfo.Hash, () => Is.EqualTo(originalHash)); - checkDisplayedCount(10); - checkStoredCount(30); - - clearScores(); - checkDisplayedCount(0); - checkStoredCount(0); - } - - [Test] - public void TestGlobalScoresDisplay() - { - AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Global); - AddStep(@"New Scores", () => leaderboard.SetScores(GenerateSampleScores(new BeatmapInfo()))); - AddStep(@"New Scores with teams", () => leaderboard.SetScores(GenerateSampleScores(new BeatmapInfo()).Select(s => - { - s.User.Team = new APITeam(); - return s; - }))); - } - - [Test] - public void TestPersonalBest() - { - AddStep(@"Show personal best", showPersonalBest); - AddStep("null personal best position", showPersonalBestWithNullPosition); - } - - [Test] - public void TestPlaceholderStates() - { - AddStep("ensure no scores displayed", () => leaderboard.SetScores(null)); - - AddStep(@"Network failure", () => leaderboard.SetErrorState(LeaderboardState.NetworkFailure)); - AddStep(@"No team", () => leaderboard.SetErrorState(LeaderboardState.NoTeam)); - AddStep(@"No supporter", () => leaderboard.SetErrorState(LeaderboardState.NotSupporter)); - AddStep(@"Not logged in", () => leaderboard.SetErrorState(LeaderboardState.NotLoggedIn)); - AddStep(@"Ruleset unavailable", () => leaderboard.SetErrorState(LeaderboardState.RulesetUnavailable)); - AddStep(@"Beatmap unavailable", () => leaderboard.SetErrorState(LeaderboardState.BeatmapUnavailable)); - AddStep(@"None selected", () => leaderboard.SetErrorState(LeaderboardState.NoneSelected)); - } - - [Test] - public void TestUseTheseModsDoesNotCopySystemMods() - { - AddStep(@"set scores", () => leaderboard.SetScores(leaderboard.Scores, new ScoreInfo - { - Position = 999, - Rank = ScoreRank.XH, - Accuracy = 1, - MaxCombo = 244, - TotalScore = 1707827, - Ruleset = new OsuRuleset().RulesetInfo, - Mods = new Mod[] { new OsuModHidden(), new ModScoreV2(), }, - User = new APIUser - { - Id = 6602580, - Username = @"waaiiru", - CountryCode = CountryCode.ES, - } - })); - AddUntilStep("wait for scores", () => this.ChildrenOfType().Count(), () => Is.GreaterThan(0)); - AddStep("right click panel", () => - { - InputManager.MoveMouseTo(this.ChildrenOfType().Single()); - InputManager.Click(MouseButton.Right); - }); - AddStep("click use these mods", () => - { - InputManager.MoveMouseTo(this.ChildrenOfType().Single()); - InputManager.Click(MouseButton.Left); - }); - AddAssert("song select received HD", () => songSelect.Mods.Value.Any(m => m is OsuModHidden)); - AddAssert("song select did not receive SV2", () => !songSelect.Mods.Value.Any(m => m is ModScoreV2)); - } - - private void showPersonalBestWithNullPosition() - { - leaderboard.SetScores(leaderboard.Scores, new ScoreInfo - { - Rank = ScoreRank.XH, - Accuracy = 1, - MaxCombo = 244, - TotalScore = 1707827, - Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock() }, - Ruleset = new OsuRuleset().RulesetInfo, - User = new APIUser - { - Id = 6602580, - Username = @"waaiiru", - CountryCode = CountryCode.ES, - }, - }); - } - - private void showPersonalBest() - { - leaderboard.SetScores(leaderboard.Scores, new ScoreInfo - { - Position = 999, - Rank = ScoreRank.XH, - Accuracy = 1, - MaxCombo = 244, - TotalScore = 1707827, - Ruleset = new OsuRuleset().RulesetInfo, - Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - User = new APIUser - { - Id = 6602580, - Username = @"waaiiru", - CountryCode = CountryCode.ES, - } - }); - } - - private void importMoreScores(Func beatmapInfo) - { - AddStep(@"Import new scores", () => - { - foreach (var score in GenerateSampleScores(beatmapInfo())) - scoreManager.Import(score); - }); - } - - private void clearScores() - { - AddStep("Clear all scores", () => scoreManager.Delete()); - } - - private void checkDisplayedCount(int expected) => - AddUntilStep($"{expected} scores displayed", () => leaderboard.ChildrenOfType().Count(), () => Is.EqualTo(expected)); - - private void checkStoredCount(int expected) => - AddUntilStep($"Total scores stored is {expected}", () => Realm.Run(r => r.All().Count(s => !s.DeletePending)), () => Is.EqualTo(expected)); - - public static ScoreInfo[] GenerateSampleScores(BeatmapInfo beatmapInfo) - { - return new[] - { - new ScoreInfo - { - Rank = ScoreRank.XH, - Accuracy = 1, - MaxCombo = 244, - TotalScore = 1707827, - Date = DateTime.Now, - Mods = new Mod[] - { - new OsuModHidden(), - new OsuModFlashlight - { - FollowDelay = { Value = 200 }, - SizeMultiplier = { Value = 5 }, - }, - new OsuModDifficultyAdjust - { - CircleSize = { Value = 11 }, - ApproachRate = { Value = 10 }, - OverallDifficulty = { Value = 10 }, - DrainRate = { Value = 10 }, - ExtendedLimits = { Value = true } - } - }, - Ruleset = new OsuRuleset().RulesetInfo, - BeatmapInfo = beatmapInfo, - BeatmapHash = beatmapInfo.Hash, - User = new APIUser - { - Id = 6602580, - Username = @"waaiiru", - CountryCode = CountryCode.ES, - }, - }, - new ScoreInfo - { - Rank = ScoreRank.X, - Accuracy = 1, - MaxCombo = 244, - TotalScore = 1707827, - Date = DateTime.Now.AddSeconds(-30), - Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - BeatmapInfo = beatmapInfo, - BeatmapHash = beatmapInfo.Hash, - Ruleset = new OsuRuleset().RulesetInfo, - User = new APIUser - { - Id = 4608074, - Username = @"Skycries", - CountryCode = CountryCode.BR, - }, - }, - new ScoreInfo - { - Rank = ScoreRank.SH, - Accuracy = 1, - MaxCombo = 244, - TotalScore = 1707827, - Date = DateTime.Now.AddSeconds(-70), - Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - BeatmapInfo = beatmapInfo, - BeatmapHash = beatmapInfo.Hash, - Ruleset = new OsuRuleset().RulesetInfo, - - User = new APIUser - { - Id = 1014222, - Username = @"eLy", - CountryCode = CountryCode.JP, - }, - }, - new ScoreInfo - { - Rank = ScoreRank.S, - Accuracy = 1, - MaxCombo = 244, - TotalScore = 1707827, - Date = DateTime.Now.AddMinutes(-40), - Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - BeatmapInfo = beatmapInfo, - BeatmapHash = beatmapInfo.Hash, - Ruleset = new OsuRuleset().RulesetInfo, - - User = new APIUser - { - Id = 1541390, - Username = @"Toukai", - CountryCode = CountryCode.CA, - }, - }, - new ScoreInfo - { - Rank = ScoreRank.A, - Accuracy = 1, - MaxCombo = 244, - TotalScore = 1707827, - Date = DateTime.Now.AddHours(-2), - Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - BeatmapInfo = beatmapInfo, - BeatmapHash = beatmapInfo.Hash, - Ruleset = new OsuRuleset().RulesetInfo, - - User = new APIUser - { - Id = 2243452, - Username = @"Satoruu", - CountryCode = CountryCode.VE, - }, - }, - new ScoreInfo - { - Rank = ScoreRank.B, - Accuracy = 0.9826, - MaxCombo = 244, - TotalScore = 1707827, - Date = DateTime.Now.AddHours(-25), - Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - BeatmapInfo = beatmapInfo, - BeatmapHash = beatmapInfo.Hash, - Ruleset = new OsuRuleset().RulesetInfo, - - User = new APIUser - { - Id = 2705430, - Username = @"Mooha", - CountryCode = CountryCode.FR, - }, - }, - new ScoreInfo - { - Rank = ScoreRank.C, - Accuracy = 0.9654, - MaxCombo = 244, - TotalScore = 1707827, - Date = DateTime.Now.AddHours(-50), - Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - BeatmapInfo = beatmapInfo, - BeatmapHash = beatmapInfo.Hash, - Ruleset = new OsuRuleset().RulesetInfo, - - User = new APIUser - { - Id = 7151382, - Username = @"Mayuri Hana", - CountryCode = CountryCode.TH, - }, - }, - new ScoreInfo - { - Rank = ScoreRank.D, - Accuracy = 0.6025, - MaxCombo = 244, - TotalScore = 1707827, - Date = DateTime.Now.AddHours(-72), - Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - BeatmapInfo = beatmapInfo, - BeatmapHash = beatmapInfo.Hash, - Ruleset = new OsuRuleset().RulesetInfo, - - User = new APIUser - { - Id = 2051389, - Username = @"FunOrange", - CountryCode = CountryCode.CA, - }, - }, - new ScoreInfo - { - Rank = ScoreRank.D, - Accuracy = 0.5140, - MaxCombo = 244, - TotalScore = 1707827, - Date = DateTime.Now.AddMonths(-10), - Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - BeatmapInfo = beatmapInfo, - BeatmapHash = beatmapInfo.Hash, - Ruleset = new OsuRuleset().RulesetInfo, - - User = new APIUser - { - Id = 6169483, - Username = @"-Hebel-", - CountryCode = CountryCode.MX, - }, - }, - new ScoreInfo - { - Rank = ScoreRank.D, - Accuracy = 0.4222, - MaxCombo = 244, - TotalScore = 1707827, - Date = DateTime.Now.AddYears(-2), - Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - BeatmapInfo = beatmapInfo, - BeatmapHash = beatmapInfo.Hash, - Ruleset = new OsuRuleset().RulesetInfo, - - User = new APIUser - { - Id = 6702666, - Username = @"prhtnsm", - CountryCode = CountryCode.DE, - }, - }, - }; - } - - private partial class FailableLeaderboard : BeatmapLeaderboard - { - public new void SetErrorState(LeaderboardState state) => base.SetErrorState(state); - public new void SetScores(IEnumerable? scores, ScoreInfo? userScore = null) => base.SetScores(scores, userScore); - } - } -} diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index d459eac3c2..832e8fc90f 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -23,6 +23,7 @@ using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Taiko; +using osu.Game.Screens.SelectV2; using osu.Game.Tests.Resources; using osu.Game.Users; using osu.Game.Utils; @@ -248,7 +249,7 @@ namespace osu.Game.Tests.Visual.SongSelect { AddStep("present beatmap", () => Game.PresentBeatmap(getImport())); - AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded); + AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect select && select.CarouselItemsPresented); AddUntilStep("recommended beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(getImport().Beatmaps[expectedDiff - 1].OnlineID)); } diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs deleted file mode 100644 index 9dc6bc8a33..0000000000 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ /dev/null @@ -1,1445 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Bindables; -using osu.Framework.Extensions; -using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Extensions.ObjectExtensions; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input; -using osu.Framework.Platform; -using osu.Framework.Screens; -using osu.Framework.Testing; -using osu.Framework.Utils; -using osu.Game.Beatmaps; -using osu.Game.Configuration; -using osu.Game.Database; -using osu.Game.Extensions; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Chat; -using osu.Game.Overlays; -using osu.Game.Overlays.Dialog; -using osu.Game.Overlays.Mods; -using osu.Game.Overlays.Notifications; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Mania.Mods; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Scoring; -using osu.Game.Screens.Play; -using osu.Game.Screens.Select; -using osu.Game.Screens.Select.Carousel; -using osu.Game.Screens.Select.Filter; -using osu.Game.Tests.Resources; -using osuTK.Input; - -namespace osu.Game.Tests.Visual.SongSelect -{ - [TestFixture] - public partial class TestScenePlaySongSelect : ScreenTestScene - { - private BeatmapManager manager = null!; - private RulesetStore rulesets = null!; - private MusicController music = null!; - private WorkingBeatmap defaultBeatmap = null!; - private OsuConfigManager config = null!; - private TestSongSelect? songSelect; - - [BackgroundDependencyLoader] - private void load(GameHost host, AudioManager audio) - { - BeatmapStore beatmapStore; - - // These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install. - // At a point we have isolated interactive test runs enough, this can likely be removed. - Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); - Dependencies.Cache(Realm); - Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, defaultBeatmap = Beatmap.Default)); - Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore()); - - Dependencies.Cache(music = new MusicController()); - - // required to get bindables attached - Add(music); - Add(beatmapStore); - - Dependencies.Cache(config = new OsuConfigManager(LocalStorage)); - } - - public override void SetUpSteps() - { - base.SetUpSteps(); - - AddStep("reset defaults", () => - { - Ruleset.Value = new OsuRuleset().RulesetInfo; - - Beatmap.SetDefault(); - SelectedMods.SetDefault(); - - songSelect = null; - }); - - AddStep("delete all beatmaps", () => manager.Delete()); - } - - [Test] - public void TestSpeedChange() - { - createSongSelect(); - changeMods(); - - decreaseModSpeed(); - AddAssert("half time activated at 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005)); - - decreaseModSpeed(); - AddAssert("half time speed changed to 0.9x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.9).Within(0.005)); - - increaseModSpeed(); - AddAssert("half time speed changed to 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005)); - - increaseModSpeed(); - AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0); - - increaseModSpeed(); - AddAssert("double time activated at 1.05x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005)); - - increaseModSpeed(); - AddAssert("double time speed changed to 1.1x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.1).Within(0.005)); - - decreaseModSpeed(); - AddAssert("double time speed changed to 1.05x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005)); - - OsuModNightcore nc = new OsuModNightcore - { - SpeedChange = { Value = 1.05 } - }; - changeMods(nc); - - increaseModSpeed(); - AddAssert("nightcore speed changed to 1.1x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.1).Within(0.005)); - - decreaseModSpeed(); - AddAssert("nightcore speed changed to 1.05x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005)); - - decreaseModSpeed(); - AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0); - - decreaseModSpeed(); - AddAssert("daycore activated at 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005)); - - decreaseModSpeed(); - AddAssert("daycore activated at 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.9).Within(0.005)); - - increaseModSpeed(); - AddAssert("daycore activated at 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005)); - - OsuModDoubleTime dt = new OsuModDoubleTime - { - SpeedChange = { Value = 1.02 }, - AdjustPitch = { Value = true }, - }; - changeMods(dt); - - decreaseModSpeed(); - AddAssert("half time activated at 0.97x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.97).Within(0.005)); - AddAssert("adjust pitch preserved", () => songSelect!.Mods.Value.OfType().Single().AdjustPitch.Value, () => Is.True); - - OsuModHalfTime ht = new OsuModHalfTime - { - SpeedChange = { Value = 0.97 }, - AdjustPitch = { Value = true }, - }; - Mod[] modlist = { ht, new OsuModHardRock(), new OsuModHidden() }; - changeMods(modlist); - - increaseModSpeed(); - AddAssert("double time activated at 1.02x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.02).Within(0.005)); - AddAssert("double time activated at 1.02x", () => songSelect!.Mods.Value.OfType().Single().AdjustPitch.Value, () => Is.True); - AddAssert("HD still enabled", () => songSelect!.Mods.Value.OfType().SingleOrDefault(), () => Is.Not.Null); - AddAssert("HR still enabled", () => songSelect!.Mods.Value.OfType().SingleOrDefault(), () => Is.Not.Null); - - changeMods(new ModWindUp()); - increaseModSpeed(); - AddAssert("windup still active", () => songSelect!.Mods.Value.First() is ModWindUp); - - changeMods(new ModAdaptiveSpeed()); - increaseModSpeed(); - AddAssert("adaptive speed still active", () => songSelect!.Mods.Value.First() is ModAdaptiveSpeed); - - OsuModDoubleTime dtWithAdjustPitch = new OsuModDoubleTime - { - SpeedChange = { Value = 1.05 }, - AdjustPitch = { Value = true }, - }; - changeMods(dtWithAdjustPitch); - - decreaseModSpeed(); - AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0); - - decreaseModSpeed(); - AddAssert("half time activated at 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005)); - AddAssert("half time has adjust pitch active", () => songSelect!.Mods.Value.OfType().Single().AdjustPitch.Value, () => Is.True); - - AddStep("turn off adjust pitch", () => songSelect!.Mods.Value.OfType().Single().AdjustPitch.Value = false); - - increaseModSpeed(); - AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0); - - increaseModSpeed(); - AddAssert("double time activated at 1.05x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005)); - AddAssert("double time has adjust pitch inactive", () => songSelect!.Mods.Value.OfType().Single().AdjustPitch.Value, () => Is.False); - - void increaseModSpeed() => AddStep("increase mod speed", () => - { - InputManager.PressKey(Key.ControlLeft); - InputManager.Key(Key.Up); - InputManager.ReleaseKey(Key.ControlLeft); - }); - - void decreaseModSpeed() => AddStep("decrease mod speed", () => - { - InputManager.PressKey(Key.ControlLeft); - InputManager.Key(Key.Down); - InputManager.ReleaseKey(Key.ControlLeft); - }); - } - - [Test] - public void TestPlaceholderBeatmapPresence() - { - createSongSelect(); - - AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible); - - addRulesetImportStep(0); - AddUntilStep("wait for placeholder hidden", () => getPlaceholder()?.State.Value == Visibility.Hidden); - - AddStep("delete all beatmaps", () => manager.Delete()); - AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible); - } - - [Test] - public void TestPlaceholderStarDifficulty() - { - addRulesetImportStep(0); - AddStep("change star filter", () => config.SetValue(OsuSetting.DisplayStarsMinimum, 10.0)); - - createSongSelect(); - - AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible); - - AddStep("click link in placeholder", () => getPlaceholder().ChildrenOfType().First().TriggerClick()); - - AddUntilStep("star filter reset", () => config.Get(OsuSetting.DisplayStarsMinimum) == 0.0); - AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Hidden); - } - - [Test] - public void TestPlaceholderConvertSetting() - { - addRulesetImportStep(0); - AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, false)); - - createSongSelect(); - - changeRuleset(2); - - AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible); - - AddStep("click link in placeholder", () => getPlaceholder().ChildrenOfType().First().TriggerClick()); - - AddUntilStep("convert setting changed", () => config.Get(OsuSetting.ShowConvertedBeatmaps)); - AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Hidden); - } - - [Test] - public void TestSingleFilterOnEnter() - { - addRulesetImportStep(0); - addRulesetImportStep(0); - - createSongSelect(); - - AddAssert("filter count is 0", () => songSelect?.FilterCount, () => Is.EqualTo(0)); - } - - [Test] - public void TestChangeBeatmapBeforeEnter() - { - addRulesetImportStep(0); - - createSongSelect(); - - waitForInitialSelection(); - - WorkingBeatmap? selected = null; - - AddStep("store selected beatmap", () => selected = Beatmap.Value); - - AddStep("select next and enter", () => - { - InputManager.Key(Key.Down); - InputManager.Key(Key.Enter); - }); - - waitForDismissed(); - AddAssert("ensure selection changed", () => selected != Beatmap.Value); - } - - [Test] - public void TestChangeBeatmapAfterEnter() - { - addRulesetImportStep(0); - - createSongSelect(); - - waitForInitialSelection(); - - WorkingBeatmap? selected = null; - - AddStep("store selected beatmap", () => selected = Beatmap.Value); - - AddStep("select next and enter", () => - { - InputManager.Key(Key.Enter); - InputManager.Key(Key.Down); - }); - - waitForDismissed(); - AddAssert("ensure selection didn't change", () => selected == Beatmap.Value); - } - - [Test] - public void TestChangeBeatmapViaMouseBeforeEnter() - { - addRulesetImportStep(0); - - createSongSelect(); - - AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault); - - WorkingBeatmap? selected = null; - - AddStep("store selected beatmap", () => selected = Beatmap.Value); - - AddUntilStep("wait for beatmaps to load", () => songSelect!.Carousel.ChildrenOfType().Any()); - - AddStep("select next and enter", () => - { - InputManager.MoveMouseTo(songSelect!.Carousel.ChildrenOfType() - .First(b => !((CarouselBeatmap)b.Item!).BeatmapInfo.Equals(songSelect!.Carousel.SelectedBeatmapInfo))); - - InputManager.Click(MouseButton.Left); - - InputManager.Key(Key.Enter); - }); - - waitForDismissed(); - AddAssert("ensure selection changed", () => selected != Beatmap.Value); - } - - [Test] - public void TestChangeBeatmapViaMouseAfterEnter() - { - addRulesetImportStep(0); - - createSongSelect(); - - waitForInitialSelection(); - - WorkingBeatmap? selected = null; - - AddStep("store selected beatmap", () => selected = Beatmap.Value); - - AddStep("select next and enter", () => - { - InputManager.MoveMouseTo(songSelect!.Carousel.ChildrenOfType() - .First(b => !((CarouselBeatmap)b.Item!).BeatmapInfo.Equals(songSelect!.Carousel.SelectedBeatmapInfo))); - - InputManager.PressButton(MouseButton.Left); - - InputManager.Key(Key.Enter); - - InputManager.ReleaseButton(MouseButton.Left); - }); - - waitForDismissed(); - AddAssert("ensure selection didn't change", () => selected == Beatmap.Value); - } - - [Test] - public void TestNoFilterOnSimpleResume() - { - addRulesetImportStep(0); - addRulesetImportStep(0); - - createSongSelect(); - - AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child"))); - waitForDismissed(); - - AddStep("return", () => songSelect!.MakeCurrent()); - AddUntilStep("wait for current", () => songSelect!.IsCurrentScreen()); - AddAssert("filter count is 0", () => songSelect!.FilterCount, () => Is.EqualTo(0)); - } - - [Test] - public void TestFilterOnResumeAfterChange() - { - addRulesetImportStep(0); - addRulesetImportStep(0); - - AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, false)); - - createSongSelect(); - - AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child"))); - waitForDismissed(); - - AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true)); - - AddStep("return", () => songSelect!.MakeCurrent()); - AddUntilStep("wait for current", () => songSelect!.IsCurrentScreen()); - AddAssert("filter count is 1", () => songSelect!.FilterCount, () => Is.EqualTo(1)); - } - - [Test] - public void TestCarouselSelectionUpdatesOnResume() - { - addRulesetImportStep(0); - - createSongSelect(); - - AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child"))); - waitForDismissed(); - - AddStep("update beatmap", () => - { - var selectedBeatmap = Beatmap.Value.BeatmapInfo; - var anotherBeatmap = Beatmap.Value.BeatmapSetInfo.Beatmaps.Except(selectedBeatmap.Yield()).First(); - Beatmap.Value = manager.GetWorkingBeatmap(anotherBeatmap); - }); - - AddStep("return", () => songSelect!.MakeCurrent()); - AddUntilStep("wait for current", () => songSelect!.IsCurrentScreen()); - AddAssert("carousel updated", () => songSelect!.Carousel.SelectedBeatmapInfo?.Equals(Beatmap.Value.BeatmapInfo) == true); - } - - [Test] - public void TestAudioResuming() - { - createSongSelect(); - - // We need to use one real beatmap to trigger the "same-track-transfer" logic that we're looking to test here. - // See `SongSelect.ensurePlayingSelected` and `WorkingBeatmap.TryTransferTrack`. - AddStep("import test beatmap", () => manager.Import(new ImportTask(TestResources.GetTestBeatmapForImport())).WaitSafely()); - addRulesetImportStep(0); - - checkMusicPlaying(true); - AddStep("select first", () => songSelect!.Carousel.SelectBeatmap(songSelect!.Carousel.BeatmapSets.First().Beatmaps.First())); - checkMusicPlaying(true); - - AddStep("manual pause", () => music.TogglePause()); - checkMusicPlaying(false); - - // Track should not have changed, so music should still not be playing. - AddStep("select next difficulty", () => songSelect!.Carousel.SelectNext(skipDifficulties: false)); - checkMusicPlaying(false); - - AddStep("select next set", () => songSelect!.Carousel.SelectNext()); - checkMusicPlaying(true); - } - - [TestCase(false)] - [TestCase(true)] - public void TestAudioRemainsCorrectOnRulesetChange(bool rulesetsInSameBeatmap) - { - createSongSelect(); - - // start with non-osu! to avoid convert confusion - changeRuleset(1); - - if (rulesetsInSameBeatmap) - { - AddStep("import multi-ruleset map", () => - { - var usableRulesets = rulesets.AvailableRulesets.Where(r => r.OnlineID != 2).ToArray(); - manager.Import(TestResources.CreateTestBeatmapSetInfo(rulesets: usableRulesets)); - }); - } - else - { - addRulesetImportStep(1); - addRulesetImportStep(0); - } - - checkMusicPlaying(true); - - AddStep("manual pause", () => music.TogglePause()); - checkMusicPlaying(false); - - changeRuleset(0); - checkMusicPlaying(!rulesetsInSameBeatmap); - } - - [Test] - public void TestDummy() - { - createSongSelect(); - AddUntilStep("dummy selected", () => songSelect!.CurrentBeatmap == defaultBeatmap); - - AddUntilStep("dummy shown on wedge", () => songSelect!.CurrentBeatmapDetailsBeatmap == defaultBeatmap); - - addManyTestMaps(); - - AddUntilStep("random map selected", () => songSelect!.CurrentBeatmap != defaultBeatmap); - } - - [Test] - public void TestSorting() - { - createSongSelect(); - addManyTestMaps(); - - AddUntilStep("random map selected", () => songSelect!.CurrentBeatmap != defaultBeatmap); - - AddStep(@"Sort by Artist", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Artist)); - AddStep(@"Sort by Title", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title)); - AddStep(@"Sort by Author", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Author)); - AddStep(@"Sort by DateAdded", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.DateAdded)); - AddStep(@"Sort by BPM", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.BPM)); - AddStep(@"Sort by Length", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Length)); - AddStep(@"Sort by Difficulty", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Difficulty)); - AddStep(@"Sort by Source", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Source)); - } - - [Test] - public void TestImportUnderDifferentRuleset() - { - createSongSelect(); - addRulesetImportStep(2); - AddUntilStep("no selection", () => songSelect!.Carousel.SelectedBeatmapInfo == null); - } - - [Test] - public void TestImportUnderCurrentRuleset() - { - createSongSelect(); - changeRuleset(2); - addRulesetImportStep(2); - addRulesetImportStep(1); - AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.Ruleset.OnlineID == 2); - - changeRuleset(1); - AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.Ruleset.OnlineID == 1); - - changeRuleset(0); - AddUntilStep("no selection", () => songSelect!.Carousel.SelectedBeatmapInfo == null); - } - - [Test] - [Ignore("temporary while peppy investigates. probably realm batching related.")] - public void TestSelectionRetainedOnBeatmapUpdate() - { - createSongSelect(); - changeRuleset(0); - - Live? original = null; - int originalOnlineSetID = 0; - - AddStep(@"Sort by artist", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Artist)); - - AddStep("import original", () => - { - original = manager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).GetResultSafely(); - - Debug.Assert(original != null); - - originalOnlineSetID = original.Value.OnlineID; - }); - - // This will move the beatmap set to a different location in the carousel. - AddStep("Update original with bogus info", () => - { - Debug.Assert(original != null); - - original.PerformWrite(set => - { - foreach (var beatmap in set.Beatmaps) - { - beatmap.Metadata.Artist = "ZZZZZ"; - beatmap.OnlineID = 12804; - } - }); - }); - - AddRepeatStep("import other beatmaps", () => - { - var testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(); - - foreach (var beatmap in testBeatmapSetInfo.Beatmaps) - beatmap.Metadata.Artist = ((char)RNG.Next('A', 'Z')).ToString(); - - manager.Import(testBeatmapSetInfo); - }, 10); - - AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID, () => Is.EqualTo(originalOnlineSetID)); - - Task?> updateTask = null!; - - AddStep("update beatmap", () => - { - Debug.Assert(original != null); - - updateTask = manager.ImportAsUpdate(new ProgressNotification(), new ImportTask(TestResources.GetQuickTestBeatmapForImport()), original.Value); - }); - AddUntilStep("wait for update completion", () => updateTask.IsCompleted); - - AddUntilStep("retained selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID, () => Is.EqualTo(originalOnlineSetID)); - } - - [Test] - public void TestPresentNewRulesetNewBeatmap() - { - createSongSelect(); - changeRuleset(2); - - addRulesetImportStep(2); - AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.Ruleset.OnlineID == 2); - - addRulesetImportStep(0); - addRulesetImportStep(0); - addRulesetImportStep(0); - - BeatmapInfo? target = null; - - AddStep("select beatmap/ruleset externally", () => - { - target = manager.GetAllUsableBeatmapSets() - .Last(b => b.Beatmaps.Any(bi => bi.Ruleset.OnlineID == 0)).Beatmaps.Last(); - - Ruleset.Value = rulesets.AvailableRulesets.First(r => r.OnlineID == 0); - Beatmap.Value = manager.GetWorkingBeatmap(target); - }); - - AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.Equals(target) == true); - - // this is an important check, to make sure updateComponentFromBeatmap() was actually run - AddUntilStep("selection shown on wedge", () => songSelect!.CurrentBeatmapDetailsBeatmap.BeatmapInfo.MatchesOnlineID(target)); - } - - [Test] - public void TestPresentNewBeatmapNewRuleset() - { - createSongSelect(); - changeRuleset(2); - - addRulesetImportStep(2); - AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.Ruleset.OnlineID == 2); - - addRulesetImportStep(0); - addRulesetImportStep(0); - addRulesetImportStep(0); - - BeatmapInfo? target = null; - - AddStep("select beatmap/ruleset externally", () => - { - target = manager.GetAllUsableBeatmapSets() - .Last(b => b.Beatmaps.Any(bi => bi.Ruleset.OnlineID == 0)).Beatmaps.Last(); - - Beatmap.Value = manager.GetWorkingBeatmap(target); - Ruleset.Value = rulesets.AvailableRulesets.First(r => r.OnlineID == 0); - }); - - AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.Equals(target) == true); - - AddUntilStep("has correct ruleset", () => Ruleset.Value.OnlineID == 0); - - // this is an important check, to make sure updateComponentFromBeatmap() was actually run - AddUntilStep("selection shown on wedge", () => songSelect!.CurrentBeatmapDetailsBeatmap.BeatmapInfo.MatchesOnlineID(target)); - } - - [Test] - public void TestModsRetainedBetweenSongSelect() - { - AddAssert("empty mods", () => !SelectedMods.Value.Any()); - - createSongSelect(); - - addRulesetImportStep(0); - - changeMods(new OsuModHardRock()); - - createSongSelect(); - - AddAssert("mods retained", () => SelectedMods.Value.Any()); - } - - [Test] - public void TestStartAfterUnMatchingFilterDoesNotStart() - { - createSongSelect(); - addManyTestMaps(); - AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo != null); - - bool startRequested = false; - - AddStep("set filter and finalize", () => - { - songSelect!.StartRequested = () => startRequested = true; - - songSelect!.Carousel.Filter(new FilterCriteria { SearchText = "somestringthatshouldn'tbematchable" }); - songSelect!.FinaliseSelection(); - - songSelect!.StartRequested = null; - }); - - AddAssert("start not requested", () => !startRequested); - } - - [Test] - public void TestSearchTextWithRulesetCriteria() - { - createSongSelect(); - - addRulesetImportStep(0); - - AddStep("disallow convert display", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, false)); - - AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo != null); - - AddStep("set filter to match all", () => songSelect!.FilterControl.CurrentTextSearch.Value = "Some"); - - changeRuleset(1); - - AddUntilStep("has no selection", () => songSelect!.Carousel.SelectedBeatmapInfo == null); - } - - [TestCase(false)] - [TestCase(true)] - public void TestExternalBeatmapChangeWhileFiltered(bool differentRuleset) - { - createSongSelect(); - // ensure there is at least 1 difficulty for each of the rulesets - // (catch is excluded inside of addManyTestMaps). - addManyTestMaps(3); - - changeRuleset(0); - - // used for filter check below - AddStep("allow convert display", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true)); - - AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo != null); - - AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType().First().Text = "nonono"); - - AddUntilStep("dummy selected", () => Beatmap.Value is DummyWorkingBeatmap); - - AddUntilStep("has no selection", () => songSelect!.Carousel.SelectedBeatmapInfo == null); - - BeatmapInfo? target = null; - - int targetRuleset = differentRuleset ? 1 : 0; - - AddStep("select beatmap externally", () => - { - target = manager.GetAllUsableBeatmapSets() - .First(b => b.Beatmaps.Any(bi => bi.Ruleset.OnlineID == targetRuleset)) - .Beatmaps - .First(bi => bi.Ruleset.OnlineID == targetRuleset); - - Beatmap.Value = manager.GetWorkingBeatmap(target); - }); - - AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo != null); - - AddAssert("selected only shows expected ruleset (plus converts)", () => - { - var selectedPanel = songSelect!.Carousel.ChildrenOfType().First(s => s.Item!.State.Value == CarouselItemState.Selected); - - // special case for converts checked here. - return selectedPanel.ChildrenOfType().All(i => - i.IsFiltered || i.Item.BeatmapInfo.Ruleset.OnlineID == targetRuleset || i.Item.BeatmapInfo.Ruleset.OnlineID == 0); - }); - - AddUntilStep("carousel has correct", () => songSelect!.Carousel.SelectedBeatmapInfo?.MatchesOnlineID(target) == true); - AddUntilStep("game has correct", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(target)); - - AddStep("reset filter text", () => songSelect!.FilterControl.ChildrenOfType().First().Text = string.Empty); - - AddAssert("game still correct", () => Beatmap.Value?.BeatmapInfo.MatchesOnlineID(target) == true); - AddAssert("carousel still correct", () => songSelect!.Carousel.SelectedBeatmapInfo.MatchesOnlineID(target)); - } - - [Test] - public void TestExternalBeatmapChangeWhileFilteredThenRefilter() - { - createSongSelect(); - // ensure there is at least 1 difficulty for each of the rulesets - // (catch is excluded inside of addManyTestMaps). - addManyTestMaps(3); - - changeRuleset(0); - - AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo != null); - - AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType().First().Text = "nonono"); - - AddUntilStep("dummy selected", () => Beatmap.Value is DummyWorkingBeatmap); - - AddUntilStep("has no selection", () => songSelect!.Carousel.SelectedBeatmapInfo == null); - - BeatmapInfo? target = null; - - AddStep("select beatmap externally", () => - { - target = manager - .GetAllUsableBeatmapSets() - .First(b => b.Beatmaps.Any(bi => bi.Ruleset.OnlineID == 1)) - .Beatmaps.First(); - - Beatmap.Value = manager.GetWorkingBeatmap(target); - }); - - AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo != null); - - AddUntilStep("carousel has correct", () => songSelect!.Carousel.SelectedBeatmapInfo?.MatchesOnlineID(target) == true); - AddUntilStep("game has correct", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(target)); - - AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType().First().Text = "nononoo"); - - AddUntilStep("game lost selection", () => Beatmap.Value is DummyWorkingBeatmap); - AddAssert("carousel lost selection", () => songSelect!.Carousel.SelectedBeatmapInfo == null); - } - - [Test] - public void TestAutoplayShortcut() - { - addRulesetImportStep(0); - - createSongSelect(); - - AddUntilStep("wait for selection", () => !Beatmap.IsDefault); - - AddStep("press ctrl+enter", () => - { - InputManager.PressKey(Key.ControlLeft); - InputManager.Key(Key.Enter); - InputManager.ReleaseKey(Key.ControlLeft); - }); - - AddUntilStep("wait for player", () => Stack.CurrentScreen is PlayerLoader); - - AddAssert("autoplay selected", () => songSelect!.Mods.Value.Single() is ModAutoplay); - - AddUntilStep("wait for return to ss", () => songSelect!.IsCurrentScreen()); - - AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0); - } - - [Test] - public void TestAutoplayShortcutKeepsAutoplayIfSelectedAlready() - { - addRulesetImportStep(0); - - createSongSelect(); - - AddUntilStep("wait for selection", () => !Beatmap.IsDefault); - - changeMods(new OsuModAutoplay()); - - AddStep("press ctrl+enter", () => - { - InputManager.PressKey(Key.ControlLeft); - InputManager.Key(Key.Enter); - InputManager.ReleaseKey(Key.ControlLeft); - }); - - AddUntilStep("wait for player", () => Stack.CurrentScreen is PlayerLoader); - - AddAssert("autoplay selected", () => songSelect!.Mods.Value.Single() is ModAutoplay); - - AddUntilStep("wait for return to ss", () => songSelect!.IsCurrentScreen()); - - AddAssert("autoplay still selected", () => songSelect!.Mods.Value.Single() is ModAutoplay); - } - - [Test] - public void TestAutoplayShortcutReturnsInitialModsOnExit() - { - addRulesetImportStep(0); - - createSongSelect(); - - AddUntilStep("wait for selection", () => !Beatmap.IsDefault); - - changeMods(new OsuModRelax()); - - AddStep("press ctrl+enter", () => - { - InputManager.PressKey(Key.ControlLeft); - InputManager.Key(Key.Enter); - InputManager.ReleaseKey(Key.ControlLeft); - }); - - AddUntilStep("wait for player", () => Stack.CurrentScreen is PlayerLoader); - - AddAssert("only autoplay selected", () => songSelect!.Mods.Value.Single() is ModAutoplay); - - AddUntilStep("wait for return to ss", () => songSelect!.IsCurrentScreen()); - - AddAssert("relax returned", () => songSelect!.Mods.Value.Single() is ModRelax); - } - - [Test] - public void TestHideSetSelectsCorrectBeatmap() - { - Guid? previousID = null; - createSongSelect(); - addRulesetImportStep(0); - AddStep("Move to last difficulty", () => songSelect!.Carousel.SelectBeatmap(songSelect!.Carousel.BeatmapSets.First().Beatmaps.Last())); - AddStep("Store current ID", () => previousID = songSelect!.Carousel.SelectedBeatmapInfo!.ID); - AddStep("Hide first beatmap", () => manager.Hide(songSelect!.Carousel.SelectedBeatmapSet!.Beatmaps.First())); - AddAssert("Selected beatmap has not changed", () => songSelect!.Carousel.SelectedBeatmapInfo?.ID == previousID); - } - - [Test] - public void TestDifficultyIconSelecting() - { - addRulesetImportStep(0); - createSongSelect(); - - AddUntilStep("wait for selection", () => !Beatmap.IsDefault); - - DrawableCarouselBeatmapSet set = null!; - AddStep("Find the DrawableCarouselBeatmapSet", () => - { - set = songSelect!.Carousel.ChildrenOfType().First(); - }); - - FilterableDifficultyIcon difficultyIcon = null!; - - AddUntilStep("Find an icon", () => - { - var foundIcon = set.ChildrenOfType() - .FirstOrDefault(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex()); - - if (foundIcon == null) - return false; - - difficultyIcon = foundIcon; - return true; - }); - - AddStep("Click on a difficulty", () => - { - InputManager.MoveMouseTo(difficultyIcon); - - InputManager.Click(MouseButton.Left); - }); - - AddAssert("Selected beatmap correct", () => getCurrentBeatmapIndex() == getDifficultyIconIndex(set, difficultyIcon)); - - double? maxBPM = null; - AddStep("Filter some difficulties", () => songSelect!.Carousel.Filter(new FilterCriteria - { - BPM = new FilterCriteria.OptionalRange - { - Min = maxBPM = songSelect!.Carousel.SelectedBeatmapSet!.MaxBPM, - IsLowerInclusive = true - } - })); - - BeatmapInfo? filteredBeatmap = null; - FilterableDifficultyIcon? filteredIcon = null; - - AddStep("Get filtered icon", () => - { - var selectedSet = songSelect!.Carousel.SelectedBeatmapSet; - - Debug.Assert(selectedSet != null); - - filteredBeatmap = selectedSet.Beatmaps.First(b => b.BPM < maxBPM); - int filteredBeatmapIndex = getBeatmapIndex(selectedSet, filteredBeatmap); - filteredIcon = set.ChildrenOfType().ElementAt(filteredBeatmapIndex); - }); - - AddStep("Click on a filtered difficulty", () => - { - Debug.Assert(filteredIcon != null); - - InputManager.MoveMouseTo(filteredIcon); - - InputManager.Click(MouseButton.Left); - }); - - AddAssert("Selected beatmap correct", () => songSelect!.Carousel.SelectedBeatmapInfo?.Equals(filteredBeatmap) == true); - } - - [Test] - public void TestChangingRulesetOnMultiRulesetBeatmap() - { - int changeCount = 0; - - AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, false)); - AddStep("bind beatmap changed", () => - { - Beatmap.ValueChanged += onChange; - changeCount = 0; - }); - - changeRuleset(0); - - createSongSelect(); - - AddStep("import multi-ruleset map", () => - { - var usableRulesets = rulesets.AvailableRulesets.Where(r => r.OnlineID != 2).ToArray(); - manager.Import(TestResources.CreateTestBeatmapSetInfo(3, usableRulesets)); - }); - - int previousSetID = 0; - - AddUntilStep("wait for selection", () => !Beatmap.IsDefault); - - AddStep("record set ID", () => previousSetID = ((IBeatmapSetInfo)Beatmap.Value.BeatmapSetInfo).OnlineID); - AddAssert("selection changed once", () => changeCount == 1); - - AddAssert("Check ruleset is osu!", () => Ruleset.Value.OnlineID == 0); - - changeRuleset(3); - - AddUntilStep("Check ruleset changed to mania", () => Ruleset.Value.OnlineID == 3); - - AddUntilStep("selection changed", () => changeCount > 1); - - AddAssert("Selected beatmap still same set", () => Beatmap.Value.BeatmapSetInfo.OnlineID == previousSetID); - AddAssert("Selected beatmap is mania", () => Beatmap.Value.BeatmapInfo.Ruleset.OnlineID == 3); - - AddAssert("selection changed only fired twice", () => changeCount == 2); - - AddStep("unbind beatmap changed", () => Beatmap.ValueChanged -= onChange); - AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true)); - - // ReSharper disable once AccessToModifiedClosure - void onChange(ValueChangedEvent valueChangedEvent) => changeCount++; - } - - [Test] - public void TestDifficultyIconSelectingForDifferentRuleset() - { - changeRuleset(0); - - createSongSelect(); - - AddStep("import multi-ruleset map", () => - { - var usableRulesets = rulesets.AvailableRulesets.Where(r => r.OnlineID != 2).ToArray(); - manager.Import(TestResources.CreateTestBeatmapSetInfo(3, usableRulesets)); - }); - - DrawableCarouselBeatmapSet? set = null; - AddUntilStep("Find the DrawableCarouselBeatmapSet", () => - { - set = songSelect!.Carousel.ChildrenOfType().FirstOrDefault(); - return set != null; - }); - - FilterableDifficultyIcon? difficultyIcon = null; - AddUntilStep("Find an icon for different ruleset", () => - { - difficultyIcon = set.ChildrenOfType() - .FirstOrDefault(icon => icon.Item.BeatmapInfo.Ruleset.OnlineID == 3); - return difficultyIcon != null; - }); - - AddAssert("Check ruleset is osu!", () => Ruleset.Value.OnlineID == 0); - - int previousSetID = 0; - - AddStep("record set ID", () => previousSetID = ((IBeatmapSetInfo)Beatmap.Value.BeatmapSetInfo).OnlineID); - - AddStep("Click on a difficulty", () => - { - Debug.Assert(difficultyIcon != null); - - InputManager.MoveMouseTo(difficultyIcon); - - InputManager.Click(MouseButton.Left); - }); - - AddUntilStep("Check ruleset changed to mania", () => Ruleset.Value.OnlineID == 3); - - AddAssert("Selected beatmap still same set", () => songSelect!.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID == previousSetID); - AddAssert("Selected beatmap is mania", () => Beatmap.Value.BeatmapInfo.Ruleset.OnlineID == 3); - } - - [Test] - public void TestGroupedDifficultyIconSelecting() - { - changeRuleset(0); - - createSongSelect(); - - BeatmapSetInfo? imported = null; - - AddStep("import huge difficulty count map", () => - { - var usableRulesets = rulesets.AvailableRulesets.Where(r => r.OnlineID != 2).ToArray(); - imported = manager.Import(TestResources.CreateTestBeatmapSetInfo(50, usableRulesets))?.Value; - }); - - AddStep("select the first beatmap of import", () => Beatmap.Value = manager.GetWorkingBeatmap(imported?.Beatmaps.First())); - - DrawableCarouselBeatmapSet? set = null; - AddUntilStep("Find the DrawableCarouselBeatmapSet", () => - { - set = songSelect!.Carousel.ChildrenOfType().FirstOrDefault(); - return set != null; - }); - - GroupedDifficultyIcon groupIcon = null!; - - AddUntilStep("Find group icon for different ruleset", () => - { - var foundIcon = set.ChildrenOfType() - .FirstOrDefault(icon => icon.Items.First().BeatmapInfo.Ruleset.OnlineID == 3); - - if (foundIcon == null) - return false; - - groupIcon = foundIcon; - return true; - }); - - AddAssert("Check ruleset is osu!", () => Ruleset.Value.OnlineID == 0); - - AddStep("Click on group", () => - { - InputManager.MoveMouseTo(groupIcon); - - InputManager.Click(MouseButton.Left); - }); - - AddUntilStep("Check ruleset changed to mania", () => Ruleset.Value.OnlineID == 3); - - AddAssert("Check first item in group selected", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(groupIcon.Items.First().BeatmapInfo)); - } - - [Test] - public void TestChangeRulesetWhilePresentingScore() - { - BeatmapInfo getPresentBeatmap() => manager.GetAllUsableBeatmapSets().Where(s => !s.DeletePending).SelectMany(s => s.Beatmaps).First(b => b.Ruleset.OnlineID == 0); - BeatmapInfo getSwitchBeatmap() => manager.GetAllUsableBeatmapSets().Where(s => !s.DeletePending).SelectMany(s => s.Beatmaps).First(b => b.Ruleset.OnlineID == 1); - - changeRuleset(0); - - createSongSelect(); - - addRulesetImportStep(0); - addRulesetImportStep(1); - - AddStep("present score", () => - { - // this ruleset change should be overridden by the present. - Ruleset.Value = getSwitchBeatmap().Ruleset; - - songSelect!.PresentScore(new ScoreInfo - { - User = new APIUser { Username = "woo" }, - BeatmapInfo = getPresentBeatmap(), - Ruleset = getPresentBeatmap().Ruleset - }); - }); - - waitForDismissed(); - - AddAssert("check beatmap is correct for score", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(getPresentBeatmap())); - AddAssert("check ruleset is correct for score", () => Ruleset.Value.OnlineID == 0); - } - - [Test] - public void TestChangeBeatmapWhilePresentingScore() - { - BeatmapInfo getPresentBeatmap() => manager.GetAllUsableBeatmapSets().Where(s => !s.DeletePending).SelectMany(s => s.Beatmaps).First(b => b.Ruleset.OnlineID == 0); - BeatmapInfo getSwitchBeatmap() => manager.GetAllUsableBeatmapSets().Where(s => !s.DeletePending).SelectMany(s => s.Beatmaps).First(b => b.Ruleset.OnlineID == 1); - - changeRuleset(0); - - addRulesetImportStep(0); - addRulesetImportStep(1); - - createSongSelect(); - - AddUntilStep("wait for selection", () => !Beatmap.IsDefault); - - AddStep("present score", () => - { - // this beatmap change should be overridden by the present. - Beatmap.Value = manager.GetWorkingBeatmap(getSwitchBeatmap()); - - songSelect!.PresentScore(TestResources.CreateTestScoreInfo(getPresentBeatmap())); - }); - - waitForDismissed(); - - AddAssert("check beatmap is correct for score", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(getPresentBeatmap())); - AddAssert("check ruleset is correct for score", () => Ruleset.Value.OnlineID == 0); - } - - [Test] - public void TestModOverlayToggling() - { - changeRuleset(0); - createSongSelect(); - - AddStep("toggle mod overlay on", () => InputManager.Key(Key.F1)); - AddUntilStep("mod overlay shown", () => songSelect!.ModSelect.State.Value == Visibility.Visible); - - AddStep("toggle mod overlay off", () => InputManager.Key(Key.F1)); - AddUntilStep("mod overlay hidden", () => songSelect!.ModSelect.State.Value == Visibility.Hidden); - } - - [Test] - public void TestBeatmapOptionsDisabled() - { - createSongSelect(); - - addRulesetImportStep(0); - - AddAssert("options enabled", () => songSelect.ChildrenOfType().Single().Enabled.Value); - AddStep("delete all beatmaps", () => manager.Delete()); - AddUntilStep("wait for no beatmap", () => Beatmap.IsDefault); - AddAssert("options disabled", () => !songSelect.ChildrenOfType().Single().Enabled.Value); - } - - [Test] - public void TestTextBoxBeatmapDifficultyCount() - { - createSongSelect(); - - AddAssert("0 matching shown", () => songSelect.ChildrenOfType().Single().InformationalText == "0 matches"); - - addRulesetImportStep(0); - - AddAssert("3 matching shown", () => songSelect.ChildrenOfType().Single().InformationalText == "3 matches"); - AddStep("delete all beatmaps", () => manager.Delete()); - AddUntilStep("wait for no beatmap", () => Beatmap.IsDefault); - AddAssert("0 matching shown", () => songSelect.ChildrenOfType().Single().InformationalText == "0 matches"); - } - - [Test] - public void TestHardDeleteHandledCorrectly() - { - createSongSelect(); - - addRulesetImportStep(0); - AddAssert("3 matching shown", () => songSelect.ChildrenOfType().Single().InformationalText == "3 matches"); - - AddStep("hard delete beatmap", () => Realm.Write(r => r.RemoveRange(r.All().Where(s => !s.Protected)))); - - AddUntilStep("0 matching shown", () => songSelect.ChildrenOfType().Single().InformationalText == "0 matches"); - } - - [Test] - public void TestDeleteHotkey() - { - createSongSelect(); - - addRulesetImportStep(0); - AddAssert("3 matching shown", () => songSelect.ChildrenOfType().Single().InformationalText == "3 matches"); - - AddStep("press shift-delete", () => - { - InputManager.PressKey(Key.ShiftLeft); - InputManager.Key(Key.Delete); - InputManager.ReleaseKey(Key.ShiftLeft); - }); - AddUntilStep("delete dialog shown", () => DialogOverlay.CurrentDialog, Is.InstanceOf); - AddStep("confirm deletion", () => DialogOverlay.CurrentDialog!.PerformAction()); - AddAssert("0 matching shown", () => songSelect.ChildrenOfType().Single().InformationalText == "0 matches"); - } - - [Test] - public void TestCutInFilterTextBox() - { - createSongSelect(); - - AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType().First().Text = "nonono"); - AddStep("select all", () => InputManager.Keys(PlatformAction.SelectAll)); - AddStep("press ctrl/cmd-x", () => InputManager.Keys(PlatformAction.Cut)); - - AddAssert("filter text cleared", () => songSelect!.FilterControl.ChildrenOfType().First().Text, () => Is.Empty); - } - - [Test] - public void TestNonFilterableModChange() - { - addRulesetImportStep(0); - - createSongSelect(); - - // Mod that is guaranteed to never re-filter. - AddStep("add non-filterable mod", () => SelectedMods.Value = new Mod[] { new OsuModCinema() }); - AddAssert("filter count is 0", () => songSelect!.FilterCount, () => Is.EqualTo(0)); - - // Removing the mod should still not re-filter. - AddStep("remove non-filterable mod", () => SelectedMods.Value = Array.Empty()); - AddAssert("filter count is 0", () => songSelect!.FilterCount, () => Is.EqualTo(0)); - } - - [Test] - public void TestFilterableModChange() - { - addRulesetImportStep(3); - - createSongSelect(); - - // Change to mania ruleset. - AddStep("filter to mania ruleset", () => Ruleset.Value = rulesets.AvailableRulesets.First(r => r.OnlineID == 3)); - AddAssert("filter count is 2", () => songSelect!.FilterCount, () => Is.EqualTo(1)); - - // Apply a mod, but this should NOT re-filter because there's no search text. - AddStep("add filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey3() }); - AddAssert("filter count is 1", () => songSelect!.FilterCount, () => Is.EqualTo(1)); - - // Set search text. Should re-filter. - AddStep("set search text to match mods", () => songSelect!.FilterControl.CurrentTextSearch.Value = "keys=3"); - AddAssert("filter count is 2", () => songSelect!.FilterCount, () => Is.EqualTo(2)); - - // Change filterable mod. Should re-filter. - AddStep("change new filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey5() }); - AddAssert("filter count is 3", () => songSelect!.FilterCount, () => Is.EqualTo(3)); - - // Add non-filterable mod. Should NOT re-filter. - AddStep("apply non-filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModNoFail(), new ManiaModKey5() }); - AddAssert("filter count is 3", () => songSelect!.FilterCount, () => Is.EqualTo(3)); - - // Remove filterable mod. Should re-filter. - AddStep("remove filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModNoFail() }); - AddAssert("filter count is 4", () => songSelect!.FilterCount, () => Is.EqualTo(4)); - - // Remove non-filterable mod. Should NOT re-filter. - AddStep("remove filterable mod", () => SelectedMods.Value = Array.Empty()); - AddAssert("filter count is 4", () => songSelect!.FilterCount, () => Is.EqualTo(4)); - - // Add filterable mod. Should re-filter. - AddStep("add filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey3() }); - AddAssert("filter count is 5", () => songSelect!.FilterCount, () => Is.EqualTo(5)); - } - - private void waitForInitialSelection() - { - AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault); - AddUntilStep("wait for difficulty panels visible", () => songSelect!.Carousel.ChildrenOfType().Any()); - } - - private int getBeatmapIndex(BeatmapSetInfo set, BeatmapInfo info) => set.Beatmaps.IndexOf(info); - - private NoResultsPlaceholder? getPlaceholder() => songSelect!.ChildrenOfType().FirstOrDefault(); - - private int getCurrentBeatmapIndex() - { - Debug.Assert(songSelect!.Carousel.SelectedBeatmapSet != null); - Debug.Assert(songSelect!.Carousel.SelectedBeatmapInfo != null); - - return getBeatmapIndex(songSelect!.Carousel.SelectedBeatmapSet, songSelect!.Carousel.SelectedBeatmapInfo); - } - - private int getDifficultyIconIndex(DrawableCarouselBeatmapSet set, FilterableDifficultyIcon icon) - { - return set.ChildrenOfType().ToList().FindIndex(i => i == icon); - } - - private void addRulesetImportStep(int id) - { - Live? imported = null; - AddStep($"import test map for ruleset {id}", () => imported = importForRuleset(id)); - // This is specifically for cases where the add is happening post song select load. - // For cases where song select is null, the assertions are provided by the load checks. - AddUntilStep("wait for imported to arrive in carousel", () => songSelect == null || songSelect!.Carousel.BeatmapSets.Any(s => s.ID == imported?.ID)); - } - - private Live? importForRuleset(int id) => manager.Import(TestResources.CreateTestBeatmapSetInfo(3, rulesets.AvailableRulesets.Where(r => r.OnlineID == id).ToArray())); - - private void checkMusicPlaying(bool playing) => - AddUntilStep($"music {(playing ? "" : "not ")}playing", () => music.IsPlaying == playing); - - private void changeMods(params Mod[] mods) => AddStep($"change mods to {string.Join(", ", mods.Select(m => m.Acronym))}", () => SelectedMods.Value = mods); - - private void changeRuleset(int id) => AddStep($"change ruleset to {id}", () => Ruleset.Value = rulesets.AvailableRulesets.First(r => r.OnlineID == id)); - - private void createSongSelect() - { - AddStep("create song select", () => LoadScreen(songSelect = new TestSongSelect())); - AddUntilStep("wait for present", () => songSelect!.IsCurrentScreen()); - AddUntilStep("wait for carousel loaded", () => songSelect!.Carousel.IsAlive); - } - - /// - /// Imports test beatmap sets to show in the carousel. - /// - /// - /// The exact count of difficulties to create for each beatmap set. - /// A value causes the count of difficulties to be selected randomly. - /// - private void addManyTestMaps(int? difficultyCountPerSet = null) - { - AddStep("import test maps", () => - { - var usableRulesets = rulesets.AvailableRulesets.Where(r => r.OnlineID != 2).ToArray(); - - for (int i = 0; i < 10; i++) - manager.Import(TestResources.CreateTestBeatmapSetInfo(difficultyCountPerSet, usableRulesets)); - }); - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (rulesets.IsNotNull()) - rulesets.Dispose(); - } - - private void waitForDismissed() => AddUntilStep("wait for not current", () => !songSelect.AsNonNull().IsCurrentScreen()); - - private partial class TestSongSelect : PlaySongSelect - { - public Action? StartRequested; - - public new FilterControl FilterControl => base.FilterControl; - - public WorkingBeatmap CurrentBeatmap => Beatmap.Value; - public IWorkingBeatmap CurrentBeatmapDetailsBeatmap => BeatmapDetails.Beatmap; - public new BeatmapCarousel Carousel => base.Carousel; - public new ModSelectOverlay ModSelect => base.ModSelect; - - public new void PresentScore(ScoreInfo score) => base.PresentScore(score); - - public int FilterCount; - - protected override bool OnStart() - { - StartRequested?.Invoke(); - return base.OnStart(); - } - - [BackgroundDependencyLoader] - private void load() - { - FilterControl.FilterChanged += _ => FilterCount++; - } - } - } -} diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs index 992651d73c..8fcb3d7acc 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs @@ -28,7 +28,6 @@ using osu.Game.Scoring; using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Resources; -using osu.Game.Tests.Visual.SongSelect; using osu.Game.Users; using osuTK.Input; @@ -118,8 +117,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { setScope(BeatmapLeaderboardScope.Global); - AddStep(@"New Scores", () => leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()))); - AddStep(@"New Scores with teams", () => leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()).Select(s => + AddStep(@"New Scores", () => leaderboard.SetScores(GenerateSampleScores(new BeatmapInfo()))); + AddStep(@"New Scores with teams", () => leaderboard.SetScores(GenerateSampleScores(new BeatmapInfo()).Select(s => { s.User.Team = new APITeam(); return s; @@ -150,7 +149,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] public void TestUseTheseModsDoesNotCopySystemMods() { - AddStep(@"set scores", () => leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()), new ScoreInfo + AddStep(@"set scores", () => leaderboard.SetScores(GenerateSampleScores(new BeatmapInfo()), new ScoreInfo { OnlineID = 1337, Position = 999, @@ -297,7 +296,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 private void showPersonalBestWithNullPosition() { - leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()), new ScoreInfo + leaderboard.SetScores(GenerateSampleScores(new BeatmapInfo()), new ScoreInfo { OnlineID = 1337, Rank = ScoreRank.XH, @@ -318,7 +317,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 private void showPersonalBest() { - leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()), new ScoreInfo + leaderboard.SetScores(GenerateSampleScores(new BeatmapInfo()), new ScoreInfo { OnlineID = 1337, Position = 999, @@ -347,7 +346,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { AddStep(@"Import new scores", () => { - foreach (var score in TestSceneBeatmapLeaderboard.GenerateSampleScores(beatmapInfo())) + foreach (var score in GenerateSampleScores(beatmapInfo())) scoreManager.Import(score); }); } @@ -368,5 +367,216 @@ namespace osu.Game.Tests.Visual.SongSelectV2 public new void SetState(LeaderboardState state) => base.SetState(state); public new void SetScores(IEnumerable scores, ScoreInfo? userScore = null, int? totalCount = null) => base.SetScores(scores, userScore, totalCount); } + + public static ScoreInfo[] GenerateSampleScores(BeatmapInfo beatmapInfo) + { + return new[] + { + new ScoreInfo + { + Rank = ScoreRank.XH, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + Date = DateTime.Now, + Mods = new Mod[] + { + new OsuModHidden(), + new OsuModFlashlight + { + FollowDelay = { Value = 200 }, + SizeMultiplier = { Value = 5 }, + }, + new OsuModDifficultyAdjust + { + CircleSize = { Value = 11 }, + ApproachRate = { Value = 10 }, + OverallDifficulty = { Value = 10 }, + DrainRate = { Value = 10 }, + ExtendedLimits = { Value = true } + } + }, + Ruleset = new OsuRuleset().RulesetInfo, + BeatmapInfo = beatmapInfo, + BeatmapHash = beatmapInfo.Hash, + User = new APIUser + { + Id = 6602580, + Username = @"waaiiru", + CountryCode = CountryCode.ES, + }, + }, + new ScoreInfo + { + Rank = ScoreRank.X, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + Date = DateTime.Now.AddSeconds(-30), + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + BeatmapInfo = beatmapInfo, + BeatmapHash = beatmapInfo.Hash, + Ruleset = new OsuRuleset().RulesetInfo, + User = new APIUser + { + Id = 4608074, + Username = @"Skycries", + CountryCode = CountryCode.BR, + }, + }, + new ScoreInfo + { + Rank = ScoreRank.SH, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + Date = DateTime.Now.AddSeconds(-70), + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + BeatmapInfo = beatmapInfo, + BeatmapHash = beatmapInfo.Hash, + Ruleset = new OsuRuleset().RulesetInfo, + + User = new APIUser + { + Id = 1014222, + Username = @"eLy", + CountryCode = CountryCode.JP, + }, + }, + new ScoreInfo + { + Rank = ScoreRank.S, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + Date = DateTime.Now.AddMinutes(-40), + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + BeatmapInfo = beatmapInfo, + BeatmapHash = beatmapInfo.Hash, + Ruleset = new OsuRuleset().RulesetInfo, + + User = new APIUser + { + Id = 1541390, + Username = @"Toukai", + CountryCode = CountryCode.CA, + }, + }, + new ScoreInfo + { + Rank = ScoreRank.A, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + Date = DateTime.Now.AddHours(-2), + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + BeatmapInfo = beatmapInfo, + BeatmapHash = beatmapInfo.Hash, + Ruleset = new OsuRuleset().RulesetInfo, + + User = new APIUser + { + Id = 2243452, + Username = @"Satoruu", + CountryCode = CountryCode.VE, + }, + }, + new ScoreInfo + { + Rank = ScoreRank.B, + Accuracy = 0.9826, + MaxCombo = 244, + TotalScore = 1707827, + Date = DateTime.Now.AddHours(-25), + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + BeatmapInfo = beatmapInfo, + BeatmapHash = beatmapInfo.Hash, + Ruleset = new OsuRuleset().RulesetInfo, + + User = new APIUser + { + Id = 2705430, + Username = @"Mooha", + CountryCode = CountryCode.FR, + }, + }, + new ScoreInfo + { + Rank = ScoreRank.C, + Accuracy = 0.9654, + MaxCombo = 244, + TotalScore = 1707827, + Date = DateTime.Now.AddHours(-50), + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + BeatmapInfo = beatmapInfo, + BeatmapHash = beatmapInfo.Hash, + Ruleset = new OsuRuleset().RulesetInfo, + + User = new APIUser + { + Id = 7151382, + Username = @"Mayuri Hana", + CountryCode = CountryCode.TH, + }, + }, + new ScoreInfo + { + Rank = ScoreRank.D, + Accuracy = 0.6025, + MaxCombo = 244, + TotalScore = 1707827, + Date = DateTime.Now.AddHours(-72), + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + BeatmapInfo = beatmapInfo, + BeatmapHash = beatmapInfo.Hash, + Ruleset = new OsuRuleset().RulesetInfo, + + User = new APIUser + { + Id = 2051389, + Username = @"FunOrange", + CountryCode = CountryCode.CA, + }, + }, + new ScoreInfo + { + Rank = ScoreRank.D, + Accuracy = 0.5140, + MaxCombo = 244, + TotalScore = 1707827, + Date = DateTime.Now.AddMonths(-10), + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + BeatmapInfo = beatmapInfo, + BeatmapHash = beatmapInfo.Hash, + Ruleset = new OsuRuleset().RulesetInfo, + + User = new APIUser + { + Id = 6169483, + Username = @"-Hebel-", + CountryCode = CountryCode.MX, + }, + }, + new ScoreInfo + { + Rank = ScoreRank.D, + Accuracy = 0.4222, + MaxCombo = 244, + TotalScore = 1707827, + Date = DateTime.Now.AddYears(-2), + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + BeatmapInfo = beatmapInfo, + BeatmapHash = beatmapInfo.Hash, + Ruleset = new OsuRuleset().RulesetInfo, + + User = new APIUser + { + Id = 6702666, + Username = @"prhtnsm", + CountryCode = CountryCode.DE, + }, + }, + }; + } } } diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index 66e8aaf008..2f046b3754 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -307,7 +307,7 @@ namespace osu.Game.Graphics.Carousel /// /// Retrieve a list of all s currently displayed. /// - protected IReadOnlyCollection? GetCarouselItems() => carouselItems; + public IReadOnlyCollection? GetCarouselItems() => carouselItems; private List? carouselItems; @@ -1028,7 +1028,7 @@ namespace osu.Game.Graphics.Carousel /// Implementation of scroll container which handles very large vertical lists by internally using double precision /// for pre-display Y values. /// - protected partial class CarouselScrollContainer : UserTrackingScrollContainer, IKeyBindingHandler + public partial class CarouselScrollContainer : UserTrackingScrollContainer, IKeyBindingHandler { public readonly Container Panels; diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs index e8dc58ff1b..be507e7b36 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs @@ -46,6 +46,8 @@ namespace osu.Game.Screens.SelectV2 { public const int HEIGHT = 50; + public readonly ScoreInfo Score; + public Bindable> SelectedMods = new Bindable>(); /// @@ -115,8 +117,6 @@ namespace osu.Game.Screens.SelectV2 private Container rankLabelStandalone = null!; private Container rankLabelOverlay = null!; - private readonly ScoreInfo score; - private readonly bool sheared; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) @@ -130,7 +130,8 @@ namespace osu.Game.Screens.SelectV2 public BeatmapLeaderboardScore(ScoreInfo score, bool sheared = true) { - this.score = score; + Score = score; + this.sheared = sheared; Shear = sheared ? OsuGame.SHEAR : Vector2.Zero; @@ -198,7 +199,7 @@ namespace osu.Game.Screens.SelectV2 new UserCoverBackground { RelativeSizeAxes = Axes.Both, - User = score.User, + User = Score.User, Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, @@ -224,7 +225,7 @@ namespace osu.Game.Screens.SelectV2 Masking = true, Children = new Drawable[] { - new DelayedLoadWrapper(innerAvatar = new ClickableAvatar(score.User) + new DelayedLoadWrapper(innerAvatar = new ClickableAvatar(Score.User) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -276,19 +277,19 @@ namespace osu.Game.Screens.SelectV2 Masking = true, Children = new Drawable[] { - new UpdateableFlag(score.User.CountryCode) + new UpdateableFlag(Score.User.CountryCode) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Size = new Vector2(20, 14), }, - new UpdateableTeamFlag(score.User.Team) + new UpdateableTeamFlag(Score.User.Team) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Size = new Vector2(30, 15), }, - new DateLabel(score.Date) + new DateLabel(Score.Date) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -301,7 +302,7 @@ namespace osu.Game.Screens.SelectV2 { RelativeSizeAxes = Axes.X, Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, - Text = score.User.Username, + Text = Score.User.Username, Font = OsuFont.Style.Heading2, } } @@ -323,9 +324,9 @@ namespace osu.Game.Screens.SelectV2 Direction = FillDirection.Horizontal, Children = new Drawable[] { - new ScoreComponentLabel(BeatmapsetsStrings.ShowScoreboardHeadersCombo.ToUpper(), $"{score.MaxCombo.ToString()}x", - score.MaxCombo == score.GetMaximumAchievableCombo(), 60), - new ScoreComponentLabel(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy.ToUpper(), score.DisplayAccuracy, score.Accuracy == 1, + new ScoreComponentLabel(BeatmapsetsStrings.ShowScoreboardHeadersCombo.ToUpper(), $"{Score.MaxCombo.ToString()}x", + Score.MaxCombo == Score.GetMaximumAchievableCombo(), 60), + new ScoreComponentLabel(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy.ToUpper(), Score.DisplayAccuracy, Score.Accuracy == 1, 55), }, Alpha = 0, @@ -357,7 +358,7 @@ namespace osu.Game.Screens.SelectV2 Child = new Box { RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank)), + Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(Score.Rank)), }, }, new Box @@ -366,7 +367,7 @@ namespace osu.Game.Screens.SelectV2 Width = grade_width, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Colour = OsuColour.ForRank(score.Rank), + Colour = OsuColour.ForRank(Score.Rank), }, new TrianglesV2 { @@ -376,7 +377,7 @@ namespace osu.Game.Screens.SelectV2 Origin = Anchor.TopRight, SpawnRatio = 2, Velocity = 0.7f, - Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank).Darken(0.2f)), + Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(Score.Rank).Darken(0.2f)), }, new Container { @@ -390,9 +391,9 @@ namespace osu.Game.Screens.SelectV2 Anchor = Anchor.Centre, Origin = Anchor.Centre, Spacing = new Vector2(-2), - Colour = DrawableRank.GetRankNameColour(score.Rank), + Colour = DrawableRank.GetRankNameColour(Score.Rank), Font = OsuFont.Numeric.With(size: 14), - Text = DrawableRank.GetRankName(score.Rank), + Text = DrawableRank.GetRankName(Score.Rank), ShadowColour = Color4.Black.Opacity(0.3f), ShadowOffset = new Vector2(0, 0.08f), Shadow = true, @@ -420,7 +421,7 @@ namespace osu.Game.Screens.SelectV2 new Box { RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank).Opacity(0.5f)), + Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(Score.Rank).Opacity(0.5f)), }, new FillFlowContainer { @@ -437,7 +438,7 @@ namespace osu.Game.Screens.SelectV2 Anchor = Anchor.TopRight, Origin = Anchor.TopRight, UseFullGlyphHeight = false, - Current = scoreManager.GetBindableTotalScoreString(score), + Current = scoreManager.GetBindableTotalScoreString(Score), Spacing = new Vector2(-1.5f), Font = OsuFont.Style.Subtitle.With(weight: FontWeight.Light, fixedWidth: true), Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, @@ -503,10 +504,10 @@ namespace osu.Game.Screens.SelectV2 private void updateModDisplay() { - if (score.Mods.Length > 0) + if (Score.Mods.Length > 0) { modsContainer.Padding = new MarginPadding { Top = 4f }; - modsContainer.ChildrenEnumerable = score.Mods.AsOrdered().Select(mod => new ModIcon(mod) + modsContainer.ChildrenEnumerable = Score.Mods.AsOrdered().Select(mod => new ModIcon(mod) { Scale = new Vector2(0.3f), // trim mod icon height down to its true height for alignment purposes. @@ -608,7 +609,7 @@ namespace osu.Game.Screens.SelectV2 ITooltip IHasCustomTooltip.GetCustomTooltip() => new LeaderboardScoreTooltip(colourProvider); - ScoreInfo IHasCustomTooltip.TooltipContent => score; + ScoreInfo IHasCustomTooltip.TooltipContent => Score; MenuItem[] IHasContextMenu.ContextMenuItems { @@ -617,18 +618,18 @@ namespace osu.Game.Screens.SelectV2 List items = new List(); // system mods should never be copied across regardless of anything. - var copyableMods = score.Mods.Where(m => IsValidMod.Invoke(m) && m.Type != ModType.System).ToArray(); + var copyableMods = Score.Mods.Where(m => IsValidMod.Invoke(m) && m.Type != ModType.System).ToArray(); if (copyableMods.Length > 0) items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => SelectedMods.Value = copyableMods)); - if (score.OnlineID > 0) - items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{score.OnlineID}"))); + if (Score.OnlineID > 0) + items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{Score.OnlineID}"))); - if (score.Files.Count <= 0) return items.ToArray(); + if (Score.Files.Count <= 0) return items.ToArray(); - items.Add(new OsuMenuItem(CommonStrings.Export, MenuItemType.Standard, () => scoreManager.Export(score))); - items.Add(new OsuMenuItem(Resources.Localisation.Web.CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(score)))); + items.Add(new OsuMenuItem(CommonStrings.Export, MenuItemType.Standard, () => scoreManager.Export(Score))); + items.Add(new OsuMenuItem(Resources.Localisation.Web.CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(Score)))); return items.ToArray(); } diff --git a/osu.Game/Screens/SelectV2/FilterControl.cs b/osu.Game/Screens/SelectV2/FilterControl.cs index c0ccf0ab93..fdc61ad37e 100644 --- a/osu.Game/Screens/SelectV2/FilterControl.cs +++ b/osu.Game/Screens/SelectV2/FilterControl.cs @@ -269,7 +269,7 @@ namespace osu.Game.Screens.SelectV2 .FadeOut(SongSelect.ENTER_DURATION / 3, Easing.In); } - private partial class SongSelectSearchTextBox : ShearedFilterTextBox + internal partial class SongSelectSearchTextBox : ShearedFilterTextBox { protected override InnerSearchTextBox CreateInnerTextBox() => new InnerTextBox(); diff --git a/osu.Game/Screens/SelectV2/SoloSongSelect.cs b/osu.Game/Screens/SelectV2/SoloSongSelect.cs index 4ef73d4c49..5c3e1453ea 100644 --- a/osu.Game/Screens/SelectV2/SoloSongSelect.cs +++ b/osu.Game/Screens/SelectV2/SoloSongSelect.cs @@ -61,7 +61,7 @@ namespace osu.Game.Screens.SelectV2 public override IEnumerable GetForwardActions(BeatmapInfo beatmap) { yield return new OsuMenuItem(ButtonSystemStrings.Play.ToSentence(), MenuItemType.Highlighted, () => SelectAndRun(beatmap, OnStart)) { Icon = FontAwesome.Solid.Check }; - yield return new OsuMenuItem(ButtonSystemStrings.Edit.ToSentence(), MenuItemType.Standard, () => edit(beatmap)) { Icon = FontAwesome.Solid.PencilAlt }; + yield return new OsuMenuItem(ButtonSystemStrings.Edit.ToSentence(), MenuItemType.Standard, () => Edit(beatmap)) { Icon = FontAwesome.Solid.PencilAlt }; yield return new OsuMenuItemSpacer(); @@ -138,7 +138,7 @@ namespace osu.Game.Screens.SelectV2 } } - private void edit(BeatmapInfo beatmap) + public void Edit(BeatmapInfo beatmap) { if (!this.IsCurrentScreen()) return; diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index e26c72575a..feb7f21d61 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -670,7 +670,7 @@ namespace osu.Game.Screens.SelectV2 // This avoids a flicker of a placeholder or invalid beatmap before a proper selection. // // After the carousel finishes filtering, it will attempt a selection then call this method again. - if (!carouselItemsPresented && !checkBeatmapValidForSelection(Beatmap.Value.BeatmapInfo, filterControl.CreateCriteria())) + if (!CarouselItemsPresented && !checkBeatmapValidForSelection(Beatmap.Value.BeatmapInfo, filterControl.CreateCriteria())) return; if (carousel.VisuallyFocusSelected) @@ -703,7 +703,10 @@ namespace osu.Game.Screens.SelectV2 #region Filtering - private bool carouselItemsPresented; + /// + /// Whether the carousel has finished initial presentation of beatmap panels. + /// + public bool CarouselItemsPresented { get; private set; } private const double filter_delay = 250; @@ -727,7 +730,7 @@ namespace osu.Game.Screens.SelectV2 if (carousel.Criteria == null) return; - carouselItemsPresented = true; + CarouselItemsPresented = true; int count = carousel.MatchedBeatmapsCount; diff --git a/osu.Game/Tests/Visual/EditorSavingTestScene.cs b/osu.Game/Tests/Visual/EditorSavingTestScene.cs index 78188d7cf7..d2b216caa8 100644 --- a/osu.Game/Tests/Visual/EditorSavingTestScene.cs +++ b/osu.Game/Tests/Visual/EditorSavingTestScene.cs @@ -13,7 +13,7 @@ using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Menu; -using osu.Game.Screens.Select; +using osu.Game.Screens.SelectV2; using osuTK.Input; namespace osu.Game.Tests.Visual @@ -74,15 +74,14 @@ namespace osu.Game.Tests.Visual AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu); - SongSelect songSelect = null; + SoloSongSelect songSelect = null; - PushAndConfirm(() => songSelect = new PlaySongSelect()); - AddUntilStep("wait for carousel load", () => songSelect.BeatmapSetsLoaded); + PushAndConfirm(() => songSelect = new SoloSongSelect()); + AddUntilStep("wait for carousel load", () => songSelect.CarouselItemsPresented); AddStep("Present same beatmap", () => Game.PresentBeatmap(Game.BeatmapManager.QueryBeatmapSet(set => set.ID == beatmapSetGuid)!.Value, beatmap => beatmap.ID == beatmapGuid)); AddUntilStep("Wait for beatmap selected", () => Game.Beatmap.Value.BeatmapInfo.ID == beatmapGuid); - AddStep("Open options", () => InputManager.Key(Key.F3)); - AddStep("Enter editor", () => InputManager.Key(Key.Number5)); + AddStep("Open editor", () => songSelect.Edit(Game.Beatmap.Value.BeatmapInfo)); AddUntilStep("Wait for editor load", () => Editor != null); } From 7fcbda6cd0cae36763f44301553258b038e0fa6d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Jun 2025 18:15:40 +0900 Subject: [PATCH 337/370] Fix mod select not being hidden when `CloseAllOverlays` is called --- osu.Game/OsuGame.cs | 2 ++ osu.Game/Screens/Footer/ScreenFooter.cs | 19 ++++++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 394917dc62..767ad78b04 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -312,6 +312,8 @@ namespace osu.Game foreach (var overlay in focusedOverlays) overlay.Hide(); + ScreenFooter.ActiveOverlay?.Hide(); + if (hideToolbar) Toolbar.Hide(); } diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index ad3aaaa2c9..6d7a32d57a 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -226,20 +226,21 @@ namespace osu.Game.Screens.Footer } } - private ShearedOverlayContainer? activeOverlay; + public ShearedOverlayContainer? ActiveOverlay { get; private set; } + private VisibilityContainer? activeFooterContent; private readonly List temporarilyHiddenButtons = new List(); public IDisposable RegisterActiveOverlayContainer(ShearedOverlayContainer overlay, out VisibilityContainer? footerContent) { - if (activeOverlay != null) + if (ActiveOverlay != null) { throw new InvalidOperationException(@"Cannot set overlay content while one is already present. " + - $@"The previous overlay ({activeOverlay.GetType().Name}) should be hidden first."); + $@"The previous overlay ({ActiveOverlay.GetType().Name}) should be hidden first."); } - activeOverlay = overlay; + ActiveOverlay = overlay; Debug.Assert(temporarilyHiddenButtons.Count == 0); @@ -277,7 +278,7 @@ namespace osu.Game.Screens.Footer private void clearActiveOverlayContainer() { - if (activeOverlay == null) + if (ActiveOverlay == null) return; Debug.Assert(activeFooterContent != null); @@ -300,7 +301,7 @@ namespace osu.Game.Screens.Footer activeFooterContent.Delay(timeUntilRun).Expire(); activeFooterContent = null; - activeOverlay = null; + ActiveOverlay = null; } private void updateColourScheme(int hue) @@ -337,12 +338,12 @@ namespace osu.Game.Screens.Footer private void onBackPressed() { - if (activeOverlay != null) + if (ActiveOverlay != null) { - if (activeOverlay.OnBackButton()) + if (ActiveOverlay.OnBackButton()) return; - activeOverlay.Hide(); + ActiveOverlay.Hide(); return; } From 94d6260d9b37b492d14d1730f30042d7ee115493 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Jun 2025 18:42:41 +0900 Subject: [PATCH 338/370] Remove incorrectly placed `ensureGlobalBeatmapValid` call This is being run in the flow where we are providing a specific beatmap for immediately selection. In an edge case scenario, the carousel may be pending on a filter operation, which would cause the whole `SelectAndRun` call to fail when it doesn't need to. This is reproduced by multiple test scenes. One example is `TestSceneOpenEditorTimestamp.TestErrorNotifications`. --- osu.Game/Screens/SelectV2/SongSelect.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index feb7f21d61..317ed743c0 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -449,9 +449,6 @@ namespace osu.Game.Screens.SelectV2 if (Beatmap.IsDefault) return; - if (!ensureGlobalBeatmapValid()) - return; - startAction(); } From 99220408f63044dbe85b22dda6c0e5630981237d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Jul 2025 17:34:52 +0900 Subject: [PATCH 339/370] Remove duplicated test method --- .../Navigation/TestSceneScreenNavigation.cs | 47 +------------------ 1 file changed, 2 insertions(+), 45 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index bcab3c7672..d50fc69823 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -251,51 +251,6 @@ namespace osu.Game.Tests.Visual.Navigation SoloSongSelect songSelect = null; double scrollPosition = 0; - AddStep("set game volume to max", () => Game.Dependencies.Get().SetValue(FrameworkSetting.VolumeUniversal, 1d)); - AddUntilStep("wait for volume overlay to hide", () => Game.ChildrenOfType().SingleOrDefault()?.State.Value, () => Is.EqualTo(Visibility.Hidden)); - PushAndConfirm(() => songSelect = new SoloSongSelect()); - AddUntilStep("wait for song select", () => songSelect.CarouselItemsPresented); - AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); - AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); - - AddUntilStep("store scroll position", () => - { - double s = getCarouselScrollPosition(); - - // TODO: this logic can likely be removed when we fix https://github.com/ppy/osu/issues/33379 - if (scrollPosition == s) - return true; - - scrollPosition = s; - return false; - }); - - AddStep("move to left side", () => InputManager.MoveMouseTo( - songSelect.ChildrenOfType().Single().ScreenSpaceDrawQuad.Centre)); - AddStep("scroll down", () => InputManager.ScrollVerticalBy(-1)); - AddAssert("carousel didn't move", getCarouselScrollPosition, () => Is.EqualTo(scrollPosition)); - - AddRepeatStep("alt-scroll down", () => - { - InputManager.PressKey(Key.AltLeft); - InputManager.ScrollVerticalBy(-1); - InputManager.ReleaseKey(Key.AltLeft); - }, 5); - AddAssert("game volume decreased", () => Game.Dependencies.Get().Get(FrameworkSetting.VolumeUniversal), () => Is.LessThan(1)); - - AddStep("move to carousel", () => InputManager.MoveMouseTo(songSelect.ChildrenOfType().Single())); - AddStep("scroll down", () => InputManager.ScrollVerticalBy(-1)); - AddAssert("carousel moved", getCarouselScrollPosition, () => Is.Not.EqualTo(scrollPosition)); - - double getCarouselScrollPosition() => Game.ChildrenOfType.CarouselScrollContainer>().Single().Current; - } - - [Test] - public void TestNewSongSelectScrollHandling() - { - SoloSongSelect songSelect = null; - double scrollPosition = 0; - AddStep("set game volume to max", () => Game.Dependencies.Get().SetValue(FrameworkSetting.VolumeUniversal, 1d)); AddUntilStep("wait for volume overlay to hide", () => Game.ChildrenOfType().SingleOrDefault()?.State.Value, () => Is.EqualTo(Visibility.Hidden)); PushAndConfirm(() => songSelect = new SoloSongSelect()); @@ -303,6 +258,8 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); AddUntilStep("wait for beatmap", () => Game.ChildrenOfType().Any()); + // TODO: this logic can likely be removed when we fix https://github.com/ppy/osu/issues/33379 + // It should be probably be immediate in this case. AddWaitStep("wait for scroll", 10); AddStep("store scroll position", () => scrollPosition = getCarouselScrollPosition()); From 709bc9c33077d8c41ceda5079c31eb8ec7121aaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Tue, 1 Jul 2025 10:38:22 +0200 Subject: [PATCH 340/370] Use OverlayColourProvider for BeatDivisorControl icon colour --- .../Screens/Edit/Compose/Components/BeatDivisorControl.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index 5883b6d89d..da145f0994 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -373,11 +373,11 @@ namespace osu.Game.Screens.Edit.Compose.Components } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, OverlayColourProvider colourProvider) { - IconColour = colours.GrayB; + IconColour = colourProvider.Light3; IconHoverColour = Color4.White; - HoverColour = colours.Gray7; + HoverColour = colours.Gray6; FlashColour = colours.Gray9; } } From 46dab76eba748926cafa0b0f85efdb25d8dcf4a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Jul 2025 17:47:39 +0900 Subject: [PATCH 341/370] Fix footer not showing in first run overlay --- osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs index aec1859176..edadc333c8 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs @@ -148,6 +148,8 @@ namespace osu.Game.Overlays.FirstRunSetup protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => new DependencyContainer(new DependencyIsolationContainer(base.CreateChildDependencies(parent))); + private ScreenFooter footer; + [BackgroundDependencyLoader] private void load(AudioManager audio, TextureStore textures, RulesetStore rulesets) { @@ -157,7 +159,6 @@ namespace osu.Game.Overlays.FirstRunSetup OsuScreenStack stack; OsuLogo logo; - ScreenFooter footer; Padding = new MarginPadding(5); @@ -195,6 +196,13 @@ namespace osu.Game.Overlays.FirstRunSetup // intentionally load synchronously so it is included in the initial load of the first run screen. stack.PushSynchronously(screen); } + + protected override void LoadComplete() + { + base.LoadComplete(); + + footer.Show(); + } } private class DependencyIsolationContainer : IReadOnlyDependencyContainer From 2c22158dbdaa13f3e27622e1df4317536e94300e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Jul 2025 11:23:55 +0200 Subject: [PATCH 342/370] Move external skin edit overlay out of `OsuGame` --- osu.Game/OsuGame.cs | 1 - .../Overlays/SkinEditor/SkinEditorOverlay.cs | 22 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 9e524878dc..394917dc62 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1225,7 +1225,6 @@ namespace osu.Game loadComponentSingleFile(beatmapSetOverlay = new BeatmapSetOverlay(), overlayContent.Add, true); loadComponentSingleFile(wikiOverlay = new WikiOverlay(), overlayContent.Add, true); loadComponentSingleFile(skinEditor = new SkinEditorOverlay(ScreenContainer), overlayContent.Add, true); - loadComponentSingleFile(new ExternalEditOverlay(), overlayContent.Add, true); loadComponentSingleFile(new LoginOverlay { diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index 344dcc0d66..7553c83056 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -49,9 +49,15 @@ namespace osu.Game.Overlays.SkinEditor [Resolved] private IPerformFromScreenRunner? performer { get; set; } + [Resolved] + private IOverlayManager? overlayManager { get; set; } + [Cached] public readonly EditorClipboard Clipboard = new EditorClipboard(); + [Cached] + private readonly ExternalEditOverlay externalEditOverlay = new ExternalEditOverlay(); + [Resolved] private OsuGame game { get; set; } = null!; @@ -69,6 +75,7 @@ namespace osu.Game.Overlays.SkinEditor private OsuScreen? lastTargetScreen; private InvokeOnDisposal? nestedInputManagerDisable; + private IDisposable? externalEditOverlayRegistration; private readonly LayoutValue drawSizeLayout; @@ -86,6 +93,13 @@ namespace osu.Game.Overlays.SkinEditor config.BindWith(OsuSetting.BeatmapSkins, beatmapSkins); } + protected override void LoadComplete() + { + base.LoadComplete(); + + externalEditOverlayRegistration = overlayManager?.RegisterBlockingOverlay(externalEditOverlay); + } + public bool OnPressed(KeyBindingPressEvent e) { switch (e.Action) @@ -342,6 +356,14 @@ namespace osu.Game.Overlays.SkinEditor base.ToggleVisibility(); } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + externalEditOverlayRegistration?.Dispose(); + externalEditOverlayRegistration = null; + } + private partial class EndlessPlayer : ReplayPlayer { protected override UserActivity? InitialActivity => null; From 142984c07f28394ad64aaea9edbd0ecd48804e3b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Jul 2025 18:58:15 +0900 Subject: [PATCH 343/370] Change audio ducking at song select v2 to be temporary to avoid conflict with overlays Closes https://github.com/ppy/osu/issues/33539. --- osu.Game/Screens/SelectV2/SongSelect.cs | 35 +++++++++++-------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index e26c72575a..2f26f123de 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -402,22 +402,6 @@ namespace osu.Game.Screens.SelectV2 private void ensureTrackLooping(IWorkingBeatmap beatmap, TrackChangeDirection changeDirection) => beatmap.PrepareTrackForPreview(true); - private IDisposable? trackDuck; - - private void attachTrackDuckingIfShould() - { - bool shouldDuck = noResultsPlaceholder.State.Value == Visibility.Visible; - - if (shouldDuck && trackDuck == null) - trackDuck = music.Duck(new DuckParameters { DuckVolumeTo = 1, DuckCutoffTo = 500 }); - } - - private void detachTrackDucking() - { - trackDuck?.Dispose(); - trackDuck = null; - } - #endregion #region Selection handling @@ -604,7 +588,6 @@ namespace osu.Game.Screens.SelectV2 updateWedgeVisibility(); beginLooping(); - attachTrackDuckingIfShould(); ensureGlobalBeatmapValid(); @@ -622,7 +605,6 @@ namespace osu.Game.Screens.SelectV2 updateWedgeVisibility(); endLooping(); - detachTrackDucking(); } protected override void LogoArriving(OsuLogo logo, bool resuming) @@ -748,17 +730,30 @@ namespace osu.Game.Screens.SelectV2 if (count == 0) { + if (noResultsPlaceholder.State.Value == Visibility.Hidden) + { + // Duck audio temporarily when the no results placeholder becomes visible. + // + // Temporary ducking makes it easier to avoid scenarios where the ducking interacts badly + // with other global UI components (like overlays). + music.DuckMomentarily(400, new DuckParameters + { + DuckVolumeTo = 1, + DuckCutoffTo = 500, + DuckDuration = 250, + RestoreDuration = 2000, + }); + } + noResultsPlaceholder.Show(); noResultsPlaceholder.Filter = carousel.Criteria!; - attachTrackDuckingIfShould(); rightGradientBackground.ResizeWidthTo(3, 1000, Easing.OutPow10); } else { noResultsPlaceholder.Hide(); - detachTrackDucking(); rightGradientBackground.ResizeWidthTo(1, 400, Easing.OutPow10); } } From 68ab89a8f1d0c15f1ea35afa82fedd9ba73ff4e6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Jul 2025 19:19:18 +0900 Subject: [PATCH 344/370] Add note about `LATEST_VERSION` cross-project usage --- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 6c290c4f1c..6fb762b9ee 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -16,6 +16,8 @@ namespace osu.Game.Beatmaps.Formats public abstract class LegacyDecoder : Decoder where T : new() { + // If this is updated, a new release of `osu-server-beatmap-submission` is required with updated packages. + // See usage at https://github.com/ppy/osu-server-beatmap-submission/blob/master/osu.Server.BeatmapSubmission/Services/BeatmapPackageParser.cs#L96-L97. public const int LATEST_VERSION = 14; public const int MAX_COMBO_COLOUR_COUNT = 8; From ed189fecf4287142b5094672d7aad7a5a33179b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Jul 2025 19:30:47 +0900 Subject: [PATCH 345/370] Make `ShearedButton` block mouse down events Closes https://github.com/ppy/osu/issues/33748. I (and tests) can't find any regressions from this. One would hope we aren't relying on fall-through mouse down anywhere beneath buttons.. --- osu.Game/Graphics/UserInterface/ShearedButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/ShearedButton.cs b/osu.Game/Graphics/UserInterface/ShearedButton.cs index 16891babf3..2047fc74f4 100644 --- a/osu.Game/Graphics/UserInterface/ShearedButton.cs +++ b/osu.Game/Graphics/UserInterface/ShearedButton.cs @@ -179,7 +179,7 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnMouseDown(MouseDownEvent e) { Content.ScaleTo(0.9f, 2000, Easing.OutQuint); - return base.OnMouseDown(e); + return true; } protected override void OnMouseUp(MouseUpEvent e) From 5f48124a94f673a58a5269ad69ed442079741c2e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Jul 2025 19:47:33 +0900 Subject: [PATCH 346/370] Also fix leaderboard scores not eating mouse down Closes second portion of https://github.com/ppy/osu/issues/33748. --- osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs index 10917f08ac..0554b1b815 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Graphics; @@ -82,6 +83,13 @@ namespace osu.Game.Screens.SelectV2 private const float personal_best_height = 112; + // Blocking mouse down is required to avoid song select's background reveal logic happening while hovering scores. + // Our horizontal alignment doesn't really align with the rest of the sheared components (protrudes a touch to the right) which makes + // it complicated to handle this at a higher level. + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => scoresScroll.ReceivePositionalInputAt(screenSpacePos); + + protected override bool OnMouseDown(MouseDownEvent e) => true; + [BackgroundDependencyLoader] private void load() { From 2aca8eecb9cc926f7ec6863d832b4e14ab0df645 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Jul 2025 19:56:13 +0900 Subject: [PATCH 347/370] Fix footer appearing at loader screen on quick retries --- osu.Game/Screens/Play/PlayerLoader.cs | 16 ++++++++-------- osu.Game/Screens/SelectV2/SoloSongSelect.cs | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index f1a31b809f..1d73f7c0e1 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -59,7 +59,7 @@ namespace osu.Game.Screens.Play // this makes the game stay in portrait mode when restarting gameplay rather than switching back to landscape. public override bool RequiresPortraitOrientation => CurrentPlayer?.RequiresPortraitOrientation == true; - public override float BackgroundParallaxAmount => quickRestart ? 0 : 1; + public override float BackgroundParallaxAmount => QuickRestart ? 0 : 1; // Here because IsHovered will not update unless we do so. public override bool HandlePositionalInput => true; @@ -158,7 +158,7 @@ namespace osu.Game.Screens.Play private PlayerLoaderDisclaimer? epilepsyWarning; - private bool quickRestart; + protected bool QuickRestart { get; private set; } private IDisposable? highPerformanceSession; @@ -380,7 +380,7 @@ namespace osu.Game.Screens.Play logo.ScaleTo(new Vector2(0.15f), duration, Easing.OutQuint); - if (quickRestart) + if (QuickRestart) { logo.Delay(quick_restart_initial_delay) .FadeIn(350); @@ -430,7 +430,7 @@ namespace osu.Game.Screens.Play // We need to perform this check here rather than in OnHover as any number of children of VisualSettings // may also be handling the hover events. - if (inputManager.HoveredDrawables.Contains(VisualSettings) || quickRestart) + if (inputManager.HoveredDrawables.Contains(VisualSettings) || QuickRestart) { // Preview user-defined background dim and blur when hovered on the visual settings panel. ApplyToBackground(b => @@ -469,7 +469,7 @@ namespace osu.Game.Screens.Play return; CurrentPlayer = createPlayer(); - CurrentPlayer.Configuration.AutomaticallySkipIntro |= quickRestart; + CurrentPlayer.Configuration.AutomaticallySkipIntro |= QuickRestart; CurrentPlayer.RestartCount = restartCount++; CurrentPlayer.PrepareLoaderForRestart = prepareForRestart; @@ -486,7 +486,7 @@ namespace osu.Game.Screens.Play private void prepareForRestart(bool quickRestartRequested) { - quickRestart = quickRestartRequested; + QuickRestart = quickRestartRequested; hideOverlays = true; ValidForResume = true; } @@ -495,7 +495,7 @@ namespace osu.Game.Screens.Play { MetadataInfo.Loading = true; - if (quickRestart) + if (QuickRestart) { BackButtonVisibility.Value = false; @@ -635,7 +635,7 @@ namespace osu.Game.Screens.Play }, // When a quick restart is activated, the metadata content will display some time later if it's taking too long. // To avoid it appearing too briefly, if it begins to fade in let's induce a standard delay. - quickRestart && content.Alpha == 0 ? 0 : 500); + QuickRestart && content.Alpha == 0 ? 0 : 500); } private void cancelLoad() diff --git a/osu.Game/Screens/SelectV2/SoloSongSelect.cs b/osu.Game/Screens/SelectV2/SoloSongSelect.cs index 4ef73d4c49..1c8f041bfe 100644 --- a/osu.Game/Screens/SelectV2/SoloSongSelect.cs +++ b/osu.Game/Screens/SelectV2/SoloSongSelect.cs @@ -173,7 +173,7 @@ namespace osu.Game.Screens.SelectV2 private partial class PlayerLoader : Play.PlayerLoader { - public override bool ShowFooter => true; + public override bool ShowFooter => !QuickRestart; public PlayerLoader(Func createPlayer) : base(createPlayer) From 5b584227a0abfe9d6eee3e79a653dd0d559a6aa0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Jul 2025 19:56:52 +0900 Subject: [PATCH 348/370] Fix back button appearing on second quick retry when it shouldn't --- osu.Game/Screens/Play/PlayerLoader.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 1d73f7c0e1..848b8292d4 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -98,6 +98,8 @@ namespace osu.Game.Screens.Play private Box? quickRestartBlackLayer; + private ScheduledDelegate? quickRestartBackButtonRestore; + [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); @@ -518,7 +520,8 @@ namespace osu.Game.Screens.Play .ScaleTo(1) .FadeInFromZero(500, Easing.OutQuint); - this.Delay(quick_restart_initial_delay).Schedule(() => BackButtonVisibility.Value = true); + quickRestartBackButtonRestore?.Cancel(); + quickRestartBackButtonRestore = Scheduler.AddDelayed(() => BackButtonVisibility.Value = true, quick_restart_initial_delay); } else { From 01f5068535a468bb502bfafc91ef16684b9bb5a8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Jul 2025 20:19:46 +0900 Subject: [PATCH 349/370] Fix potential test deadlock Disposal woes. --- osu.Game/OsuGame.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 767ad78b04..57ed6a5dbf 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -227,6 +227,8 @@ namespace osu.Game private Bindable configSkin; + private RealmDetachedBeatmapStore detachedBeatmapStore; + private readonly string[] args; private readonly List focusedOverlays = new List(); @@ -1002,6 +1004,10 @@ namespace osu.Game protected override void Dispose(bool isDisposing) { + // Without this, tests may deadlock due to cancellation token not becoming cancelled before disposal. + // To reproduce, run `TestSceneButtonSystemNavigation` ensuring `TestConstructor` runs before `TestFastShortcutKeys`. + detachedBeatmapStore?.Dispose(); + base.Dispose(isDisposing); SentryLogger.Dispose(); } @@ -1245,7 +1251,7 @@ namespace osu.Game loadComponentSingleFile(new MedalOverlay(), topMostOverlayContent.Add); loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add); - loadComponentSingleFile(new RealmDetachedBeatmapStore(), Add, true); + loadComponentSingleFile(detachedBeatmapStore = new RealmDetachedBeatmapStore(), Add, true); Add(externalLinkOpener = new ExternalLinkOpener()); Add(new MusicKeyBindingHandler()); From 08a3edcb5cadb21dc4a15dc775440f2339533ec3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Jul 2025 14:47:22 +0900 Subject: [PATCH 350/370] Remove comment referencing removed line --- osu.Game/Screens/SelectV2/SongSelect.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 317ed743c0..25f3b160c0 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -434,8 +434,6 @@ namespace osu.Game.Screens.SelectV2 if (!this.IsCurrentScreen()) return; - // `ensureGlobalBeatmapValid` also performs this checks, but it will change the active selection on fail. - // By checking locally first, we can correctly perform a no-op rather than changing selection. if (!checkBeatmapValidForSelection(beatmap, carousel.Criteria)) return; From fe118b4e978f91361b68061e4f78f56b38439347 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Jul 2025 17:00:04 +0900 Subject: [PATCH 351/370] Add menu tip exposing song select right click scroll behaviour Since this is now considered a permanent stay, let's inform users that it exists since it's quite useful to know. --- osu.Game/Localisation/MenuTipStrings.cs | 5 +++++ osu.Game/Screens/Menu/MenuTipDisplay.cs | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Localisation/MenuTipStrings.cs b/osu.Game/Localisation/MenuTipStrings.cs index 9d398e8e64..977c0928b2 100644 --- a/osu.Game/Localisation/MenuTipStrings.cs +++ b/osu.Game/Localisation/MenuTipStrings.cs @@ -149,6 +149,11 @@ namespace osu.Game.Localisation /// public static LocalisableString DragAndDropImageInSkinEditor => new TranslatableString(getKey(@"drag_and_drop_image_in_skin_editor"), @"Drag and drop any image into the skin editor to load it in quickly!"); + /// + /// "Try holding your right mouse button near the beatmap carousel to quickly scroll to an absolute position!" + /// + public static LocalisableString RightMouseAbsoluteScroll => new TranslatableString(getKey(@"right_mouse_absolute_scroll"), @"Try holding your right mouse button near the beatmap carousel to quickly scroll to an absolute position!"); + /// /// "a tip for you:" /// diff --git a/osu.Game/Screens/Menu/MenuTipDisplay.cs b/osu.Game/Screens/Menu/MenuTipDisplay.cs index 283528d22a..9430d65433 100644 --- a/osu.Game/Screens/Menu/MenuTipDisplay.cs +++ b/osu.Game/Screens/Menu/MenuTipDisplay.cs @@ -128,7 +128,8 @@ namespace osu.Game.Screens.Menu MenuTipStrings.ToggleReplaySettingsShortcut, MenuTipStrings.CopyModsFromScore, MenuTipStrings.AutoplayBeatmapShortcut, - MenuTipStrings.LazerIsNotAWord + MenuTipStrings.LazerIsNotAWord, + MenuTipStrings.RightMouseAbsoluteScroll, }; return tips[RNG.Next(0, tips.Length)]; From de61f8519c5184dc1aab512c1f3241ca9953e898 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 2 Jul 2025 13:24:26 +0300 Subject: [PATCH 352/370] Fix tablet FAQ linked incorrectly and not linked on macOS --- .../Settings/Sections/Input/TabletSettings.cs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index 3ce546785a..6aebec88a9 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; -using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -113,15 +112,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input AutoSizeAxes = Axes.Y, }.With(t => { - if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows || RuntimeInfo.OS == RuntimeInfo.Platform.Linux) - { - t.NewLine(); - var formattedSource = MessageFormatter.FormatText(localisation.GetLocalisedString(TabletSettingsStrings.NoTabletDetectedDescription( - RuntimeInfo.OS == RuntimeInfo.Platform.Windows - ? @"https://opentabletdriver.net/Wiki/FAQ/Windows" - : @"https://opentabletdriver.net/Wiki/FAQ/Linux"))); - t.AddLinks(formattedSource.Text, formattedSource.Links); - } + t.NewLine(); + + const string url = @"https://opentabletdriver.net/Wiki/FAQ/General"; + var formattedSource = MessageFormatter.FormatText(localisation.GetLocalisedString(TabletSettingsStrings.NoTabletDetectedDescription(url))); + + t.AddLinks(formattedSource.Text, formattedSource.Links); }), } }, From 86b25a0b3e5690ca83efb2007440dfa07bebd8c1 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 3 Jul 2025 06:30:30 +0300 Subject: [PATCH 353/370] Add "pp" suffix to PP statistic in score tooltip --- osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs index bc684dfc13..c1089cf764 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs @@ -262,7 +262,7 @@ namespace osu.Game.Screens.SelectV2 private readonly ScoreInfo score; public PerformanceStatisticRow(LocalisableString label, Color4 labelColour, ScoreInfo score) - : base(label, labelColour, 0.ToLocalisableString("N0")) + : base(label, labelColour, @"0pp") { this.score = score; } @@ -296,7 +296,7 @@ namespace osu.Game.Screens.SelectV2 if (pp.HasValue) { int ppValue = (int)Math.Round(pp.Value, MidpointRounding.AwayFromZero); - ValueLabel.Text = ppValue.ToLocalisableString("N0"); + ValueLabel.Text = LocalisableString.Interpolate(@$"{ppValue:N0}pp"); if (!scoreInfo.BeatmapInfo!.Status.GrantsPerformancePoints() || hasUnrankedMods(scoreInfo)) Alpha = 0.5f; From fe558b8660c46fa3549975e8920d87ecaa12dc72 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 3 Jul 2025 06:31:06 +0300 Subject: [PATCH 354/370] Tint hit result numbers in score tooltip --- .../BeatmapLeaderboardScore_Tooltip.cs | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs index c1089cf764..d8bbe52b01 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs @@ -117,7 +117,7 @@ namespace osu.Game.Screens.SelectV2 relativeDate.Date = value.Date; var judgementsStatistics = value.GetStatisticsForDisplay().Select(s => - new StatisticRow(s.DisplayName.ToUpper(), colours.ForHitResult(s.Result), s.Count.ToLocalisableString("N0"))); + new StatisticRow(s.DisplayName.ToUpper(), s.Count.ToLocalisableString("N0"), colours.ForHitResult(s.Result))); double multiplier = 1.0; @@ -126,10 +126,10 @@ namespace osu.Game.Screens.SelectV2 var generalStatistics = new[] { - new PerformanceStatisticRow(BeatmapsetsStrings.ShowScoreboardHeaderspp.ToUpper(), colourProvider.Content2, score), - new StatisticRow(ModSelectOverlayStrings.ScoreMultiplier, colourProvider.Content2, ModUtils.FormatScoreMultiplier(multiplier)), - new StatisticRow(BeatmapsetsStrings.ShowScoreboardHeadersCombo, colourProvider.Content2, value.MaxCombo.ToLocalisableString(@"0\x")), - new StatisticRow(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy, colourProvider.Content2, value.Accuracy.FormatAccuracy()), + new PerformanceStatisticRow(BeatmapsetsStrings.ShowScoreboardHeaderspp.ToUpper(), score), + new StatisticRow(ModSelectOverlayStrings.ScoreMultiplier, ModUtils.FormatScoreMultiplier(multiplier)), + new StatisticRow(BeatmapsetsStrings.ShowScoreboardHeadersCombo, value.MaxCombo.ToLocalisableString(@"0\x")), + new StatisticRow(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy, value.Accuracy.FormatAccuracy()), }; statistics.ChildrenEnumerable = judgementsStatistics @@ -230,22 +230,26 @@ namespace osu.Game.Screens.SelectV2 public partial class StatisticRow : CompositeDrawable { - protected OsuSpriteText ValueLabel; + private readonly OsuSpriteText labelText; + protected readonly OsuSpriteText ValueText; - public StatisticRow(LocalisableString label, Color4 labelColour, LocalisableString value) + private readonly Color4? colour; + + public StatisticRow(LocalisableString label, LocalisableString value, Color4? colour = null) { + this.colour = colour; + RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; InternalChildren = new[] { - new OsuSpriteText + labelText = new OsuSpriteText { Text = label, - Colour = labelColour, Font = OsuFont.Style.Caption2.With(weight: FontWeight.SemiBold), }, - ValueLabel = new OsuSpriteText + ValueText = new OsuSpriteText { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, @@ -255,14 +259,21 @@ namespace osu.Game.Screens.SelectV2 }, }; } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + labelText.Colour = colour ?? colourProvider.Content2; + ValueText.Colour = colour ?? colourProvider.Content1; + } } public partial class PerformanceStatisticRow : StatisticRow { private readonly ScoreInfo score; - public PerformanceStatisticRow(LocalisableString label, Color4 labelColour, ScoreInfo score) - : base(label, labelColour, @"0pp") + public PerformanceStatisticRow(LocalisableString label, ScoreInfo score) + : base(label, @"0pp") { this.score = score; } @@ -296,7 +307,7 @@ namespace osu.Game.Screens.SelectV2 if (pp.HasValue) { int ppValue = (int)Math.Round(pp.Value, MidpointRounding.AwayFromZero); - ValueLabel.Text = LocalisableString.Interpolate(@$"{ppValue:N0}pp"); + ValueText.Text = LocalisableString.Interpolate(@$"{ppValue:N0}pp"); if (!scoreInfo.BeatmapInfo!.Status.GrantsPerformancePoints() || hasUnrankedMods(scoreInfo)) Alpha = 0.5f; From b06fd979fd9cbb212c69d1662a4df6a08ef8d494 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Jul 2025 12:43:51 +0900 Subject: [PATCH 355/370] Add back background blur support in song select v2 Looks like shit, but whatever. Defaults to `false` for all new installs because it looks stupid. --- osu.Game/Configuration/OsuConfigManager.cs | 2 +- osu.Game/Screens/SelectV2/SongSelect.cs | 20 ++++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index df3e7d88af..bca905e7bb 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -63,7 +63,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.ToolbarClockDisplayMode, ToolbarClockDisplayMode.Full); - SetDefault(OsuSetting.SongSelectBackgroundBlur, true); + SetDefault(OsuSetting.SongSelectBackgroundBlur, false); // Online settings SetDefault(OsuSetting.Username, string.Empty); diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 421f4a6f11..65e5257b13 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -24,6 +25,7 @@ using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Collections; +using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Graphics.Carousel; using osu.Game.Graphics.Containers; @@ -131,8 +133,10 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private IDialogOverlay? dialogOverlay { get; set; } + private Bindable configBackgroundBlur = null!; + [BackgroundDependencyLoader] - private void load(AudioManager audio) + private void load(AudioManager audio, OsuConfigManager config) { errorSample = audio.Samples.Get(@"UI/generic-error"); @@ -273,6 +277,15 @@ namespace osu.Game.Screens.SelectV2 modSpeedHotkeyHandler = new ModSpeedHotkeyHandler(), modSelectOverlay, }); + + configBackgroundBlur = config.GetBindable(OsuSetting.SongSelectBackgroundBlur); + configBackgroundBlur.BindValueChanged(e => + { + if (!this.IsCurrentScreen()) + return; + + updateBackgroundDim(); + }); } /// @@ -666,14 +679,17 @@ namespace osu.Game.Screens.SelectV2 private void updateBackgroundDim() => ApplyToBackground(backgroundModeBeatmap => { - backgroundModeBeatmap.BlurAmount.Value = 0; backgroundModeBeatmap.Beatmap = Beatmap.Value; backgroundModeBeatmap.IgnoreUserSettings.Value = true; + + backgroundModeBeatmap.BlurAmount.Value = 0; backgroundModeBeatmap.DimWhenUserSettingsIgnored.Value = 0.1f; // Required to undo results screen dimming the background. // Probably needs more thought because this needs to be in every `ApplyToBackground` currently to restore sane defaults. backgroundModeBeatmap.FadeColour(Color4.White, 250); + + backgroundModeBeatmap.BlurAmount.Value = configBackgroundBlur.Value ? 20 : 0f; }); #endregion From 71210bedeb9a13c5f421ea85742d95c43169702c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Jul 2025 08:16:51 +0200 Subject: [PATCH 356/370] Fix skins containing subdirectories breaking on external edit on windows Closes https://github.com/ppy/osu/issues/33994. The reason for the breakage is that `Directory.EnumerateFiles()` used in https://github.com/ppy/osu/blob/b1435d35e56eed08a57d1909fa0b16e67bd9c2a2/osu.Game/Skinning/SkinImporter.cs#L63 will use the primary platform directory separator character, which is `\` on windows and `/` on unices. The internal realm storage structure is expecting paths to be normalised to the unix convention, which is evident in https://github.com/ppy/osu/blob/b1435d35e56eed08a57d1909fa0b16e67bd9c2a2/osu.Game/Database/RealmArchiveModelImporter.cs#L499 on the write side and in https://github.com/ppy/osu/blob/b1435d35e56eed08a57d1909fa0b16e67bd9c2a2/osu.Game/Skinning/RealmBackedResourceStore.cs#L50 on the read side. Rather than applying this locally to the skin importer I kinda think it's better to have this call in `ModelManager` to hopefully avoid future footgunnage of this kind. --- osu.Game/Database/ModelManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Database/ModelManager.cs b/osu.Game/Database/ModelManager.cs index 7a5fb5efbf..e96a8cc1b1 100644 --- a/osu.Game/Database/ModelManager.cs +++ b/osu.Game/Database/ModelManager.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using osu.Framework.Extensions; using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Extensions; @@ -85,6 +86,7 @@ namespace osu.Game.Database /// public void AddFile(TModel item, Stream contents, string filename, Realm realm) { + filename = filename.ToStandardisedPath(); var existing = item.GetFile(filename); if (existing != null) From 8a5ca85b1066d28b477b57bfcdfa429b0c7ca84b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Jul 2025 14:26:30 +0200 Subject: [PATCH 357/370] Make test fail --- osu.Game.Tests/Mods/ModUtilsTest.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index b780d60817..f29fdeabf6 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -342,12 +342,18 @@ namespace osu.Game.Tests.Mods { foreach (var mod in ruleset.CreateAllMods()) { - if (mod.ValidForFreestyleAsRequiredMod && mod.UserPlayable && !commonAcronyms.Contains(mod.Acronym)) + if (mod.ValidForFreestyleAsRequiredMod && !mod.UserPlayable) + Assert.Fail($"Mod {mod.GetType().ReadableName()} declares {nameof(Mod.ValidForFreestyleAsRequiredMod)} but is not playable!"); + + if (mod.ValidForFreestyleAsRequiredMod && !mod.HasImplementation) + Assert.Fail($"Mod {mod.GetType().ReadableName()} declares {nameof(Mod.ValidForFreestyleAsRequiredMod)} but is not implemented!"); + + if (mod.ValidForFreestyleAsRequiredMod && mod.UserPlayable && mod.HasImplementation && !commonAcronyms.Contains(mod.Acronym)) Assert.Fail($"{mod.GetType().ReadableName()} declares {nameof(Mod.ValidForFreestyleAsRequiredMod)} but does not exist in all four basic rulesets!"); // downgraded to warning, because there are valid reasons why they may still not be specified to be valid for freestyle as required // (see `TestModsValidForRequiredFreestyleAreConsistentlyCompatibleAcrossRulesets()` test case below). - if (!mod.ValidForFreestyleAsRequiredMod && mod.UserPlayable && commonAcronyms.Contains(mod.Acronym)) + if (!mod.ValidForFreestyleAsRequiredMod && mod.UserPlayable && mod.HasImplementation && commonAcronyms.Contains(mod.Acronym)) Assert.Warn($"{mod.GetType().ReadableName()} does not declare {nameof(Mod.ValidForFreestyleAsRequiredMod)} but exists in all four basic rulesets."); } } From 6013d4c0deaf6af02fc0d8c46a45d5c4fb01e7cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Jul 2025 14:28:04 +0200 Subject: [PATCH 358/370] Disallow Classic mod from being valid in freestyle as required mod Because it's not implemented for all rulesets. Closes https://github.com/ppy/osu/issues/34004. --- osu.Game/Rulesets/Mods/ModClassic.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModClassic.cs b/osu.Game/Rulesets/Mods/ModClassic.cs index e20ac5dfc7..66d6ea2e66 100644 --- a/osu.Game/Rulesets/Mods/ModClassic.cs +++ b/osu.Game/Rulesets/Mods/ModClassic.cs @@ -31,6 +31,6 @@ namespace osu.Game.Rulesets.Mods /// public sealed override bool Ranked => false; - public sealed override bool ValidForFreestyleAsRequiredMod => true; + public sealed override bool ValidForFreestyleAsRequiredMod => false; } } From f613e78b75c086f8d5156025d969a1872e054ad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Jul 2025 14:27:47 +0200 Subject: [PATCH 359/370] Remove test warning It does more bad than good at this stage. --- osu.Game.Tests/Mods/ModUtilsTest.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index f29fdeabf6..6ec4e799e6 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -350,11 +350,6 @@ namespace osu.Game.Tests.Mods if (mod.ValidForFreestyleAsRequiredMod && mod.UserPlayable && mod.HasImplementation && !commonAcronyms.Contains(mod.Acronym)) Assert.Fail($"{mod.GetType().ReadableName()} declares {nameof(Mod.ValidForFreestyleAsRequiredMod)} but does not exist in all four basic rulesets!"); - - // downgraded to warning, because there are valid reasons why they may still not be specified to be valid for freestyle as required - // (see `TestModsValidForRequiredFreestyleAreConsistentlyCompatibleAcrossRulesets()` test case below). - if (!mod.ValidForFreestyleAsRequiredMod && mod.UserPlayable && mod.HasImplementation && commonAcronyms.Contains(mod.Acronym)) - Assert.Warn($"{mod.GetType().ReadableName()} does not declare {nameof(Mod.ValidForFreestyleAsRequiredMod)} but exists in all four basic rulesets."); } } }); From d54fdce5c79d37056bcf11ecf2b590c4214466c5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Jul 2025 11:59:43 +0900 Subject: [PATCH 360/370] Unblur when revealing background --- osu.Game/Screens/SelectV2/SongSelect.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 65e5257b13..0a63d19d54 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -682,14 +682,13 @@ namespace osu.Game.Screens.SelectV2 backgroundModeBeatmap.Beatmap = Beatmap.Value; backgroundModeBeatmap.IgnoreUserSettings.Value = true; - backgroundModeBeatmap.BlurAmount.Value = 0; backgroundModeBeatmap.DimWhenUserSettingsIgnored.Value = 0.1f; // Required to undo results screen dimming the background. // Probably needs more thought because this needs to be in every `ApplyToBackground` currently to restore sane defaults. backgroundModeBeatmap.FadeColour(Color4.White, 250); - backgroundModeBeatmap.BlurAmount.Value = configBackgroundBlur.Value ? 20 : 0f; + backgroundModeBeatmap.BlurAmount.Value = revealingBackground == null && configBackgroundBlur.Value ? 20 : 0f; }); #endregion @@ -809,6 +808,8 @@ namespace osu.Game.Screens.SelectV2 skinnableContent.ScaleTo(1.2f, 600, Easing.OutQuint); skinnableContent.FadeOut(200, Easing.OutQuint); + updateBackgroundDim(); + Footer?.Hide(); }, 200); } @@ -842,6 +843,8 @@ namespace osu.Game.Screens.SelectV2 revealingBackground.Cancel(); revealingBackground = null; + + updateBackgroundDim(); } public virtual bool OnPressed(KeyBindingPressEvent e) From 01139613394fc5a0f6d3d07cbb68227b386a86e7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Jul 2025 15:21:09 +0900 Subject: [PATCH 361/370] Remove pointless nullable --- .../BeatmapLeaderboardScore_Tooltip.cs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs index d8bbe52b01..7f303f41d8 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs @@ -296,24 +296,21 @@ namespace osu.Game.Screens.SelectV2 if (attributes?.DifficultyAttributes == null || performanceCalculator == null) return; - var result = await performanceCalculator.CalculateAsync(score, attributes.Value.DifficultyAttributes, cancellationToken ?? default).ConfigureAwait(false); + var result = await performanceCalculator.CalculateAsync(score, attributes.Value.DifficultyAttributes, cancellationToken ?? CancellationToken.None).ConfigureAwait(false); Schedule(() => setPerformanceValue(score, result.Total)); }, cancellationToken ?? default); } - private void setPerformanceValue(ScoreInfo scoreInfo, double? pp) + private void setPerformanceValue(ScoreInfo scoreInfo, double pp) { - if (pp.HasValue) - { - int ppValue = (int)Math.Round(pp.Value, MidpointRounding.AwayFromZero); - ValueText.Text = LocalisableString.Interpolate(@$"{ppValue:N0}pp"); + int ppValue = (int)Math.Round(pp, MidpointRounding.AwayFromZero); + ValueText.Text = LocalisableString.Interpolate(@$"{ppValue:N0}pp"); - if (!scoreInfo.BeatmapInfo!.Status.GrantsPerformancePoints() || hasUnrankedMods(scoreInfo)) - Alpha = 0.5f; - else - Alpha = 1f; - } + if (!scoreInfo.BeatmapInfo!.Status.GrantsPerformancePoints() || hasUnrankedMods(scoreInfo)) + Alpha = 0.5f; + else + Alpha = 1f; } private static bool hasUnrankedMods(ScoreInfo scoreInfo) From 0c91dedfbb4636f1ae4efb4ec25e8029d8ccbca3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Jul 2025 15:29:22 +0900 Subject: [PATCH 362/370] Tint colours to avoid illegible text --- osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs index 7f303f41d8..0e26ca84cb 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs @@ -17,6 +17,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics; @@ -264,7 +265,7 @@ namespace osu.Game.Screens.SelectV2 private void load(OverlayColourProvider colourProvider) { labelText.Colour = colour ?? colourProvider.Content2; - ValueText.Colour = colour ?? colourProvider.Content1; + ValueText.Colour = Interpolation.ValueAt(0.85f, colourProvider.Content1, colour ?? colourProvider.Content1, 0, 1); } } From ce42a98fd923dd5f820823c1e6e302f4ffd8692f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Jul 2025 15:32:05 +0900 Subject: [PATCH 363/370] Adjust spacing and ordering of data in tooltip --- .../Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs index 0e26ca84cb..1f92699887 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs @@ -127,10 +127,11 @@ namespace osu.Game.Screens.SelectV2 var generalStatistics = new[] { - new PerformanceStatisticRow(BeatmapsetsStrings.ShowScoreboardHeaderspp.ToUpper(), score), - new StatisticRow(ModSelectOverlayStrings.ScoreMultiplier, ModUtils.FormatScoreMultiplier(multiplier)), new StatisticRow(BeatmapsetsStrings.ShowScoreboardHeadersCombo, value.MaxCombo.ToLocalisableString(@"0\x")), new StatisticRow(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy, value.Accuracy.FormatAccuracy()), + new PerformanceStatisticRow(BeatmapsetsStrings.ShowScoreboardHeaderspp.ToUpper(), score), + Empty().With(d => d.Height = 20), + new StatisticRow(ModSelectOverlayStrings.ScoreMultiplier, ModUtils.FormatScoreMultiplier(multiplier)), }; statistics.ChildrenEnumerable = judgementsStatistics @@ -206,7 +207,7 @@ namespace osu.Game.Screens.SelectV2 { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Spacing = new Vector2(0f, 4f), + Spacing = new Vector2(0f, 2f), Padding = new MarginPadding(8f), }, }, From 0715ed5e2ee5ca49187c65d2b20c623d0258af76 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Jul 2025 14:34:42 +0900 Subject: [PATCH 364/370] Adjust carousel sizing to better accommodate to ultra-wide-screen displays Roughly matches old song select now at widescreen resolutions. Does not change things much at standard 16:9 / 16:10. --- osu.Game/Screens/SelectV2/SongSelect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 0a63d19d54..8030290aab 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -170,7 +170,7 @@ namespace osu.Game.Screens.SelectV2 { new Dimension(GridSizeMode.Relative, 0.5f, maxSize: 700), new Dimension(), - new Dimension(GridSizeMode.Relative, 0.5f, maxSize: 660), + new Dimension(GridSizeMode.Relative, 0.5f, minSize: 500, maxSize: 900), }, Content = new[] { From 0fcd04d6710fd0a27809d567233c1f0a6eb58fdf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Jul 2025 20:28:53 +0900 Subject: [PATCH 365/370] 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 aa7b343f38..de3fe31ee6 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 ab3fc11cca..bb5e3da49e 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 567d09209bee47c54d476c7c18ae857eb2de3254 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 4 Jul 2025 20:40:37 +0900 Subject: [PATCH 366/370] Tweak SSv2 navigation sfx --- osu.Game/Graphics/Carousel/Carousel.cs | 2 +- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 11 +++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index 2f046b3754..b0e2ad428e 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -670,7 +670,7 @@ namespace osu.Game.Graphics.Carousel private void loadSamples(AudioManager audio) { - sampleKeyboardTraversal = audio.Samples.Get(@"UI/button-hover"); + sampleKeyboardTraversal = audio.Samples.Get(@"SongSelect/select-difficulty"); } private void playTraversalSound() diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index ce7bd7582e..4119807692 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -451,8 +451,7 @@ namespace osu.Game.Screens.SelectV2 private Sample? sampleChangeDifficulty; private Sample? sampleChangeSet; - private Sample? sampleOpen; - private Sample? sampleClose; + private Sample? sampleToggleGroup; private double audioFeedbackLastPlaybackTime; @@ -460,8 +459,7 @@ namespace osu.Game.Screens.SelectV2 { sampleChangeDifficulty = audio.Samples.Get(@"SongSelect/select-difficulty"); sampleChangeSet = audio.Samples.Get(@"SongSelect/select-expand"); - sampleOpen = audio.Samples.Get(@"UI/menu-open"); - sampleClose = audio.Samples.Get(@"UI/menu-close"); + sampleToggleGroup = audio.Samples.Get(@"SongSelect/select-group"); spinSample = audio.Samples.Get("SongSelect/random-spin"); randomSelectSample = audio.Samples.Get(@"SongSelect/select-random"); @@ -474,10 +472,7 @@ namespace osu.Game.Screens.SelectV2 switch (item.Model) { case GroupDefinition: - if (item.IsExpanded) - sampleOpen?.Play(); - else - sampleClose?.Play(); + sampleToggleGroup?.Play(); return; case BeatmapSetInfo: From c0e7771bc56784d6c5cc900c69e82653fb0b7c52 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 5 Jul 2025 01:39:03 +0900 Subject: [PATCH 367/370] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 2db11ecdfa..1c5456915a 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 71529d1a673552802af559abdea0f99c88ce51b4 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Fri, 4 Jul 2025 13:27:23 -0700 Subject: [PATCH 368/370] Fix song select group count pills shaking when expanding/collapsing --- osu.Game/Screens/SelectV2/PanelGroup.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelGroup.cs b/osu.Game/Screens/SelectV2/PanelGroup.cs index c0c4676a30..b7288f1da4 100644 --- a/osu.Game/Screens/SelectV2/PanelGroup.cs +++ b/osu.Game/Screens/SelectV2/PanelGroup.cs @@ -150,9 +150,9 @@ namespace osu.Game.Screens.SelectV2 countText.Text = Item.NestedItemCount.ToString("N0"); } - protected override void Update() + protected override void UpdateAfterChildren() { - base.Update(); + base.UpdateAfterChildren(); // Move the count pill in the opposite direction to keep it pinned to the screen regardless of the X position of TopLevelContent. countPill.X = -TopLevelContent.X; From ab6eda09a29eb93b566ff10a3195d8725ee4b6f6 Mon Sep 17 00:00:00 2001 From: jyc76 Date: Mon, 7 Jul 2025 11:51:26 +0900 Subject: [PATCH 369/370] Add fade transition in BeatmapLearderboardWedge --- osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs index 0554b1b815..11e1f281e5 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs @@ -397,8 +397,7 @@ namespace osu.Game.Screens.SelectV2 float fadeBottom = (float)(scoresScroll.Current + scoresScroll.DrawHeight); float fadeTop = (float)(scoresScroll.Current); - if (!scoresScroll.IsScrolledToStart()) - fadeTop += height; + fadeTop += (float)Math.Min(height, Math.Log10(Math.Max(fadeTop, 0) + 1) * height); foreach (var c in scoresContainer) { From 65cc89a64d07d47ddb1f6d49d0b32a307308bf55 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Jul 2025 15:02:04 +0900 Subject: [PATCH 370/370] Update resources --- osu.Game/Online/Rooms/MatchType.cs | 2 +- osu.Game/osu.Game.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Rooms/MatchType.cs b/osu.Game/Online/Rooms/MatchType.cs index 28f2da897a..ade28458e8 100644 --- a/osu.Game/Online/Rooms/MatchType.cs +++ b/osu.Game/Online/Rooms/MatchType.cs @@ -15,7 +15,7 @@ namespace osu.Game.Online.Rooms [LocalisableDescription(typeof(MatchesStrings), nameof(MatchesStrings.MatchTeamTypesHeadToHead))] HeadToHead, - [LocalisableDescription(typeof(MatchesStrings), nameof(MatchesStrings.MatchTeamTypesTeamVs))] + [LocalisableDescription(typeof(MatchesStrings), nameof(MatchesStrings.MatchTeamTypesTeamVersus))] TeamVersus, } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 1c5456915a..107f54d4ac 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - +