From eaadd507d3d253f44674495002bc70fe3023aa7e Mon Sep 17 00:00:00 2001 From: diquoks Date: Tue, 24 Jun 2025 19:15:02 +0300 Subject: [PATCH 01/11] 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 8e228e17b70a4585369240526b63ea205b04e73b Mon Sep 17 00:00:00 2001 From: diquoks Date: Wed, 25 Jun 2025 11:52:33 +0300 Subject: [PATCH 02/11] 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 3923b0a949113db8a65df2876a7636f320726c77 Mon Sep 17 00:00:00 2001 From: diquoks Date: Wed, 25 Jun 2025 12:04:04 +0300 Subject: [PATCH 03/11] 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 e06e74d84e579eda77a1b36de317af31dbe62df4 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 26 Jun 2025 09:58:30 +0300 Subject: [PATCH 04/11] 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 05/11] 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 06/11] 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 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 07/11] 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 08/11] 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 09/11] 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 10/11] 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 f19bc18f494e237802d89b8f6cf230ff9cd77c92 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Sat, 5 Jul 2025 12:24:36 +0300 Subject: [PATCH 11/11] 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)