From 4d09e94367ef308c031750aa1e96d1ace1ad1df4 Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Tue, 10 Sep 2024 11:46:34 -0400 Subject: [PATCH 001/259] 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/259] 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/259] 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/259] 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/259] 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/259] 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/259] 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/259] 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/259] 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/259] 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/259] 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 1d232dca8d242e8a08eb5cc239b7685856810597 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 5 Nov 2024 14:16:36 +0900 Subject: [PATCH 012/259] Add default multiplier for mania key mods --- osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs b/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs index 88d6a19822..8ff131d3c8 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override string Acronym => Name; public abstract int KeyCount { get; } public override ModType Type => ModType.Conversion; - public override double ScoreMultiplier => 1; // TODO: Implement the mania key mod score multiplier + public override double ScoreMultiplier => 0.9; public override bool Ranked => UsesDefaultConfiguration; public void ApplyToBeatmapConverter(IBeatmapConverter beatmapConverter) From 165afe357f5a52050f2337e54478f87739f125fb Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Wed, 11 Dec 2024 10:19:10 -0500 Subject: [PATCH 013/259] 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 014/259] 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 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 015/259] 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 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 016/259] 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 017/259] 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 cad389722ebef46ce10ad288ca7327ece4b894c2 Mon Sep 17 00:00:00 2001 From: marvin Date: Tue, 10 Jun 2025 21:22:57 +0200 Subject: [PATCH 018/259] 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 18979d0ed4bab65d94511d1deb14c2d5ae21f303 Mon Sep 17 00:00:00 2001 From: marvin Date: Wed, 11 Jun 2025 09:47:39 +0200 Subject: [PATCH 019/259] 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 020/259] 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 0cbc5e3593b37fa807cbfa24997c647707bc6286 Mon Sep 17 00:00:00 2001 From: marvin Date: Wed, 11 Jun 2025 10:15:54 +0200 Subject: [PATCH 021/259] 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 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 022/259] 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 3816c5d95f3ec88e0ab4a0ebf433d0740de86931 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Jun 2025 19:15:51 +0900 Subject: [PATCH 023/259] 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 024/259] 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 025/259] 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 026/259] 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 027/259] 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 028/259] 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 029/259] 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 030/259] 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 031/259] 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 27d4ad7991f239d0bc6055cffd36f57639eb2217 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Jun 2025 19:41:05 +0900 Subject: [PATCH 032/259] 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 033/259] 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 065fc446da3afe7fd44388da3c6f7dcfa80fec54 Mon Sep 17 00:00:00 2001 From: Fayar35 Date: Tue, 17 Jun 2025 23:20:50 +0200 Subject: [PATCH 034/259] change StrictTrackingTailJudgement to regular TailJudgement but with LargeTickMiss in case of break --- osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs index 5ee8814b5a..926700389d 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs @@ -15,8 +15,10 @@ using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; +using static osu.Game.Rulesets.Osu.Objects.SliderTailCircle; namespace osu.Game.Rulesets.Osu.Mods { @@ -83,7 +85,12 @@ namespace osu.Game.Rulesets.Osu.Mods { } - public override Judgement CreateJudgement() => new OsuJudgement(); + public override Judgement CreateJudgement() => new StrictTrackingTailJudgement(); + } + + public class StrictTrackingTailJudgement : TailJudgement + { + public override HitResult MinResult => HitResult.LargeTickMiss; } private partial class StrictTrackingDrawableSliderTail : DrawableSliderTail From 48e6f09b4dd18edaa5e2749b7db4e8197e1f8f92 Mon Sep 17 00:00:00 2001 From: Fayar35 Date: Wed, 18 Jun 2025 00:15:18 +0200 Subject: [PATCH 035/259] remove using not used --- osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs index 926700389d..ee4d0ae04c 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs @@ -12,7 +12,6 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Beatmaps; -using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Scoring; 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 036/259] 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 037/259] 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 038/259] 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 039/259] 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 040/259] 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 041/259] 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 042/259] 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 043/259] 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 044/259] 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 045/259] 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 046/259] 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 047/259] 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 048/259] 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 049/259] 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 050/259] 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 051/259] 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 052/259] 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 053/259] 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 054/259] 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 055/259] 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 056/259] 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 057/259] 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 058/259] 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 059/259] 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 060/259] 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 061/259] 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 062/259] 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 063/259] 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 064/259] 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 065/259] 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 066/259] 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 067/259] 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 068/259] 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 069/259] 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 070/259] 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 071/259] 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 072/259] 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 073/259] 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 074/259] 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 075/259] 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 076/259] 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 077/259] 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 078/259] 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 079/259] 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 080/259] 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 081/259] 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 082/259] 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 083/259] 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 084/259] 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 085/259] 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 086/259] 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 087/259] 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 eaadd507d3d253f44674495002bc70fe3023aa7e Mon Sep 17 00:00:00 2001 From: diquoks Date: Tue, 24 Jun 2025 19:15:02 +0300 Subject: [PATCH 088/259] Add custom keybinds to tips in `Main menu` --- osu.Game/Localisation/MenuTipStrings.cs | 32 ++++++++++++------------- osu.Game/Screens/Menu/MenuTipDisplay.cs | 22 ++++++++++------- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/osu.Game/Localisation/MenuTipStrings.cs b/osu.Game/Localisation/MenuTipStrings.cs index 9d398e8e64..9a52f2e279 100644 --- a/osu.Game/Localisation/MenuTipStrings.cs +++ b/osu.Game/Localisation/MenuTipStrings.cs @@ -10,14 +10,14 @@ namespace osu.Game.Localisation private const string prefix = @"osu.Game.Resources.Localisation.MenuTip"; /// - /// "Press Ctrl-T anywhere in the game to toggle the toolbar!" + /// "Press {0} anywhere in the game to toggle the toolbar!" /// - public static LocalisableString ToggleToolbarShortcut => new TranslatableString(getKey(@"toggle_toolbar_shortcut"), @"Press Ctrl-T anywhere in the game to toggle the toolbar!"); + public static LocalisableString ToggleToolbarShortcut(LocalisableString keybind) => new TranslatableString(getKey(@"toggle_toolbar_shortcut"), @"Press {0} anywhere in the game to toggle the toolbar!", keybind); /// - /// "Press Ctrl-O anywhere in the game to access settings!" + /// "Press {0} anywhere in the game to access settings!" /// - public static LocalisableString GameSettingsShortcut => new TranslatableString(getKey(@"game_settings_shortcut"), @"Press Ctrl-O anywhere in the game to access settings!"); + public static LocalisableString GameSettingsShortcut(LocalisableString keybind) => new TranslatableString(getKey(@"game_settings_shortcut"), @"Press {0} anywhere in the game to access settings!", keybind); /// /// "All settings are dynamic and take effect in real-time. Try changing the skin while watching autoplay!" @@ -40,9 +40,9 @@ namespace osu.Game.Localisation public static LocalisableString ScreenScalingSettings => new TranslatableString(getKey(@"screen_scaling_settings"), @"Try adjusting the ""Screen Scaling"" mode to change your gameplay or UI area, even in fullscreen!"); /// - /// "What used to be "osu!direct" is available to all users just like on the website. You can access it anywhere using Ctrl-B!" + /// "What used to be "osu!direct" is available to all users just like on the website. You can access it anywhere using {0}!" /// - public static LocalisableString FreeOsuDirect => new TranslatableString(getKey(@"free_osu_direct"), @"What used to be ""osu!direct"" is available to all users just like on the website. You can access it anywhere using Ctrl-B!"); + public static LocalisableString FreeOsuDirect(LocalisableString keybind) => new TranslatableString(getKey(@"free_osu_direct"), @"What used to be ""osu!direct"" is available to all users just like on the website. You can access it anywhere using {0}!", keybind); /// /// "Seeking in replays is available by dragging on the progress bar at the bottom of the screen or by using the left and right arrow keys!" @@ -75,9 +75,9 @@ namespace osu.Game.Localisation public static LocalisableString ToggleAdvancedFPSCounter => new TranslatableString(getKey(@"toggle_advanced_fps_counter"), @"Toggle advanced frame / thread statistics with Ctrl-F11!"); /// - /// "You can pause during a replay by pressing Space!" + /// "You can pause during a replay by pressing {0}!" /// - public static LocalisableString ReplayPausing => new TranslatableString(getKey(@"replay_pausing"), @"You can pause during a replay by pressing Space!"); + public static LocalisableString ReplayPausing(LocalisableString keybind) => new TranslatableString(getKey(@"replay_pausing"), @"You can pause during a replay by pressing {0}!", keybind); /// /// "Most of the hotkeys in the game are configurable and can be changed to anything you want. Check the bindings panel under input settings!" @@ -85,9 +85,9 @@ namespace osu.Game.Localisation public static LocalisableString ConfigurableHotkeys => new TranslatableString(getKey(@"configurable_hotkeys"), @"Most of the hotkeys in the game are configurable and can be changed to anything you want. Check the bindings panel under input settings!"); /// - /// "Your gameplay HUD can be customised by using the skin layout editor. Open it at any time via Ctrl-Shift-S!" + /// "Your gameplay HUD can be customised by using the skin layout editor. Open it at any time via {0}!" /// - public static LocalisableString SkinEditor => new TranslatableString(getKey(@"skin_editor"), @"Your gameplay HUD can be customised by using the skin layout editor. Open it at any time via Ctrl-Shift-S!"); + public static LocalisableString SkinEditor(LocalisableString keybind) => new TranslatableString(getKey(@"skin_editor"), @"Your gameplay HUD can be customised by using the skin layout editor. Open it at any time via {0}!", keybind); /// /// "You can create mod presets to make toggling your favourite mod combinations easier!" @@ -100,14 +100,14 @@ namespace osu.Game.Localisation public static LocalisableString ModCustomisationSettings => new TranslatableString(getKey(@"mod_customisation_settings"), @"Many mods have customisation settings that drastically change how they function. Click the Customise button in mod select to view settings!"); /// - /// "Press Ctrl-Shift-R to switch to a random skin!" + /// "Press {0} to switch to a random skin!" /// - public static LocalisableString RandomSkinShortcut => new TranslatableString(getKey(@"random_skin_shortcut"), @"Press Ctrl-Shift-R to switch to a random skin!"); + public static LocalisableString RandomSkinShortcut(LocalisableString keybind) => new TranslatableString(getKey(@"random_skin_shortcut"), @"Press {0} to switch to a random skin!", keybind); /// - /// "While watching a replay, press Ctrl-H to toggle replay settings!" + /// "While watching a replay, press {0} to toggle replay settings!" /// - public static LocalisableString ToggleReplaySettingsShortcut => new TranslatableString(getKey(@"toggle_replay_settings_shortcut"), @"While watching a replay, press Ctrl-H to toggle replay settings!"); + public static LocalisableString ToggleReplaySettingsShortcut(LocalisableString keybind) => new TranslatableString(getKey(@"toggle_replay_settings_shortcut"), @"While watching a replay, press {0} to toggle replay settings!", keybind); /// /// "You can easily copy the mods from scores on a leaderboard by right-clicking on them!" @@ -140,9 +140,9 @@ namespace osu.Game.Localisation public static LocalisableString GlobalStatisticsShortcut => new TranslatableString(getKey(@"global_statistics_shortcut"), @"Take a look under the hood at performance counters and enable verbose performance logging with Ctrl-F2!"); /// - /// "When your gameplay HUD is hidden, you can press and hold Ctrl to view it temporarily!" + /// "When your gameplay HUD is hidden, you can press and hold {0} to view it temporarily!" /// - public static LocalisableString PeekHUDWhenHidden => new TranslatableString(getKey(@"peek_hud_when_hidden"), @"When your gameplay HUD is hidden, you can press and hold Ctrl to view it temporarily!"); + public static LocalisableString PeekHUDWhenHidden(LocalisableString keybind) => new TranslatableString(getKey(@"peek_hud_when_hidden"), @"When your gameplay HUD is hidden, you can press and hold {0} to view it temporarily!", keybind); /// /// "Drag and drop any image into the skin editor to load it in quickly!" diff --git a/osu.Game/Screens/Menu/MenuTipDisplay.cs b/osu.Game/Screens/Menu/MenuTipDisplay.cs index 283528d22a..f1464fcba7 100644 --- a/osu.Game/Screens/Menu/MenuTipDisplay.cs +++ b/osu.Game/Screens/Menu/MenuTipDisplay.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -12,6 +13,8 @@ using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Input; +using osu.Game.Input.Bindings; using osuTK; using osuTK.Graphics; using osu.Game.Localisation; @@ -27,6 +30,9 @@ namespace osu.Game.Screens.Menu private Bindable showMenuTips = null!; + [Resolved] + private RealmKeyBindingStore keyBindingStore { get; set; } = null!; + [BackgroundDependencyLoader] private void load() { @@ -101,13 +107,13 @@ namespace osu.Game.Screens.Menu { LocalisableString[] tips = { - MenuTipStrings.ToggleToolbarShortcut, - MenuTipStrings.GameSettingsShortcut, + MenuTipStrings.ToggleToolbarShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleToolbar).FirstOrDefault("Ctrl+T")), + MenuTipStrings.GameSettingsShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleSettings).FirstOrDefault("Ctrl+O")), MenuTipStrings.DynamicSettings, MenuTipStrings.NewFeaturesAreComingOnline, MenuTipStrings.UIScalingSettings, MenuTipStrings.ScreenScalingSettings, - MenuTipStrings.FreeOsuDirect, + MenuTipStrings.FreeOsuDirect(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleBeatmapListing).FirstOrDefault("Ctrl+B")), MenuTipStrings.ReplaySeeking, MenuTipStrings.MultithreadingSupport, MenuTipStrings.TryNewMods, @@ -117,15 +123,15 @@ namespace osu.Game.Screens.Menu MenuTipStrings.DiscoverPlaylists, MenuTipStrings.ToggleAdvancedFPSCounter, MenuTipStrings.GlobalStatisticsShortcut, - MenuTipStrings.ReplayPausing, + MenuTipStrings.ReplayPausing(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.TogglePauseReplay).FirstOrDefault("Space")), MenuTipStrings.ConfigurableHotkeys, - MenuTipStrings.PeekHUDWhenHidden, - MenuTipStrings.SkinEditor, + MenuTipStrings.PeekHUDWhenHidden(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.HoldForHUD).FirstOrDefault("Ctrl")), + MenuTipStrings.SkinEditor(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleSkinEditor).FirstOrDefault("Ctrl+Shift+S")), MenuTipStrings.DragAndDropImageInSkinEditor, MenuTipStrings.ModPresets, MenuTipStrings.ModCustomisationSettings, - MenuTipStrings.RandomSkinShortcut, - MenuTipStrings.ToggleReplaySettingsShortcut, + MenuTipStrings.RandomSkinShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.RandomSkin).FirstOrDefault("Ctrl+Shift+R")), + MenuTipStrings.ToggleReplaySettingsShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleReplaySettings).FirstOrDefault("Ctrl+H")), MenuTipStrings.CopyModsFromScore, MenuTipStrings.AutoplayBeatmapShortcut, MenuTipStrings.LazerIsNotAWord From bb15df1ba586b38f745ba9db56f56555d485a04f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Jun 2025 11:13:05 +0900 Subject: [PATCH 089/259] 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 090/259] 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 091/259] 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 092/259] 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 093/259] 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 094/259] 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 095/259] 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 096/259] 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 097/259] 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 098/259] 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 099/259] 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 100/259] 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 101/259] 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 8e228e17b70a4585369240526b63ea205b04e73b Mon Sep 17 00:00:00 2001 From: diquoks Date: Wed, 25 Jun 2025 11:52:33 +0300 Subject: [PATCH 102/259] Fix default values and remove tips' array --- osu.Game/Screens/Menu/MenuTipDisplay.cs | 123 ++++++++++++++++++------ 1 file changed, 91 insertions(+), 32 deletions(-) diff --git a/osu.Game/Screens/Menu/MenuTipDisplay.cs b/osu.Game/Screens/Menu/MenuTipDisplay.cs index f1464fcba7..f0934b838d 100644 --- a/osu.Game/Screens/Menu/MenuTipDisplay.cs +++ b/osu.Game/Screens/Menu/MenuTipDisplay.cs @@ -103,41 +103,100 @@ namespace osu.Game.Screens.Menu .FadeOutFromOne(2000, Easing.OutQuint); } + private const int availableTips = 28; + private LocalisableString getRandomTip() { - LocalisableString[] tips = - { - MenuTipStrings.ToggleToolbarShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleToolbar).FirstOrDefault("Ctrl+T")), - MenuTipStrings.GameSettingsShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleSettings).FirstOrDefault("Ctrl+O")), - MenuTipStrings.DynamicSettings, - MenuTipStrings.NewFeaturesAreComingOnline, - MenuTipStrings.UIScalingSettings, - MenuTipStrings.ScreenScalingSettings, - MenuTipStrings.FreeOsuDirect(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleBeatmapListing).FirstOrDefault("Ctrl+B")), - MenuTipStrings.ReplaySeeking, - MenuTipStrings.MultithreadingSupport, - MenuTipStrings.TryNewMods, - MenuTipStrings.EmbeddedWebContent, - MenuTipStrings.BeatmapRightClick, - MenuTipStrings.TemporaryDeleteOperations, - MenuTipStrings.DiscoverPlaylists, - MenuTipStrings.ToggleAdvancedFPSCounter, - MenuTipStrings.GlobalStatisticsShortcut, - MenuTipStrings.ReplayPausing(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.TogglePauseReplay).FirstOrDefault("Space")), - MenuTipStrings.ConfigurableHotkeys, - MenuTipStrings.PeekHUDWhenHidden(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.HoldForHUD).FirstOrDefault("Ctrl")), - MenuTipStrings.SkinEditor(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleSkinEditor).FirstOrDefault("Ctrl+Shift+S")), - MenuTipStrings.DragAndDropImageInSkinEditor, - MenuTipStrings.ModPresets, - MenuTipStrings.ModCustomisationSettings, - MenuTipStrings.RandomSkinShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.RandomSkin).FirstOrDefault("Ctrl+Shift+R")), - MenuTipStrings.ToggleReplaySettingsShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleReplaySettings).FirstOrDefault("Ctrl+H")), - MenuTipStrings.CopyModsFromScore, - MenuTipStrings.AutoplayBeatmapShortcut, - MenuTipStrings.LazerIsNotAWord - }; + int tipIndex = RNG.Next(0, availableTips); - return tips[RNG.Next(0, tips.Length)]; + switch (tipIndex) + { + case 0: + return MenuTipStrings.ToggleToolbarShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleToolbar).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding); + + case 1: + return MenuTipStrings.GameSettingsShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleSettings).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding); + + case 2: + return MenuTipStrings.DynamicSettings; + + case 3: + return MenuTipStrings.NewFeaturesAreComingOnline; + + case 4: + return MenuTipStrings.UIScalingSettings; + + case 5: + return MenuTipStrings.ScreenScalingSettings; + + case 6: + return MenuTipStrings.FreeOsuDirect(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleBeatmapListing).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding); + + case 7: + return MenuTipStrings.ReplaySeeking; + + case 8: + return MenuTipStrings.MultithreadingSupport; + + case 9: + return MenuTipStrings.TryNewMods; + + case 10: + return MenuTipStrings.EmbeddedWebContent; + + case 11: + return MenuTipStrings.BeatmapRightClick; + + case 12: + return MenuTipStrings.TemporaryDeleteOperations; + + case 13: + return MenuTipStrings.DiscoverPlaylists; + + case 14: + return MenuTipStrings.ToggleAdvancedFPSCounter; + + case 15: + return MenuTipStrings.GlobalStatisticsShortcut; + + case 16: + return MenuTipStrings.ReplayPausing(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.TogglePauseReplay).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding); + + case 17: + return MenuTipStrings.ConfigurableHotkeys; + + case 18: + return MenuTipStrings.PeekHUDWhenHidden(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.HoldForHUD).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding); + + case 19: + return MenuTipStrings.SkinEditor(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleSkinEditor).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding); + + case 20: + return MenuTipStrings.DragAndDropImageInSkinEditor; + + case 21: + return MenuTipStrings.ModPresets; + + case 22: + return MenuTipStrings.ModCustomisationSettings; + + case 23: + return MenuTipStrings.RandomSkinShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.RandomSkin).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding); + + case 24: + return MenuTipStrings.ToggleReplaySettingsShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleReplaySettings).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding); + + case 25: + return MenuTipStrings.CopyModsFromScore; + + case 26: + return MenuTipStrings.AutoplayBeatmapShortcut; + + case 27: + return MenuTipStrings.LazerIsNotAWord; + } + + return string.Empty; } } } From 49a9652fa5359732d95dcff8e9caf0a93222f274 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 25 Jun 2025 17:58:54 +0900 Subject: [PATCH 103/259] 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 104/259] 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 3923b0a949113db8a65df2876a7636f320726c77 Mon Sep 17 00:00:00 2001 From: diquoks Date: Wed, 25 Jun 2025 12:04:04 +0300 Subject: [PATCH 105/259] Rename variable --- osu.Game/Screens/Menu/MenuTipDisplay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/MenuTipDisplay.cs b/osu.Game/Screens/Menu/MenuTipDisplay.cs index f0934b838d..b2c2822b49 100644 --- a/osu.Game/Screens/Menu/MenuTipDisplay.cs +++ b/osu.Game/Screens/Menu/MenuTipDisplay.cs @@ -103,11 +103,11 @@ namespace osu.Game.Screens.Menu .FadeOutFromOne(2000, Easing.OutQuint); } - private const int availableTips = 28; + private const int available_tips = 28; private LocalisableString getRandomTip() { - int tipIndex = RNG.Next(0, availableTips); + int tipIndex = RNG.Next(0, available_tips); switch (tipIndex) { 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 106/259] 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 107/259] 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 108/259] 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 109/259] 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 110/259] 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 111/259] 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 112/259] 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 113/259] 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 114/259] 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 115/259] 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 e06e74d84e579eda77a1b36de317af31dbe62df4 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 26 Jun 2025 09:58:30 +0300 Subject: [PATCH 116/259] Change `GroupDefinition` equality to be case insensitive --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 28 +++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index b09490ce32..063323af82 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -823,9 +823,31 @@ namespace osu.Game.Screens.SelectV2 /// /// Defines a grouping header for a set of carousel items. /// - /// The order of this group in the carousel, sorted using ascending order. - /// The title of this group. - public record GroupDefinition(int Order, string Title); + public record GroupDefinition + { + /// + /// The order of this group in the carousel, sorted using ascending order. + /// + public int Order { get; } + + /// + /// The title of this group. + /// + public string Title { get; } + + private readonly string uncasedTitle; + + public GroupDefinition(int order, string title) + { + Order = order; + Title = title; + uncasedTitle = title.ToLowerInvariant(); + } + + public virtual bool Equals(GroupDefinition? other) => uncasedTitle == other?.uncasedTitle; + + public override int GetHashCode() => HashCode.Combine(uncasedTitle); + } /// /// Defines a grouping header for a set of carousel items grouped by star difficulty. From b4833d80d1e292650ff5f44b908e646a7daec415 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 26 Jun 2025 09:59:12 +0300 Subject: [PATCH 117/259] Add source grouping mode --- osu.Game/Screens/Select/Filter/GroupMode.cs | 7 +++++-- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 12 ++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/Filter/GroupMode.cs b/osu.Game/Screens/Select/Filter/GroupMode.cs index b3a4f36c91..04aef2fe18 100644 --- a/osu.Game/Screens/Select/Filter/GroupMode.cs +++ b/osu.Game/Screens/Select/Filter/GroupMode.cs @@ -34,6 +34,9 @@ namespace osu.Game.Screens.Select.Filter // [Description("Favourites")] // Favourites, + [Description("Last Played")] + LastPlayed, + [Description("Length")] Length, @@ -46,8 +49,8 @@ namespace osu.Game.Screens.Select.Filter [Description("Ranked Status")] RankedStatus, - [Description("Last Played")] - LastPlayed, + [Description("Source")] + Source, [Description("Title")] Title, diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index c68f377fbb..59737baab2 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -202,6 +202,9 @@ namespace osu.Game.Screens.SelectV2 return defineGroupByLength(length); }, items); + case GroupMode.Source: + return getGroupsBy(b => defineGroupBySource(b.BeatmapSet!.Metadata.Source), items); + // TODO: need implementation // // case GroupMode.Collections: @@ -225,6 +228,7 @@ namespace osu.Game.Screens.SelectV2 { return items.GroupBy(i => getGroup((BeatmapInfo)i.Model)) .OrderBy(s => s.Key.Order) + .ThenBy(s => s.Key.Title) .Select(g => new GroupMapping(g.Key, g.ToList())) .ToList(); } @@ -354,6 +358,14 @@ namespace osu.Game.Screens.SelectV2 return new GroupDefinition(11, "Over 10 minutes"); } + private GroupDefinition defineGroupBySource(string source) + { + if (string.IsNullOrEmpty(source)) + return new GroupDefinition(1, "Unsourced"); + + return new GroupDefinition(0, source); + } + private static T? aggregateMax(BeatmapInfo b, Func func) { var beatmaps = b.BeatmapSet!.Beatmaps.Where(bb => !bb.Hidden); From 7a1c7fbd7daf0c1f42bd04d77ab43544abb93719 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 26 Jun 2025 10:05:21 +0300 Subject: [PATCH 118/259] Add test coverage --- .../BeatmapCarouselFilterGroupingTest.cs | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs index 7f34d7a901..617836a5da 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs @@ -333,22 +333,32 @@ namespace osu.Game.Tests.Visual.SongSelectV2 #endregion + #region Source grouping + + [Test] + public async Task TestGroupingBySource() + { + int total = 0; + + var beatmapSets = new List(); + addBeatmapSet(s => s.Beatmaps[0].Metadata.Source = "Cool Game", beatmapSets, out var beatmapCoolGame); + addBeatmapSet(s => s.Beatmaps[0].Metadata.Source = "Cool game", beatmapSets, out var beatmapCoolGameB); + addBeatmapSet(s => s.Beatmaps[0].Metadata.Source = "Nice Movie", beatmapSets, out var beatmapNiceMovie); + addBeatmapSet(s => s.Beatmaps[0].Metadata.Source = string.Empty, beatmapSets, out var beatmapUnsourced); + + var results = await runGrouping(GroupMode.Source, beatmapSets); + assertGroup(results, 0, "Cool Game", new[] { beatmapCoolGame, beatmapCoolGameB }, ref total); + assertGroup(results, 1, "Nice Movie", new[] { beatmapNiceMovie }, ref total); + assertGroup(results, 2, "Unsourced", new[] { beatmapUnsourced }, ref total); + assertTotal(results, total); + } + + #endregion + private static async Task> runGrouping(GroupMode group, List beatmapSets) { var groupingFilter = new BeatmapCarouselFilterGrouping(() => new FilterCriteria { Group = group }); - var carouselItems = await groupingFilter.Run(beatmapSets.SelectMany(s => s.Beatmaps.Select(b => new CarouselItem(b))).ToList(), CancellationToken.None); - - // sanity check to ensure no detection of two group items with equal order value. - var groups = carouselItems.Select(i => i.Model).OfType(); - - foreach (var header in groups) - { - var sameOrder = groups.FirstOrDefault(g => g != header && g.Order == header.Order); - if (sameOrder != null) - Assert.Fail($"Detected two groups with equal order number: \"{header.Title}\" vs. \"{sameOrder.Title}\""); - } - - return carouselItems; + return await groupingFilter.Run(beatmapSets.SelectMany(s => s.Beatmaps.Select(b => new CarouselItem(b))).ToList(), CancellationToken.None); } private static void assertGroup(List items, int index, string expectedTitle, IEnumerable expectedBeatmapSets, ref int totalItems) From cd354a0de827a0601d8f78fdd74c6e61d64c55da Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 27 Jun 2025 00:58:10 +0300 Subject: [PATCH 119/259] 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 120/259] 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 121/259] 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 122/259] 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 123/259] 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 124/259] 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 125/259] 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 126/259] 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 127/259] 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 128/259] 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 129/259] 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 130/259] 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 131/259] 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 132/259] 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 133/259] 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 134/259] 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 135/259] 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 136/259] 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 137/259] 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 138/259] 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 139/259] 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 140/259] 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 141/259] 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 142/259] 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 143/259] 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 144/259] 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 145/259] 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 146/259] 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 147/259] 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 148/259] 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 149/259] 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 150/259] 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 151/259] 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 152/259] 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 153/259] 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 154/259] 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 155/259] 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 156/259] 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 157/259] 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 158/259] 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 159/259] 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 160/259] 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 161/259] 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 162/259] 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 163/259] 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 164/259] 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 165/259] 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 166/259] 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 167/259] 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 168/259] 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 169/259] 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 170/259] 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 171/259] 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 2016fc5ea1f3c5dd33a0a5fd1f51eeb467ec7d93 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Jul 2025 18:14:13 +0900 Subject: [PATCH 172/259] Replace `using static` with inner class name --- osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs index ee4d0ae04c..129c03149f 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs @@ -17,7 +17,6 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; -using static osu.Game.Rulesets.Osu.Objects.SliderTailCircle; namespace osu.Game.Rulesets.Osu.Mods { @@ -87,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override Judgement CreateJudgement() => new StrictTrackingTailJudgement(); } - public class StrictTrackingTailJudgement : TailJudgement + public class StrictTrackingTailJudgement : SliderTailCircle.TailJudgement { public override HitResult MinResult => HitResult.LargeTickMiss; } From de61f8519c5184dc1aab512c1f3241ca9953e898 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 2 Jul 2025 13:24:26 +0300 Subject: [PATCH 173/259] 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 6afdf99df8a42d91197c53692502bc7737610cfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 13:42:06 +0200 Subject: [PATCH 174/259] Support mania-specific hit window quirks The quirks in question being that lazer's hit windows in mania preceding this change are used in stable *if and only if* Score V2 is active. If Score V2 is *not* active, stable has two disparate other sets of hit window ranges, dependent on whether the beatmap is a convert or not. With this commit, those hit windows are used in lazer when the Classic mod is active. Open points for discussion would be: - What does this mean for plays already set on lazer using the Classic mod? Are there even enough of them to care about? Also, on `master` the Classic mod does precisely nothing, so maybe such scores should just have Classic mod stripped from them? - What does this mean for the mod multiplier of Classic in mania? (I don't expect an answer to this one.) --- .../TestSceneLegacyReplayPlayback.cs | 3 +- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 4 +- .../Mods/IManiaRateAdjustmentMod.cs | 18 +-- .../Mods/ManiaModClassic.cs | 32 +++++- .../Mods/ManiaModDaycore.cs | 3 - .../Mods/ManiaModDoubleTime.cs | 4 - .../Mods/ManiaModHalfTime.cs | 3 - .../Mods/ManiaModNightcore.cs | 4 - .../Mods/ManiaModScoreV2.cs | 37 +++++++ .../Scoring/ManiaHitWindows.cs | 104 +++++++++++++++--- osu.Game/Rulesets/Mods/ModScoreV2.cs | 2 +- 11 files changed, 163 insertions(+), 51 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/Mods/ManiaModScoreV2.cs diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneLegacyReplayPlayback.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneLegacyReplayPlayback.cs index 2c17cd8015..040dc995e2 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneLegacyReplayPlayback.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneLegacyReplayPlayback.cs @@ -9,7 +9,6 @@ using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Replays; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; @@ -521,7 +520,7 @@ namespace osu.Game.Rulesets.Mania.Tests ScoreInfo = new ScoreInfo { Ruleset = CreateRuleset().RulesetInfo, - Mods = [new ModScoreV2()] + Mods = [new ManiaModScoreV2()] } }; diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index cdc7b0a951..c2bcba38ab 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -161,7 +161,7 @@ namespace osu.Game.Rulesets.Mania yield return new ManiaModMirror(); if (mods.HasFlag(LegacyMods.ScoreV2)) - yield return new ModScoreV2(); + yield return new ManiaModScoreV2(); } public override LegacyMods ConvertToLegacyMods(Mod[] mods) @@ -296,7 +296,7 @@ namespace osu.Game.Rulesets.Mania case ModType.System: return new Mod[] { - new ModScoreV2(), + new ManiaModScoreV2(), }; default: diff --git a/osu.Game.Rulesets.Mania/Mods/IManiaRateAdjustmentMod.cs b/osu.Game.Rulesets.Mania/Mods/IManiaRateAdjustmentMod.cs index ea01bd4436..ca364a1ec8 100644 --- a/osu.Game.Rulesets.Mania/Mods/IManiaRateAdjustmentMod.cs +++ b/osu.Game.Rulesets.Mania/Mods/IManiaRateAdjustmentMod.cs @@ -2,12 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; -using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Mods { @@ -17,29 +15,21 @@ namespace osu.Game.Rulesets.Mania.Mods /// /// Historically, in osu!mania, hit windows are expected to adjust relative to the gameplay rate such that the real-world hit window remains the same. /// - public interface IManiaRateAdjustmentMod : IApplicableToDifficulty, IApplicableToHitObject + public interface IManiaRateAdjustmentMod : IApplicableToHitObject { BindableNumber SpeedChange { get; } - HitWindows HitWindows { get; set; } - - void IApplicableToDifficulty.ApplyToDifficulty(BeatmapDifficulty difficulty) - { - HitWindows = new ManiaHitWindows(SpeedChange.Value); - HitWindows.SetDifficulty(difficulty.OverallDifficulty); - } - void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject) { switch (hitObject) { case Note: - hitObject.HitWindows = HitWindows; + ((ManiaHitWindows)hitObject.HitWindows).SpeedMultiplier = SpeedChange.Value; break; case HoldNote hold: - hold.Head.HitWindows = HitWindows; - hold.Tail.HitWindows = HitWindows; + ((ManiaHitWindows)hold.Head.HitWindows).SpeedMultiplier = SpeedChange.Value; + ((ManiaHitWindows)hold.Tail.HitWindows).SpeedMultiplier = SpeedChange.Value; break; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModClassic.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModClassic.cs index 073dda9de8..5e46250dd2 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModClassic.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModClassic.cs @@ -1,11 +1,41 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Mania.Mods { - public class ManiaModClassic : ModClassic + public class ManiaModClassic : ModClassic, IApplicableToBeatmap { + public void ApplyToBeatmap(IBeatmap beatmap) + { + bool isConvert = !beatmap.BeatmapInfo.Ruleset.Equals(new ManiaRuleset().RulesetInfo); + + foreach (var ho in beatmap.HitObjects) + { + switch (ho) + { + case Note note: + { + var hitWindows = (ManiaHitWindows)note.HitWindows; + hitWindows.IsConvert = isConvert; + hitWindows.ClassicModActive = true; + break; + } + + case HoldNote hold: + { + var headWindows = (ManiaHitWindows)hold.Head.HitWindows; + var tailWindows = (ManiaHitWindows)hold.Tail.HitWindows; + headWindows.IsConvert = tailWindows.IsConvert = isConvert; + headWindows.ClassicModActive = tailWindows.ClassicModActive = true; + break; + } + } + } + } } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs index dbe2a9a9fc..9e9d671006 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs @@ -1,14 +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 osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModDaycore : ModDaycore, IManiaRateAdjustmentMod { - public HitWindows HitWindows { get; set; } = new ManiaHitWindows(); } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs index bea1a14110..043fa1c40c 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs @@ -1,16 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModDoubleTime : ModDoubleTime, IManiaRateAdjustmentMod { - public HitWindows HitWindows { get; set; } = new ManiaHitWindows(); - // For now, all rate-increasing mods should be given a 1x multiplier in mania because it doesn't always // make the map harder and is more of a personal preference. // In the future, we can consider adjusting this by experimenting with not applying the hitwindow leniency. diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs index b0fbb11396..f8d2758914 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs @@ -1,14 +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 osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModHalfTime : ModHalfTime, IManiaRateAdjustmentMod { - public HitWindows HitWindows { get; set; } = new ManiaHitWindows(); } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs index 7e5e80db6c..0eb4ddc7d0 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs @@ -2,16 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Mania.Objects; -using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModNightcore : ModNightcore, IManiaRateAdjustmentMod { - public HitWindows HitWindows { get; set; } = new ManiaHitWindows(); - // For now, all rate-increasing mods should be given a 1x multiplier in mania because it doesn't always // make the map any harder and is more of a personal preference. // In the future, we can consider adjusting this by experimenting with not applying the hitwindow leniency. diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModScoreV2.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModScoreV2.cs new file mode 100644 index 0000000000..46bb75a480 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModScoreV2.cs @@ -0,0 +1,37 @@ +// 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.Beatmaps; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Scoring; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Mania.Mods +{ + public class ManiaModScoreV2 : ModScoreV2, IApplicableToBeatmap + { + public void ApplyToBeatmap(IBeatmap beatmap) + { + foreach (var ho in beatmap.HitObjects) + { + switch (ho) + { + case Note note: + { + var hitWindows = (ManiaHitWindows)note.HitWindows; + hitWindows.ScoreV2Active = true; + break; + } + + case HoldNote hold: + { + var headWindows = (ManiaHitWindows)hold.Head.HitWindows; + var tailWindows = (ManiaHitWindows)hold.Tail.HitWindows; + headWindows.ScoreV2Active = tailWindows.ScoreV2Active = true; + break; + } + } + } + } + } +} diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs index 96dbd957ae..d81039a61d 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs @@ -16,7 +16,55 @@ namespace osu.Game.Rulesets.Mania.Scoring 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 speedMultiplier = 1; + + public double SpeedMultiplier + { + get => speedMultiplier; + set + { + speedMultiplier = value; + updateWindows(); + } + } + + private double overallDifficulty; + + private bool classicModActive; + + public bool ClassicModActive + { + get => classicModActive; + set + { + classicModActive = value; + updateWindows(); + } + } + + private bool scoreV2Active; + + public bool ScoreV2Active + { + get => scoreV2Active; + set + { + scoreV2Active = value; + updateWindows(); + } + } + + private bool isConvert; + + public bool IsConvert + { + get => isConvert; + set + { + isConvert = value; + updateWindows(); + } + } private double perfect; private double great; @@ -25,16 +73,6 @@ namespace osu.Game.Rulesets.Mania.Scoring private double meh; private double miss; - public ManiaHitWindows() - : this(1) - { - } - - public ManiaHitWindows(double multiplier) - { - this.multiplier = multiplier; - } - public override bool IsHitResultAllowed(HitResult result) { switch (result) @@ -53,12 +91,44 @@ namespace osu.Game.Rulesets.Mania.Scoring public override void SetDifficulty(double difficulty) { - 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; + overallDifficulty = difficulty; + updateWindows(); + } + + private void updateWindows() + { + if (ClassicModActive && !ScoreV2Active) + { + if (IsConvert) + { + perfect = Math.Floor(16 * speedMultiplier) + 0.5; + great = Math.Floor((Math.Round(overallDifficulty) > 4 ? 34 : 47) * speedMultiplier) + 0.5; + good = Math.Floor((Math.Round(overallDifficulty) > 4 ? 67 : 77) * speedMultiplier) + 0.5; + ok = Math.Floor(97 * speedMultiplier) + 0.5; + meh = Math.Floor(121 * speedMultiplier) + 0.5; + miss = Math.Floor(158 * speedMultiplier) + 0.5; + } + else + { + double invertedOd = Math.Clamp(10 - overallDifficulty, 0, 10); + + perfect = Math.Floor(16 * speedMultiplier) + 0.5; + great = Math.Floor((34 + 3 * invertedOd) * speedMultiplier) + 0.5; + good = Math.Floor((67 + 3 * invertedOd) * speedMultiplier) + 0.5; + ok = Math.Floor((97 + 3 * invertedOd) * speedMultiplier) + 0.5; + meh = Math.Floor((121 + 3 * invertedOd) * speedMultiplier) + 0.5; + miss = Math.Floor((158 + 3 * invertedOd) * speedMultiplier) + 0.5; + } + } + else + { + perfect = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, perfect_window_range) * speedMultiplier) + 0.5; + great = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, great_window_range) * speedMultiplier) + 0.5; + good = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, good_window_range) * speedMultiplier) + 0.5; + ok = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, ok_window_range) * speedMultiplier) + 0.5; + meh = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, meh_window_range) * speedMultiplier) + 0.5; + miss = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, miss_window_range) * speedMultiplier) + 0.5; + } } public override double WindowFor(HitResult result) diff --git a/osu.Game/Rulesets/Mods/ModScoreV2.cs b/osu.Game/Rulesets/Mods/ModScoreV2.cs index 6a77cafa30..854f3916a1 100644 --- a/osu.Game/Rulesets/Mods/ModScoreV2.cs +++ b/osu.Game/Rulesets/Mods/ModScoreV2.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Mods /// This mod is used strictly to mark osu!stable scores set with the "Score V2" mod active. /// It should not be used in any real capacity going forward. /// - public sealed class ModScoreV2 : Mod + public class ModScoreV2 : Mod { public override string Name => "Score V2"; public override string Acronym => @"SV2"; From 8e53f47e78573562cc604f959d045ba36a65f559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 9 May 2025 11:15:20 +0200 Subject: [PATCH 175/259] Fix mania Hard Rock & Easy mods not matching stable The implementation in `master` was presuming that Hard Rock and Easy worked the same way across all rulesets, but actually, in stable mania, the two mods have special treatment as per https://github.com/peppy/osu-stable-reference/blob/996648fba06baf4e7d2e0b248959399444017895/osu!/GameplayElements/HitObjectManagerMania.cs#L147-L150 The open question here would be what this means for existing scores set on lazer using this mod. --- osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs | 8 +++ .../Mods/CatchModHardRock.cs | 1 + osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs | 22 ++++++- .../Mods/ManiaModHardRock.cs | 22 ++++++- .../Scoring/ManiaHitWindows.cs | 65 ++++++++++++++----- osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs | 8 +++ osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs | 1 + osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs | 2 + .../Mods/TaikoModHardRock.cs | 3 + osu.Game/Rulesets/Mods/ModEasy.cs | 10 +-- osu.Game/Rulesets/Mods/ModHardRock.cs | 1 - 11 files changed, 117 insertions(+), 26 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs b/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs index f2c77d6a05..f40d2bb45e 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Localisation; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Catch.Mods @@ -9,5 +10,12 @@ namespace osu.Game.Rulesets.Catch.Mods public class CatchModEasy : ModEasyWithExtraLives { public override LocalisableString Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and extra lives!"; + + public override void ApplyToDifficulty(BeatmapDifficulty difficulty) + { + base.ApplyToDifficulty(difficulty); + + difficulty.OverallDifficulty *= ADJUST_RATIO; + } } } diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs index 62fded0980..f7d64dc57b 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs @@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Catch.Mods { base.ApplyToDifficulty(difficulty); + difficulty.OverallDifficulty = Math.Min(difficulty.OverallDifficulty * ADJUST_RATIO, 10.0f); difficulty.CircleSize = Math.Min(difficulty.CircleSize * 1.3f, 10.0f); // CS uses a custom 1.3 ratio. difficulty.ApproachRate = Math.Min(difficulty.ApproachRate * ADJUST_RATIO, 10.0f); } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs index 275643ca44..c9a84051d5 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs @@ -2,12 +2,32 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Localisation; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Mania.Mods { - public class ManiaModEasy : ModEasyWithExtraLives + public class ManiaModEasy : ModEasyWithExtraLives, IApplicableToHitObject { public override LocalisableString Description => @"More forgiving HP drain, less accuracy required, and extra lives!"; + + void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject) + { + const double multiplier = 1 / 1.4; + + switch (hitObject) + { + case Note: + ((ManiaHitWindows)hitObject.HitWindows).DifficultyMultiplier = multiplier; + break; + + case HoldNote hold: + ((ManiaHitWindows)hold.Head.HitWindows).DifficultyMultiplier = multiplier; + ((ManiaHitWindows)hold.Tail.HitWindows).DifficultyMultiplier = multiplier; + break; + } + } } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHardRock.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHardRock.cs index 189c4b3a5f..a73bd94566 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHardRock.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHardRock.cs @@ -1,13 +1,33 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Mania.Mods { - public class ManiaModHardRock : ModHardRock + public class ManiaModHardRock : ModHardRock, IApplicableToHitObject { public override double ScoreMultiplier => 1; public override bool Ranked => false; + + void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject) + { + const double multiplier = 1.4; + + switch (hitObject) + { + case Note: + ((ManiaHitWindows)hitObject.HitWindows).DifficultyMultiplier = multiplier; + break; + + case HoldNote hold: + ((ManiaHitWindows)hold.Head.HitWindows).DifficultyMultiplier = multiplier; + ((ManiaHitWindows)hold.Tail.HitWindows).DifficultyMultiplier = multiplier; + break; + } + } } } diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs index d81039a61d..fe47b297dd 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs @@ -18,6 +18,14 @@ namespace osu.Game.Rulesets.Mania.Scoring private double speedMultiplier = 1; + /// + /// Multiplier used to compensate for the playback speed of the track speeding up or slowing down. + /// The goal of this multiplier is to keep hit windows independent of track speed. + /// + /// When the track speed is above 1, the hit window ranges are multiplied by , because the time elapses faster. + /// When the track speed is below 1, the hit window ranges are also multiplied by , because the time elapses slower. + /// + /// public double SpeedMultiplier { get => speedMultiplier; @@ -28,6 +36,27 @@ namespace osu.Game.Rulesets.Mania.Scoring } } + private double difficultyMultiplier = 1; + + /// + /// Multiplier used to make the gameplay more or less difficult. + /// + /// When the is above 1, the hit windows decrease to make the gameplay harder. + /// When the is below 1, the hit windows increase to make the gameplay easier. + /// + /// + public double DifficultyMultiplier + { + get => difficultyMultiplier; + set + { + difficultyMultiplier = value; + updateWindows(); + } + } + + private double totalMultiplier => speedMultiplier / difficultyMultiplier; + private double overallDifficulty; private bool classicModActive; @@ -101,33 +130,33 @@ namespace osu.Game.Rulesets.Mania.Scoring { if (IsConvert) { - perfect = Math.Floor(16 * speedMultiplier) + 0.5; - great = Math.Floor((Math.Round(overallDifficulty) > 4 ? 34 : 47) * speedMultiplier) + 0.5; - good = Math.Floor((Math.Round(overallDifficulty) > 4 ? 67 : 77) * speedMultiplier) + 0.5; - ok = Math.Floor(97 * speedMultiplier) + 0.5; - meh = Math.Floor(121 * speedMultiplier) + 0.5; - miss = Math.Floor(158 * speedMultiplier) + 0.5; + perfect = Math.Floor(16 * totalMultiplier) + 0.5; + great = Math.Floor((Math.Round(overallDifficulty) > 4 ? 34 : 47) * totalMultiplier) + 0.5; + good = Math.Floor((Math.Round(overallDifficulty) > 4 ? 67 : 77) * totalMultiplier) + 0.5; + ok = Math.Floor(97 * totalMultiplier) + 0.5; + meh = Math.Floor(121 * totalMultiplier) + 0.5; + miss = Math.Floor(158 * totalMultiplier) + 0.5; } else { double invertedOd = Math.Clamp(10 - overallDifficulty, 0, 10); - perfect = Math.Floor(16 * speedMultiplier) + 0.5; - great = Math.Floor((34 + 3 * invertedOd) * speedMultiplier) + 0.5; - good = Math.Floor((67 + 3 * invertedOd) * speedMultiplier) + 0.5; - ok = Math.Floor((97 + 3 * invertedOd) * speedMultiplier) + 0.5; - meh = Math.Floor((121 + 3 * invertedOd) * speedMultiplier) + 0.5; - miss = Math.Floor((158 + 3 * invertedOd) * speedMultiplier) + 0.5; + perfect = Math.Floor(16 * totalMultiplier) + 0.5; + great = Math.Floor((34 + 3 * invertedOd) * totalMultiplier) + 0.5; + good = Math.Floor((67 + 3 * invertedOd) * totalMultiplier) + 0.5; + ok = Math.Floor((97 + 3 * invertedOd) * totalMultiplier) + 0.5; + meh = Math.Floor((121 + 3 * invertedOd) * totalMultiplier) + 0.5; + miss = Math.Floor((158 + 3 * invertedOd) * totalMultiplier) + 0.5; } } else { - perfect = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, perfect_window_range) * speedMultiplier) + 0.5; - great = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, great_window_range) * speedMultiplier) + 0.5; - good = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, good_window_range) * speedMultiplier) + 0.5; - ok = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, ok_window_range) * speedMultiplier) + 0.5; - meh = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, meh_window_range) * speedMultiplier) + 0.5; - miss = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, miss_window_range) * speedMultiplier) + 0.5; + perfect = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, perfect_window_range) * totalMultiplier) + 0.5; + great = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, great_window_range) * totalMultiplier) + 0.5; + good = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, good_window_range) * totalMultiplier) + 0.5; + ok = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, ok_window_range) * totalMultiplier) + 0.5; + meh = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, meh_window_range) * totalMultiplier) + 0.5; + miss = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, miss_window_range) * totalMultiplier) + 0.5; } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs b/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs index 97fe0d0bf2..9725a42674 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Localisation; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Osu.Mods @@ -9,5 +10,12 @@ namespace osu.Game.Rulesets.Osu.Mods public class OsuModEasy : ModEasyWithExtraLives { public override LocalisableString Description => @"Larger circles, more forgiving HP drain, less accuracy required, and extra lives!"; + + public override void ApplyToDifficulty(BeatmapDifficulty difficulty) + { + base.ApplyToDifficulty(difficulty); + + difficulty.OverallDifficulty *= ADJUST_RATIO; + } } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs index d24597eeed..e7ac63599d 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs @@ -28,6 +28,7 @@ namespace osu.Game.Rulesets.Osu.Mods { base.ApplyToDifficulty(difficulty); + difficulty.OverallDifficulty = Math.Min(difficulty.OverallDifficulty * ADJUST_RATIO, 10.0f); difficulty.CircleSize = Math.Min(difficulty.CircleSize * 1.3f, 10.0f); // CS uses a custom 1.3 ratio. difficulty.ApproachRate = Math.Min(difficulty.ApproachRate * ADJUST_RATIO, 10.0f); } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs index 009f2854f8..1bc9277210 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs @@ -19,6 +19,8 @@ namespace osu.Game.Rulesets.Taiko.Mods public override void ApplyToDifficulty(BeatmapDifficulty difficulty) { base.ApplyToDifficulty(difficulty); + + difficulty.OverallDifficulty *= ADJUST_RATIO; difficulty.SliderMultiplier *= slider_multiplier; } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs index ba41175461..8f01c21894 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; @@ -23,6 +24,8 @@ namespace osu.Game.Rulesets.Taiko.Mods public override void ApplyToDifficulty(BeatmapDifficulty difficulty) { base.ApplyToDifficulty(difficulty); + + difficulty.OverallDifficulty = Math.Min(difficulty.OverallDifficulty * ADJUST_RATIO, 10.0f); difficulty.SliderMultiplier *= slider_multiplier; } } diff --git a/osu.Game/Rulesets/Mods/ModEasy.cs b/osu.Game/Rulesets/Mods/ModEasy.cs index 3ee4d7846e..0ee384c0f7 100644 --- a/osu.Game/Rulesets/Mods/ModEasy.cs +++ b/osu.Game/Rulesets/Mods/ModEasy.cs @@ -19,13 +19,13 @@ namespace osu.Game.Rulesets.Mods public override bool Ranked => UsesDefaultConfiguration; public override bool ValidForFreestyleAsRequiredMod => true; + protected const float ADJUST_RATIO = 0.5f; + public virtual void ApplyToDifficulty(BeatmapDifficulty difficulty) { - const float ratio = 0.5f; - difficulty.CircleSize *= ratio; - difficulty.ApproachRate *= ratio; - difficulty.DrainRate *= ratio; - difficulty.OverallDifficulty *= ratio; + difficulty.CircleSize *= ADJUST_RATIO; + difficulty.ApproachRate *= ADJUST_RATIO; + difficulty.DrainRate *= ADJUST_RATIO; } } } diff --git a/osu.Game/Rulesets/Mods/ModHardRock.cs b/osu.Game/Rulesets/Mods/ModHardRock.cs index 6149a9c712..713bfe0623 100644 --- a/osu.Game/Rulesets/Mods/ModHardRock.cs +++ b/osu.Game/Rulesets/Mods/ModHardRock.cs @@ -25,7 +25,6 @@ namespace osu.Game.Rulesets.Mods public virtual void ApplyToDifficulty(BeatmapDifficulty difficulty) { difficulty.DrainRate = Math.Min(difficulty.DrainRate * ADJUST_RATIO, 10.0f); - difficulty.OverallDifficulty = Math.Min(difficulty.OverallDifficulty * ADJUST_RATIO, 10.0f); } } } From 110fcf96347fff3272472f9729b5fac82943d6d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 2 Jul 2025 12:14:38 +0200 Subject: [PATCH 176/259] Unignore relevant test cases to demonstrate improved behaviour --- .../TestSceneLegacyReplayPlayback.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneLegacyReplayPlayback.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneLegacyReplayPlayback.cs index 040dc995e2..f95c0c186f 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneLegacyReplayPlayback.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneLegacyReplayPlayback.cs @@ -527,7 +527,6 @@ 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) { @@ -555,7 +554,6 @@ 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) { @@ -584,7 +582,6 @@ 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) { @@ -613,7 +610,6 @@ 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) { @@ -642,7 +638,6 @@ 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) { @@ -671,7 +666,6 @@ 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) { From 9562e83fc0df986c1f3ec5b2b6b0e687090feecf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 2 Jul 2025 14:00:13 +0200 Subject: [PATCH 177/259] Fix test --- osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs index cb2abc1595..2ffc1ee0ef 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs @@ -5,7 +5,6 @@ using System; using NUnit.Framework; using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Mania.Mods; -using osu.Game.Rulesets.Mods; using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Mania.Tests @@ -38,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests new object[] { LegacyMods.Key2, new[] { typeof(ManiaModKey2) } }, new object[] { LegacyMods.Mirror, new[] { typeof(ManiaModMirror) } }, new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) } }, - new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } }, + new object[] { LegacyMods.ScoreV2, new[] { typeof(ManiaModScoreV2) } }, }; [TestCaseSource(nameof(mania_mod_mapping))] From 86b25a0b3e5690ca83efb2007440dfa07bebd8c1 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 3 Jul 2025 06:30:30 +0300 Subject: [PATCH 178/259] 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 179/259] 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 180/259] 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 181/259] 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 182/259] 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 183/259] 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 184/259] 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 185/259] 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 186/259] 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 187/259] 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 188/259] 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 189/259] 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 190/259] 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 191/259] 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 192/259] 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 193/259] 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 f19bc18f494e237802d89b8f6cf230ff9cd77c92 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Sat, 5 Jul 2025 12:24:36 +0300 Subject: [PATCH 194/259] Update OsuPerformanceCalculator.cs --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index a667d12a44..41b0947fbb 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate; - overallDifficulty = (80 - greatHitWindow) / 6; + overallDifficulty = (79.5 - greatHitWindow) / 6; approachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5; if (osuAttributes.SliderCount > 0) From 26ede9ca592d4deb9ba1f061707914c6c2ee63d4 Mon Sep 17 00:00:00 2001 From: marvin Date: Sun, 6 Jul 2025 14:50:13 +0200 Subject: [PATCH 195/259] Add support for ruleset-select sample for custom rulesets --- .../Menus/TestSceneToolbarRulesetSelector.cs | 75 +++++++++++++++++++ .../Toolbar/ToolbarRulesetSelector.cs | 21 +++++- 2 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Tests/Visual/Menus/TestSceneToolbarRulesetSelector.cs diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbarRulesetSelector.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbarRulesetSelector.cs new file mode 100644 index 0000000000..6f1ecb9025 --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbarRulesetSelector.cs @@ -0,0 +1,75 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.IO.Stores; +using osu.Game.Beatmaps; +using osu.Game.Overlays.Toolbar; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Tests.Visual.Menus +{ + public partial class TestSceneToolbarRulesetSelector : OsuTestScene + { + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets, OsuGameBase game) + { + TestRuleset.Resources = new TestResourceStore(game.Resources); + + Dependencies.CacheAs(new TestRulesetStore(rulesets)); + + Child = new Container + { + RelativeSizeAxes = Axes.X, + Height = Toolbar.HEIGHT, + Child = new ToolbarRulesetSelector(), + }; + } + + private class TestRulesetStore : RulesetStore + { + public TestRulesetStore(RulesetStore store) + { + AvailableRulesets = store.AvailableRulesets.Append(new TestRuleset().RulesetInfo); + } + + public override IEnumerable AvailableRulesets { get; } + } + + private class TestRuleset : Ruleset + { + public static IResourceStore Resources { get; set; } = null!; + + public override IEnumerable GetModsFor(ModType type) => Enumerable.Empty(); + + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList? mods = null) => null!; + + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null!; + + public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null!; + + public override IResourceStore CreateResourceStore() => Resources; + + public override string Description => "Test Ruleset"; + public override string ShortName => "test"; + } + + private class TestResourceStore : ResourceStore + { + public TestResourceStore(IResourceStore store) + : base(store) + { + } + + protected override IEnumerable GetFilenames(string name) => base.GetFilenames(name) + .Select(s => s.Replace("UI/ruleset-select-test", "Gameplay/failsound")); + } + } +} diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs index a979575a0b..0e2fa6688d 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Framework.IO.Stores; using osu.Game.Rulesets; using osuTK; using osuTK.Graphics; @@ -32,6 +33,8 @@ namespace osu.Game.Overlays.Toolbar private readonly Dictionary rulesetSelectionChannel = new Dictionary(); private Sample defaultSelectSample; + private ISampleStore samples; + public ToolbarRulesetSelector() { RelativeSizeAxes = Axes.Y; @@ -39,7 +42,7 @@ namespace osu.Game.Overlays.Toolbar } [BackgroundDependencyLoader] - private void load(AudioManager audio) + private void load(AudioManager audio, OsuGameBase game) { AddRangeInternal(new[] { @@ -66,8 +69,15 @@ namespace osu.Game.Overlays.Toolbar }, }); + var store = new ResourceStore(game.Resources); + samples = audio.GetSampleStore(new NamespacedResourceStore(store, "Samples"), audio.SampleMixer); + foreach (var r in Rulesets.AvailableRulesets) - rulesetSelectionSample[r] = audio.Samples.Get($@"UI/ruleset-select-{r.ShortName}"); + { + store.AddStore(r.CreateInstance().CreateResourceStore()); + + rulesetSelectionSample[r] = samples.Get($@"UI/ruleset-select-{r.ShortName}"); + } defaultSelectSample = audio.Samples.Get(@"UI/default-select"); @@ -159,5 +169,12 @@ namespace osu.Game.Overlays.Toolbar return false; } + + protected override void Dispose(bool isDisposing) + { + samples?.Dispose(); + + base.Dispose(isDisposing); + } } } From ab6eda09a29eb93b566ff10a3195d8725ee4b6f6 Mon Sep 17 00:00:00 2001 From: jyc76 Date: Mon, 7 Jul 2025 11:51:26 +0900 Subject: [PATCH 196/259] 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 197/259] 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 @@ - + From 8298374e598ea1d044680da094b20bf5fa5be784 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 7 Jul 2025 23:00:59 +0900 Subject: [PATCH 198/259] Fix leak from inverted event unbind --- osu.Game/Online/OnlineStatusNotifier.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/OnlineStatusNotifier.cs b/osu.Game/Online/OnlineStatusNotifier.cs index dda430ce6f..10d766c729 100644 --- a/osu.Game/Online/OnlineStatusNotifier.cs +++ b/osu.Game/Online/OnlineStatusNotifier.cs @@ -151,7 +151,7 @@ namespace osu.Game.Online base.Dispose(isDisposing); if (notificationsClient.IsNotNull()) - notificationsClient.MessageReceived += notifyAboutForcedDisconnection; + notificationsClient.MessageReceived -= notifyAboutForcedDisconnection; if (spectatorClient.IsNotNull()) spectatorClient.Disconnecting -= notifyAboutForcedDisconnection; From d80e4b7960731c387ada2c7c94f2cedbfeef22ea Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Jul 2025 00:22:35 +0900 Subject: [PATCH 199/259] Fix leak from no unbind from static event --- osu.Game/OsuGame.cs | 171 +++++++++++++++++++++----------------------- 1 file changed, 83 insertions(+), 88 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 57ed6a5dbf..8a4a3319e3 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -113,6 +113,9 @@ namespace osu.Game /// public const float SCREEN_EDGE_MARGIN = 12f; + private const double general_log_debounce = 60000; + private const string tablet_log_prefix = @"[Tablet] "; + public Toolbar Toolbar { get; private set; } private ChatOverlay chatOverlay; @@ -241,12 +244,26 @@ namespace osu.Game /// public virtual bool HideUnlicensedContent => false; + private bool tabletLogNotifyOnWarning = true; + private bool tabletLogNotifyOnError = true; + private int generalLogRecentCount; + public OsuGame(string[] args = null) { this.args = args; - forwardGeneralLogsToNotifications(); - forwardTabletLogsToNotifications(); + Logger.NewEntry += forwardGeneralLogToNotifications; + Logger.NewEntry += forwardTabletLogToNotifications; + + Schedule(() => + { + ITabletHandler tablet = Host.AvailableInputHandlers.OfType().SingleOrDefault(); + tablet?.Tablet.BindValueChanged(_ => + { + tabletLogNotifyOnWarning = true; + tabletLogNotifyOnError = true; + }, true); + }); } #region IOverlayManager @@ -1010,6 +1027,9 @@ namespace osu.Game base.Dispose(isDisposing); SentryLogger.Dispose(); + + Logger.NewEntry -= forwardGeneralLogToNotifications; + Logger.NewEntry -= forwardTabletLogToNotifications; } protected override IDictionary GetFrameworkConfigDefaults() @@ -1365,115 +1385,90 @@ namespace osu.Game overlay.Depth = (float)-Clock.CurrentTime; } - private void forwardGeneralLogsToNotifications() + private void forwardGeneralLogToNotifications(LogEntry entry) { - int recentLogCount = 0; + if (entry.Level < LogLevel.Important || entry.Target > LoggingTarget.Database || entry.Target == null) return; - const double debounce = 60000; + if (entry.Exception is SentryOnlyDiagnosticsException) + return; - Logger.NewEntry += entry => + const int short_term_display_limit = 3; + + if (generalLogRecentCount < short_term_display_limit) { - if (entry.Level < LogLevel.Important || entry.Target > LoggingTarget.Database || entry.Target == null) return; - - if (entry.Exception is SentryOnlyDiagnosticsException) - return; - - const int short_term_display_limit = 3; - - if (recentLogCount < short_term_display_limit) + Schedule(() => Notifications.Post(new SimpleErrorNotification { - Schedule(() => Notifications.Post(new SimpleErrorNotification - { - Icon = entry.Level == LogLevel.Important ? FontAwesome.Solid.ExclamationCircle : FontAwesome.Solid.Bomb, - Text = entry.Message.Truncate(256) + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty), - })); - } - else if (recentLogCount == short_term_display_limit) + Icon = entry.Level == LogLevel.Important ? FontAwesome.Solid.ExclamationCircle : FontAwesome.Solid.Bomb, + Text = entry.Message.Truncate(256) + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty), + })); + } + else if (generalLogRecentCount == short_term_display_limit) + { + string logFile = Logger.GetLogger(entry.Target.Value).Filename; + + Schedule(() => Notifications.Post(new SimpleNotification { - string logFile = Logger.GetLogger(entry.Target.Value).Filename; - - Schedule(() => Notifications.Post(new SimpleNotification + Icon = FontAwesome.Solid.EllipsisH, + Text = NotificationsStrings.SubsequentMessagesLogged, + Activated = () => { - Icon = FontAwesome.Solid.EllipsisH, - Text = NotificationsStrings.SubsequentMessagesLogged, - Activated = () => - { - Logger.Storage.PresentFileExternally(logFile); - return true; - } - })); - } + Logger.Storage.PresentFileExternally(logFile); + return true; + } + })); + } - Interlocked.Increment(ref recentLogCount); - Scheduler.AddDelayed(() => Interlocked.Decrement(ref recentLogCount), debounce); - }; + Interlocked.Increment(ref generalLogRecentCount); + Scheduler.AddDelayed(() => Interlocked.Decrement(ref generalLogRecentCount), general_log_debounce); } - private void forwardTabletLogsToNotifications() + private void forwardTabletLogToNotifications(LogEntry entry) { - const string tablet_prefix = @"[Tablet] "; + if (entry.Level < LogLevel.Important || entry.Target != LoggingTarget.Input || !entry.Message.StartsWith(tablet_log_prefix, StringComparison.OrdinalIgnoreCase)) + return; - bool notifyOnWarning = true; - bool notifyOnError = true; + string message = entry.Message.Replace(tablet_log_prefix, string.Empty); - Logger.NewEntry += entry => + if (entry.Level == LogLevel.Error) { - if (entry.Level < LogLevel.Important || entry.Target != LoggingTarget.Input || !entry.Message.StartsWith(tablet_prefix, StringComparison.OrdinalIgnoreCase)) + if (!tabletLogNotifyOnError) return; - string message = entry.Message.Replace(tablet_prefix, string.Empty); + tabletLogNotifyOnError = false; - if (entry.Level == LogLevel.Error) + Schedule(() => { - if (!notifyOnError) - return; - - notifyOnError = false; - - Schedule(() => + Notifications.Post(new SimpleNotification { - Notifications.Post(new SimpleNotification - { - Text = NotificationsStrings.TabletSupportDisabledDueToError(message), - Icon = FontAwesome.Solid.PenSquare, - IconColour = Colours.RedDark, - }); - - // We only have one tablet handler currently. - // The loop here is weakly guarding against a future where more than one is added. - // If this is ever the case, this logic needs adjustment as it should probably only - // disable the relevant tablet handler rather than all. - foreach (var tabletHandler in Host.AvailableInputHandlers.OfType()) - tabletHandler.Enabled.Value = false; - }); - } - else if (notifyOnWarning) - { - Schedule(() => Notifications.Post(new SimpleNotification - { - Text = NotificationsStrings.EncounteredTabletWarning, + Text = NotificationsStrings.TabletSupportDisabledDueToError(message), Icon = FontAwesome.Solid.PenSquare, - IconColour = Colours.YellowDark, - Activated = () => - { - OpenUrlExternally("https://opentabletdriver.net/Tablets", LinkWarnMode.NeverWarn); - return true; - } - })); + IconColour = Colours.RedDark, + }); - notifyOnWarning = false; - } - }; - - Schedule(() => + // We only have one tablet handler currently. + // The loop here is weakly guarding against a future where more than one is added. + // If this is ever the case, this logic needs adjustment as it should probably only + // disable the relevant tablet handler rather than all. + foreach (var tabletHandler in Host.AvailableInputHandlers.OfType()) + tabletHandler.Enabled.Value = false; + }); + } + else if (tabletLogNotifyOnWarning) { - ITabletHandler tablet = Host.AvailableInputHandlers.OfType().SingleOrDefault(); - tablet?.Tablet.BindValueChanged(_ => + Schedule(() => Notifications.Post(new SimpleNotification { - notifyOnWarning = true; - notifyOnError = true; - }, true); - }); + Text = NotificationsStrings.EncounteredTabletWarning, + Icon = FontAwesome.Solid.PenSquare, + IconColour = Colours.YellowDark, + Activated = () => + { + OpenUrlExternally("https://opentabletdriver.net/Tablets", LinkWarnMode.NeverWarn); + return true; + } + })); + + tabletLogNotifyOnWarning = false; + } } private Task asyncLoadStream; From 2f374555dc5e937d5fd6e5120007037dff1bb3f8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Jul 2025 01:54:43 +0900 Subject: [PATCH 200/259] Fix potential leak from multiple games on the same host --- osu.Game/OsuGame.cs | 58 +++++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 8a4a3319e3..153e6acb3b 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -364,40 +364,42 @@ namespace osu.Game if (host.Window != null) { host.Window.CursorState |= CursorState.Hidden; - host.Window.DragDrop += path => - { - // on macOS/iOS, URL associations are handled via SDL_DROPFILE events. - if (path.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal)) - { - HandleLink(path); - return; - } - - lock (dragDropFiles) - { - dragDropFiles.Add(path); - - Logger.Log($@"Adding ""{Path.GetFileName(path)}"" for import"); - - // File drag drop operations can potentially trigger hundreds or thousands of these calls on some platforms. - // In order to avoid spawning multiple import tasks for a single drop operation, debounce a touch. - dragDropImportSchedule?.Cancel(); - dragDropImportSchedule = Scheduler.AddDelayed(handlePendingDragDropImports, 100); - } - }; + host.Window.DragDrop += onWindowDragDrop; } } - private void handlePendingDragDropImports() + private void onWindowDragDrop(string path) { + // on macOS/iOS, URL associations are handled via SDL_DROPFILE events. + if (path.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal)) + { + HandleLink(path); + return; + } + lock (dragDropFiles) { - Logger.Log($"Handling batch import of {dragDropFiles.Count} files"); + dragDropFiles.Add(path); - string[] paths = dragDropFiles.ToArray(); - dragDropFiles.Clear(); + Logger.Log($@"Adding ""{Path.GetFileName(path)}"" for import"); - Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning); + // File drag drop operations can potentially trigger hundreds or thousands of these calls on some platforms. + // In order to avoid spawning multiple import tasks for a single drop operation, debounce a touch. + dragDropImportSchedule?.Cancel(); + dragDropImportSchedule = Scheduler.AddDelayed(handlePendingDragDropImports, 100); + } + + void handlePendingDragDropImports() + { + lock (dragDropFiles) + { + Logger.Log($"Handling batch import of {dragDropFiles.Count} files"); + + string[] paths = dragDropFiles.ToArray(); + dragDropFiles.Clear(); + + Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning); + } } } @@ -1026,8 +1028,12 @@ namespace osu.Game detachedBeatmapStore?.Dispose(); base.Dispose(isDisposing); + SentryLogger.Dispose(); + if (Host?.Window != null) + Host.Window.DragDrop -= onWindowDragDrop; + Logger.NewEntry -= forwardGeneralLogToNotifications; Logger.NewEntry -= forwardTabletLogToNotifications; } From 02cb93d854a4fdd911a07d5316e8d15c859666dd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Jul 2025 20:39:08 +0900 Subject: [PATCH 201/259] Fix leak from polling chat client being initialised too early This one is quite dumb. `OsuGame` uses [`loadComponentSingleFile`](https://github.com/ppy/osu/blob/15878f7f9fc7088494d3b66e98a7bc1004a1a06d/osu.Game/OsuGame.cs#L1228) to load the `ChannelManager`. Importantly, this process does _not_ add the component to any place in the hierarchy where it would normally be disposed - this includes `InternalChildren`, but _also_ a lesser known list of [currently-loading components](https://github.com/ppy/osu-framework/blob/cfb0d7b4b673583f0cf56273e94352769aa5bc9a/osu.Framework/Graphics/Containers/CompositeDrawable.cs#L316-L323) (those which have been sent through a `LoadComponentAsync` call). The end result of this is that, `ChannelManager` creates the `IChatClient` in its constructor, expecting to be able to dispose it, but `Dispose` is never called! And the failure case here is that `PollingChatClient` creates a background task to continuously poll the API, unfortunately keeping a reference to the rest of the world in the process. --- osu.Game/Online/Chat/ChannelManager.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index e9ca0a8ed2..fde6c4db06 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -9,6 +9,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Framework.Threading; @@ -64,7 +65,6 @@ namespace osu.Game.Online.Chat public IBindableList AvailableChannels => availableChannels; private readonly IAPIProvider api; - private readonly IChatClient chatClient; [Resolved] private UserLookupCache users { get; set; } @@ -72,6 +72,7 @@ namespace osu.Game.Online.Chat private readonly IBindable apiState = new Bindable(); private ScheduledDelegate scheduledAck; + private IChatClient chatClient = null!; private long? lastSilenceMessageId; private uint? lastSilenceId; @@ -79,14 +80,13 @@ namespace osu.Game.Online.Chat { this.api = api; - chatClient = api.GetChatClient(); - CurrentChannel.ValueChanged += currentChannelChanged; } [BackgroundDependencyLoader] private void load() { + chatClient = api.GetChatClient(); chatClient.ChannelJoined += ch => Schedule(() => joinChannel(ch)); chatClient.ChannelParted += ch => Schedule(() => leaveChannel(getChannel(ch), false)); chatClient.NewMessages += msgs => Schedule(() => addMessages(msgs)); @@ -282,8 +282,7 @@ namespace osu.Game.Online.Chat // Check if the user has joined the requested channel already. // This uses the channel name for comparison as the PM user's username is unavailable after a restart. - var privateChannel = JoinedChannels.FirstOrDefault( - c => c.Type == ChannelType.PM && c.Users.Count == 1 && c.Name.Equals(content, StringComparison.OrdinalIgnoreCase)); + var privateChannel = JoinedChannels.FirstOrDefault(c => c.Type == ChannelType.PM && c.Users.Count == 1 && c.Name.Equals(content, StringComparison.OrdinalIgnoreCase)); if (privateChannel != null) { @@ -645,7 +644,9 @@ namespace osu.Game.Online.Chat protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - chatClient?.Dispose(); + + if (chatClient.IsNotNull()) + chatClient.Dispose(); } } From 74516a1eaa305c46cffa9cd067250d903b18ea24 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Jul 2025 20:39:21 +0900 Subject: [PATCH 202/259] Fix leak due to missing `Game` disposal --- osu.Game/Tests/Visual/OsuGameTestScene.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs index b86273b4a3..fb229194d9 100644 --- a/osu.Game/Tests/Visual/OsuGameTestScene.cs +++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs @@ -82,10 +82,11 @@ namespace osu.Game.Tests.Visual [TearDownSteps] public virtual void TearDownSteps() { - if (DebugUtils.IsNUnitRunning && Game != null) + if (DebugUtils.IsNUnitRunning) { - AddStep("exit game", () => Game.Exit()); - AddUntilStep("wait for game exit", () => Game.Parent == null); + AddStep("exit game", () => Game?.Exit()); + AddUntilStep("wait for game exit", () => Game?.Parent == null); + AddStep("dispose game", () => Game?.Dispose()); } } From cce09ceadb32c25e5c8c2dbb6f503801c60099b6 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Jul 2025 22:00:44 +0900 Subject: [PATCH 203/259] Fix leaks from directly binding API bindable events --- osu.Game.Tournament/Screens/Setup/SetupScreen.cs | 5 ++++- osu.Game/Online/LocalUserStatisticsProvider.cs | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Screens/Setup/SetupScreen.cs b/osu.Game.Tournament/Screens/Setup/SetupScreen.cs index fed9d625ee..536e8ba767 100644 --- a/osu.Game.Tournament/Screens/Setup/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/Setup/SetupScreen.cs @@ -12,6 +12,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Tournament.IPC; @@ -42,6 +43,7 @@ namespace osu.Game.Tournament.Screens.Setup [Resolved] private TournamentSceneManager? sceneManager { get; set; } + private readonly IBindable localUser = new Bindable(); private Bindable windowSize = null!; [BackgroundDependencyLoader] @@ -70,7 +72,8 @@ namespace osu.Game.Tournament.Screens.Setup }, }; - api.LocalUser.BindValueChanged(_ => Schedule(reload)); + localUser.BindTo(api.LocalUser); + localUser.BindValueChanged(_ => Schedule(reload)); stableInfo.OnStableInfoSaved += () => Schedule(reload); reload(); } diff --git a/osu.Game/Online/LocalUserStatisticsProvider.cs b/osu.Game/Online/LocalUserStatisticsProvider.cs index 22d5788c87..061f0c7e03 100644 --- a/osu.Game/Online/LocalUserStatisticsProvider.cs +++ b/osu.Game/Online/LocalUserStatisticsProvider.cs @@ -5,10 +5,12 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Users; @@ -35,6 +37,8 @@ namespace osu.Game.Online [Resolved] private IAPIProvider api { get; set; } = null!; + private readonly IBindable localUser = new Bindable(); + private readonly Dictionary statisticsCache = new Dictionary(); /// @@ -48,7 +52,8 @@ namespace osu.Game.Online { base.LoadComplete(); - api.LocalUser.BindValueChanged(_ => + localUser.BindTo(api.LocalUser); + localUser.BindValueChanged(_ => { // queuing up requests directly on user change is unsafe, as the API status may have not been updated yet. // schedule a frame to allow the API to be in its correct state sending requests. From 53df90da2c4a07470e1bdfbe5fe772c664bd6172 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Jul 2025 12:46:14 +0900 Subject: [PATCH 204/259] Update various licence years --- Directory.Build.props | 2 +- LICENCE | 2 +- Templates/osu.Game.Templates.csproj | 2 +- osu.Desktop/osu.nuspec | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 580e61dafb..a856825d87 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -50,7 +50,7 @@ https://github.com/ppy/osu Automated release. ppy Pty Ltd - Copyright (c) 2024 ppy Pty Ltd + Copyright (c) 2025 ppy Pty Ltd osu game diff --git a/LICENCE b/LICENCE index 3bb8b62d5d..9ffcc70c13 100644 --- a/LICENCE +++ b/LICENCE @@ -1,4 +1,4 @@ -Copyright (c) 2024 ppy Pty Ltd . +Copyright (c) 2025 ppy Pty Ltd . Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Templates/osu.Game.Templates.csproj b/Templates/osu.Game.Templates.csproj index 186a6093f5..ecac2e4794 100644 --- a/Templates/osu.Game.Templates.csproj +++ b/Templates/osu.Game.Templates.csproj @@ -8,7 +8,7 @@ https://github.com/ppy/osu/blob/master/Templates https://github.com/ppy/osu Automated release. - Copyright (c) 2024 ppy Pty Ltd + Copyright (c) 2025 ppy Pty Ltd Templates to use when creating a ruleset for consumption in osu!. dotnet-new;templates;osu netstandard2.1 diff --git a/osu.Desktop/osu.nuspec b/osu.Desktop/osu.nuspec index 66b3970351..14af4d0334 100644 --- a/osu.Desktop/osu.nuspec +++ b/osu.Desktop/osu.nuspec @@ -12,7 +12,7 @@ false A free-to-win rhythm game. Rhythm is just a *click* away! testing - Copyright (c) 2024 ppy Pty Ltd + Copyright (c) 2025 ppy Pty Ltd en-AU From 80de563530932dc1125072c10bfd2fea5d44141d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Jul 2025 18:58:44 +0900 Subject: [PATCH 205/259] 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 107f54d4ac..3de0342db2 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From cb61c9e3fd7e9ca8430d5d4a1c7332fa7b19d2fb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Jul 2025 18:58:46 +0900 Subject: [PATCH 206/259] 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 de3fe31ee6..d071607a83 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 bb5e3da49e..c6a8c00b6c 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 4f560996947bd9d787f2917aa4c3d26efa4e0e15 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 9 Jul 2025 20:30:29 +0900 Subject: [PATCH 207/259] Mark `TestSpinPerMinuteOnRewind` as flaky See: https://github.com/ppy/osu/actions/runs/16167239898/job/45631919718 --- osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 8d81fe3017..367a00ad3b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -21,6 +21,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Storyboards; +using osu.Game.Tests; using osu.Game.Tests.Visual; using osuTK; @@ -152,6 +153,7 @@ namespace osu.Game.Rulesets.Osu.Tests } [Test] + [FlakyTest] public void TestSpinPerMinuteOnRewind() { double estimatedSpm = 0; From 994bd4abfeb2a9b3eb8b5a8f0551c8a706148dcb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Jul 2025 17:41:35 +0900 Subject: [PATCH 208/259] Ensure adjacent selection when currently selected beatmap(set) is deleted --- .../SongSelectV2/SongSelectTestScene.cs | 14 ++ ...neSongSelectCurrentSelectionInvalidated.cs | 198 ++++++++++++++++++ .../TestSceneSongSelectFiltering.cs | 16 +- osu.Game/Graphics/Carousel/Carousel.cs | 7 +- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 67 ++++++ 5 files changed, 293 insertions(+), 9 deletions(-) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs index c704f21fa4..8eb132dce7 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Extensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -145,6 +146,19 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddUntilStep("wait for filtering", () => !Carousel.IsFiltering); } + protected void SortBy(SortMode mode) => AddStep($"sort by {mode.GetDescription().ToLowerInvariant()}", () => Config.SetValue(OsuSetting.SongSelectSortingMode, mode)); + + protected void GroupBy(GroupMode mode) => AddStep($"group by {mode.GetDescription().ToLowerInvariant()}", () => Config.SetValue(OsuSetting.SongSelectGroupMode, mode)); + + protected void SortAndGroupBy(SortMode sort, GroupMode group) + { + AddStep($"sort by {sort.GetDescription().ToLowerInvariant()} & group by {group.GetDescription().ToLowerInvariant()}", () => + { + Config.SetValue(OsuSetting.SongSelectSortingMode, sort); + Config.SetValue(OsuSetting.SongSelectGroupMode, group); + }); + } + protected void ImportBeatmapForRuleset(int rulesetId) { int beatmapsCount = 0; diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs new file mode 100644 index 0000000000..6bb7e0f375 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs @@ -0,0 +1,198 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +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 +{ + /// + /// The fallback behaviour guaranteed by SongSelect is that a random selection will happen in worst case scenario. + /// Every case we're testing here is expected to have a *custom behaviour* – engaging and overriding this random selection fallback. + /// + /// The scenarios we care abouts are: + /// - Beatmap set deleted (select closest valid beatmap post-deletion) + /// + /// We are working with 5 sets, each with 3 difficulties (all osu! ruleset). + /// + public partial class TestSceneSongSelectCurrentSelectionInvalidated : SongSelectTestScene + { + private BeatmapInfo? selectedBeatmap => (BeatmapInfo?)Carousel.CurrentSelection; + private BeatmapSetInfo? selectedBeatmapSet => selectedBeatmap?.BeatmapSet; + + [SetUpSteps] + public override void SetUpSteps() + { + base.SetUpSteps(); + + for (int i = 0; i < 5; i++) + ImportBeatmapForRuleset(0); + + LoadSongSelect(); + } + + /// + /// Make sure that deleting all sets doesn't hit some weird edge case / crash. + /// + [TestCase(SortMode.Title)] + [TestCase(SortMode.Artist)] + [TestCase(SortMode.Difficulty)] + public void TestDeleteAllSets(SortMode sortMode) + { + int filterCount = sortMode != SortMode.Title ? 2 : 1; + + SortBy(sortMode); + waitForFiltering(filterCount); + + BeatmapSetInfo deletedSet = null!; + + for (int i = 0; i < 4; i++) + { + AddStep("delete selected", () => Beatmaps.Delete(deletedSet = selectedBeatmapSet!)); + waitForFiltering(filterCount + 1 + i); + selectionChangedFrom(() => deletedSet); + } + + // The carousel still holds an invalid selection after the final deletion. Probably fine? + AddStep("delete selected", () => Beatmaps.Delete(deletedSet = selectedBeatmapSet!)); + AddUntilStep("wait for no global selection", () => Beatmap.IsDefault, () => Is.True); + } + + [Test] + public void DifficultiesGrouped_DeleteSet_SelectsAdjacent() + { + SortAndGroupBy(SortMode.Difficulty, GroupMode.Difficulty); + waitForFiltering(2); + + makePanelSelected(2); + makePanelSelected(3); + + // Deleting second-last, should select last + BeatmapSetInfo deletedSet = null!; + AddStep("delete selected", () => Beatmaps.Delete(deletedSet = selectedBeatmapSet!)); + waitForFiltering(3); + + selectionChangedFrom(() => deletedSet); + assertPanelSelected(3); + + // Deleting last, should select previous + AddStep("delete selected", () => Beatmaps.Delete(deletedSet = selectedBeatmapSet!)); + waitForFiltering(4); + + selectionChangedFrom(() => deletedSet); + assertPanelSelected(2); + } + + [TestCase(SortMode.Title)] + [TestCase(SortMode.Artist)] + public void SetsGrouped_DeleteSet_SelectsAdjacent(SortMode sortMode) + { + int filterCount = sortMode != SortMode.Title ? 2 : 1; + + SortBy(sortMode); + waitForFiltering(filterCount); + + makePanelSelected(3); + + // Deleting second-last, should select last + BeatmapSetInfo deletedSet = null!; + AddStep("delete selected", () => Beatmaps.Delete(deletedSet = selectedBeatmapSet!)); + waitForFiltering(filterCount + 1); + + selectionChangedFrom(() => deletedSet); + assertPanelSelected(3); + assertPanelSelected(0); + + // Deleting last, should select previous + AddStep("delete selected", () => Beatmaps.Delete(deletedSet = selectedBeatmapSet!)); + waitForFiltering(filterCount + 2); + + selectionChangedFrom(() => deletedSet); + assertPanelSelected(2); + assertPanelSelected(0); + } + + // Same scenario as the test case above, but where selected difficulty before deletion is not first index in the expanded set. + // Basically ensures that the reselection is running `RequestRecommendedSelection` and not just relying on indices. + [TestCase(SortMode.Title)] + [TestCase(SortMode.Artist)] + public void SetsGrouped_DeleteSet_SelectsNextSetRecommendedDifficulty(SortMode sortMode) + { + int filterCount = sortMode != SortMode.Title ? 2 : 1; + + SortBy(sortMode); + waitForFiltering(filterCount); + + makePanelSelected(2); + makePanelSelected(2); + + AddUntilStep("wait for beatmap to be selected", () => selectedBeatmapSet != null); + + BeatmapSetInfo deletedSet = null!; + AddStep("delete selected", () => Beatmaps.Delete(deletedSet = selectedBeatmapSet!)); + waitForFiltering(++filterCount); + + selectionChangedFrom(() => deletedSet); + assertPanelSelected(2); + assertPanelSelected(0); + } + + private void waitForFiltering(int filterCount = 1) + { + AddUntilStep("wait for filter count", () => Carousel.FilterCount, () => Is.EqualTo(filterCount)); + AddUntilStep("filtering finished", () => Carousel.IsFiltering, () => Is.False); + } + + private void makePanelSelected(int index) + where T : Panel + { + AddStep($"click panel at index {index} if not selected", () => + { + var panel = allPanels().ElementAt(index).ChildrenOfType().Single(); + + // May have already been selected randomly. Don't click a second time or gameplay will start. + if (!panel.Selected.Value) + panel.TriggerClick(); + }); + + assertPanelSelected(index); + } + + private void selectionChangedFrom(Func deletedSet) => + AddUntilStep("selection changed", () => selectedBeatmapSet, () => Is.Not.EqualTo(deletedSet())); + + private void assertPanelSelected(int index) + where T : Panel + => AddUntilStep($"selected panel at index {index}", getActivePanelIndex, () => Is.EqualTo(index)); + + private int getActivePanelIndex() + where T : Panel + => allPanels().ToList().FindIndex(p => + { + switch (p) + { + case PanelBeatmapStandalone pb: + return pb.Selected.Value; + + case PanelBeatmap pb: + return pb.Selected.Value; + + case Panel pbs: + return pbs.Expanded.Value; + + default: + throw new InvalidOperationException(); + } + }); + + private IEnumerable allPanels() + where T : Panel + => Carousel.ChildrenOfType().Where(p => p.Item != null).OrderBy(p => p.Y); + } +} diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs index 19fccdf94d..838a294e53 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs @@ -117,14 +117,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2 // TODO: old test has this step, but there doesn't seem to be any purpose for it. // AddUntilStep("random map selected", () => Beatmap.Value != 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)); + SortBy(SortMode.Artist); + SortBy(SortMode.Title); + SortBy(SortMode.Author); + SortBy(SortMode.DateAdded); + SortBy(SortMode.BPM); + SortBy(SortMode.Length); + SortBy(SortMode.Difficulty); + SortBy(SortMode.Source); } [Test] diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index b0e2ad428e..eaf075cd83 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. /// - public IReadOnlyCollection? GetCarouselItems() => carouselItems; + public IList? GetCarouselItems() => carouselItems; private List? carouselItems; @@ -691,6 +691,11 @@ namespace osu.Game.Graphics.Carousel /// protected CarouselItem? CurrentSelectionItem => currentSelection.CarouselItem; + /// + /// The index in of the current selection, if available. + /// + protected int? CurrentSelectionIndex => currentSelection.Index; + /// /// Becomes invalid when the current selection has changed and needs to be updated visually. /// diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 1b8d8b506d..eb360a1f60 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -143,10 +143,77 @@ namespace osu.Game.Screens.SelectV2 break; case NotifyCollectionChangedAction.Remove: + bool selectedSetDeleted = false; + foreach (var set in oldItems!) { foreach (var beatmap in set.Beatmaps) + { Items.RemoveAll(i => i is BeatmapInfo bi && beatmap.Equals(bi)); + selectedSetDeleted |= CheckModelEquality(CurrentSelection, beatmap); + } + } + + // After removing all items in this batch, we want to make an immediate reselection + // based on adjacency to the previous selection if it was deleted. + // + // This needs to be done immediately to avoid song select making a random selection. + // This needs to be done in this class because we need to know final display order. + // This needs to be done with attention to detail of which beatmaps have not been deleted. + if (selectedSetDeleted && CurrentSelectionIndex != null) + { + var items = GetCarouselItems()!; + if (items.Count == 0) + break; + + bool success = false; + + // Try selecting forwards first + for (int i = CurrentSelectionIndex.Value + 1; i < items.Count; i++) + { + if (attemptSelection(items[i])) + { + success = true; + break; + } + } + + if (success) + break; + + // Then try backwards (we might be at the end of available items). + for (int i = Math.Min(items.Count - 1, CurrentSelectionIndex.Value); i >= 0; i--) + { + if (attemptSelection(items[i])) + break; + } + + bool attemptSelection(CarouselItem item) + { + if (CheckValidForSetSelection(item)) + { + if (item.Model is BeatmapInfo beatmapInfo) + { + // check the new selection wasn't deleted above + if (!Items.Contains(beatmapInfo)) + return false; + + RequestSelection(beatmapInfo); + return true; + } + + if (item.Model is BeatmapSetInfo beatmapSetInfo) + { + if (oldItems.Contains(beatmapSetInfo)) + return false; + + RequestRecommendedSelection(beatmapSetInfo.Beatmaps); + return true; + } + } + + return false; + } } break; From 44bbb7f029b075ba66600734906bc6d2831f9a65 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Jul 2025 17:41:51 +0900 Subject: [PATCH 209/259] Ensure adjacent selection when currently selected beatmap is hidden --- ...neSongSelectCurrentSelectionInvalidated.cs | 23 +++++++++ osu.Game/Screens/SelectV2/SongSelect.cs | 48 +++++++++++++++++-- 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs index 6bb7e0f375..ad36be4a52 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs @@ -17,6 +17,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 /// Every case we're testing here is expected to have a *custom behaviour* – engaging and overriding this random selection fallback. /// /// The scenarios we care abouts are: + /// - Ruleset change (select another difficulty from same set for the new ruleset, if possible). + /// - Beatmap difficulty hidden (select closest valid difficulty from same set) /// - Beatmap set deleted (select closest valid beatmap post-deletion) /// /// We are working with 5 sets, each with 3 difficulties (all osu! ruleset). @@ -143,6 +145,27 @@ namespace osu.Game.Tests.Visual.SongSelectV2 assertPanelSelected(0); } + [Test] + public void TestHideBeatmap() + { + makePanelSelected(2); + makePanelSelected(1); + + BeatmapInfo hiddenBeatmap = null!; + + AddStep("hide selected", () => Beatmaps.Hide(hiddenBeatmap = selectedBeatmap!)); + waitForFiltering(2); + + AddAssert("selected beatmap below", () => selectedBeatmap, () => Is.Not.EqualTo(hiddenBeatmap)); + assertPanelSelected(1); + + AddStep("hide selected", () => Beatmaps.Hide(hiddenBeatmap = selectedBeatmap!)); + waitForFiltering(3); + + AddAssert("selected difficulty above", () => selectedBeatmap, () => Is.Not.EqualTo(hiddenBeatmap)); + assertPanelSelected(0); + } + private void waitForFiltering(int filterCount = 1) { AddUntilStep("wait for filter count", () => Carousel.FilterCount, () => Is.EqualTo(filterCount)); diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 8030290aab..712202f4db 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -502,20 +502,58 @@ namespace osu.Game.Screens.SelectV2 var currentBeatmap = beatmaps.GetWorkingBeatmap(Beatmap.Value.BeatmapInfo, true); bool validSelection = checkBeatmapValidForSelection(currentBeatmap.BeatmapInfo, filterControl.CreateCriteria()); - if (Beatmap.IsDefault || !validSelection) + if (validSelection) + { + carousel.CurrentSelection = currentBeatmap.BeatmapInfo; + return true; + } + + // If there was no beatmap selected, pick a random one. + if (Beatmap.IsDefault) { validSelection = carousel.NextRandom(); finaliseBeatmapSelection(); + return validSelection; } - if (validSelection) - carousel.CurrentSelection = Beatmap.Value.BeatmapInfo; - else - Beatmap.SetDefault(); + // If a previous non-default selection became non-valid, it was likely hidden or deleted. + if (!validSelection) + { + // In the case a difficulty was hidden or removed, prefer selecting another difficulty from the same set. + var activeSet = currentBeatmap.BeatmapSetInfo; + BeatmapInfo? nextValidBeatmap = findNextValidBeatmap(activeSet.Beatmaps, currentBeatmap.BeatmapInfo); + + if (nextValidBeatmap != null) + { + carousel.CurrentSelection = nextValidBeatmap; + return true; + } + } + + // If all else fails, use the default beatmap. + Beatmap.SetDefault(); + if (selectionDebounce?.State == ScheduledDelegate.RunState.Waiting) + selectionDebounce?.RunTask(); return validSelection; } + private BeatmapInfo? findNextValidBeatmap(IEnumerable beatmaps, BeatmapInfo current) + { + beatmaps = beatmaps.OrderBy(b => b.StarRating).ToList(); + var criteria = filterControl.CreateCriteria(); + + // Find the first valid beatmap after `current`. + BeatmapInfo? nextValidBeatmap = beatmaps.Reverse() + .TakeWhile(b => !b.Equals(current)) + .LastOrDefault(b => checkBeatmapValidForSelection(b, criteria)); + + // If `current` is the last beatmap, we need to get the new last beatmap. + nextValidBeatmap ??= beatmaps.LastOrDefault(b => !b.Equals(current) && checkBeatmapValidForSelection(b, criteria)); + + return nextValidBeatmap; + } + private bool checkBeatmapValidForSelection(BeatmapInfo beatmap, FilterCriteria? criteria) { if (criteria == null) From 789ddcf3959624f0fbfe975350054dfd7f398755 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Jul 2025 17:28:33 +0900 Subject: [PATCH 210/259] Disallow hiding beatmap difficulties if only one difficulty remains --- .../TestSceneSongSelectFiltering.cs | 18 ++++++++++++++++++ osu.Game/Beatmaps/BeatmapManager.cs | 19 ++++++++++++++++--- .../Carousel/DrawableCarouselBeatmap.cs | 2 +- osu.Game/Screens/SelectV2/SoloSongSelect.cs | 4 +++- 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs index 19fccdf94d..eed233945d 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs @@ -309,6 +309,24 @@ namespace osu.Game.Tests.Visual.SongSelectV2 checkMatchedBeatmaps(3); } + [Test] + public void TestCantHideAllBeatmaps() + { + LoadSongSelect(); + ImportBeatmapForRuleset(0); + + checkMatchedBeatmaps(3); + + AddStep("hide selected", () => Beatmaps.Hide(Beatmap.Value.BeatmapInfo)); + checkMatchedBeatmaps(2); + + AddStep("hide selected", () => Beatmaps.Hide(Beatmap.Value.BeatmapInfo)); + checkMatchedBeatmaps(1); + + AddAssert("hide fails", () => Beatmaps.Hide(Beatmap.Value.BeatmapInfo), () => Is.False); + checkMatchedBeatmaps(1); + } + private NoResultsPlaceholder? getPlaceholder() => SongSelect.ChildrenOfType().FirstOrDefault(); private void checkMatchedBeatmaps(int expected) => AddUntilStep($"{expected} matching shown", () => Carousel.MatchedBeatmapsCount, () => Is.EqualTo(expected)); diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 2c17908487..a3e7c1365e 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -218,24 +218,37 @@ namespace osu.Game.Beatmaps } /// - /// Delete a beatmap difficulty. + /// Hide a beatmap difficulty. + /// Will fail if all difficulties are about to be hidden. /// /// The beatmap difficulty to hide. - public void Hide(BeatmapInfo beatmapInfo) + public bool Hide(BeatmapInfo beatmapInfo) { - Realm.Run(r => + return Realm.Run(r => { using (var transaction = r.BeginWrite()) { if (!beatmapInfo.IsManaged) beatmapInfo = r.Find(beatmapInfo.ID)!; + if (!CanHide(beatmapInfo)) + return false; + beatmapInfo.Hidden = true; transaction.Commit(); + return true; } }); } + public bool CanHide(BeatmapInfo beatmapInfo) => Realm.Run(r => + { + if (!beatmapInfo.IsManaged) + beatmapInfo = r.Find(beatmapInfo.ID)!; + + return beatmapInfo.BeatmapSet!.Beatmaps.Count(b => !b.Hidden) > 1; + }); + /// /// Restore a beatmap difficulty. /// diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 74f0c714a3..a8f5b6dd24 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -112,7 +112,7 @@ namespace osu.Game.Screens.Select.Carousel } if (manager != null) - hideRequested = manager.Hide; + hideRequested = b => manager.Hide(b); Header.Children = new Drawable[] { diff --git a/osu.Game/Screens/SelectV2/SoloSongSelect.cs b/osu.Game/Screens/SelectV2/SoloSongSelect.cs index 4c4df7f389..2b0ff66f91 100644 --- a/osu.Game/Screens/SelectV2/SoloSongSelect.cs +++ b/osu.Game/Screens/SelectV2/SoloSongSelect.cs @@ -84,7 +84,9 @@ namespace osu.Game.Screens.SelectV2 { Icon = FontAwesome.Solid.Eraser }; - yield return new OsuMenuItem(WebCommonStrings.ButtonsHide.ToSentence(), MenuItemType.Destructive, () => beatmaps.Hide(beatmap)); + + if (beatmaps.CanHide(beatmap)) + yield return new OsuMenuItem(WebCommonStrings.ButtonsHide.ToSentence(), MenuItemType.Destructive, () => beatmaps.Hide(beatmap)); } protected override void OnStart() From 5bc1dd1415791bcf9cec649fc11ee580abda73c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Jul 2025 18:11:20 +0900 Subject: [PATCH 211/259] Add test coverage of ruleset changes, ensuring same beatmap is preferred if possible --- .../SongSelectV2/SongSelectTestScene.cs | 6 ++-- ...neSongSelectCurrentSelectionInvalidated.cs | 33 +++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs index 8eb132dce7..ba7759d8a5 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs @@ -159,14 +159,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }); } - protected void ImportBeatmapForRuleset(int rulesetId) + protected void ImportBeatmapForRuleset(params int[] rulesetIds) { int beatmapsCount = 0; - AddStep($"import test map for ruleset {rulesetId}", () => + AddStep($"import test map for ruleset {rulesetIds}", () => { beatmapsCount = SongSelect.IsNull() ? 0 : Carousel.Filters.OfType().Single().SetItems.Count; - Beatmaps.Import(TestResources.CreateTestBeatmapSetInfo(3, Rulesets.AvailableRulesets.Where(r => r.OnlineID == rulesetId).ToArray())); + Beatmaps.Import(TestResources.CreateTestBeatmapSetInfo(3, Rulesets.AvailableRulesets.Where(r => rulesetIds.Contains(r.OnlineID)).ToArray())); }); // This is specifically for cases where the add is happening post song select load. diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs index ad36be4a52..eb46241c83 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs @@ -7,6 +7,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; @@ -39,6 +40,38 @@ namespace osu.Game.Tests.Visual.SongSelectV2 LoadSongSelect(); } + [Test] + public void TestRulesetChange() + { + AddStep("disable converts", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, false)); + + ImportBeatmapForRuleset(0, 1); + ImportBeatmapForRuleset(0, 1); + ImportBeatmapForRuleset(0, 2); + waitForFiltering(5); + + ChangeRuleset(1); + waitForFiltering(6); + + BeatmapInfo? initiallySelected = null; + AddAssert("selected is taiko", () => (initiallySelected = selectedBeatmap)?.Ruleset.OnlineID, () => Is.EqualTo(1)); + + ChangeRuleset(0); + waitForFiltering(7); + AddAssert("selected is osu", () => selectedBeatmap?.Ruleset.OnlineID, () => Is.EqualTo(0)); + AddAssert("selected is same set as original", () => selectedBeatmap?.BeatmapSet, () => Is.EqualTo(initiallySelected!.BeatmapSet)); + + ChangeRuleset(1); + waitForFiltering(8); + AddAssert("selected is taiko", () => selectedBeatmap?.Ruleset.OnlineID, () => Is.EqualTo(1)); + AddAssert("selected is same set as original", () => selectedBeatmap?.BeatmapSet, () => Is.EqualTo(initiallySelected!.BeatmapSet)); + + ChangeRuleset(2); + waitForFiltering(9); + AddAssert("selected is catch", () => selectedBeatmap?.Ruleset.OnlineID, () => Is.EqualTo(2)); + AddAssert("selected is different set", () => selectedBeatmap?.BeatmapSet, () => Is.Not.EqualTo(initiallySelected!.BeatmapSet)); + } + /// /// Make sure that deleting all sets doesn't hit some weird edge case / crash. /// From 9240c61deede50e231d07f1bf27b10608abc5571 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Jul 2025 18:32:43 +0900 Subject: [PATCH 212/259] Use new finalise selection method instead of inlined code doing the same thing --- 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 712202f4db..495e5cb537 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -532,8 +532,7 @@ namespace osu.Game.Screens.SelectV2 // If all else fails, use the default beatmap. Beatmap.SetDefault(); - if (selectionDebounce?.State == ScheduledDelegate.RunState.Waiting) - selectionDebounce?.RunTask(); + finaliseBeatmapSelection(); return validSelection; } From ddf4860a1a9a89fd9cabf59981cd7710ec452763 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Jul 2025 20:41:07 +0900 Subject: [PATCH 213/259] 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 d071607a83..b98ed1a455 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 c6a8c00b6c..9a54c51a3d 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From acdd2465180b710dc7a334cc0a36a8705822b271 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Jul 2025 21:08:57 +0900 Subject: [PATCH 214/259] Simplify post-hide selection --- ...neSongSelectCurrentSelectionInvalidated.cs | 5 ++- osu.Game/Screens/SelectV2/SongSelect.cs | 31 +++++++------------ 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs index eb46241c83..857691a399 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs @@ -189,13 +189,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("hide selected", () => Beatmaps.Hide(hiddenBeatmap = selectedBeatmap!)); waitForFiltering(2); - AddAssert("selected beatmap below", () => selectedBeatmap, () => Is.Not.EqualTo(hiddenBeatmap)); - assertPanelSelected(1); + AddAssert("selected beatmap below", () => selectedBeatmap!.BeatmapSet, () => Is.EqualTo(hiddenBeatmap.BeatmapSet)); AddStep("hide selected", () => Beatmaps.Hide(hiddenBeatmap = selectedBeatmap!)); waitForFiltering(3); - AddAssert("selected difficulty above", () => selectedBeatmap, () => Is.Not.EqualTo(hiddenBeatmap)); + AddAssert("selected beatmap below", () => selectedBeatmap!.BeatmapSet, () => Is.EqualTo(hiddenBeatmap.BeatmapSet)); assertPanelSelected(0); } diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 495e5cb537..1e16fa335a 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -244,7 +244,7 @@ namespace osu.Game.Screens.SelectV2 RelativeSizeAxes = Axes.Both, RequestPresentBeatmap = b => SelectAndRun(b, OnStart), RequestSelection = queueBeatmapSelection, - RequestRecommendedSelection = b => queueBeatmapSelection(difficultyRecommender?.GetRecommendedBeatmap(b) ?? b.First()), + RequestRecommendedSelection = requestRecommendedSelection, NewItemsPresented = newItemsPresented, }, noResultsPlaceholder = new NoResultsPlaceholder @@ -288,6 +288,11 @@ namespace osu.Game.Screens.SelectV2 }); } + private void requestRecommendedSelection(IEnumerable b) + { + queueBeatmapSelection(difficultyRecommender?.GetRecommendedBeatmap(b) ?? b.First()); + } + /// /// Called when a selection is made to progress away from the song select screen. /// @@ -521,11 +526,13 @@ namespace osu.Game.Screens.SelectV2 { // In the case a difficulty was hidden or removed, prefer selecting another difficulty from the same set. var activeSet = currentBeatmap.BeatmapSetInfo; - BeatmapInfo? nextValidBeatmap = findNextValidBeatmap(activeSet.Beatmaps, currentBeatmap.BeatmapInfo); + var criteria = filterControl.CreateCriteria(); - if (nextValidBeatmap != null) + var validBeatmaps = activeSet.Beatmaps.Where(b => checkBeatmapValidForSelection(b, criteria)).ToArray(); + + if (validBeatmaps.Any()) { - carousel.CurrentSelection = nextValidBeatmap; + requestRecommendedSelection(validBeatmaps); return true; } } @@ -537,22 +544,6 @@ namespace osu.Game.Screens.SelectV2 return validSelection; } - private BeatmapInfo? findNextValidBeatmap(IEnumerable beatmaps, BeatmapInfo current) - { - beatmaps = beatmaps.OrderBy(b => b.StarRating).ToList(); - var criteria = filterControl.CreateCriteria(); - - // Find the first valid beatmap after `current`. - BeatmapInfo? nextValidBeatmap = beatmaps.Reverse() - .TakeWhile(b => !b.Equals(current)) - .LastOrDefault(b => checkBeatmapValidForSelection(b, criteria)); - - // If `current` is the last beatmap, we need to get the new last beatmap. - nextValidBeatmap ??= beatmaps.LastOrDefault(b => !b.Equals(current) && checkBeatmapValidForSelection(b, criteria)); - - return nextValidBeatmap; - } - private bool checkBeatmapValidForSelection(BeatmapInfo beatmap, FilterCriteria? criteria) { if (criteria == null) From b4502d3d90e3263f35b5cc8c9bfc8d91ec5b6d73 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Jul 2025 01:43:02 +0900 Subject: [PATCH 215/259] empty commit for tachyon forced release From ec0baaaba9effd6d23278289c393dce89cc2b22f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Jul 2025 20:09:45 +0900 Subject: [PATCH 216/259] Adjust song select sizing in response to user feedback Special casing for very wide, old-ish (initial ssv2 preview) sizing for others. --- 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 1e16fa335a..435f4df32e 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -163,7 +163,7 @@ namespace osu.Game.Screens.SelectV2 Width = 0.6f, Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.3f), Color4.Black.Opacity(0f)), }, - new GridContainer // used for max width implementation + mainGridContainer = new GridContainer // used for max width implementation { RelativeSizeAxes = Axes.Both, ColumnDimensions = new[] @@ -359,6 +359,15 @@ namespace osu.Game.Screens.SelectV2 base.Update(); detailsArea.Height = wedgesContainer.DrawHeight - titleWedge.LayoutSize.Y - 4; + + float widescreenBonusWidth = Math.Max(0, DrawWidth / DrawHeight - 2); + + mainGridContainer.ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Relative, 0.5f, maxSize: 700 + widescreenBonusWidth * 100), + new Dimension(), + new Dimension(GridSizeMode.Relative, 0.5f, minSize: 500, maxSize: 600 + widescreenBonusWidth * 300), + }; } #region Audio @@ -805,6 +814,8 @@ namespace osu.Game.Screens.SelectV2 private ScheduledDelegate? revealingBackground; + private GridContainer mainGridContainer = null!; + protected override bool OnMouseDown(MouseDownEvent e) { var containingInputManager = GetContainingInputManager(); From db93c2ad6f09d2f762aca137981dc99df75d23df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 11 Jul 2025 11:03:56 +0200 Subject: [PATCH 217/259] Write new name to `skin.ini` when renaming skin via settings Reported at https://osu.ppy.sh/comments/3681620, with appropriate levels of rage bait (DID ANYONE TEST THIS?!?!?!?!?!?!?!?!?!111!!) Reasoning for this is that without this, users' skin names can be dropped after an external edit because they're never persisted anywhere outside of realm. The only other choice I see is to stop re-populating skin metadata from the `.ini` upon completing an external edit, which is very doable but seems worse than this. Dunno. --- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 6 +++--- osu.Game/Skinning/SkinImporter.cs | 6 +++--- osu.Game/Skinning/SkinManager.cs | 9 +++++++++ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index eef8030121..764f5fdfb6 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -310,11 +310,11 @@ namespace osu.Game.Overlays.Settings.Sections base.PopIn(); } - private void rename() => skins.CurrentSkinInfo.Value.PerformWrite(skin => + private void rename() { - skin.Name = textBox.Text; + skins.Rename(skins.CurrentSkinInfo.Value, textBox.Text); PopOut(); - }); + } } } } diff --git a/osu.Game/Skinning/SkinImporter.cs b/osu.Game/Skinning/SkinImporter.cs index 382a7b56c2..3a50fb9f9a 100644 --- a/osu.Game/Skinning/SkinImporter.cs +++ b/osu.Game/Skinning/SkinImporter.cs @@ -157,17 +157,17 @@ namespace osu.Game.Skinning // Regardless of whether this is an import or not, let's write the skin.ini if non-existing or non-matching. // This is (weirdly) done inside ComputeHash to avoid adding a new method to handle this case. After switching to realm it can be moved into another place. if (skinIniSourcedName != item.Name) - updateSkinIniMetadata(item, realm); + UpdateSkinIniMetadata(item, realm); } - private void updateSkinIniMetadata(SkinInfo item, Realm realm) + public void UpdateSkinIniMetadata(SkinInfo item, Realm realm) { string nameLine = @$"Name: {item.Name}"; string authorLine = @$"Author: {item.Creator}"; List newLines = new List { - @"// The following content was automatically added by osu! during import, based on filename / folder metadata.", + @"// The following content was automatically added by osu! in order to use metadata that more closely matches user expectations.", @"[General]", nameLine, authorLine, diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 825d2f59c5..1be6f1bc4a 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -349,6 +349,15 @@ namespace osu.Game.Skinning }); } + public void Rename(Live skin, string newName) + { + skin.PerformWrite(s => + { + s.Name = newName; + skinImporter.UpdateSkinIniMetadata(s, s.Realm!); + }); + } + public void SetSkinFromConfiguration(string guidString) { Live skinInfo = null; From 449b6d36ae959d8cfd671ed12bc9a57a6950a9f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 11 Jul 2025 11:44:22 +0200 Subject: [PATCH 218/259] Fix text flow arbitrary drawable wrapper accessing child in an unsafe manner Closes https://github.com/ppy/osu/issues/34126. I'm not really sure how that issue could have ever happened to begin with but I can see a way to make it hopefully safer. If it fails again then it's clearly goblins. --- .../Graphics/Containers/OsuTextFlowContainer.cs | 16 +++++++++------- osu.Game/Screens/Menu/SupporterDisplay.cs | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/osu.Game/Graphics/Containers/OsuTextFlowContainer.cs b/osu.Game/Graphics/Containers/OsuTextFlowContainer.cs index 8da8b7ed7d..dcb7f8efdd 100644 --- a/osu.Game/Graphics/Containers/OsuTextFlowContainer.cs +++ b/osu.Game/Graphics/Containers/OsuTextFlowContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; @@ -14,23 +12,27 @@ namespace osu.Game.Graphics.Containers { public partial class OsuTextFlowContainer : TextFlowContainer { - public OsuTextFlowContainer(Action defaultCreationParameters = null) + public OsuTextFlowContainer(Action? defaultCreationParameters = null) : base(defaultCreationParameters) { } protected override SpriteText CreateSpriteText() => new OsuSpriteText(); - public ITextPart AddArbitraryDrawable(Drawable drawable) => AddPart(new TextPartManual(new ArbitraryDrawableWrapper { Child = drawable }.Yield())); + public ITextPart AddArbitraryDrawable(Drawable drawable) => AddPart(new TextPartManual(new ArbitraryDrawableWrapper(drawable).Yield())); - public ITextPart AddIcon(IconUsage icon, Action creationParameters = null) => AddText(icon.Icon.ToString(), creationParameters); + public ITextPart AddIcon(IconUsage icon, Action? creationParameters = null) => AddText(icon.Icon.ToString(), creationParameters); private partial class ArbitraryDrawableWrapper : Container, IHasLineBaseHeight { - public float LineBaseHeight => (Child as IHasLineBaseHeight)?.LineBaseHeight ?? DrawHeight; + private readonly IHasLineBaseHeight? lineBaseHeightSource; - public ArbitraryDrawableWrapper() + public float LineBaseHeight => lineBaseHeightSource?.LineBaseHeight ?? DrawHeight; + + public ArbitraryDrawableWrapper(Drawable drawable) { + Child = drawable; + lineBaseHeightSource = drawable as IHasLineBaseHeight; AutoSizeAxes = Axes.Both; } } diff --git a/osu.Game/Screens/Menu/SupporterDisplay.cs b/osu.Game/Screens/Menu/SupporterDisplay.cs index 9602f4f61d..d33698a8a8 100644 --- a/osu.Game/Screens/Menu/SupporterDisplay.cs +++ b/osu.Game/Screens/Menu/SupporterDisplay.cs @@ -105,7 +105,7 @@ namespace osu.Game.Screens.Menu Schedule(() => { - heart?.FlashColour(Color4.White, 750, Easing.OutQuint).Loop(); + heart.FlashColour(Color4.White, 750, Easing.OutQuint).Loop(); }); }); }, true); From 20995e853ad0751b08fd50da955cc661a211993f Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Fri, 11 Jul 2025 23:46:56 -0700 Subject: [PATCH 219/259] Use difficulty background on standalone beatmap panels --- osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs index 28a6bfc83a..a6a54eeade 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -227,8 +226,7 @@ namespace osu.Game.Screens.SelectV2 var beatmap = (BeatmapInfo)Item.Model; var beatmapSet = beatmap.BeatmapSet!; - // Choice of background image matches BSS implementation (always uses the lowest `beatmap_id` from the set). - background.Beatmap = beatmaps.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID)); + background.Beatmap = beatmaps.GetWorkingBeatmap(beatmap); titleText.Text = new RomanisableString(beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title); artistText.Text = new RomanisableString(beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist); From 39808970d3048ae177a60248a1c7a3d622e8ab25 Mon Sep 17 00:00:00 2001 From: eyhn Date: Sat, 12 Jul 2025 21:51:10 +0800 Subject: [PATCH 220/259] Fix failed to load beatmaps host by deleted user --- osu.Game/Online/API/Requests/Responses/APIUser.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index 4e219cdf22..5915e503df 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -22,7 +22,10 @@ namespace osu.Game.Online.API.Requests.Responses /// public const int SYSTEM_USER_ID = 0; - [JsonProperty(@"id")] + /// + /// In osu-web, deleted users have a null ID. When deserializing, we ignore the null value and use -1 instead. + /// + [JsonProperty(@"id", NullValueHandling = NullValueHandling.Ignore)] public int Id { get; set; } = 1; [JsonProperty(@"join_date")] From 2b26506c4cbe3c70358cc6b74f7e939d99d11e01 Mon Sep 17 00:00:00 2001 From: eyhn Date: Sat, 12 Jul 2025 23:35:54 +0800 Subject: [PATCH 221/259] Fix typo --- osu.Game/Online/API/Requests/Responses/APIUser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index 5915e503df..0393206d8a 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -23,7 +23,7 @@ namespace osu.Game.Online.API.Requests.Responses public const int SYSTEM_USER_ID = 0; /// - /// In osu-web, deleted users have a null ID. When deserializing, we ignore the null value and use -1 instead. + /// In osu-web, deleted users have a null ID. When deserializing, we ignore the null value and use 1 instead. /// [JsonProperty(@"id", NullValueHandling = NullValueHandling.Ignore)] public int Id { get; set; } = 1; From 6c4d80501ea83cde45cd2a4b562b3525c5486089 Mon Sep 17 00:00:00 2001 From: VocalFan <45863583+FluffyOMC@users.noreply.github.com> Date: Sat, 12 Jul 2025 12:34:25 -0400 Subject: [PATCH 222/259] Fix duration by having it update on Beatmap creation --- .../Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs index 66218c0e9e..bdbe16732d 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs @@ -99,15 +99,18 @@ namespace osu.Game.Screens.OnlinePlay private readonly PlaylistItem item; private double itemLength; private int beatmapSetId; + [Resolved] + private RealmAccess realm { get; set; } = null!; public DifficultySelectFilterControl(PlaylistItem item) { this.item = item; } - [BackgroundDependencyLoader] - private void load(RealmAccess realm) + public override FilterCriteria CreateCriteria() { + var criteria = base.CreateCriteria(); + realm.Run(r => { int beatmapId = item.Beatmap.OnlineID; @@ -116,11 +119,6 @@ namespace osu.Game.Screens.OnlinePlay itemLength = beatmap?.Length ?? 0; beatmapSetId = beatmap?.BeatmapSet?.OnlineID ?? 0; }); - } - - public override FilterCriteria CreateCriteria() - { - var criteria = base.CreateCriteria(); // Must be from the same set as the playlist item. criteria.BeatmapSetId = beatmapSetId; From ae0292e7c161cf49a7e66aa9a3225dc4b16ee47e Mon Sep 17 00:00:00 2001 From: VocalFan <45863583+FluffyOMC@users.noreply.github.com> Date: Sat, 12 Jul 2025 12:44:42 -0400 Subject: [PATCH 223/259] There's your blank line :P --- osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs index bdbe16732d..afc0253edb 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs @@ -99,6 +99,7 @@ namespace osu.Game.Screens.OnlinePlay private readonly PlaylistItem item; private double itemLength; private int beatmapSetId; + [Resolved] private RealmAccess realm { get; set; } = null!; From 8c1e0b4d6691df4daf46664f9a98152d19a840ef Mon Sep 17 00:00:00 2001 From: Hivie Date: Sat, 12 Jul 2025 19:48:27 +0100 Subject: [PATCH 224/259] add more predefined divisors to match stable --- osu.Game/Screens/Edit/BindableBeatDivisor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/BindableBeatDivisor.cs b/osu.Game/Screens/Edit/BindableBeatDivisor.cs index bd9c9bab9a..53a1441a81 100644 --- a/osu.Game/Screens/Edit/BindableBeatDivisor.cs +++ b/osu.Game/Screens/Edit/BindableBeatDivisor.cs @@ -14,7 +14,7 @@ namespace osu.Game.Screens.Edit { public class BindableBeatDivisor : BindableInt { - public static readonly int[] PREDEFINED_DIVISORS = { 1, 2, 3, 4, 6, 8, 12, 16 }; + public static readonly int[] PREDEFINED_DIVISORS = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 16 }; public const int MINIMUM_DIVISOR = 1; public const int MAXIMUM_DIVISOR = 64; From fccfdbf393862cd49071beec40843a6ca939d360 Mon Sep 17 00:00:00 2001 From: Hivie Date: Sun, 13 Jul 2025 04:05:46 +0100 Subject: [PATCH 225/259] move audio format checks to reusable audio check utils --- .../Rulesets/Edit/Checks/CheckSongFormat.cs | 32 ++++--------- .../Edit/Checks/Components/AudioCheckUtils.cs | 46 +++++++++++++++++++ 2 files changed, 55 insertions(+), 23 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs b/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs index dd01fe110a..aa039630d4 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs @@ -2,12 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.IO; using System.Linq; using ManagedBass; -using osu.Framework.Audio.Callbacks; using osu.Game.Beatmaps; -using osu.Game.Extensions; using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Rulesets.Edit.Checks @@ -36,28 +33,17 @@ namespace osu.Game.Rulesets.Edit.Checks if (beatmapSet == null) yield break; if (audioFile == null) yield break; - using (Stream data = context.WorkingBeatmap.GetStream(audioFile.File.GetStoragePath())) + var audioFormat = AudioCheckUtils.GetAudioFormatFromFile(context, context.Beatmap.Metadata.AudioFile); + + // If the format is not supported by BASS + if (audioFormat == 0) { - if (data == null || data.Length <= 0) yield break; - - var fileCallbacks = new FileCallbacks(new DataStreamFileProcedures(data)); - int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode, fileCallbacks.Callbacks, fileCallbacks.Handle); - - // If the format is not supported by BASS - if (decodeStream == 0) - { - yield return new IssueTemplateFormatUnsupported(this).Create(audioFile.Filename); - - yield break; - } - - var audioInfo = Bass.ChannelGetInfo(decodeStream); - - if (!allowedFormats.Contains(audioInfo.ChannelType)) - yield return new IssueTemplateIncorrectFormat(this).Create(audioFile.Filename); - - Bass.StreamFree(decodeStream); + yield return new IssueTemplateFormatUnsupported(this).Create(audioFile.Filename); + yield break; } + + if (!allowedFormats.Contains(audioFormat)) + yield return new IssueTemplateIncorrectFormat(this).Create(audioFile.Filename); } public class IssueTemplateFormatUnsupported : IssueTemplate diff --git a/osu.Game/Rulesets/Edit/Checks/Components/AudioCheckUtils.cs b/osu.Game/Rulesets/Edit/Checks/Components/AudioCheckUtils.cs index 8a35b84170..7cd7738f69 100644 --- a/osu.Game/Rulesets/Edit/Checks/Components/AudioCheckUtils.cs +++ b/osu.Game/Rulesets/Edit/Checks/Components/AudioCheckUtils.cs @@ -3,6 +3,10 @@ using System.IO; using System.Linq; +using ManagedBass; +using osu.Framework.Audio.Callbacks; +using osu.Game.Beatmaps; +using osu.Game.Extensions; using osu.Game.Utils; namespace osu.Game.Rulesets.Edit.Checks.Components @@ -10,5 +14,47 @@ namespace osu.Game.Rulesets.Edit.Checks.Components public static class AudioCheckUtils { public static bool HasAudioExtension(string filename) => SupportedExtensions.AUDIO_EXTENSIONS.Contains(Path.GetExtension(filename).ToLowerInvariant()); + + /// + /// Gets the audio format (ChannelType) from a stream using BASS. + /// + /// The audio file stream. + /// The ChannelType of the audio, or 0 if detection fails. + public static ChannelType GetAudioFormat(Stream data) + { + if (data.Length <= 0) + return 0; + + var fileCallbacks = new FileCallbacks(new DataStreamFileProcedures(data)); + int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode, fileCallbacks.Callbacks, fileCallbacks.Handle); + + if (decodeStream == 0) + return 0; + + var audioInfo = Bass.ChannelGetInfo(decodeStream); + Bass.StreamFree(decodeStream); + + return audioInfo.ChannelType; + } + + /// + /// Gets the audio format for a specific file in a beatmapset. + /// + /// The beatmap verifier context. + /// The filename to check. + /// The ChannelType of the audio file, or 0 if detection fails. + public static ChannelType GetAudioFormatFromFile(BeatmapVerifierContext context, string filename) + { + var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet; + var audioFile = beatmapSet?.GetFile(filename); + + if (beatmapSet == null || audioFile == null) + return 0; + + using (Stream data = context.WorkingBeatmap.GetStream(audioFile.File.GetStoragePath())) + { + return GetAudioFormat(data); + } + } } } From cf6641e2412618a8ecbca7a9eea0d88f89915af2 Mon Sep 17 00:00:00 2001 From: Hivie Date: Sun, 13 Jul 2025 04:06:16 +0100 Subject: [PATCH 226/259] update audio quality check to account for ogg files --- .../Rulesets/Edit/Checks/CheckAudioQuality.cs | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAudioQuality.cs b/osu.Game/Rulesets/Edit/Checks/CheckAudioQuality.cs index 440d4e8e62..8c0c01d5da 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckAudioQuality.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckAudioQuality.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using ManagedBass; using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Rulesets.Edit.Checks @@ -9,8 +10,9 @@ namespace osu.Game.Rulesets.Edit.Checks public class CheckAudioQuality : ICheck { // This is a requirement as stated in the Ranking Criteria. - // See https://osu.ppy.sh/wiki/en/Ranking_Criteria#rules.4 - private const int max_bitrate = 192; + // See https://osu.ppy.sh/wiki/en/Ranking_criteria#audio + private const int max_bitrate_default = 192; + private const int max_bitrate_ogg = 208; // "A song's audio file /.../ must be of reasonable quality. Try to find the highest quality source file available" // There not existing a version with a bitrate of 128 kbps or higher is extremely rare. @@ -35,10 +37,17 @@ namespace osu.Game.Rulesets.Edit.Checks if (track?.Bitrate == null || track.Bitrate.Value == 0) yield return new IssueTemplateNoBitrate(this).Create(); - else if (track.Bitrate.Value > max_bitrate) - yield return new IssueTemplateTooHighBitrate(this).Create(track.Bitrate.Value); - else if (track.Bitrate.Value < min_bitrate) - yield return new IssueTemplateTooLowBitrate(this).Create(track.Bitrate.Value); + else + { + // Determine max bitrate based on audio format + var audioFormat = AudioCheckUtils.GetAudioFormatFromFile(context, audioFile); + int upperBitrateLimit = audioFormat.HasFlag(ChannelType.OGG) ? max_bitrate_ogg : max_bitrate_default; + + if (track.Bitrate.Value > upperBitrateLimit) + yield return new IssueTemplateTooHighBitrate(this).Create(track.Bitrate.Value, upperBitrateLimit); + else if (track.Bitrate.Value < min_bitrate) + yield return new IssueTemplateTooLowBitrate(this).Create(track.Bitrate.Value); + } } public class IssueTemplateTooHighBitrate : IssueTemplate @@ -48,7 +57,7 @@ namespace osu.Game.Rulesets.Edit.Checks { } - public Issue Create(int bitrate) => new Issue(this, bitrate, max_bitrate); + public Issue Create(int bitrate, int maxBitrate) => new Issue(this, bitrate, maxBitrate); } public class IssueTemplateTooLowBitrate : IssueTemplate From 6bd9ea76d428608dbb417923ce8eb91a4483a410 Mon Sep 17 00:00:00 2001 From: Hivie Date: Sun, 13 Jul 2025 04:08:18 +0100 Subject: [PATCH 227/259] add tests for ogg audios and improve context setup to handle mp3/ogg audios --- .../Editing/Checks/CheckAudioQualityTest.cs | 51 +++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Editing/Checks/CheckAudioQualityTest.cs b/osu.Game.Tests/Editing/Checks/CheckAudioQualityTest.cs index 61ee6a3663..5465b85710 100644 --- a/osu.Game.Tests/Editing/Checks/CheckAudioQualityTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckAudioQualityTest.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; +using ManagedBass; using Moq; using NUnit.Framework; using osu.Framework.Audio.Track; @@ -10,7 +11,9 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Checks; using osu.Game.Rulesets.Objects; +using osu.Game.Tests.Resources; using osu.Game.Tests.Visual; +using osuTK.Audio; namespace osu.Game.Tests.Editing.Checks { @@ -28,9 +31,13 @@ namespace osu.Game.Tests.Editing.Checks { BeatmapInfo = new BeatmapInfo { - Metadata = new BeatmapMetadata { AudioFile = "abc123.jpg" } + Metadata = new BeatmapMetadata() } }; + + // 0 = No output device. This still allows decoding. + if (!Bass.Init(0) && Bass.LastError != Errors.Already) + throw new AudioException("Could not initialize Bass."); } [Test] @@ -54,6 +61,14 @@ namespace osu.Game.Tests.Editing.Checks Assert.That(check.Run(context), Is.Empty); } + [Test] + public void TestAcceptableOgg() + { + var context = getContext(208, useOgg: true); + + Assert.That(check.Run(context), Is.Empty); + } + [Test] public void TestNullBitrate() { @@ -87,6 +102,17 @@ namespace osu.Game.Tests.Editing.Checks Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateTooHighBitrate); } + [Test] + public void TestTooHighBitrateOgg() + { + var context = getContext(250, useOgg: true); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateTooHighBitrate); + } + [Test] public void TestTooLowBitrate() { @@ -98,24 +124,41 @@ namespace osu.Game.Tests.Editing.Checks Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateTooLowBitrate); } - private BeatmapVerifierContext getContext(int? audioBitrate) + private BeatmapVerifierContext getContext(int? audioBitrate, bool useOgg = false) { - return new BeatmapVerifierContext(beatmap, getMockWorkingBeatmap(audioBitrate).Object); + // Update the audio file name and beatmap set files based on the format being tested + string audioFileName = useOgg ? "abc123.ogg" : "abc123.mp3"; + string fileExtension = useOgg ? "ogg" : "mp3"; + + beatmap.Metadata.AudioFile = audioFileName; + beatmap.BeatmapInfo.BeatmapSet = new BeatmapSetInfo + { + Files = { CheckTestHelpers.CreateMockFile(fileExtension) } + }; + + return new BeatmapVerifierContext(beatmap, getMockWorkingBeatmap(audioBitrate, useOgg).Object); } /// /// Returns the mock of the working beatmap with the given audio properties. /// /// The bitrate of the audio file the beatmap uses. - private Mock getMockWorkingBeatmap(int? audioBitrate) + /// Whether to use an OGG sample instead of MP3. + private Mock getMockWorkingBeatmap(int? audioBitrate, bool useOgg = false) { var mockTrack = new Mock(new FramedClock(), "virtual"); mockTrack.SetupGet(t => t.Bitrate).Returns(audioBitrate); + // Use real audio samples for format detection + string samplePath = useOgg ? "Samples/test-sample.ogg" : "Samples/test-sample-cut.mp3"; + var mockWorkingBeatmap = new Mock(); mockWorkingBeatmap.SetupGet(w => w.Beatmap).Returns(beatmap); mockWorkingBeatmap.SetupGet(w => w.Track).Returns(mockTrack.Object); + // Return a fresh stream each time GetStream is called to avoid disposed stream issues + mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(() => TestResources.OpenResource(samplePath)); + return mockWorkingBeatmap; } } From e03d8123594eb1fd57ee8ef2e5c23861beacf6da Mon Sep 17 00:00:00 2001 From: Hivie Date: Sun, 13 Jul 2025 04:17:47 +0100 Subject: [PATCH 228/259] comment --- osu.Game.Tests/Editing/Checks/CheckAudioQualityTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Editing/Checks/CheckAudioQualityTest.cs b/osu.Game.Tests/Editing/Checks/CheckAudioQualityTest.cs index 5465b85710..c2a712b580 100644 --- a/osu.Game.Tests/Editing/Checks/CheckAudioQualityTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckAudioQualityTest.cs @@ -126,7 +126,7 @@ namespace osu.Game.Tests.Editing.Checks private BeatmapVerifierContext getContext(int? audioBitrate, bool useOgg = false) { - // Update the audio file name and beatmap set files based on the format being tested + // Update the audio filename and beatmapset files based on the format being tested string audioFileName = useOgg ? "abc123.ogg" : "abc123.mp3"; string fileExtension = useOgg ? "ogg" : "mp3"; From c36960a06e2c9a38efbb675ea8b68dbaaefd711f Mon Sep 17 00:00:00 2001 From: Hivie Date: Sun, 13 Jul 2025 04:37:22 +0100 Subject: [PATCH 229/259] formatting --- osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs b/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs index aa039630d4..592d61852f 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs @@ -39,6 +39,7 @@ namespace osu.Game.Rulesets.Edit.Checks if (audioFormat == 0) { yield return new IssueTemplateFormatUnsupported(this).Create(audioFile.Filename); + yield break; } From 8e0ed85ad2d0c24dabf70b340bf6509c86c64473 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 14 Jul 2025 15:09:18 +0900 Subject: [PATCH 230/259] Minor cleanups Using `ChannelType.Unknown` instead of `0`, adds missing disposal. --- .../Edit/Checks/Components/AudioCheckUtils.cs | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/Components/AudioCheckUtils.cs b/osu.Game/Rulesets/Edit/Checks/Components/AudioCheckUtils.cs index 7cd7738f69..c72e0288c2 100644 --- a/osu.Game/Rulesets/Edit/Checks/Components/AudioCheckUtils.cs +++ b/osu.Game/Rulesets/Edit/Checks/Components/AudioCheckUtils.cs @@ -19,22 +19,23 @@ namespace osu.Game.Rulesets.Edit.Checks.Components /// Gets the audio format (ChannelType) from a stream using BASS. /// /// The audio file stream. - /// The ChannelType of the audio, or 0 if detection fails. + /// The ChannelType of the audio, or if detection fails. public static ChannelType GetAudioFormat(Stream data) { if (data.Length <= 0) - return 0; + return ChannelType.Unknown; - var fileCallbacks = new FileCallbacks(new DataStreamFileProcedures(data)); - int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode, fileCallbacks.Callbacks, fileCallbacks.Handle); + using (var fileCallbacks = new FileCallbacks(new DataStreamFileProcedures(data))) + { + int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode, fileCallbacks.Callbacks, fileCallbacks.Handle); + if (decodeStream == 0) + return ChannelType.Unknown; - if (decodeStream == 0) - return 0; + var audioInfo = Bass.ChannelGetInfo(decodeStream); + Bass.StreamFree(decodeStream); - var audioInfo = Bass.ChannelGetInfo(decodeStream); - Bass.StreamFree(decodeStream); - - return audioInfo.ChannelType; + return audioInfo.ChannelType; + } } /// @@ -42,19 +43,17 @@ namespace osu.Game.Rulesets.Edit.Checks.Components /// /// The beatmap verifier context. /// The filename to check. - /// The ChannelType of the audio file, or 0 if detection fails. + /// The ChannelType of the audio file, or if detection fails. public static ChannelType GetAudioFormatFromFile(BeatmapVerifierContext context, string filename) { var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet; var audioFile = beatmapSet?.GetFile(filename); if (beatmapSet == null || audioFile == null) - return 0; + return ChannelType.Unknown; using (Stream data = context.WorkingBeatmap.GetStream(audioFile.File.GetStoragePath())) - { return GetAudioFormat(data); - } } } } From 0f272ea0f94892ac98ce1b2cad80578398963223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Jul 2025 09:01:28 +0200 Subject: [PATCH 231/259] Fix multiplayer spectator leaderboard respecting "show leaderboard" config setting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes https://github.com/ppy/osu/issues/34128 Simplest is best? 🤷 --- .../OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs | 3 ++- osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 7ad8bdf454..1f96f0d371 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -154,7 +154,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate }); leaderboardFlow.Insert(0, Leaderboard = new DrawableGameplayLeaderboard { - CollapseDuringGameplay = { Value = false } + CollapseDuringGameplay = { Value = false }, + AlwaysShown = true, }); LoadComponentAsync(new GameplayChatDisplay(room) diff --git a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs index dd55e5f926..e02ef03dea 100644 --- a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs @@ -28,6 +28,8 @@ namespace osu.Game.Screens.Play.HUD public DrawableGameplayLeaderboardScore? TrackedScore { get; private set; } + public bool AlwaysShown { get; init; } + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.CollapseDuringGameplay), nameof(SkinnableComponentStrings.CollapseDuringGameplayDescription))] public Bindable CollapseDuringGameplay { get; } = new BindableBool(true); @@ -109,7 +111,7 @@ namespace osu.Game.Screens.Play.HUD if (Flow.Alpha < 1) scroll.ScrollToStart(false); - Flow.FadeTo(player?.Configuration.ShowLeaderboard != false && configVisibility.Value ? 1 : 0, 100, Easing.OutQuint); + Flow.FadeTo(player?.Configuration.ShowLeaderboard != false && (configVisibility.Value || AlwaysShown) ? 1 : 0, 100, Easing.OutQuint); expanded.Value = !CollapseDuringGameplay.Value || userPlayingState.Value != LocalUserPlayingState.Playing || holdingForHUD.Value; } From 64f6fce91893e67f866c38f7d46314f174fc6c6a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 14 Jul 2025 16:04:43 +0900 Subject: [PATCH 232/259] Use comment instead of xmldoc --- osu.Game/Online/API/Requests/Responses/APIUser.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index 0393206d8a..6f122c58af 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -22,9 +22,7 @@ namespace osu.Game.Online.API.Requests.Responses /// public const int SYSTEM_USER_ID = 0; - /// - /// In osu-web, deleted users have a null ID. When deserializing, we ignore the null value and use 1 instead. - /// + // In osu-web, deleted users have a null ID. When deserializing, we ignore the null value and use 1 instead. [JsonProperty(@"id", NullValueHandling = NullValueHandling.Ignore)] public int Id { get; set; } = 1; From 7f1c37b0923ca7b76324cd3c817926639124a726 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Jul 2025 10:06:29 +0200 Subject: [PATCH 233/259] Improve safety of external skin editing around back binding handling Addresses https://osu.ppy.sh/comments/3691012. Was dodgy both when the operation was starting, and when the operation was wrapping up. --- osu.Game/Overlays/SkinEditor/ExternalEditOverlay.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SkinEditor/ExternalEditOverlay.cs b/osu.Game/Overlays/SkinEditor/ExternalEditOverlay.cs index e4ac157936..7bbfcd4b8e 100644 --- a/osu.Game/Overlays/SkinEditor/ExternalEditOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/ExternalEditOverlay.cs @@ -47,6 +47,7 @@ namespace osu.Game.Overlays.SkinEditor private ExternalEditOperation? editOperation; private TaskCompletionSource? taskCompletionSource; + private bool finishingEdit; protected override bool DimMainContent => false; @@ -181,6 +182,11 @@ namespace osu.Game.Overlays.SkinEditor private async Task finish() { + if (finishingEdit) + return; + + finishingEdit = true; + Debug.Assert(taskCompletionSource != null); showSpinner("Cleaning up..."); @@ -236,6 +242,7 @@ namespace osu.Game.Overlays.SkinEditor { // Set everything to a clean state editOperation = null; + finishingEdit = false; flow.Children = Array.Empty(); }); } @@ -249,7 +256,8 @@ namespace osu.Game.Overlays.SkinEditor { case GlobalAction.Back: case GlobalAction.Select: - if (editOperation == null) return base.OnPressed(e); + if (editOperation == null) + return false; finish().FireAndForget(); return true; From fb179e8117e18a3289da5e95f855b148576c7240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Jul 2025 10:15:06 +0200 Subject: [PATCH 234/259] Improve safety of external skin editing around target screen changes Closes https://github.com/ppy/osu/issues/34133. --- osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index 5e71b6922c..27317518a0 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -249,7 +249,8 @@ namespace osu.Game.Overlays.SkinEditor Scheduler.AddOnce(updateScreenSizing); game.Toolbar.Hide(); - game.CloseAllOverlays(); + if (externalEditOverlay.State.Value != Visibility.Visible) + game.CloseAllOverlays(); } else { @@ -298,7 +299,8 @@ namespace osu.Game.Overlays.SkinEditor if (skinEditor.State.Value == Visibility.Visible) { - skinEditor.Save(false); + if (externalEditOverlay.State.Value != Visibility.Visible) + skinEditor.Save(false); skinEditor.UpdateTargetScreen(target); disableNestedInputManagers(); } From e36c59031569bf317137bbe7893a5ad97ab14700 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 14 Jul 2025 16:46:30 +0900 Subject: [PATCH 235/259] Add failing tests --- .../Multiplayer/TestSceneMultiplayer.cs | 102 +++++++++++++++++- .../TestSceneMultiplayerMatchSubScreen.cs | 8 ++ osu.Game/Online/Rooms/PlaylistItem.cs | 2 +- .../Multiplayer/MultiplayerMatchSubScreen.cs | 17 ++- 4 files changed, 126 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 03fe9b8b58..69cf174f34 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -53,6 +53,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { private BeatmapManager beatmaps = null!; private BeatmapSetInfo importedSet = null!; + private BeatmapSetInfo importedSet2 = null!; private TestMultiplayerComponents multiplayerComponents = null!; @@ -81,12 +82,15 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("import beatmap", () => { beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); + + importedSet = beatmaps.GetAllUsableBeatmapSets().First(); + importedSet2 = beatmaps.Import(CreateBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet!)!.Value.Detach(); + Realm.Write(r => { foreach (var beatmapInfo in r.All()) beatmapInfo.OnlineMD5Hash = beatmapInfo.MD5Hash; }); - importedSet = beatmaps.GetAllUsableBeatmapSets().First(); }); AddStep("load multiplayer", () => LoadScreen(multiplayerComponents = new TestMultiplayerComponents())); @@ -1095,6 +1099,102 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("global beatmap matches second playlist item", () => Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(multiplayerClient.ClientRoom!.Playlist[1].BeatmapID)); } + /// + /// Tests that the local user is not able to change their play style if they haven't downloaded the beatmap (beatmap carousel will be empty). + /// + [Test] + public void TestCanNotEditDifficultyIfNotDownloaded() + { + IBeatmap roomBeatmap = null!; + + createRoom(() => + { + roomBeatmap = CreateBeatmap(new OsuRuleset().RulesetInfo); + + return new Room + { + Name = "Test Room", + QueueMode = QueueMode.AllPlayers, + Playlist = + [ + new PlaylistItem(CreateAPIBeatmap(roomBeatmap.BeatmapInfo)) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + Freestyle = true + } + ] + }; + }); + + AddAssert("editing disallowed", () => !this.ChildrenOfType().Single().UserStyleEditingEnabled); + AddStep("import beatmap", () => beatmaps.Import(roomBeatmap.BeatmapInfo.BeatmapSet!)); + AddAssert("editing allowed", () => this.ChildrenOfType().Single().UserStyleEditingEnabled); + } + + /// + /// Test that the user selection screen is not exited when the beatmap is changed to the same set. + /// + [Test] + public void TestUserStyleSelectionDoesNotExitWhenBeatmapSetNotChanged() + { + createRoom(() => new Room + { + Name = "Test Room", + QueueMode = QueueMode.AllPlayers, + Playlist = + [ + new PlaylistItem(importedSet.Beatmaps.First()) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + Freestyle = true + } + ] + }); + + AddStep("open user style selection", () => this.ChildrenOfType().Single().ShowUserStyleSelect()); + AddUntilStep("style selection screen opened", () => this.ChildrenOfType().SingleOrDefault()?.IsCurrentScreen() == true); + + AddStep("change beatmap", () => multiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem(multiplayerClient.ServerRoom!.Playlist[0]) + { + Beatmap = importedSet.Beatmaps.Last(), + }))); + + AddWaitStep("wait for potential beatmap change", 2); + AddAssert("style selection screen still open", () => this.ChildrenOfType().SingleOrDefault()?.IsCurrentScreen() == true); + } + + /// + /// Tests that the user selection screen is exited when the beatmap is changed to another set. + /// + [Test] + public void TestUserStyleSelectionExitedWhenBeatmapSetChanged() + { + createRoom(() => new Room + { + Name = "Test Room", + QueueMode = QueueMode.AllPlayers, + Playlist = + [ + new PlaylistItem(importedSet.Beatmaps.First()) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + Freestyle = true + } + ] + }); + + AddStep("open user style selection", () => this.ChildrenOfType().Single().ShowUserStyleSelect()); + AddAssert("style selection screen still open", () => this.ChildrenOfType().SingleOrDefault()?.IsCurrentScreen() == true); + + AddStep("change beatmap set", () => multiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem(multiplayerClient.ServerRoom!.Playlist[0]) + { + Beatmap = importedSet2.Beatmaps.First(), + }))); + + AddUntilStep("selected beatmap changed", () => Beatmap.Value.BeatmapInfo.Equals(importedSet2.Beatmaps.First())); + AddUntilStep("style selection screen closed", () => this.ChildrenOfType().SingleOrDefault()?.IsCurrentScreen() != true); + } + private void enterGameplay() { pressReadyButton(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index e0a0e5a785..aa4c4949fb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -13,6 +13,7 @@ using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; @@ -53,10 +54,17 @@ namespace osu.Game.Tests.Visual.Multiplayer Dependencies.Cache(new RealmRulesetStore(Realm)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); + Dependencies.CacheAs(new RealmDetachedBeatmapStore()); beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); importedSet = beatmaps.GetAllUsableBeatmapSets().First(); + + Realm.Write(r => + { + foreach (var beatmapInfo in r.All()) + beatmapInfo.OnlineMD5Hash = beatmapInfo.MD5Hash; + }); } public override void SetUpSteps() diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index b7b6a2d7b3..52f943b536 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -90,7 +90,7 @@ namespace osu.Game.Online.Rooms /// In many cases, this will *not* contain any usable information apart from OnlineID. /// [JsonIgnore] - public IBeatmapInfo Beatmap { get; private set; } + public IBeatmapInfo Beatmap { get; set; } [JsonIgnore] public IBindable Valid => valid; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index db1b8262b7..9f360eca72 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -83,6 +83,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer /// protected bool ExitConfirmed { get; private set; } + /// + /// Used for testing - whether the local user style can be edited. + /// False if the beatmap hasn't been downloaded yet, or if freestyle isn't enabled. + /// + internal bool UserStyleEditingEnabled + { + get + { + if (!userStyleDisplayContainer.IsPresent) + return false; + + return userStyleDisplayContainer.SingleOrDefault()?.AllowEditing == true; + } + } + [Resolved] private IAPIProvider api { get; set; } = null!; @@ -677,7 +692,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer /// /// Shows the user style selection. /// - private void showUserStyleSelect() + public void ShowUserStyleSelect() { if (!this.IsCurrentScreen() || client.Room == null || client.LocalUser == null) return; From 9d2ba062878f53aeff9c7d5a46b4e73f95db0bef Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 14 Jul 2025 17:57:24 +0900 Subject: [PATCH 236/259] Hide user style edit button when not downloaded --- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 6 ++++-- .../Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 9f360eca72..7708bd7b50 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -657,10 +657,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer userStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(apiItem, true) { AllowReordering = false, - AllowEditing = true, - RequestEdit = _ => showUserStyleSelect() + RequestEdit = _ => ShowUserStyleSelect() }; } + + DrawableRoomPlaylistItem panel = userStyleDisplayContainer.Single(); + panel.AllowEditing = localBeatmap != null; } else userStyleSection.Hide(); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index cfd651ba4d..a0aca4b166 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -641,6 +641,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists RequestEdit = _ => showUserStyleSelect() }; } + + DrawableRoomPlaylistItem panel = userStyleDisplayContainer.Single(); + panel.AllowEditing = localBeatmap != null; } else userStyleSection.Hide(); From 3401706e7e9ff8ae07ebb461a5c7f4787519c6bd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 14 Jul 2025 17:57:49 +0900 Subject: [PATCH 237/259] Exit user style selection when beatmap set changes --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 7708bd7b50..700d8b9678 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -481,7 +481,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { if (settings.PlaylistItemId != lastPlaylistItemId) { - Scheduler.AddOnce(updateGameplayState); + onActivePlaylistItemChanged(); lastPlaylistItemId = settings.PlaylistItemId; } @@ -494,7 +494,29 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onItemChanged(MultiplayerPlaylistItem item) { if (item.ID == client.Room?.Settings.PlaylistItemId) - Scheduler.AddOnce(updateGameplayState); + onActivePlaylistItemChanged(); + } + + /// + /// Responds to changes in the active playlist item resulting from the playlist item being edited or the room settings changing. + /// + private void onActivePlaylistItemChanged() + { + if (client.Room == null) + return; + + // Check if we need to make this the current screen as a result of the beatmap set changing while the user's selecting a style. + if (this.GetChildScreen() is MultiplayerMatchFreestyleSelect) + { + MultiplayerPlaylistItem item = client.Room.CurrentPlaylistItem; + + var newBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", item.BeatmapID); + + if (!Beatmap.Value.BeatmapSetInfo.Equals(newBeatmap?.BeatmapSet)) + this.MakeCurrent(); + } + + Scheduler.AddOnce(updateGameplayState); } /// From bcf087346b10063283404021b3f4d7d39619c9a3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 14 Jul 2025 18:16:43 +0900 Subject: [PATCH 238/259] Move variables local --- osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs index afc0253edb..13ac406396 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs @@ -97,8 +97,6 @@ namespace osu.Game.Screens.OnlinePlay private partial class DifficultySelectFilterControl : FilterControl { private readonly PlaylistItem item; - private double itemLength; - private int beatmapSetId; [Resolved] private RealmAccess realm { get; set; } = null!; @@ -112,6 +110,9 @@ namespace osu.Game.Screens.OnlinePlay { var criteria = base.CreateCriteria(); + double itemLength = 0; + int beatmapSetId = 0; + realm.Run(r => { int beatmapId = item.Beatmap.OnlineID; @@ -130,7 +131,6 @@ namespace osu.Game.Screens.OnlinePlay criteria.Length.Max = itemLength + 30000; criteria.Length.IsLowerInclusive = true; criteria.Length.IsUpperInclusive = true; - return criteria; } } From fd776c58ad71e6c79b466057a08b5d4a1f24ed74 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 14 Jul 2025 20:30:15 +0900 Subject: [PATCH 239/259] Add failing test --- .../TestScenePlaylistsResultsScreen.cs | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 61269a7bf4..1b8e330f2a 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -274,6 +274,29 @@ namespace osu.Game.Tests.Visual.Playlists AddUntilStep("all panels have non-negative position", () => this.ChildrenOfType().All(p => p.ScorePosition.Value > 0)); } + [Test] + public void TestPresentInvalidOnlineScore() + { + AddStep("set user score ID -1 and total score -1", () => + { + userScore.OnlineID = -1; + userScore.TotalScore = 0; + }); + + AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); + + createResultsWithScore(() => userScore); + + AddUntilStep("wait for user score to be displayed", () => resultsScreen.ChildrenOfType().Single().GetScorePanels().Any()); + AddWaitStep("wait for any more potential scores", 5); + AddAssert("only 1 score visible", () => resultsScreen.ChildrenOfType().Single().GetScorePanels().Count(), () => Is.EqualTo(1)); + + AddUntilStep("left loading spinner hidden", () => + resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Hidden); + AddUntilStep("right loading spinner hidden", () => + resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden); + } + private void createResultsWithScore(Func getScore) { AddStep("load results", () => @@ -359,7 +382,7 @@ namespace osu.Game.Tests.Visual.Playlists switch (request) { case ShowPlaylistScoreRequest s: - if (userScore == null) + if (userScore == null || userScore.OnlineID == -1) triggerFail(s); else triggerSuccess(s, () => createUserResponse(userScore)); @@ -367,7 +390,7 @@ namespace osu.Game.Tests.Visual.Playlists break; case ShowPlaylistUserScoreRequest u: - if (userScore == null) + if (userScore == null || userScore.OnlineID == -1) triggerFail(u); else triggerSuccess(u, () => createUserResponse(userScore)); From 5a455864557deb26bbf05255c594a86ca89e68a7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 14 Jul 2025 20:31:22 +0900 Subject: [PATCH 240/259] Make online play results not request leaderboard on failed submission --- .../Playlists/PlaylistItemScoreResultsScreen.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs index 74b12b6d3c..4b7ffe42ea 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; +using System.Threading.Tasks; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Scoring; @@ -27,6 +28,20 @@ namespace osu.Game.Screens.OnlinePlay.Playlists this.scoreId = scoreId; } + protected override Task FetchScores() + { + // Don't attempt to index scores if the given score has an invalid online ID. + // This can happen if the score failed to submit but is otherwise in a presentable state. + return scoreId <= 0 ? Task.FromResult([]) : base.FetchScores(); + } + + protected override Task FetchNextPage(int direction) + { + // Don't attempt to index scores if the given score has an invalid online ID. + // This can happen if the score failed to submit but is otherwise in a presentable state. + return scoreId <= 0 ? Task.FromResult([]) : base.FetchNextPage(direction); + } + protected override APIRequest CreateScoreRequest() => new ShowPlaylistScoreRequest(RoomId, PlaylistItem.ID, scoreId); protected override void OnScoresAdded(ScoreInfo[] scores) From fc44301713ebe8d20b12c5b5586e8ccbc9bda03d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Jul 2025 13:51:46 +0200 Subject: [PATCH 241/259] Change failing test to use until step instead --- osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 69cf174f34..050fcf8675 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -1184,7 +1184,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddStep("open user style selection", () => this.ChildrenOfType().Single().ShowUserStyleSelect()); - AddAssert("style selection screen still open", () => this.ChildrenOfType().SingleOrDefault()?.IsCurrentScreen() == true); + AddUntilStep("style selection screen opened", () => this.ChildrenOfType().SingleOrDefault()?.IsCurrentScreen() == true); AddStep("change beatmap set", () => multiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem(multiplayerClient.ServerRoom!.Playlist[0]) { From bd922e288864f0ad7b9bb40875b67f4ef0b9aa91 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 14 Jul 2025 21:16:15 +0900 Subject: [PATCH 242/259] Refactor order of operations --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 8 ++++---- .../OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 10 ++++------ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 700d8b9678..689a8df12f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -673,18 +673,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer userStyleSection.Show(); PlaylistItem apiItem = new PlaylistItem(item).With(beatmap: new Optional(new APIBeatmap { OnlineID = gameplayBeatmapId }), ruleset: gameplayRulesetId); + DrawableRoomPlaylistItem? currentDisplay = userStyleDisplayContainer.SingleOrDefault(); - if (!apiItem.Equals(userStyleDisplayContainer.SingleOrDefault()?.Item)) + if (!apiItem.Equals(currentDisplay?.Item)) { - userStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(apiItem, true) + userStyleDisplayContainer.Child = currentDisplay = new DrawableRoomPlaylistItem(apiItem, true) { AllowReordering = false, RequestEdit = _ => ShowUserStyleSelect() }; } - DrawableRoomPlaylistItem panel = userStyleDisplayContainer.Single(); - panel.AllowEditing = localBeatmap != null; + currentDisplay.AllowEditing = localBeatmap != null; } else userStyleSection.Hide(); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index a0aca4b166..5b42bcf254 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -630,20 +630,18 @@ namespace osu.Game.Screens.OnlinePlay.Playlists userStyleSection.Show(); PlaylistItem gameplayItem = item.With(ruleset: gameplayRuleset.OnlineID, beatmap: new Optional(gameplayBeatmap)); - PlaylistItem? currentItem = userStyleDisplayContainer.SingleOrDefault()?.Item; + DrawableRoomPlaylistItem? currentDisplay = userStyleDisplayContainer.SingleOrDefault(); - if (!gameplayItem.Equals(currentItem)) + if (!gameplayItem.Equals(currentDisplay?.Item)) { - userStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true) + userStyleDisplayContainer.Child = currentDisplay = new DrawableRoomPlaylistItem(gameplayItem, true) { AllowReordering = false, - AllowEditing = true, RequestEdit = _ => showUserStyleSelect() }; } - DrawableRoomPlaylistItem panel = userStyleDisplayContainer.Single(); - panel.AllowEditing = localBeatmap != null; + currentDisplay.AllowEditing = localBeatmap != null; } else userStyleSection.Hide(); From 13fc37b2de614e7b8ed9ef09c710ed259db1d806 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 14 Jul 2025 21:19:20 +0900 Subject: [PATCH 243/259] Re-privatise `PlaylistItem.Beatmap`, adjust tests --- .../Multiplayer/TestSceneMultiplayer.cs | 22 ++++++++++++++----- osu.Game/Online/Rooms/PlaylistItem.cs | 2 +- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 050fcf8675..083b5b14fb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -1154,10 +1154,15 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("open user style selection", () => this.ChildrenOfType().Single().ShowUserStyleSelect()); AddUntilStep("style selection screen opened", () => this.ChildrenOfType().SingleOrDefault()?.IsCurrentScreen() == true); - AddStep("change beatmap", () => multiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem(multiplayerClient.ServerRoom!.Playlist[0]) + AddStep("change beatmap", () => { - Beatmap = importedSet.Beatmaps.Last(), - }))); + var newItem = multiplayerClient.ServerRoom!.Playlist[0].Clone(); + var newBeatmap = importedSet.Beatmaps.Last(); + newItem.BeatmapID = newBeatmap.OnlineID; + newItem.BeatmapChecksum = newBeatmap.MD5Hash; + + multiplayerClient.EditPlaylistItem(newItem); + }); AddWaitStep("wait for potential beatmap change", 2); AddAssert("style selection screen still open", () => this.ChildrenOfType().SingleOrDefault()?.IsCurrentScreen() == true); @@ -1186,10 +1191,15 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("open user style selection", () => this.ChildrenOfType().Single().ShowUserStyleSelect()); AddUntilStep("style selection screen opened", () => this.ChildrenOfType().SingleOrDefault()?.IsCurrentScreen() == true); - AddStep("change beatmap set", () => multiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem(multiplayerClient.ServerRoom!.Playlist[0]) + AddStep("change beatmap set", () => { - Beatmap = importedSet2.Beatmaps.First(), - }))); + var newItem = multiplayerClient.ServerRoom!.Playlist[0].Clone(); + var newBeatmap = importedSet2.Beatmaps.Last(); + newItem.BeatmapID = newBeatmap.OnlineID; + newItem.BeatmapChecksum = newBeatmap.MD5Hash; + + multiplayerClient.EditPlaylistItem(newItem); + }); AddUntilStep("selected beatmap changed", () => Beatmap.Value.BeatmapInfo.Equals(importedSet2.Beatmaps.First())); AddUntilStep("style selection screen closed", () => this.ChildrenOfType().SingleOrDefault()?.IsCurrentScreen() != true); diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 52f943b536..b7b6a2d7b3 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -90,7 +90,7 @@ namespace osu.Game.Online.Rooms /// In many cases, this will *not* contain any usable information apart from OnlineID. /// [JsonIgnore] - public IBeatmapInfo Beatmap { get; set; } + public IBeatmapInfo Beatmap { get; private set; } [JsonIgnore] public IBindable Valid => valid; From 9cc5fab3838c9210a962692ae4c563f908761d83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Jul 2025 14:47:33 +0200 Subject: [PATCH 244/259] Use more descriptive test step name --- .../Visual/Playlists/TestScenePlaylistsResultsScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 1b8e330f2a..e3137d77d7 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -277,7 +277,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestPresentInvalidOnlineScore() { - AddStep("set user score ID -1 and total score -1", () => + AddStep("set up invalid user score", () => { userScore.OnlineID = -1; userScore.TotalScore = 0; From 3eca3897edc87a5eb0b8029a167a938b500ec3d0 Mon Sep 17 00:00:00 2001 From: eyhn Date: Mon, 14 Jul 2025 20:55:44 +0800 Subject: [PATCH 245/259] Adjust behavior of beatmap set with deleted author --- .../Online/TestSceneBeatmapSetOverlay.cs | 38 ++++++++++++++++++- .../API/Requests/Responses/APIBeatmapSet.cs | 21 ++++++++-- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs index f36ef7a8e8..5da05826cf 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs @@ -10,6 +10,8 @@ using osu.Game.Rulesets; using System; using System.Collections.Generic; using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using osu.Framework.Testing; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics.Sprites; @@ -193,7 +195,8 @@ namespace osu.Game.Tests.Visual.Online overlay.ShowBeatmapSet(set); }); - AddAssert("shown beatmaps of current ruleset", () => overlay.Header.HeaderContent.Picker.Difficulties.All(b => b.Beatmap.Ruleset.OnlineID == overlay.Header.RulesetSelector.Current.Value.OnlineID)); + AddAssert("shown beatmaps of current ruleset", + () => overlay.Header.HeaderContent.Picker.Difficulties.All(b => b.Beatmap.Ruleset.OnlineID == overlay.Header.RulesetSelector.Current.Value.OnlineID)); AddAssert("left-most beatmap selected", () => overlay.Header.HeaderContent.Picker.Difficulties.First().State == BeatmapPicker.DifficultySelectorState.Selected); } @@ -373,6 +376,39 @@ namespace osu.Game.Tests.Visual.Online }); } + [Test] + public void TestBeatmapsetWithDeletedUser() + { + AddStep("show map with deleted user", () => + { + JObject jsonBlob = JObject.FromObject(getBeatmapSet(), new JsonSerializer + { + ReferenceLoopHandling = ReferenceLoopHandling.Ignore + }); + + jsonBlob["user"] = JToken.Parse( + """ + { + "avatar_url": null, + "country_code": null, + "default_group": "default", + "id": null, + "is_active": false, + "is_bot": false, + "is_deleted": true, + "is_online": false, + "is_supporter": false, + "last_visit": null, + "pm_friends_only": false, + "profile_colour": null, + "username": "[deleted user]" + } + """); + + overlay.ShowBeatmapSet(JsonConvert.DeserializeObject(JsonConvert.SerializeObject(jsonBlob))); + }); + } + private APIBeatmapSet createManyDifficultiesBeatmapSet() { var set = getBeatmapSet(); diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs index e8e08059b9..dc6c433f29 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs @@ -84,11 +84,26 @@ namespace osu.Game.Online.API.Requests.Responses /// 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. + /// This property is set differently depending on the API endpoint. When retrieved via , + /// detailed user info is not included and the creator's ID and username are filled from the and + /// properties. For other API endpoints, this property is set by the setter. + /// + public APIUser Author = new APIUser(); + + /// + /// Helper property to deserialize the detailed user info to + /// + /// + /// This setter implements special handling for deleted users. When received a user with ID 1, it indicates + /// the original user has been deleted. In such cases, the existing data + /// (filled from and ) is preserved. For valid user, + /// the provided user info replaces the existing . /// [JsonProperty(@"user")] - public APIUser Author = new APIUser(); + private APIUser author + { + set => Author = value.Id != 1 ? value : Author; + } /// /// The ID of the beatmap set's creator. From 3a3c1c4e092c70af999e201719709a3fce607507 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 14 Jul 2025 22:40:03 +0900 Subject: [PATCH 246/259] Fix flaky playlists navigation test See: https://github.com/ppy/osu/actions/runs/16267403227/job/45929099025 Repro: ```diff diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index a4e808ff76..db0a0e83a6 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -88,6 +88,7 @@ public abstract partial class LoungeSubScreen : OnlinePlaySubScreen, IOnlinePlay [BackgroundDependencyLoader(true)] private void load() { + System.Threading.Thread.Sleep(1000); Masking = true; const float controls_area_height = 25f; ``` --- osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index d50fc69823..53cd411bb0 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -75,6 +75,7 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType().SingleOrDefault() != null); PushAndConfirm(() => playlistScreen = new Screens.OnlinePlay.Playlists.Playlists()); + AddUntilStep("wait for lounge", () => (playlistScreen.CurrentSubScreen as LoungeSubScreen)?.IsLoaded == true); AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); From c59447c407c8bc705df3f7a0e0270727909324a2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Jul 2025 10:24:14 +0900 Subject: [PATCH 247/259] Re-enable debug settings --- .../Overlays/FirstRunSetup/ScreenBehaviour.cs | 9 +- .../Settings/Sections/DebugSection.cs | 13 +- .../Sections/DebugSettings/GeneralSettings.cs | 21 +-- .../Sections/DebugSettings/MemorySettings.cs | 123 +++++++++--------- osu.Game/Overlays/SettingsOverlay.cs | 9 +- 5 files changed, 86 insertions(+), 89 deletions(-) diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs b/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs index a583ba5f6b..00a753f481 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs @@ -1,11 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Localisation; @@ -22,7 +19,7 @@ namespace osu.Game.Overlays.FirstRunSetup [LocalisableDescription(typeof(FirstRunSetupOverlayStrings), nameof(FirstRunSetupOverlayStrings.Behaviour))] public partial class ScreenBehaviour : WizardScreen { - private SearchContainer searchContainer; + private SearchContainer searchContainer = null!; [BackgroundDependencyLoader] private void load(OsuColour colours) @@ -91,13 +88,11 @@ namespace osu.Game.Overlays.FirstRunSetup new GraphicsSection(), new OnlineSection(), new MaintenanceSection(), + new DebugSection() }, SearchTerm = SettingsItem.CLASSIC_DEFAULT_SEARCH_TERM, } }; - - if (DebugUtils.IsDebugBuild) - searchContainer.Add(new DebugSection()); } private void applyClassic() diff --git a/osu.Game/Overlays/Settings/Sections/DebugSection.cs b/osu.Game/Overlays/Settings/Sections/DebugSection.cs index 1d2129413c..37fab9cac3 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSection.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSection.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.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; @@ -20,12 +21,12 @@ namespace osu.Game.Overlays.Settings.Sections public DebugSection() { - Children = new Drawable[] - { - new GeneralSettings(), - new BatchImportSettings(), - new MemorySettings(), - }; + Add(new GeneralSettings()); + + if (DebugUtils.IsDebugBuild) + Add(new BatchImportSettings()); + + Add(new MemorySettings()); } } } diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs index bd6ada4ca7..3251b93d9f 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs @@ -3,7 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Configuration; -using osu.Framework.Graphics; +using osu.Framework.Development; using osu.Framework.Localisation; namespace osu.Game.Overlays.Settings.Sections.DebugSettings @@ -15,19 +15,20 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings [BackgroundDependencyLoader] private void load(FrameworkDebugConfigManager config, FrameworkConfigManager frameworkConfig) { - Children = new Drawable[] + Add(new SettingsCheckbox { - new SettingsCheckbox - { - LabelText = @"Show log overlay", - Current = frameworkConfig.GetBindable(FrameworkSetting.ShowLogOverlay) - }, - new SettingsCheckbox + LabelText = @"Show log overlay", + Current = frameworkConfig.GetBindable(FrameworkSetting.ShowLogOverlay) + }); + + if (DebugUtils.IsDebugBuild) + { + Add(new SettingsCheckbox { LabelText = @"Bypass front-to-back render pass", Current = config.GetBindable(DebugSetting.BypassFrontToBackPass) - }, - }; + }); + } } } } diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs index b693822838..1272d1396c 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs @@ -5,6 +5,7 @@ using System; using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Development; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Localisation; @@ -24,73 +25,77 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings SettingsButton blockAction; SettingsButton unblockAction; - Children = new Drawable[] + Add(new SettingsButton { - new SettingsButton + Text = @"Clear all caches", + Action = host.Collect + }); + + if (DebugUtils.IsDebugBuild) + { + AddRange(new Drawable[] { - Text = @"Clear all caches", - Action = host.Collect - }, - new SettingsButton - { - Text = @"Compact realm", - Action = () => + new SettingsButton { - // Blocking operations implicitly causes a Compact(). - using (realm.BlockAllOperations(@"compact")) + Text = @"Compact realm", + Action = () => { + // Blocking operations implicitly causes a Compact(). + using (realm.BlockAllOperations(@"compact")) + { + } + } + }, + blockAction = new SettingsButton + { + Text = @"Block realm", + }, + unblockAction = new SettingsButton + { + Text = @"Unblock realm", + } + }); + + blockAction.Action = () => + { + try + { + IDisposable? token = realm.BlockAllOperations(@"maintenance"); + + blockAction.Enabled.Value = false; + + // As a safety measure, unblock after 10 seconds. + // This is to handle the case where a dev may block, but then something on the update thread + // accesses realm and blocks for eternity. + Task.Factory.StartNew(() => + { + Thread.Sleep(10000); + unblock(); + }); + + unblockAction.Action = unblock; + + void unblock() + { + if (token.IsNull()) + return; + + token.Dispose(); + token = null; + + Scheduler.Add(() => + { + blockAction.Enabled.Value = true; + unblockAction.Action = null; + }); } } - }, - blockAction = new SettingsButton - { - Text = @"Block realm", - }, - unblockAction = new SettingsButton - { - Text = @"Unblock realm", - }, - }; - - blockAction.Action = () => - { - try - { - IDisposable? token = realm.BlockAllOperations(@"maintenance"); - - blockAction.Enabled.Value = false; - - // As a safety measure, unblock after 10 seconds. - // This is to handle the case where a dev may block, but then something on the update thread - // accesses realm and blocks for eternity. - Task.Factory.StartNew(() => + catch (Exception e) { - Thread.Sleep(10000); - unblock(); - }); - - unblockAction.Action = unblock; - - void unblock() - { - if (token.IsNull()) - return; - - token.Dispose(); - token = null; - - Scheduler.Add(() => - { - blockAction.Enabled.Value = true; - unblockAction.Action = null; - }); + Logger.Error(e, @"Blocking realm failed"); } - } - catch (Exception e) - { - Logger.Error(e, @"Blocking realm failed"); - } - }; + }; + } } } } diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index a498f2fe1f..3065a4d1bd 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -31,7 +30,7 @@ namespace osu.Game.Overlays protected override IEnumerable CreateSections() { - var sections = new List + return new List { // This list should be kept in sync with ScreenBehaviour. new GeneralSection(), @@ -44,12 +43,8 @@ namespace osu.Game.Overlays new GraphicsSection(), new OnlineSection(), new MaintenanceSection(), + new DebugSection() }; - - if (DebugUtils.IsDebugBuild) - sections.Add(new DebugSection()); - - return sections; } private readonly List subPanels = new List(); From adc054f08eece15c0e9f002d7c27826c4a8de347 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Jul 2025 10:25:42 +0900 Subject: [PATCH 248/259] Clear caches as aggressively as possible --- .../Settings/Sections/DebugSettings/MemorySettings.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs index 1272d1396c..0c060197bf 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs @@ -28,7 +28,13 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings Add(new SettingsButton { Text = @"Clear all caches", - Action = host.Collect + Action = () => + { + host.Collect(); + + // host.Collect() uses GCCollectionMode.Optimized, but we should be as aggressive as possible here. + GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true); + } }); if (DebugUtils.IsDebugBuild) From 1c243b25b23127ddfc47c02bdd31ff9d96b975f8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Jul 2025 11:07:37 +0900 Subject: [PATCH 249/259] Add GC mode dropdown --- .../Sections/DebugSettings/MemorySettings.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs index 0c060197bf..4b9ed22c29 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Runtime; using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; @@ -37,6 +38,27 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings } }); + SettingsEnumDropdown latencyModeDropdown; + Add(latencyModeDropdown = new SettingsEnumDropdown + { + LabelText = "GC mode", + }); + + latencyModeDropdown.Current.BindValueChanged(mode => + { + switch (mode.NewValue) + { + case GCLatencyMode.Default: + // https://github.com/ppy/osu-framework/blob/1d5301018dfed1a28702be56e1d53c4835b199f2/osu.Framework/Platform/GameHost.cs#L703 + GCSettings.LatencyMode = System.Runtime.GCLatencyMode.SustainedLowLatency; + break; + + case GCLatencyMode.Interactive: + GCSettings.LatencyMode = System.Runtime.GCLatencyMode.Interactive; + break; + } + }); + if (DebugUtils.IsDebugBuild) { AddRange(new Drawable[] @@ -103,5 +125,11 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings }; } } + + private enum GCLatencyMode + { + Default, + Interactive, + } } } From 27f93d56a073867c12811777b3bc78c36870cdd9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Jul 2025 13:03:35 +0900 Subject: [PATCH 250/259] Add failing test --- .../NonVisual/TestSceneUpdateManager.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/osu.Game.Tests/NonVisual/TestSceneUpdateManager.cs b/osu.Game.Tests/NonVisual/TestSceneUpdateManager.cs index bdb4dce354..8a9ee4b81b 100644 --- a/osu.Game.Tests/NonVisual/TestSceneUpdateManager.cs +++ b/osu.Game.Tests/NonVisual/TestSceneUpdateManager.cs @@ -136,14 +136,39 @@ namespace osu.Game.Tests.NonVisual AddUntilStep("no check pending", () => !manager.IsPending); } + [Test] + public void TestFixedReleaseStreamWrittenToConfig() + { + AddStep("add manager", () => + { + config = new OsuConfigManager(LocalStorage); + config.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Lazer); + + Child = new DependencyProvidingContainer + { + CachedDependencies = [(typeof(OsuConfigManager), config)], + Child = manager = new TestUpdateManager(ReleaseStream.Tachyon) + }; + }); + + AddAssert("release stream set to tachyon", () => config.Get(OsuSetting.ReleaseStream), () => Is.EqualTo(ReleaseStream.Tachyon)); + } + private partial class TestUpdateManager : UpdateManager { + public override ReleaseStream? FixedReleaseStream { get; } + public bool IsPending { get; private set; } public int Invocations { get; private set; } public int Completions { get; private set; } private TaskCompletionSource? pendingCheck; + public TestUpdateManager(ReleaseStream? fixedReleaseStream = null) + { + FixedReleaseStream = fixedReleaseStream; + } + protected override async Task PerformUpdateCheck(CancellationToken cancellationToken) { Invocations++; From 55befe9efbb20a20d95a801910d53f3a2d967c22 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Jul 2025 13:14:44 +0900 Subject: [PATCH 251/259] Write only fixed release streams back to config --- osu.Game/OsuGame.cs | 4 ---- osu.Game/Updater/UpdateManager.cs | 8 ++++++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 153e6acb3b..e060450a5e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1058,10 +1058,6 @@ 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 => diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index 335f6085a9..4a067e3f68 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -56,12 +56,16 @@ namespace osu.Game.Updater string version = game.Version; string lastVersion = config.Get(OsuSetting.Version); - if (game.IsDeployedBuild && version != lastVersion) + if (game.IsDeployedBuild) { // only show a notification if we've previously saved a version to the config file (ie. not the first run). - if (!string.IsNullOrEmpty(lastVersion)) + if (!string.IsNullOrEmpty(lastVersion) && version != lastVersion) Notifications.Post(new UpdateCompleteNotification(version)); + // make sure the release stream setting matches the build which was just run. + if (FixedReleaseStream != null) + config.SetValue(OsuSetting.ReleaseStream, FixedReleaseStream.Value); + if (RuntimeInfo.EntryAssembly.GetCustomAttribute() == null) Notifications.Post(new SimpleNotification { Text = NotificationsStrings.NotOfficialBuild }); } From 35a3186bf0bdea67d82cf8d2be237079960e171d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Jul 2025 13:18:09 +0900 Subject: [PATCH 252/259] Centralise logging of non-official builds --- osu.Game/OsuGame.cs | 4 ---- osu.Game/Updater/UpdateManager.cs | 8 ++++++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index e060450a5e..bf08023242 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -8,7 +8,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; -using System.Reflection; using System.Threading; using System.Threading.Tasks; using Humanizer; @@ -1055,9 +1054,6 @@ namespace osu.Game { base.LoadComplete(); - if (RuntimeInfo.EntryAssembly.GetCustomAttribute() == null) - Logger.Log(NotificationsStrings.NotOfficialBuild.ToString()); - var languages = Enum.GetValues(); var mappings = languages.Select(language => diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index 4a067e3f68..4ce3914df0 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -10,6 +10,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Logging; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Localisation; @@ -66,9 +67,16 @@ namespace osu.Game.Updater if (FixedReleaseStream != null) config.SetValue(OsuSetting.ReleaseStream, FixedReleaseStream.Value); + // notify the user if they're using a build that is not officially sanctioned. if (RuntimeInfo.EntryAssembly.GetCustomAttribute() == null) Notifications.Post(new SimpleNotification { Text = NotificationsStrings.NotOfficialBuild }); } + else + { + // log that this is not an official build, for if users build their own game without an assembly version. + // this is only logged because a notification would be too spammy in local test builds. + Logger.Log(NotificationsStrings.NotOfficialBuild.ToString()); + } // 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). From a80a9bbfd10719b321a4f319125b8b33bfc0cef0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Jul 2025 13:44:49 +0900 Subject: [PATCH 253/259] Hide entire general section --- osu.Game/Overlays/Settings/Sections/DebugSection.cs | 5 +++-- .../Sections/DebugSettings/GeneralSettings.cs | 12 ++++-------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/DebugSection.cs b/osu.Game/Overlays/Settings/Sections/DebugSection.cs index 37fab9cac3..969e65e823 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSection.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSection.cs @@ -21,10 +21,11 @@ namespace osu.Game.Overlays.Settings.Sections public DebugSection() { - Add(new GeneralSettings()); - if (DebugUtils.IsDebugBuild) + { + Add(new GeneralSettings()); Add(new BatchImportSettings()); + } Add(new MemorySettings()); } diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs index 3251b93d9f..914fc9d141 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs @@ -3,7 +3,6 @@ using osu.Framework.Allocation; using osu.Framework.Configuration; -using osu.Framework.Development; using osu.Framework.Localisation; namespace osu.Game.Overlays.Settings.Sections.DebugSettings @@ -21,14 +20,11 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings Current = frameworkConfig.GetBindable(FrameworkSetting.ShowLogOverlay) }); - if (DebugUtils.IsDebugBuild) + Add(new SettingsCheckbox { - Add(new SettingsCheckbox - { - LabelText = @"Bypass front-to-back render pass", - Current = config.GetBindable(DebugSetting.BypassFrontToBackPass) - }); - } + LabelText = @"Bypass front-to-back render pass", + Current = config.GetBindable(DebugSetting.BypassFrontToBackPass) + }); } } } From 985241c63ea3b740480b8988827d024849b83887 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Jul 2025 13:44:58 +0900 Subject: [PATCH 254/259] Log changes to latency mode --- .../Overlays/Settings/Sections/DebugSettings/MemorySettings.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs index 4b9ed22c29..7b9b88a213 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs @@ -46,6 +46,8 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings latencyModeDropdown.Current.BindValueChanged(mode => { + Logger.Log($"Changing latency mode: {mode.NewValue}"); + switch (mode.NewValue) { case GCLatencyMode.Default: From 0d58b2d53a9339de9167a42d479d0d3818a69e48 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Jul 2025 14:03:02 +0900 Subject: [PATCH 255/259] Add safeguard against skin resources getting left in place on game exit --- .../Overlays/SkinEditor/ExternalEditOverlay.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game/Overlays/SkinEditor/ExternalEditOverlay.cs b/osu.Game/Overlays/SkinEditor/ExternalEditOverlay.cs index 7bbfcd4b8e..4d91b4ebfd 100644 --- a/osu.Game/Overlays/SkinEditor/ExternalEditOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/ExternalEditOverlay.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -96,6 +97,8 @@ namespace osu.Game.Overlays.SkinEditor } } }; + + gameHost.ExitRequested += tryFinishOnExit; } public async Task Begin(SkinInfo skinInfo) @@ -180,6 +183,12 @@ namespace osu.Game.Overlays.SkinEditor gameHost.OpenFileExternally(editOperation.MountedPath.TrimDirectorySeparator() + Path.DirectorySeparatorChar); } + private void tryFinishOnExit() + { + if (editOperation != null && !finishingEdit) + finish().FireAndForget(onSuccess: () => Schedule(() => finishingEdit = false)); + } + private async Task finish() { if (finishingEdit) @@ -288,5 +297,13 @@ namespace osu.Game.Overlays.SkinEditor }, }; } + + protected override void Dispose(bool isDisposing) + { + if (gameHost.IsNotNull()) + gameHost.ExitRequested -= tryFinishOnExit; + + base.Dispose(isDisposing); + } } } From 131f828e6a6c704f54e320180c9d17a632aeeb2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 15 Jul 2025 08:59:11 +0200 Subject: [PATCH 256/259] Attempt to properly quantify the impact of mania Hard Rock / Easy mod application on overall difficulty In stable mania, Hard Rock and Easy mods do not work the same way as they do on all of the rulesets. The difference is that mania HR and EZ, rather than apply a multiplier to the map's original Overall Difficulty, apply multipliers to *the durations of hit windows themselves*. Prior to the last release, lazer was oblivious to this reality and just treated mania HR / EZ as it did every other ruleset. Last release, for the sake for gameplay parity across rulesets, the mods in question were adjusted to match stable, but in the process, it started looking like HR / EZ did not change OD anymore. The problem is that they do, but applying a multiplier to the map's OD and applying a multiplier to the hit window duration is not the same thing. The second thing is actually *much harsher* in magnitude, to the point where applying HR to any map is almost guaranteed to exceed "the effective OD" of 10, and applying EZ to any map is almost guaranteed to result in "negative effective OD". This change attempts to convey that reality by displaying "effective OD", similar to what's already done in other rulesets when rate-changing mods are active. Note that the values this will display *do not match* stable *and that is correct*, because stable song select *lies* about the actual impact on OD by just assuming it can treat all rulesets in the same way. --- Would close https://github.com/ppy/osu/issues/34150 I guess. And yes I would like *all of the above* to land on the changelog if possible if this is merged. For further convincing that this makes any semblance of sense please see the following: https://www.desmos.com/calculator/yigt7jycdv --- .../CatchRateAdjustedDisplayDifficultyTest.cs | 7 ++--- osu.Game.Rulesets.Catch/CatchRuleset.cs | 4 ++- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 26 +++++++++++++++++++ osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs | 10 +++---- .../Mods/ManiaModHardRock.cs | 10 +++---- .../Scoring/ManiaHitWindows.cs | 4 +-- .../OsuRateAdjustedDisplayDifficultyTest.cs | 9 ++++--- osu.Game.Rulesets.Osu/OsuRuleset.cs | 4 ++- .../TaikoRateAdjustedDisplayDifficultyTest.cs | 7 ++--- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 4 ++- .../Drawables/DifficultyIconTooltip.cs | 2 +- .../Overlays/Mods/BeatmapAttributesDisplay.cs | 2 +- osu.Game/Rulesets/Ruleset.cs | 6 ++--- .../Screens/Select/Details/AdvancedStats.cs | 4 +-- .../BeatmapTitleWedge_DifficultyDisplay.cs | 5 +--- .../Components/BeatmapAttributeText.cs | 5 +--- 16 files changed, 68 insertions(+), 41 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/CatchRateAdjustedDisplayDifficultyTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchRateAdjustedDisplayDifficultyTest.cs index f77ec64df3..0ec3bfd911 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchRateAdjustedDisplayDifficultyTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchRateAdjustedDisplayDifficultyTest.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using NUnit.Framework; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Mods; namespace osu.Game.Rulesets.Catch.Tests { @@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Catch.Tests var ruleset = new CatchRuleset(); var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate }; - var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1); + var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, []); Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate)); } @@ -33,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Tests var ruleset = new CatchRuleset(); var difficulty = new BeatmapDifficulty(); - var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75); + var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, [new CatchModHalfTime()]); Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01)); } @@ -44,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Tests var ruleset = new CatchRuleset(); var difficulty = new BeatmapDifficulty(); - var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5); + var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, [new CatchModDoubleTime()]); Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01)); } diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index d253b9893f..c7487df0c2 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -33,6 +33,7 @@ using osu.Game.Scoring; using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Ranking.Statistics; using osu.Game.Skinning; +using osu.Game.Utils; using osuTK; namespace osu.Game.Rulesets.Catch @@ -265,9 +266,10 @@ namespace osu.Game.Rulesets.Catch } /// - public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate) + public override BeatmapDifficulty GetAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, IReadOnlyCollection mods) { BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty); + double rate = ModUtils.CalculateRateWithMods(mods); double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, CatchHitObject.PREEMPT_MAX, CatchHitObject.PREEMPT_MID, CatchHitObject.PREEMPT_MIN); preempt /= rate; diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index c2bcba38ab..90d0080d6e 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -414,6 +414,32 @@ namespace osu.Game.Rulesets.Mania }), true) }; + /// + public override BeatmapDifficulty GetAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, IReadOnlyCollection mods) + { + BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty); + + // notably, in mania, hit windows are designed to be independent of track playback rate (see `ManiaHitWindows.SpeedMultiplier`). + // *however*, to not make matters *too* simple, mania Hard Rock and Easy differ from all other rulesets + // in that they apply multipliers *to hit window durations directly* rather than to the Overall Difficulty attribute itself. + // because the duration of hit window durations as a function of OD is not a linear function, + // this means that multiplying the OD is *not* the same thing as multiplying the hit window duration. + // in fact, the second operation is *much* harsher and will produce values much farther outside of normal operating range + // (even negative in the case of Easy). + // stable handles this wrong on song select and just assumes that it can handle mania EZ / HR the same way as all other rulesets. + + double perfectHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, ManiaHitWindows.PERFECT_WINDOW_RANGE); + + if (mods.Any(m => m is ManiaModHardRock)) + perfectHitWindow /= ManiaModHardRock.HIT_WINDOW_DIFFICULTY_MULTIPLIER; + else if (mods.Any(m => m is ManiaModEasy)) + perfectHitWindow /= ManiaModEasy.HIT_WINDOW_DIFFICULTY_MULTIPLIER; + + adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(perfectHitWindow, ManiaHitWindows.PERFECT_WINDOW_RANGE); + + return adjustedDifficulty; + } + public override IRulesetFilterCriteria CreateRulesetFilterCriteria() { return new ManiaFilterCriteria(); diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs index c9a84051d5..16872c45c4 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs @@ -13,19 +13,19 @@ namespace osu.Game.Rulesets.Mania.Mods { public override LocalisableString Description => @"More forgiving HP drain, less accuracy required, and extra lives!"; + public const double HIT_WINDOW_DIFFICULTY_MULTIPLIER = 1 / 1.4; + void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject) { - const double multiplier = 1 / 1.4; - switch (hitObject) { case Note: - ((ManiaHitWindows)hitObject.HitWindows).DifficultyMultiplier = multiplier; + ((ManiaHitWindows)hitObject.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER; break; case HoldNote hold: - ((ManiaHitWindows)hold.Head.HitWindows).DifficultyMultiplier = multiplier; - ((ManiaHitWindows)hold.Tail.HitWindows).DifficultyMultiplier = multiplier; + ((ManiaHitWindows)hold.Head.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER; + ((ManiaHitWindows)hold.Tail.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER; break; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHardRock.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHardRock.cs index a73bd94566..13f86bd641 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHardRock.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHardRock.cs @@ -13,19 +13,19 @@ namespace osu.Game.Rulesets.Mania.Mods public override double ScoreMultiplier => 1; public override bool Ranked => false; + public const double HIT_WINDOW_DIFFICULTY_MULTIPLIER = 1.4; + void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject) { - const double multiplier = 1.4; - switch (hitObject) { case Note: - ((ManiaHitWindows)hitObject.HitWindows).DifficultyMultiplier = multiplier; + ((ManiaHitWindows)hitObject.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER; break; case HoldNote hold: - ((ManiaHitWindows)hold.Head.HitWindows).DifficultyMultiplier = multiplier; - ((ManiaHitWindows)hold.Tail.HitWindows).DifficultyMultiplier = multiplier; + ((ManiaHitWindows)hold.Head.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER; + ((ManiaHitWindows)hold.Tail.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER; break; } } diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs index fe47b297dd..abff91926a 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs @@ -9,7 +9,7 @@ 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); + public 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); @@ -151,7 +151,7 @@ namespace osu.Game.Rulesets.Mania.Scoring } else { - perfect = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, perfect_window_range) * totalMultiplier) + 0.5; + perfect = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, PERFECT_WINDOW_RANGE) * totalMultiplier) + 0.5; great = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, great_window_range) * totalMultiplier) + 0.5; good = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, good_window_range) * totalMultiplier) + 0.5; ok = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, ok_window_range) * totalMultiplier) + 0.5; diff --git a/osu.Game.Rulesets.Osu.Tests/OsuRateAdjustedDisplayDifficultyTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuRateAdjustedDisplayDifficultyTest.cs index aa903205c8..4108e9388d 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuRateAdjustedDisplayDifficultyTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuRateAdjustedDisplayDifficultyTest.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using NUnit.Framework; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Osu.Mods; namespace osu.Game.Rulesets.Osu.Tests { @@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Tests var ruleset = new OsuRuleset(); var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate }; - var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1); + var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, []); Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate)); } @@ -33,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests var ruleset = new OsuRuleset(); var difficulty = new BeatmapDifficulty { OverallDifficulty = originalOverallDifficulty }; - var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1); + var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, []); Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(originalOverallDifficulty)); } @@ -44,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Tests var ruleset = new OsuRuleset(); var difficulty = new BeatmapDifficulty(); - var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75); + var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, [new OsuModHalfTime()]); Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01)); Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(2.22).Within(0.01)); @@ -56,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Tests var ruleset = new OsuRuleset(); var difficulty = new BeatmapDifficulty(); - var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5); + var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, [new OsuModDoubleTime()]); Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01)); Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(7.77).Within(0.01)); diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 0edb8046b9..be9f0e276b 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -40,6 +40,7 @@ using osu.Game.Scoring; using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Ranking.Statistics; using osu.Game.Skinning; +using osu.Game.Utils; using osuTK; namespace osu.Game.Rulesets.Osu @@ -365,9 +366,10 @@ namespace osu.Game.Rulesets.Osu /// /// - public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate) + public override BeatmapDifficulty GetAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, IReadOnlyCollection mods) { BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty); + double rate = ModUtils.CalculateRateWithMods(mods); double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN); preempt /= rate; diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoRateAdjustedDisplayDifficultyTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoRateAdjustedDisplayDifficultyTest.cs index 4ab3f502ad..2a5688ab11 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoRateAdjustedDisplayDifficultyTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoRateAdjustedDisplayDifficultyTest.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using NUnit.Framework; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Taiko.Mods; namespace osu.Game.Rulesets.Taiko.Tests { @@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Tests var ruleset = new TaikoRuleset(); var difficulty = new BeatmapDifficulty { OverallDifficulty = originalOverallDifficulty }; - var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1); + var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, []); Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(originalOverallDifficulty)); } @@ -33,7 +34,7 @@ namespace osu.Game.Rulesets.Taiko.Tests var ruleset = new TaikoRuleset(); var difficulty = new BeatmapDifficulty(); - var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75); + var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, [new TaikoModHalfTime()]); Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(1.11).Within(0.01)); } @@ -44,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Tests var ruleset = new TaikoRuleset(); var difficulty = new BeatmapDifficulty(); - var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5); + var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, [new TaikoModDoubleTime()]); Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(8.89).Within(0.01)); } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 1cb41e1299..76488fdd26 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -38,6 +38,7 @@ using osu.Game.Rulesets.Taiko.Configuration; using osu.Game.Rulesets.Taiko.Edit.Setup; using osu.Game.Rulesets.Taiko.Skinning.Default; using osu.Game.Screens.Edit.Setup; +using osu.Game.Utils; namespace osu.Game.Rulesets.Taiko { @@ -270,9 +271,10 @@ namespace osu.Game.Rulesets.Taiko } /// - public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate) + public override BeatmapDifficulty GetAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, IReadOnlyCollection mods) { BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty); + double rate = ModUtils.CalculateRateWithMods(mods); double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, TaikoHitWindows.GREAT_WINDOW_RANGE); greatHitWindow /= rate; diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs b/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs index 8182fe24b2..cc76e28dfe 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs @@ -140,7 +140,7 @@ namespace osu.Game.Beatmaps.Drawables } Ruleset ruleset = displayedContent.Ruleset.CreateInstance(); - BeatmapDifficulty adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(originalDifficulty, rate); + BeatmapDifficulty adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(originalDifficulty, displayedContent.Mods ?? []); circleSize.Text = @"CS: " + adjustedDifficulty.CircleSize.ToString(@"0.##"); drainRate.Text = @" HP: " + adjustedDifficulty.DrainRate.ToString(@"0.##"); diff --git a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs index 10e3df17e5..14c02f5da7 100644 --- a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs +++ b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs @@ -180,7 +180,7 @@ namespace osu.Game.Overlays.Mods mod.ApplyToDifficulty(adjustedDifficulty); Ruleset ruleset = GameRuleset.Value.CreateInstance(); - adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(adjustedDifficulty, rate); + adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(adjustedDifficulty, Mods.Value); TooltipContent = new AdjustedAttributesTooltip.Data(originalDifficulty, adjustedDifficulty); diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index bd1f273b49..da3f628137 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -380,15 +380,15 @@ namespace osu.Game.Rulesets public virtual LocalisableString GetDisplayNameForHitResult(HitResult result) => result.GetLocalisableDescription(); /// - /// Applies changes to difficulty attributes for presenting to a user a rough estimate of how rate adjust mods affect difficulty. + /// Applies changes to difficulty attributes for presenting to a user a rough estimate of how mods affect difficulty. /// Importantly, this should NOT BE USED FOR ANY CALCULATIONS. /// /// It is also not always correct, and arguably is never correct depending on your frame of mind. /// /// >The that will be adjusted. - /// The rate adjustment multiplier from mods. For example 1.5 for DT. + /// The active mods. /// The adjusted difficulty attributes. - public virtual BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate) => new BeatmapDifficulty(difficulty); + public virtual BeatmapDifficulty GetAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, IReadOnlyCollection mods) => new BeatmapDifficulty(difficulty); /// /// Creates ruleset-specific beatmap filter criteria to be used on the song select screen. diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index 152398dee3..90a4af48f0 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -185,9 +185,7 @@ namespace osu.Game.Screens.Select.Details if (Ruleset.Value != null) { - double rate = ModUtils.CalculateRateWithMods(Mods.Value); - - adjustedDifficulty = Ruleset.Value.CreateInstance().GetRateAdjustedDisplayDifficulty(originalDifficulty, rate); + adjustedDifficulty = Ruleset.Value.CreateInstance().GetAdjustedDisplayDifficulty(originalDifficulty, Mods.Value); TooltipContent = new AdjustedAttributesTooltip.Data(originalDifficulty, adjustedDifficulty); } diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs index 7c7c3872cd..2b1469d6e2 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs @@ -26,7 +26,6 @@ using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -using osu.Game.Utils; using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 @@ -302,9 +301,7 @@ namespace osu.Game.Screens.SelectV2 Ruleset rulesetInstance = ruleset.Value.CreateInstance(); - double rate = ModUtils.CalculateRateWithMods(mods.Value); - - adjustedDifficulty = rulesetInstance.GetRateAdjustedDisplayDifficulty(adjustedDifficulty, rate); + adjustedDifficulty = rulesetInstance.GetAdjustedDisplayDifficulty(adjustedDifficulty, mods.Value); difficultyStatisticsDisplay.TooltipContent = new AdjustedAttributesTooltip.Data(originalDifficulty, adjustedDifficulty); StatisticDifficulty.Data firstStatistic; diff --git a/osu.Game/Skinning/Components/BeatmapAttributeText.cs b/osu.Game/Skinning/Components/BeatmapAttributeText.cs index 58821f869a..60a03f4351 100644 --- a/osu.Game/Skinning/Components/BeatmapAttributeText.cs +++ b/osu.Game/Skinning/Components/BeatmapAttributeText.cs @@ -243,10 +243,7 @@ namespace osu.Game.Skinning.Components mod.ApplyToDifficulty(difficulty); if (ruleset.Value is RulesetInfo rulesetInfo) - { - double rate = ModUtils.CalculateRateWithMods(mods.Value); - difficulty = rulesetInfo.CreateInstance().GetRateAdjustedDisplayDifficulty(difficulty, rate); - } + difficulty = rulesetInfo.CreateInstance().GetAdjustedDisplayDifficulty(difficulty, mods.Value); return difficulty; } From 66a4cb59315423d2d6847da5fc5e1aa85ba9c85a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Jul 2025 17:01:11 +0900 Subject: [PATCH 257/259] 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 b98ed1a455..ebe2ca782a 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 9a54c51a3d..74b56bbaf6 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 2890a19a8551bac545f4d4c45760984cfde701e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 15 Jul 2025 12:37:09 +0200 Subject: [PATCH 258/259] Fix android builds losing awareness of their release stream --- osu.Android/OsuGameAndroid.cs | 48 ++++++++--------------------------- 1 file changed, 11 insertions(+), 37 deletions(-) diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 932fc8454e..71a71db73d 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -2,10 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using Android.App; using Android.Content.PM; using Microsoft.Maui.Devices; using osu.Framework.Allocation; +using osu.Framework.Development; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Platform; using osu.Game; @@ -21,58 +23,30 @@ namespace osu.Android [Cached] private readonly OsuGameActivity gameActivity; + private readonly PackageInfo packageInfo; + public override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth); public OsuGameAndroid(OsuGameActivity activity) : base(null) { gameActivity = activity; + packageInfo = Application.Context.ApplicationContext!.PackageManager!.GetPackageInfo(Application.Context.ApplicationContext.PackageName!, 0).AsNonNull(); } - public override Version AssemblyVersion + public override string Version { get { - var packageInfo = Application.Context.ApplicationContext!.PackageManager!.GetPackageInfo(Application.Context.ApplicationContext.PackageName!, 0).AsNonNull(); + if (!IsDeployedBuild) + return @"local " + (DebugUtils.IsDebugBuild ? @"debug" : @"release"); - try - { - // We store the osu! build number in the "VersionCode" field to better support google play releases. - // If we were to use the main build number, it would require a new submission each time (similar to TestFlight). - // In order to do this, we should split it up and pad the numbers to still ensure sequential increase over time. - // - // We also need to be aware that older SDK versions store this as a 32bit int. - // - // Basic conversion format (as done in Fastfile): 2020.606.0 -> 202006060 - - // https://stackoverflow.com/questions/52977079/android-sdk-28-versioncode-in-packageinfo-has-been-deprecated - string versionName; - - if (OperatingSystem.IsAndroidVersionAtLeast(28)) - { - versionName = packageInfo.LongVersionCode.ToString(); - // ensure we only read the trailing portion of long (the part we are interested in). - versionName = versionName.Substring(versionName.Length - 9); - } - else - { -#pragma warning disable CS0618 // Type or member is obsolete - // this is required else older SDKs will report missing method exception. - versionName = packageInfo.VersionCode.ToString(); -#pragma warning restore CS0618 // Type or member is obsolete - } - - // undo play store version garbling (as mentioned above). - return new Version(int.Parse(versionName.Substring(0, 4)), int.Parse(versionName.Substring(4, 4)), int.Parse(versionName.Substring(8, 1))); - } - catch - { - } - - return new Version(packageInfo.VersionName.AsNonNull()); + return packageInfo.VersionName.AsNonNull(); } } + public override Version AssemblyVersion => new Version(packageInfo.VersionName.AsNonNull().Split('-').First()); + protected override void LoadComplete() { base.LoadComplete(); From 2a6137863bd983d3947d0366df6aa2ff78172ba8 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 15 Jul 2025 22:20:30 -0700 Subject: [PATCH 259/259] Fix game not restarting after changing renderers --- osu.Desktop/OsuGameDesktop.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 7290761d56..885ee0620e 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -123,7 +123,7 @@ namespace osu.Desktop public override bool RestartAppWhenExited() { - Task.Run(() => Velopack.UpdateExe.Start()).FireAndForget(); + Task.Run(() => Velopack.UpdateExe.Start(waitPid: (uint)Environment.ProcessId)).FireAndForget(); return true; }